Skip to content

Commit

Permalink
feat: add targeting schema, improve def. schema (#120)
Browse files Browse the repository at this point in the history
This PR:

- adds JSON-schema validation for targeting rules
  - basic JSONLogic rules validated
- custom rules (`sem_ver`, `fractional`, `starts/ends_with`) are fully
validated
  - descriptions are added for all operations and operands
  - `$evalutators` validated
  - maintained in separate schema
- improves flag-definition schema
  - descriptions added/improved

I've also added a bunch of positive and negative tests.

For a preview, add `"$schema":
"https://deploy-preview-1115--polite-licorice-3db33c.netlify.app/schema/v0/flagd-definitions.json"`
as a sibling to `"flags"` in your flagd config.

Closes: #115

⚠️ KEEP IN MIND WHEN REVIEWING that that `.yaml` files are the
source of truth; the CI checks that the json is consistent with them.


![validation](https://github.com/open-feature/flagd-schemas/assets/25272906/d29fc404-0014-47ed-b5ad-60255fc124b5)

---------

Signed-off-by: Todd Baert <[email protected]>
Co-authored-by: Michael Beemer <[email protected]>
Co-authored-by: Kavindu Dodanduwa <[email protected]>
  • Loading branch information
3 people authored Jan 8, 2024
1 parent 2602ce2 commit 6041fc7
Show file tree
Hide file tree
Showing 18 changed files with 1,667 additions and 89 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}
- name: Validate Schema
run: go test -v ./json
run: make test

# Ensure there is no diff when make gen-schema-json is run
- run: make gen-schema-json
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ gen-rust: install-buf guard-GOPATH

gen-schema-json: install-yq
yq eval -o=json json/flagd-definitions.yaml > json/flagd-definitions.json
yq eval -o=json json/targeting.yaml > json/targeting.json

.PHONY: test
test:
go test -v ./json

ajv-validate-flagd-schema:
@if ! npm ls ajv-cli; then npm ci; fi
npx ajv compile -s json/flagd-definitions.json
npx ajv compile -s json/targeting.json
# load the targeting json so flagd-definitions.json can reference it
npx ajv compile -r json/targeting.json -s json/flagd-definitions.json
241 changes: 240 additions & 1 deletion json/examples/full.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$schema": "../flagd-definitions.json",
"flags": {
"myBoolFlag": {
"state": "ENABLED",
Expand Down Expand Up @@ -35,6 +36,244 @@
}
},
"defaultVariant": "object1"
},
"fractional-flag": {
"state": "ENABLED",
"variants": {
"clubs": "clubs",
"diamonds": "diamonds",
"hearts": "hearts",
"spades": "spades",
"wild": "wild"
},
"defaultVariant": "wild",
"targeting": {
"fractional": [
{ "var": "user.name" },
["clubs", 25],
["diamonds", 25],
["hearts", 25],
["spades", 25]
]
}
},
"shorthand-fractional-flag": {
"state": "ENABLED",
"variants": {
"clubs": "clubs",
"diamonds": "diamonds",
"hearts": "hearts",
"spades": "spades",
"wild": "wild"
},
"defaultVariant": "wild",
"targeting": {
"fractional": [
["clubs", 25],
["diamonds", 25],
["hearts", 25],
["spades", 25]
]
}
},
"starts-ends-flag": {
"state": "ENABLED",
"variants": {
"prefix": "prefix",
"postfix": "postfix",
"none": "none"
},
"defaultVariant": "none",
"targeting": {
"if": [
{
"starts_with": [{ "var": "id" }, "abc"]
},
"prefix",
{
"if": [
{
"ends_with": [{ "var": "id" }, "xyz"]
},
"postfix",
{
"if": [
{
"ends_with": [{ "var": "id" }, 3]
},
"fail",
"none"
]
}
]
}
]
}
},
"equal-greater-lesser-version-flag": {
"state": "ENABLED",
"variants": {
"equal": "equal",
"greater": "greater",
"lesser": "lesser",
"none": "none"
},
"defaultVariant": "none",
"targeting": {
"if": [
{
"sem_ver": [{ "var": "version" }, "=", "2.0.0"]
},
"equal",
{
"if": [
{
"sem_ver": [{ "var": "version" }, ">", "2.0.0"]
},
"greater",
{
"if": [
{
"sem_ver": [{ "var": "version" }, "<", "2.0.0"]
},
"lesser",
{
"if": [
{
"sem_ver": [{ "var": "version" }, "=", "2.0.0.0"]
},
"fail",
null
]
}
]
}
]
}
]
}
},
"major-minor-version-flag": {
"state": "ENABLED",
"variants": {
"minor": "minor",
"major": "major",
"none": "none"
},
"defaultVariant": "none",
"targeting": {
"if": [
{
"sem_ver": [{ "var": "version" }, "~", "3.0.0"]
},
"minor",
{
"if": [
{
"sem_ver": [{ "var": "version" }, "^", "3.0.0"]
},
"major",
"none"
]
}
]
}
},
"test-cat": {
"state": "ENABLED",
"variants": {
"minor": "minor",
"major": "major",
"none": "none"
},
"defaultVariant": "none",
"targeting": {
"cat": ["1", "@"]
}
},
"context-aware": {
"state": "ENABLED",
"variants": {
"internal": "INTERNAL",
"external": "EXTERNAL"
},
"defaultVariant": "external",
"targeting": {
"if": [
{
"and": [
{
"==": [
{
"var": ["fn"]
},
"Sulisław"
]
},
{
"==": [
{
"var": ["ln"]
},
"Świętopełk"
]
},
{
"==": [
{
"var": ["age"]
},
29
]
},
{
"==": [
{
"var": ["customer"]
},
false
]
}
]
},
"internal",
"external"
]
}
},
"timestamp-flag": {
"state": "ENABLED",
"variants": {
"past": -1,
"future": 1,
"none": 0
},
"defaultVariant": "none",
"targeting": {
"if": [
{
">": [{ "var": "$flagd.timestamp" }, { "var": "time" }]
},
"past",
{
"if": [
{
"<": [{ "var": "$flagd.timestamp" }, { "var": "time" }]
},
"future",
"none"
]
}
]
}
},
"wrong-flag": {
"state": "ENABLED",
"variants": {
"one": "uno",
"two": "dos"
},
"defaultVariant": "one"
}
}
}
}
41 changes: 28 additions & 13 deletions json/flagd-definitions.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{
"$id": "https://flagd.dev/schema/v0/flagd-definitions.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "flagd Flag Configuration",
"description": "Defines flags for use in flagd, including typed variants and rules",
"type": "object",
"properties": {
"flags": {
"title": "Flags",
"description": "Top-level flags object. All flags are defined here.",
"type": "object",
"$comment": "flag objects are one of the 4 flag types defined in $defs",
"additionalProperties": false,
Expand All @@ -13,50 +16,62 @@
"oneOf": [
{
"title": "Boolean flag",
"description": "A flag associated with boolean values",
"description": "A flag having boolean values.",
"$ref": "#/$defs/booleanFlag"
},
{
"title": "String flag",
"description": "A flag associated with string values",
"description": "A flag having string values.",
"$ref": "#/$defs/stringFlag"
},
{
"title": "Numeric flag",
"description": "A flag associated with numeric values",
"description": "A flag having numeric values.",
"$ref": "#/$defs/numberFlag"
},
{
"title": "Object flag",
"description": "A flag associated with arbitrary object values",
"description": "A flag having arbitrary object values.",
"$ref": "#/$defs/objectFlag"
}
]
}
}
},
"$evaluators": {
"title": "Evaluators",
"description": "Reusable targeting rules that can be referenced with \"$ref\": \"myRule\" in multiple flags.",
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^.{1,}$": {
"$comment": "this relative ref means that targeting.json MUST be in the same dir, or available on the same HTTP path",
"$ref": "./targeting.json#/$defs/targeting"
}
}
}
},
"$defs": {
"flag": {
"title": "Flag Base",
"description": "Base object for all flags",
"$comment": "base flag object; no title/description here, allows for better UX, keep it in the overrides",
"type": "object",
"properties": {
"state": {
"description": "Indicates whether the flag is functional. Disabled flags are treated as if they don't exist",
"title": "Flag State",
"description": "Indicates whether the flag is functional. Disabled flags are treated as if they don't exist.",
"type": "string",
"enum": [
"ENABLED",
"DISABLED"
]
},
"defaultVariant": {
"description": "The variant to serve if no dynamic targeting applies",
"title": "Default Variant",
"description": "The variant to serve if no dynamic targeting applies (including if the targeting returns null).",
"type": "string"
},
"targeting": {
"type": "object",
"description": "JsonLogic expressions to be used for dynamic evaluation. The \"context\" is passed as the data. Rules must resolve one of the defined variants, or the \"defaultVariant\" will be used."
"$ref": "./targeting.json#/$defs/targeting"
}
},
"required": [
Expand All @@ -76,8 +91,8 @@
}
},
"default": {
"on": true,
"off": false
"true": true,
"false": false
}
}
}
Expand Down Expand Up @@ -124,7 +139,7 @@
}
}
},
"$comment": "Merge the variants with the base flag to build our typed flags",
"$comment": "merge the variants with the base flag to build our typed flags",
"booleanFlag": {
"allOf": [
{
Expand Down
Loading

0 comments on commit 6041fc7

Please sign in to comment.