this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

encoding/jsonschema: better version support

Currently schema version is recognized in an ad-hoc way inside the `id`
and `$id` implementations, but version-specific behavior is going to be
considerably more widespread than that, so implement a way to declare
the set of versions supported by each keyword and have the higher-level
logic do that check.

Also, we need another phase in the processing because `$schema` must be
processed first so we know how to process other keywords that are
currently in phase 0 (eg. `id`), so increment all existing phase numbers
and put `$schema` in phase 0.

The changed behaviour in the `def_jsonschema` test demonstrates that
this does fix some existing code: the `$schema` field is lexically after
the `$id` field so the `$id` field processing was not aware of the
schema that had been set and hence did not recognize the id. The new
phasing fixes this.

Signed-off-by: Roger Peppe <rogpeppe@gmail.com>
Change-Id: I4eee68845d3f0632ab062408e0a02299656f63f2
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1199533
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>

+157 -94
+1
cmd/cue/cmd/testdata/script/def_jsonschema.txtar
··· 24 24 #Person: { 25 25 // Person 26 26 @jsonschema(schema="http://json-schema.org/draft-07/schema#") 27 + @jsonschema(id="https://example.com/person.schema.json") 27 28 28 29 // The person's first name. 29 30 firstName?: string
+71 -64
encoding/jsonschema/constraints.go
··· 31 31 // "required" and thus must have a lower phase number than the latter. 32 32 phase int 33 33 34 - // Indicates the draft number in which this constraint is defined. 35 - draft int 36 - fn constraintFunc 34 + // versions holds the versions for which this constraint is defined. 35 + versions versionSet 36 + fn constraintFunc 37 37 } 38 38 39 39 // A constraintFunc converts a given JSON Schema constraint (specified in n) 40 40 // to a CUE constraint recorded in state. 41 41 type constraintFunc func(key string, n cue.Value, s *state) 42 42 43 - func p0(name string, f constraintFunc) *constraint { 44 - return &constraint{key: name, fn: f} 43 + var constraintMap = map[string]*constraint{} 44 + 45 + func init() { 46 + for _, c := range constraints { 47 + constraintMap[c.key] = c 48 + } 45 49 } 46 50 47 - func p1d(name string, draft int, f constraintFunc) *constraint { 48 - return &constraint{key: name, phase: 1, draft: draft, fn: f} 51 + // Note: the following table is ordered lexically by keyword name. 52 + // The various implementations are grouped by kind in the constaint-*.go files. 53 + 54 + const numPhases = 5 55 + 56 + var constraints = []*constraint{ 57 + p2d("$comment", constraintComment, vfrom(versionDraft07)), 58 + p2("$defs", constraintAddDefinitions), 59 + p1d("$id", constraintID, vfrom(versionDraft06)), 60 + p0("$schema", constraintSchema), 61 + p2("$ref", constraintRef), 62 + p2("additionalItems", constraintAdditionalItems), 63 + p4("additionalProperties", constraintAdditionalProperties), 64 + p3("allOf", constraintAllOf), 65 + p3("anyOf", constraintAnyOf), 66 + p2d("const", constraintConst, vfrom(versionDraft06)), 67 + p2d("contains", constraintContains, vfrom(versionDraft06)), 68 + p2d("contentEncoding", constraintContentEncoding, vfrom(versionDraft07)), 69 + p2d("contentMediaType", constraintContentMediaType, vfrom(versionDraft07)), 70 + p2("default", constraintDefault), 71 + p2("definitions", constraintAddDefinitions), 72 + p2("dependencies", constraintDependencies), 73 + p2("deprecated", constraintDeprecated), 74 + p2("description", constraintDescription), 75 + p2("enum", constraintEnum), 76 + p2d("examples", constraintExamples, vfrom(versionDraft06)), 77 + p2("exclusiveMaximum", constraintExclusiveMaximum), 78 + p2("exclusiveMinimum", constraintExclusiveMinimum), 79 + p1d("id", constraintID, vto(versionDraft04)), 80 + p2("items", constraintItems), 81 + p2("minItems", constraintMinItems), 82 + p2("maxItems", constraintMaxItems), 83 + p2("maxLength", constraintMaxLength), 84 + p2("maxProperties", constraintMaxProperties), 85 + p3("maximum", constraintMaximum), 86 + p2("minLength", constraintMinLength), 87 + p3("minimum", constraintMinimum), 88 + p2("multipleOf", constraintMultipleOf), 89 + p3("oneOf", constraintOneOf), 90 + p2("nullable", constraintNullable), 91 + p2("pattern", constraintPattern), 92 + p3("patternProperties", constraintPatternProperties), 93 + p2("properties", constraintProperties), 94 + p2d("propertyNames", constraintPropertyNames, vfrom(versionDraft06)), 95 + p3("required", constraintRequired), 96 + p2("title", constraintTitle), 97 + p2("type", constraintType), 98 + p2("uniqueItems", constraintUniqueItems), 99 + } 100 + 101 + func p0(name string, f constraintFunc) *constraint { 102 + return &constraint{key: name, phase: 0, versions: allVersions, fn: f} 49 103 } 50 104 51 105 func p1(name string, f constraintFunc) *constraint { 52 - return &constraint{key: name, phase: 1, fn: f} 106 + return &constraint{key: name, phase: 1, versions: allVersions, fn: f} 53 107 } 54 108 55 109 func p2(name string, f constraintFunc) *constraint { 56 - return &constraint{key: name, phase: 2, fn: f} 110 + return &constraint{key: name, phase: 2, versions: allVersions, fn: f} 57 111 } 58 112 59 113 func p3(name string, f constraintFunc) *constraint { 60 - return &constraint{key: name, phase: 3, fn: f} 114 + return &constraint{key: name, phase: 3, versions: allVersions, fn: f} 61 115 } 62 116 63 - // TODO: 64 - // writeOnly, readOnly 65 - 66 - var constraintMap = map[string]*constraint{} 67 - 68 - func init() { 69 - for _, c := range constraints { 70 - constraintMap[c.key] = c 71 - } 117 + func p4(name string, f constraintFunc) *constraint { 118 + return &constraint{key: name, phase: 4, versions: allVersions, fn: f} 72 119 } 73 120 74 - // Note: the following table is ordered lexically by keyword name. 75 - // The various implementations are grouped by kind in the constaint-*.go files. 121 + func p1d(name string, f constraintFunc, versions versionSet) *constraint { 122 + return &constraint{key: name, phase: 1, versions: versions, fn: f} 123 + } 76 124 77 - var constraints = []*constraint{ 78 - p1d("$comment", 7, constraintComment), 79 - p1("$defs", constraintAddDefinitions), 80 - p0("$id", constraintID), 81 - p0("$schema", constraintSchema), 82 - p1("$ref", constraintRef), 83 - p1("additionalItems", constraintAdditionalItems), 84 - p3("additionalProperties", constraintAdditionalProperties), 85 - p2("allOf", constraintAllOf), 86 - p2("anyOf", constraintAnyOf), 87 - p1d("const", 6, constraintConst), 88 - p1("contains", constraintContains), 89 - p1d("contentEncoding", 7, constraintContentEncoding), 90 - p1d("contentMediaType", 7, constraintContentMediaType), 91 - p1("default", constraintDefault), 92 - p1("definitions", constraintAddDefinitions), 93 - p1("dependencies", constraintDependencies), 94 - p1("deprecated", constraintDeprecated), 95 - p1("description", constraintDescription), 96 - p1("enum", constraintEnum), 97 - p1("examples", constraintExamples), 98 - p1("exclusiveMaximum", constraintExclusiveMaximum), 99 - p1("exclusiveMinimum", constraintExclusiveMinimum), 100 - p0("id", constraintID), 101 - p1("items", constraintItems), 102 - p1("minItems", constraintMinItems), 103 - p1("maxItems", constraintMaxItems), 104 - p1("maxLength", constraintMaxLength), 105 - p1("maxProperties", constraintMaxProperties), 106 - p2("maximum", constraintMaximum), 107 - p1("minLength", constraintMinLength), 108 - p2("minimum", constraintMinimum), 109 - p1("multipleOf", constraintMultipleOf), 110 - p2("oneOf", constraintOneOf), 111 - p1("nullable", constraintNullable), 112 - p1("pattern", constraintPattern), 113 - p2("patternProperties", constraintPatternProperties), 114 - p1("properties", constraintProperties), 115 - p1d("propertyNames", 6, constraintPropertyNames), 116 - p2("required", constraintRequired), 117 - p1("title", constraintTitle), 118 - p1("type", constraintType), 119 - p1("uniqueItems", constraintUniqueItems), 125 + func p2d(name string, f constraintFunc, versions versionSet) *constraint { 126 + return &constraint{key: name, phase: 2, versions: versions, fn: f} 120 127 }
+2 -23
encoding/jsonschema/constraints_meta.go
··· 26 26 // 27 27 // TODO: mark identifiers. 28 28 29 - // Draft-06 renamed the id field to $id. 30 - if key == "id" { 31 - // old-style id field. 32 - if s.schemaVersion >= versionDraft06 { 33 - if s.cfg.Strict { 34 - s.warnf(n.Pos(), "use of old-style id field not allowed in schema version %v", s.schemaVersion) 35 - } 36 - return 37 - } 38 - } else { 39 - // new-style $id field 40 - if s.schemaVersion < versionDraft07 { 41 - if s.cfg.Strict { 42 - s.warnf(n.Pos(), "use of $id not allowed in older schema version %v", s.schemaVersion) 43 - } 44 - return 45 - } 46 - } 47 - 48 29 // Resolution must be relative to parent $id 49 30 // https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2 50 31 u := s.resolveURI(n) ··· 62 43 s.idPos = n.Pos() 63 44 } 64 45 46 + // constraintSchema implements $schema, which 47 + // identifies this as a JSON schema and specifies its version. 65 48 func constraintSchema(key string, n cue.Value, s *state) { 66 - // Identifies this as a JSON schema and specifies its version. 67 - // TODO: extract version. 68 - 69 - s.schemaVersion = versionDraft07 // Reasonable default version. 70 49 str, ok := s.strValue(n) 71 50 if !ok { 72 51 // If there's no $schema value, use the default.
+14 -4
encoding/jsonschema/decode.go
··· 105 105 } 106 106 107 107 func (d *decoder) schema(ref []ast.Label, v cue.Value) (a []ast.Decl) { 108 - root := state{decoder: d} 108 + root := state{ 109 + decoder: d, 110 + schemaVersion: defaultVersion, 111 + } 109 112 110 113 var name ast.Label 111 114 inner := len(ref) - 1 ··· 636 639 } 637 640 638 641 // do multiple passes over the constraints to ensure they are done in order. 639 - for pass := 0; pass < 4; pass++ { 642 + for pass := 0; pass < numPhases; pass++ { 640 643 state.processMap(n, func(key string, value cue.Value) { 641 644 // Convert each constraint into a either a value or a functor. 642 645 c := constraintMap[key] ··· 647 650 } 648 651 return 649 652 } 650 - if c.phase == pass { 651 - c.fn(key, value, state) 653 + if c.phase != pass { 654 + return 655 + } 656 + if !c.versions.contains(state.schemaVersion) { 657 + if s.cfg.Strict { 658 + s.warnf(value.Pos(), "constraint %q is not supported in JSON schema version %v", key, state.schemaVersion) 659 + } 660 + return 652 661 } 662 + c.fn(key, value, state) 653 663 }) 654 664 } 655 665
+1 -2
encoding/jsonschema/testdata/newid_oldversion.txtar
··· 6 6 "type": "string" 7 7 } 8 8 -- out/decode/err -- 9 - use of $id not allowed in older schema version http://json-schema.org/draft-04/schema#: 9 + constraint "$id" is not supported in JSON schema version http://json-schema.org/draft-04/schema#: 10 10 definition.json:3:3 11 - -- out/decode/cue --
+1 -1
encoding/jsonschema/testdata/oldid_newversion.txtar
··· 6 6 "type": "string" 7 7 } 8 8 -- out/decode/err -- 9 - use of old-style id field not allowed in schema version http://json-schema.org/draft-07/schema#: 9 + constraint "id" is not supported in JSON schema version http://json-schema.org/draft-07/schema#: 10 10 definition.json:3:3
+33
encoding/jsonschema/version.go
··· 20 20 numVersions // unknown 21 21 ) 22 22 23 + const defaultVersion = versionDraft07 24 + 25 + type versionSet int 26 + 27 + const allVersions = versionSet(1<<numVersions-1) &^ (1 << versionUnknown) 28 + 29 + // contains reports whether m contains the version v. 30 + func (m versionSet) contains(v schemaVersion) bool { 31 + return (m & vset(v)) != 0 32 + } 33 + 34 + // vset returns the version set containing exactly v. 35 + func vset(v schemaVersion) versionSet { 36 + return 1 << v 37 + } 38 + 39 + // vfrom returns the set of all versions starting at v. 40 + func vfrom(v schemaVersion) versionSet { 41 + return allVersions &^ (vset(v) - 1) 42 + } 43 + 44 + // vbetween returns the set of all versions between 45 + // v0 and v1 inclusive. 46 + func vbetween(v0, v1 schemaVersion) versionSet { 47 + return vfrom(v0) & vto(v1) 48 + } 49 + 50 + // vto returns the set of all versions up to 51 + // and including v. 52 + func vto(v schemaVersion) versionSet { 53 + return allVersions & (vset(v+1) - 1) 54 + } 55 + 23 56 func parseSchemaVersion(sv string) (schemaVersion, error) { 24 57 // If this linear search is ever a performance issue, we could 25 58 // build a map, but it doesn't seem worthwhile for now.
+34
encoding/jsonschema/version_test.go
··· 1 + package jsonschema 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/go-quicktest/qt" 7 + ) 8 + 9 + func TestVFrom(t *testing.T) { 10 + qt.Assert(t, qt.IsTrue(vfrom(versionDraft04).contains(versionDraft04))) 11 + qt.Assert(t, qt.IsTrue(vfrom(versionDraft04).contains(versionDraft06))) 12 + qt.Assert(t, qt.IsTrue(vfrom(versionDraft04).contains(version2020_12))) 13 + qt.Assert(t, qt.IsFalse(vfrom(versionDraft06).contains(versionDraft04))) 14 + } 15 + 16 + func TestVTo(t *testing.T) { 17 + qt.Assert(t, qt.IsTrue(vto(versionDraft04).contains(versionDraft04))) 18 + qt.Assert(t, qt.IsFalse(vto(versionDraft04).contains(versionDraft06))) 19 + qt.Assert(t, qt.IsTrue(vto(versionDraft06).contains(versionDraft04))) 20 + qt.Assert(t, qt.IsFalse(vto(versionDraft06).contains(versionDraft07))) 21 + } 22 + 23 + func TestVBetween(t *testing.T) { 24 + qt.Assert(t, qt.IsFalse(vbetween(versionDraft06, version2019_09).contains(versionDraft04))) 25 + qt.Assert(t, qt.IsTrue(vbetween(versionDraft06, version2019_09).contains(versionDraft06))) 26 + qt.Assert(t, qt.IsTrue(vbetween(versionDraft06, version2019_09).contains(version2019_09))) 27 + qt.Assert(t, qt.IsFalse(vbetween(versionDraft06, version2019_09).contains(version2020_12))) 28 + } 29 + 30 + func TestVSet(t *testing.T) { 31 + qt.Assert(t, qt.IsTrue(vset(versionDraft06).contains(versionDraft06))) 32 + qt.Assert(t, qt.IsFalse(vset(versionDraft06).contains(versionDraft04))) 33 + qt.Assert(t, qt.IsFalse(vset(versionDraft06).contains(versionDraft07))) 34 + }