Skip to content

Commit

Permalink
encoding/jsonschema: implement if/then/else keywords
Browse files Browse the repository at this point in the history
Use the newly added `matchIf` builtin to implement the
JSON Schema conditional operator.

Signed-off-by: Roger Peppe <[email protected]>
Change-Id: I0a64a67683d573f4bc348fb6704f7b18ac3339a7
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1200945
TryBot-Result: CUEcueckoo <[email protected]>
Reviewed-by: Daniel Martí <[email protected]>
Unity-Result: CUE porcuepine <[email protected]>
  • Loading branch information
rogpeppe committed Sep 10, 2024
1 parent 276d940 commit 7648cbd
Show file tree
Hide file tree
Showing 21 changed files with 216 additions and 595 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,37 @@ import (
// converted to lowercase during runtime. We recommended using
// lowercase input ids.
inputs?: {
{[=~"^[_a-zA-Z][a-zA-Z0-9_-]*$" & !~"^()$"]: {
{[=~"^[_a-zA-Z][a-zA-Z0-9_-]*$" & !~"^()$"]: matchN(5, [matchIf(null | bool | number | string | [...] | {
type!: "string", ...
}, null | bool | number | string | [...] | {
default?: string, ...
}, _) & {
...
}, matchIf(null | bool | number | string | [...] | {
type!: "boolean", ...
}, null | bool | number | string | [...] | {
default?: bool, ...
}, _) & {
...
}, matchIf(null | bool | number | string | [...] | {
type!: "number", ...
}, null | bool | number | string | [...] | {
default?: number, ...
}, _) & {
...
}, matchIf(null | bool | number | string | [...] | {
type!: "environment", ...
}, null | bool | number | string | [...] | {
default?: string, ...
}, _) & {
...
}, matchIf(null | bool | number | string | [...] | {
type!: "choice", ...
}, null | bool | number | string | [...] | {
options!: _, ...
}, _) & {
...
}]) & {
// A string description of the input parameter.
description!: string

Expand Down
6 changes: 3 additions & 3 deletions encoding/jsonschema/constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ var constraints = []*constraint{
p2("deprecated", constraintDeprecated, vfrom(VersionDraft2019_09)|openAPI),
p2("description", constraintDescription, allVersions|openAPI),
p1("discriminator", constraintTODO, openAPI),
p1("else", constraintTODO, vfrom(VersionDraft7)),
p1("else", constraintElse, vfrom(VersionDraft7)),
p2("enum", constraintEnum, allVersions|openAPI),
p1("example", constraintTODO, openAPI),
p2("examples", constraintExamples, vfrom(VersionDraft6)),
Expand All @@ -99,7 +99,7 @@ var constraints = []*constraint{
p1("externalDocs", constraintTODO, openAPI),
p1("format", constraintTODO, allVersions|openAPI),
p1("id", constraintID, vto(VersionDraft4)),
p1("if", constraintTODO, vfrom(VersionDraft7)),
p1("if", constraintIf, vfrom(VersionDraft7)),
p2("items", constraintItems, allVersions|openAPI),
p1("maxContains", constraintMaxContains, vfrom(VersionDraft2019_09)),
p2("maxItems", constraintMaxItems, allVersions|openAPI),
Expand All @@ -122,7 +122,7 @@ var constraints = []*constraint{
p2("propertyNames", constraintPropertyNames, vfrom(VersionDraft6)),
p1("readOnly", constraintTODO, vfrom(VersionDraft7)|openAPI),
p3("required", constraintRequired, allVersions|openAPI),
p1("then", constraintTODO, vfrom(VersionDraft7)),
p1("then", constraintThen, vfrom(VersionDraft7)),
p2("title", constraintTitle, allVersions|openAPI),
p2("type", constraintType, allVersions|openAPI),
p1("unevaluatedItems", constraintTODO, vfrom(VersionDraft2019_09)),
Expand Down
12 changes: 12 additions & 0 deletions encoding/jsonschema/constraints_combinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,15 @@ func constraintNot(key string, n cue.Value, s *state) {
ast.NewList(subSchema),
))
}

func constraintIf(key string, n cue.Value, s *state) {
s.ifConstraint = s.schema(n)
}

func constraintThen(key string, n cue.Value, s *state) {
s.thenConstraint = s.schema(n)
}

func constraintElse(key string, n cue.Value, s *state) {
s.elseConstraint = s.schema(n)
}
23 changes: 23 additions & 0 deletions encoding/jsonschema/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,10 @@ type state struct {
minContains *uint64
maxContains *uint64

ifConstraint ast.Expr
thenConstraint ast.Expr
elseConstraint ast.Expr

schemaVersion Version
schemaVersionPresent bool

Expand Down Expand Up @@ -459,6 +463,7 @@ func (s *state) finalize() (e ast.Expr) {
s.addErr(errors.Newf(s.pos.Pos(), "constraints are not possible to satisfy"))
return bottom()
}
s.addIfThenElse()

conjuncts := []ast.Expr{}
disjuncts := []ast.Expr{}
Expand Down Expand Up @@ -608,6 +613,24 @@ outer:
return e
}

func (s *state) addIfThenElse() {
if s.ifConstraint == nil || (s.thenConstraint == nil && s.elseConstraint == nil) {
return
}
if s.thenConstraint == nil {
s.thenConstraint = top()
}
if s.elseConstraint == nil {
s.elseConstraint = top()
}
s.all.add(s.pos, ast.NewCall(
ast.NewIdent("matchIf"),
s.ifConstraint,
s.thenConstraint,
s.elseConstraint,
))
}

func (s *state) comment() *ast.CommentGroup {
// Create documentation.
doc := strings.TrimSpace(s.title)
Expand Down
12 changes: 6 additions & 6 deletions encoding/jsonschema/external_teststats.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Generated by teststats. DO NOT EDIT
v2:
schema extract (pass / total): 1040 / 1637 = 63.5%
tests (pass / total): 3378 / 7175 = 47.1%
tests on extracted schemas (pass / total): 3378 / 3792 = 89.1%
schema extract (pass / total): 1072 / 1637 = 65.5%
tests (pass / total): 3457 / 7175 = 48.2%
tests on extracted schemas (pass / total): 3457 / 3874 = 89.2%

v3:
schema extract (pass / total): 1028 / 1637 = 62.8%
tests (pass / total): 3329 / 7175 = 46.4%
tests on extracted schemas (pass / total): 3329 / 3748 = 88.8%
schema extract (pass / total): 1060 / 1637 = 64.8%
tests (pass / total): 3408 / 7175 = 47.5%
tests on extracted schemas (pass / total): 3408 / 3830 = 89.0%
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,6 @@
"else": true
}
},
"skip": {
"v2": "extract error: keyword \"if\" not yet implemented (and 1 more errors)",
"v3": "extract error: keyword \"if\" not yet implemented (and 1 more errors)"
},
"tests": [
{
"description": "any non-empty array is valid",
Expand All @@ -213,18 +209,14 @@
],
"valid": true,
"skip": {
"v2": "could not compile schema",
"v3": "could not compile schema"
"v2": "6 errors in empty disjunction:\nconflicting values [\"foo\"] and {...} (mismatched types list and struct):\n generated.cue:4:1\n generated.cue:4:72\n instance.json:1:1\nconflicting values bool and [\"foo\"] (mismatched types bool and list):\n generated.cue:4:8\n instance.json:1:1\nconflicting values null and [\"foo\"] (mismatched types null and list):\n generated.cue:4:1\n instance.json:1:1\nconflicting values number and [\"foo\"] (mismatched types number and list):\n generated.cue:4:15\n instance.json:1:1\nconflicting values string and [\"foo\"] (mismatched types string and list):\n generated.cue:4:24\n instance.json:1:1\nexplicit error (_|_ literal) in source:\n generated.cue:4:58\n",
"v3": "conflicting values [\"foo\"] and {...} (mismatched types list and struct):\n generated.cue:4:72\n instance.json:1:1\nconflicting values bool and [\"foo\"] (mismatched types bool and list):\n generated.cue:4:8\n instance.json:1:1\nconflicting values null and [\"foo\"] (mismatched types null and list):\n generated.cue:4:1\n instance.json:1:1\nconflicting values number and [\"foo\"] (mismatched types number and list):\n generated.cue:4:15\n instance.json:1:1\nconflicting values string and [\"foo\"] (mismatched types string and list):\n generated.cue:4:24\n instance.json:1:1\nexplicit error (_|_ literal) in source:\n generated.cue:4:58\n"
}
},
{
"description": "empty array is invalid",
"data": [],
"valid": false,
"skip": {
"v2": "could not compile schema",
"v3": "could not compile schema"
}
"valid": false
}
]
},
Expand Down
Loading

0 comments on commit 7648cbd

Please sign in to comment.