Skip to content

Commit

Permalink
feat: support negated condition in match conditions (#570)
Browse files Browse the repository at this point in the history
* feat: support negated condition in match conditions

Use `!` suffix in variable name to negate a condition. Use `:except:` to
negate a combination of conditions in a map. Add `:present:` for checking
existence of a key in a map.

Negating a condition is useful since Golang regular expression doesn't
support negative lookahead. Sometime it is needed to negate parts of the
regex condition.

* docs: matching condition special keywords
  • Loading branch information
Charles546 committed Sep 25, 2023
1 parent 1f984b0 commit efbffcc
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 4 deletions.
30 changes: 29 additions & 1 deletion docs/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,35 @@ workflows:

Please note how we use regular expression, list of options to match the contextual data, and how to match a field deep into the data structure.

Below are some examples of using list of conditions:
The matching condition also supports some special keywords.

- :absent: - true if a key is not in the data structure
- :present: - true if a key is present in the data structure
- :except: - true if the data structure does NOT match the given criteria

You can also use a `!` suffix following a key name to negate the matching criteria. See some examples below.

```yaml
---
workflows:
responding_to_github_push:
if_match:
git_repo: myorg/myrepo
git_ref!: ":regex:^refs/tags/" # except tags
...

alert_unauthorized_access:
if_match:
scope: production
":except:":
user:
role: authorized
":present:": authorized_by
...

```

Below are some more examples of using list of conditions:
<!-- {% raw %} -->
```yaml
---
Expand Down
20 changes: 17 additions & 3 deletions pkg/dipper/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,32 @@ func CompareMap(actual interface{}, criteria interface{}) bool {
case key == ":auth:":
// offload to another driver using RPC
// pass
case key == ":present:":
fallthrough
case key == ":absent:":
expectPresence := key == ":present:"
keys := []interface{}{}
for _, k := range value.MapKeys() {
keys = append(keys, k.Interface())
}
if CompareAll(keys, subCriteria) {
// key not absent
presence := CompareAll(keys, subCriteria)
if presence != expectPresence {
// key presence not matching expectation
return false
}
case key == ":except:":
if CompareAll(actual, subCriteria) {
return false
}
default:
neg := key[len(key)-1] == '!'
if neg {
key = key[0 : len(key)-1]
}

subVal := value.MapIndex(reflect.ValueOf(key))
if !subVal.IsValid() || (subVal.IsValid() && !CompareAll(subVal.Interface(), subCriteria)) {
match := subVal.IsValid() && CompareAll(subVal.Interface(), subCriteria)
if match == neg {
return false
}
}
Expand Down
15 changes: 15 additions & 0 deletions pkg/dipper/condition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,25 @@ func TestCompareAllMap(t *testing.T) {
assert.True(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2"}, map[string]interface{}{":absent:": "key3"}), "map should have key3 absent")
assert.True(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2"}, map[string]interface{}{":absent:": regexp.MustCompile("key3[1-3]")}), "map should have key3* absent")
assert.False(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2", "key3": "val3"}, map[string]interface{}{":absent:": regexp.MustCompile("key3[1-3]*")}), "map should have key3* absent and fail")
assert.False(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2"}, map[string]interface{}{":present:": "key3"}), "map should have key3 present")
assert.False(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2"}, map[string]interface{}{":present:": regexp.MustCompile("key3[1-3]")}), "map should have key3* present")
assert.True(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2", "key3": "val3"}, map[string]interface{}{":present:": regexp.MustCompile("key3[1-3]*")}), "map should have key3* present and fail")
assert.False(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2", "key3": "val3"}, "invalid condition"), "fail with invalid condition for map value")
assert.False(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2", "key3": "val3"}, map[string]interface{}{"key4": "111"}), "fail with condition that missing value")
}

func TestCompareWithNegates(t *testing.T) {
assert.False(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2"}, map[string]interface{}{"key1": "val1", "key2!": "val2"}), "map match nagated condition")
assert.True(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2"}, map[string]interface{}{"key1": "val1", "key2!": "val3"}), "map not match nagated condition")
assert.True(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2"}, map[string]interface{}{"key1": "val1", "key3!": "val3"}), "map missking key for nagated condition")
}

func TestCompareWithExcept(t *testing.T) {
assert.False(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2"}, map[string]interface{}{"key1": "val1", ":except:": map[string]interface{}{"key2": "val2"}}), "map match except condition")
assert.True(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2"}, map[string]interface{}{"key1": "val1", ":except:": map[string]interface{}{"key2": "val3"}}), "map not match except condition")
assert.True(t, CompareAll(map[string]interface{}{"key1": "val1", "key2": "val2"}, map[string]interface{}{"key1": "val1", ":except:": map[string]interface{}{"key3": "val3"}}), "map missking key for except condition")
}

func TestIsTruthy(t *testing.T) {
assert.False(t, IsTruthy(nil), "nil should NOT be truthy")
assert.False(t, IsTruthy(" "), "whitespace only string should NOT be truthy")
Expand Down

0 comments on commit efbffcc

Please sign in to comment.