Skip to content

Commit

Permalink
Changes to core/casing function that
Browse files Browse the repository at this point in the history
 1. Allows it to match on objects and arrays. When it does, it applies the casing rule to all field names (for objects) and all elements (for arrays)
 2. Allowed casing to work on multiple node matches so that we can use recursive JSONPath matching such as '$..properties'
  • Loading branch information
Calvin Lobo committed Jun 19, 2024
1 parent 4a996f8 commit 7e9b3c1
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 27 deletions.
76 changes: 49 additions & 27 deletions functions/core/casing.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,25 @@ func (c Casing) GetCategory() string {

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

if len(nodes) != 1 { // there can only be a single node passed in to this function.
// We expect at least one node to be found from the match
if len(nodes) == 0 {
return nil
}

// If we matched more than one node (eg through a recursive JSONPATH search such as '$..properties')
// Then recursively apply the casing to all nodes and bubble up all the results
if len(nodes) > 1 {
for _, n := range nodes {
results = append(results, c.RunRule([]*yaml.Node{n}, context)...)
}
return results
}

// From here on out, we are processing a single node
node := nodes[0]

var casingType string

message := context.Rule.Message
Expand All @@ -110,14 +124,13 @@ func (c Casing) RunRule(nodes []*yaml.Node, context model.RuleFunctionContext) [

// if a separator is defined, and can be used as a leading char, and the node value is that
// char (rune, what ever), then we're done.
if len(nodes[0].Value) == 1 &&
if len(node.Value) == 1 &&
c.separatorChar != "" &&
c.separatorAllowLeading &&
c.separatorChar == nodes[0].Value {
c.separatorChar == node.Value {
return nil
}

var results []model.RuleFunctionResult
var pattern string

if !c.compiled {
Expand Down Expand Up @@ -151,26 +164,15 @@ func (c Casing) RunRule(nodes []*yaml.Node, context model.RuleFunctionContext) [
ruleMessage = context.Rule.Message
}

// If the matched node is an array or map, then we should apply the casing rule to all it's children nodes:
// For maps, it applies to all field names and for arrays, it applies to all elements.
nodesToMatch := c.unravelNode(node)

var rx *regexp.Regexp
if c.separatorChar == "" {
rx := regexp.MustCompile(fmt.Sprintf("^%s$", pattern))
node := nodes[0]
if utils.IsNodeMap(nodes[0]) || utils.IsNodeArray(nodes[0]) {
if len(nodes[0].Content) > 0 {
node = nodes[0].Content[0]
}
}
rx = regexp.MustCompile(fmt.Sprintf("^%s$", pattern))

if !rx.MatchString(node.Value) {
results = append(results, model.RuleFunctionResult{
Message: vacuumUtils.SuppliedOrDefault(message, fmt.Sprintf("%s: `%s` is not %s case", ruleMessage, node.Value, casingType)),
StartNode: node,
EndNode: vacuumUtils.BuildEndNode(node),
Path: pathValue,
Rule: context.Rule,
})
}
} else {

c.separatorPattern = fmt.Sprintf("[%s]", regexp.QuoteMeta(c.separatorChar))
var leadingSepPattern string
var leadingPattern string
Expand All @@ -181,13 +183,16 @@ func (c Casing) RunRule(nodes []*yaml.Node, context model.RuleFunctionContext) [
leadingPattern = fmt.Sprintf("^(?:%[1]s)+(?:%[2]s%[1]s)*$", pattern, c.separatorPattern)
}

rx := regexp.MustCompile(leadingPattern)
if !rx.MatchString(nodes[0].Value) {
rx = regexp.MustCompile(leadingPattern)
}

// Go through each node and check if the casing is correct
for _, n := range nodesToMatch {
if !rx.MatchString(n.Value) {
results = append(results, model.RuleFunctionResult{
Message: vacuumUtils.SuppliedOrDefault(message, fmt.Sprintf("%s: `%s` is not `%s` case", ruleMessage,
nodes[0].Value, casingType)),
StartNode: nodes[0],
EndNode: vacuumUtils.BuildEndNode(nodes[0]),
Message: vacuumUtils.SuppliedOrDefault(message, fmt.Sprintf("%s: `%s` is not `%s` case", ruleMessage, n.Value, casingType)),
StartNode: n,
EndNode: vacuumUtils.BuildEndNode(n),
Path: pathValue,
Rule: context.Rule,
})
Expand All @@ -197,6 +202,23 @@ func (c Casing) RunRule(nodes []*yaml.Node, context model.RuleFunctionContext) [
return results
}

// If a node refers to an object, return a list of it's fields. If a node refers to an array, return a list of it's elements
func (c Casing) unravelNode(node *yaml.Node) []*yaml.Node {
var nodesToMatch []*yaml.Node

if utils.IsNodeMap(node) {
for ii := 0; ii < len(node.Content); ii += 2 {
nodesToMatch = append(nodesToMatch, node.Content[ii])
}
} else if utils.IsNodeArray(node) {
nodesToMatch = node.Content
} else {
nodesToMatch = append(nodesToMatch, node)
}

return nodesToMatch
}

func (c *Casing) compileExpressions() {

digits := "0-9"
Expand Down
77 changes: 77 additions & 0 deletions functions/core/casing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,80 @@ func TestCasing_GetSchema_Valid(t *testing.T) {
assert.True(t, res)

}

// This tests a recursive match on a field ('properties') and ensures that the objects that match have their fields
// (for objects) or elements (for arrays) all satisfy the casing.
func TestCasing_RunRule_MatchMapAndArray_Recursive_Success(t *testing.T) {

sampleYaml :=
`
chicken:
properties:
- NAME
- AGE
pork:
properties:
SIZE: large
GENDER: male
FROM:
properties:
COUNTRY: Canada
CITY: Guelph
`

// Recursively match any object that has a 'properties' object
path := "$..properties"

nodes, _ := gen_utils.FindNodes([]byte(sampleYaml), path)
assert.Len(t, nodes, 3, "expected 3 'properties' objects")

opts := make(map[string]string)
opts["type"] = "macro"

rule := buildCoreTestRule(path, model.SeverityError, "casing", "", nil)
ctx := buildCoreTestContext(model.CastToRuleAction(rule.Then), opts)
ctx.Given = path
ctx.Rule = &rule

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

assert.Len(t, res, 0)
}

func TestCasing_RunRule_MatchMapAndArray_Recursive_Fail(t *testing.T) {

sampleYaml :=
`
chicken:
properties:
- NAME
- Age
pork:
properties:
SIZE: large
Gender: male
FROM:
properties:
COUNTRY: Canada
city: Guelph
`

path := "$..properties"

nodes, _ := gen_utils.FindNodes([]byte(sampleYaml), path)
assert.Len(t, nodes, 3, "expected 3 'properties' objects")

opts := make(map[string]string)
opts["type"] = "macro"

rule := buildCoreTestRule(path, model.SeverityError, "casing", "", nil)
ctx := buildCoreTestContext(model.CastToRuleAction(rule.Then), opts)
ctx.Given = path
ctx.Rule = &rule

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

assert.Len(t, res, 3, "expected all fields of 'properties' objects to be MACRO case")
}

0 comments on commit 7e9b3c1

Please sign in to comment.