Skip to content

Commit

Permalink
Added new paths-kebab-case rule
Browse files Browse the repository at this point in the history
Checks all segments of a path are not empty and kebab case.
  • Loading branch information
daveshanley committed Jul 19, 2022
1 parent 0da18ba commit ac2452e
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 3 deletions.
1 change: 1 addition & 0 deletions functions/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ func MapBuiltinFunctions() Functions {
funcs["oasAPIServers"] = openapi_functions.APIServers{}
funcs["noAmbiguousPaths"] = openapi_functions.AmbiguousPaths{}
funcs["noVerbsInPath"] = openapi_functions.VerbsInPaths{}
funcs["pathsKebabCase"] = openapi_functions.PathsKebabCase{}
funcs["oasOpErrorResponse"] = openapi_functions.Operation4xResponse{}

})
Expand Down
2 changes: 1 addition & 1 deletion functions/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import (

func TestMapBuiltinFunctions(t *testing.T) {
funcs := MapBuiltinFunctions()
assert.Len(t, funcs.GetAllFunctions(), 41)
assert.Len(t, funcs.GetAllFunctions(), 42)
}
80 changes: 80 additions & 0 deletions functions/openapi/paths_kebab_case.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2022 Dave Shanley / Quobix
// SPDX-License-Identifier: MIT

package openapi

import (
"fmt"
"github.com/daveshanley/vacuum/model"
"gopkg.in/yaml.v3"
"regexp"
"strings"
)

// PathsKebabCase Checks to ensure each segment of a path is using kebab case.
type PathsKebabCase struct {
}

// GetSchema returns a model.RuleFunctionSchema defining the schema of the VerbsInPath rule.
func (vp PathsKebabCase) GetSchema() model.RuleFunctionSchema {
return model.RuleFunctionSchema{Name: "noVerbsInPath"}
}

// RunRule will execute the PathsKebabCase rule, based on supplied context and a supplied []*yaml.Node slice.
func (vp PathsKebabCase) RunRule(nodes []*yaml.Node, context model.RuleFunctionContext) []model.RuleFunctionResult {

if len(nodes) <= 0 {
return nil
}

var results []model.RuleFunctionResult

ops := context.Index.GetPathsNode()

var opPath string

if ops != nil {
for i, op := range ops.Content {
if i%2 == 0 {
opPath = op.Value
continue
}
path := fmt.Sprintf("$.paths.%s", opPath)
notKebab, segments := checkPathCase(opPath)
if notKebab {
results = append(results, model.RuleFunctionResult{
Message: fmt.Sprintf("Path segments `%s` do not use kebab-case", strings.Join(segments, "`, `")),
StartNode: op,
EndNode: op,
Path: path,
Rule: context.Rule,
})
}
}
}
return results
}

var pathKebabCaseRegex, _ = regexp.Compile("^[{}a-z\\d-.]+$")

func checkPathCase(path string) (bool, []string) {
segs := strings.Split(path, "/")[1:]
var found []string
for _, seg := range segs {
if !pathKebabCaseRegex.MatchString(seg) {
// check if it's a variable, if so, skip
if seg == "" {
found = append(found, "!empty segment!")
continue
}
if seg[0] == '{' && seg[len(seg)-1] == '}' {
continue
}
found = append(found, seg)
}
}
if len(found) > 0 {
return true, found
}
return false, nil
}
61 changes: 61 additions & 0 deletions functions/openapi/paths_kebab_case_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package openapi

import (
"github.com/daveshanley/vacuum/model"
"github.com/pb33f/libopenapi/index"
"github.com/pb33f/libopenapi/utils"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
"testing"
)

func TestPathsKebabCase_GetSchema(t *testing.T) {
def := PathsKebabCase{}
assert.Equal(t, "noVerbsInPath", def.GetSchema().Name)
}

func TestPathsKebabCase_RunRule(t *testing.T) {
def := PathsKebabCase{}
res := def.RunRule(nil, model.RuleFunctionContext{})
assert.Len(t, res, 0)
}

func TestPathsKebabCase_Success(t *testing.T) {

yml := `openapi: 3.0.0
paths:
'/woah/slow-down/you-move-too-fast':
get:
summary: not bad
'/youHave/got/to/make/the_morning last':
get:
summary: bad path
'/just-kicking/down/the/cobble-stones':
get:
summary: nice
'/looking~1/{forFun}/AND/feeling_groovy':
get:
summary: this is also doomed
'/ok//ok':
get:
summary: should we complain?`

path := "$"

var rootNode yaml.Node
err := yaml.Unmarshal([]byte(yml), &rootNode)

assert.NoError(t, err)
nodes, _ := utils.FindNodes([]byte(yml), path)

rule := buildOpenApiTestRuleAction(path, "verbsInPath", "", nil)
ctx := buildOpenApiTestContext(model.CastToRuleAction(rule.Then), nil)
ctx.Rule = &rule
ctx.Index = index.NewSpecIndex(&rootNode)

def := PathsKebabCase{}
res := def.RunRule(nodes, ctx)

assert.Len(t, res, 3)

}
4 changes: 4 additions & 0 deletions rulesets/rule_fixes.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ const (
noVerbsInPathFix string = "When HTTP verbs (get/post/put etc) are used in path segments, it muddies the semantics of REST and creates a confusing and " +
"inconsistent experience. It's highly recommended that verbs are not used in path segments. Replace those HTTP verbs with more meaningful nouns."

pathsKebabCaseFix string = "Path segments should not contain any uppercase letters, punctuation or underscores. The only valid way to separate words in a " +
"segment, is to use a hyphen '-'. The elements that are violating the rule are highlighted in the violation description. These are the elements that need to " +
"change."

operationsErrorResponseFix string = "Make sure each operation defines at least one 4xx error response. 4xx Errors are " +
"used to inform clients they are using the API incorrectly, with bad input, or malformed requests. An API with no errors" +
"defined is really hard to navigate."
Expand Down
20 changes: 20 additions & 0 deletions rulesets/ruleset_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,26 @@ func GetNoVerbsInPathRule() *model.Rule {
}
}

// GetPathsKebabCaseRule will check that each path segment is kebab-case
func GetPathsKebabCaseRule() *model.Rule {
return &model.Rule{
Name: "Path segments must be kebab-case only",
Id: pathsKebabCase,
Formats: model.AllFormats,
Description: "Path segments must only use kebab-case (no underscores or uppercase)",
Given: "$",
Resolved: false,
Recommended: true,
RuleCategory: model.RuleCategories[model.CategoryOperations],
Type: validation,
Severity: err,
Then: model.RuleAction{
Function: "pathsKebabCase",
},
HowToFix: pathsKebabCaseFix,
}
}

// GetOperationErrorResponseRule will return the rule for checking for a 4xx response defined in operations.
func GetOperationErrorResponseRule() *model.Rule {
return &model.Rule{
Expand Down
2 changes: 2 additions & 0 deletions rulesets/rulesets.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
allOperations = "[?(@.get || @.post || @.put || @.patch || @.delete || @.trace || @.options || @.head)]"

noVerbsInPath = "no-http-verbs-in-path"
pathsKebabCase = "paths-kebab-case"
noAmbiguousPaths = "no-ambiguous-paths"
operationErrorResponse = "operation-4xx-response"
operationSuccessResponse = "operation-success-response"
Expand Down Expand Up @@ -302,6 +303,7 @@ func generateDefaultOpenAPIRuleSet() *RuleSet {
rules[oas2ValidSchemaExample] = GetOAS2ExamplesRule()
rules[noAmbiguousPaths] = NoAmbiguousPaths()
rules[noVerbsInPath] = GetNoVerbsInPathRule()
rules[pathsKebabCase] = GetPathsKebabCaseRule()
rules[operationErrorResponse] = GetOperationErrorResponseRule()
rules[oas2Schema] = GetOAS2SchemaRule()
rules[oas3Schema] = GetOAS3SchemaRule()
Expand Down
4 changes: 2 additions & 2 deletions rulesets/rulesets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"testing"
)

var totalRules = 52
var totalRecommendedRules = 41
var totalRules = 53
var totalRecommendedRules = 42

func TestBuildDefaultRuleSets(t *testing.T) {

Expand Down

0 comments on commit ac2452e

Please sign in to comment.