From 9fd30573ba8b616e6ea958ea60e731ea6f44f584 Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Wed, 28 Aug 2024 15:39:29 +0100 Subject: [PATCH] encoding/jsonschema: export Version When running the external tests, we want to be able to control which schema version is used even when there is no `$schema` field present, and this functionality is likely to be useful anyway, so export the `Version` type and provide a way to set the default version in the config. Also align the names more closely with the names used in the external JSON Schema test suite, as those are likely to be more conventional. Signed-off-by: Roger Peppe Change-Id: I442e7eb4d8f26c2709458f7a733180acfc804c97 Dispatch-Trailer: {"type":"trybot","CL":1200162,"patchset":4,"ref":"refs/changes/62/1200162/4","targetBranch":"master"} --- encoding/jsonschema/constraints.go | 22 +++++------ encoding/jsonschema/constraints_meta.go | 2 +- encoding/jsonschema/decode.go | 8 ++-- encoding/jsonschema/decode_test.go | 5 +++ encoding/jsonschema/jsonschema.go | 21 +++++++++-- encoding/jsonschema/schemaversion_string.go | 29 --------------- .../testdata/txtar/defaultversion.txtar | 11 ++++++ encoding/jsonschema/version.go | 37 +++++++++---------- encoding/jsonschema/version_string.go | 29 +++++++++++++++ encoding/jsonschema/version_test.go | 30 +++++++-------- 10 files changed, 112 insertions(+), 82 deletions(-) delete mode 100644 encoding/jsonschema/schemaversion_string.go create mode 100644 encoding/jsonschema/testdata/txtar/defaultversion.txtar create mode 100644 encoding/jsonschema/version_string.go diff --git a/encoding/jsonschema/constraints.go b/encoding/jsonschema/constraints.go index 3f8e14bf2..b5446b71a 100644 --- a/encoding/jsonschema/constraints.go +++ b/encoding/jsonschema/constraints.go @@ -54,31 +54,31 @@ func init() { const numPhases = 5 var constraints = []*constraint{ - p2d("$comment", constraintComment, vfrom(versionDraft07)), + p2d("$comment", constraintComment, vfrom(VersionDraft7)), p2("$defs", constraintAddDefinitions), - p1d("$id", constraintID, vfrom(versionDraft06)), + p1d("$id", constraintID, vfrom(VersionDraft6)), p0("$schema", constraintSchema), p2("$ref", constraintRef), p2("additionalItems", constraintAdditionalItems), p4("additionalProperties", constraintAdditionalProperties), p3("allOf", constraintAllOf), p3("anyOf", constraintAnyOf), - p2d("const", constraintConst, vfrom(versionDraft06)), - p1d("minContains", constraintMinContains, vfrom(version2019_09)), - p1d("maxContains", constraintMaxContains, vfrom(version2019_09)), - p2d("contains", constraintContains, vfrom(versionDraft06)), - p2d("contentEncoding", constraintContentEncoding, vfrom(versionDraft07)), - p2d("contentMediaType", constraintContentMediaType, vfrom(versionDraft07)), + p2d("const", constraintConst, vfrom(VersionDraft6)), + p1d("minContains", constraintMinContains, vfrom(VersionDraft2019_09)), + p1d("maxContains", constraintMaxContains, vfrom(VersionDraft2019_09)), + p2d("contains", constraintContains, vfrom(VersionDraft6)), + p2d("contentEncoding", constraintContentEncoding, vfrom(VersionDraft7)), + p2d("contentMediaType", constraintContentMediaType, vfrom(VersionDraft7)), p2("default", constraintDefault), p2("definitions", constraintAddDefinitions), p2("dependencies", constraintDependencies), p2("deprecated", constraintDeprecated), p2("description", constraintDescription), p2("enum", constraintEnum), - p2d("examples", constraintExamples, vfrom(versionDraft06)), + p2d("examples", constraintExamples, vfrom(VersionDraft6)), p2("exclusiveMaximum", constraintExclusiveMaximum), p2("exclusiveMinimum", constraintExclusiveMinimum), - p1d("id", constraintID, vto(versionDraft04)), + p1d("id", constraintID, vto(VersionDraft4)), p2("items", constraintItems), p2("minItems", constraintMinItems), p2("maxItems", constraintMaxItems), @@ -93,7 +93,7 @@ var constraints = []*constraint{ p2("pattern", constraintPattern), p3("patternProperties", constraintPatternProperties), p2("properties", constraintProperties), - p2d("propertyNames", constraintPropertyNames, vfrom(versionDraft06)), + p2d("propertyNames", constraintPropertyNames, vfrom(VersionDraft6)), p3("required", constraintRequired), p2("title", constraintTitle), p2("type", constraintType), diff --git a/encoding/jsonschema/constraints_meta.go b/encoding/jsonschema/constraints_meta.go index 149ea4103..811f07bfd 100644 --- a/encoding/jsonschema/constraints_meta.go +++ b/encoding/jsonschema/constraints_meta.go @@ -51,7 +51,7 @@ func constraintSchema(key string, n cue.Value, s *state) { // If there's no $schema value, use the default. return } - sv, err := parseSchemaVersion(str) + sv, err := ParseVersion(str) if err != nil { s.errf(n, "invalid $schema URL %q: %v", str, err) return diff --git a/encoding/jsonschema/decode.go b/encoding/jsonschema/decode.go index d52ea1286..1469df570 100644 --- a/encoding/jsonschema/decode.go +++ b/encoding/jsonschema/decode.go @@ -111,7 +111,7 @@ func (d *decoder) decode(v cue.Value) *ast.File { func (d *decoder) schema(ref []ast.Label, v cue.Value) (a []ast.Decl) { root := state{ decoder: d, - schemaVersion: defaultVersion, + schemaVersion: d.cfg.DefaultVersion, } var name ast.Label @@ -379,7 +379,7 @@ type state struct { minContains *uint64 maxContains *uint64 - schemaVersion schemaVersion + schemaVersion Version schemaVersionPresent bool id *url.URL // base URI for $ref @@ -653,8 +653,8 @@ func (s *state) schemaState(n cue.Value, types cue.Kind, idRef []label, isLogica state.parent = s } if n.Kind() == cue.BoolKind { - if vfrom(versionDraft06).contains(state.schemaVersion) { - // From draft-06 onwards, boolean values signify a schema that always passes or fails. + if vfrom(VersionDraft6).contains(state.schemaVersion) { + // From draft6 onwards, boolean values signify a schema that always passes or fails. if state.boolValue(n) { return top(), state } diff --git a/encoding/jsonschema/decode_test.go b/encoding/jsonschema/decode_test.go index 9f64bcd53..30788ed59 100644 --- a/encoding/jsonschema/decode_test.go +++ b/encoding/jsonschema/decode_test.go @@ -75,6 +75,11 @@ func TestDecode(t *testing.T) { return []ast.Label{ast.NewIdent("#" + a[len(a)-1])}, nil } } + if versStr, ok := t.Value("version"); ok { + vers, err := jsonschema.ParseVersion(versStr) + qt.Assert(t, qt.IsNil(err)) + cfg.DefaultVersion = vers + } cfg.Strict = t.HasTag("strict") ctx := t.CueContext() diff --git a/encoding/jsonschema/jsonschema.go b/encoding/jsonschema/jsonschema.go index e70f6e1cf..93e8bcadf 100644 --- a/encoding/jsonschema/jsonschema.go +++ b/encoding/jsonschema/jsonschema.go @@ -44,10 +44,12 @@ import ( // The generated CUE schema is guaranteed to deem valid any value that is // a valid instance of the source JSON schema. func Extract(data cue.InstanceOrValue, cfg *Config) (f *ast.File, err error) { + cfg = ref(*cfg) if cfg.MapURL == nil { - cfg1 := *cfg - cfg = &cfg1 - cfg1.MapURL = DefaultMapURL + cfg.MapURL = DefaultMapURL + } + if cfg.DefaultVersion == VersionUnknown { + cfg.DefaultVersion = VersionDraft7 } d := &decoder{ cfg: cfg, @@ -61,6 +63,10 @@ func Extract(data cue.InstanceOrValue, cfg *Config) (f *ast.File, err error) { return f, nil } +// DefaultVersion defines the default schema version used when +// there is no $schema field and no explicit [Config.DefaultVersion]. +const DefaultVersion = VersionDraft7 + // A Config configures a JSON Schema encoding or decoding. type Config struct { PkgName string @@ -100,5 +106,14 @@ type Config struct { // them. Strict bool + // DefaultVersion holds the default schema version to use + // when no $schema field is present. If it is zero, [DefaultVersion] + // will be used. + DefaultVersion Version + _ struct{} // prohibit casting from different type. } + +func ref[T any](x T) *T { + return &x +} diff --git a/encoding/jsonschema/schemaversion_string.go b/encoding/jsonschema/schemaversion_string.go deleted file mode 100644 index 17dd57bf0..000000000 --- a/encoding/jsonschema/schemaversion_string.go +++ /dev/null @@ -1,29 +0,0 @@ -// Code generated by "stringer -type=schemaVersion -linecomment"; DO NOT EDIT. - -package jsonschema - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[versionUnknown-0] - _ = x[versionDraft04-1] - _ = x[versionDraft06-2] - _ = x[versionDraft07-3] - _ = x[version2019_09-4] - _ = x[version2020_12-5] - _ = x[numVersions-6] -} - -const _schemaVersion_name = "unknownhttp://json-schema.org/draft-04/schema#http://json-schema.org/draft-06/schema#http://json-schema.org/draft-07/schema#https://json-schema.org/draft/2019-09/schemahttps://json-schema.org/draft/2020-12/schemaunknown" - -var _schemaVersion_index = [...]uint8{0, 7, 46, 85, 124, 168, 212, 219} - -func (i schemaVersion) String() string { - if i < 0 || i >= schemaVersion(len(_schemaVersion_index)-1) { - return "schemaVersion(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _schemaVersion_name[_schemaVersion_index[i]:_schemaVersion_index[i+1]] -} diff --git a/encoding/jsonschema/testdata/txtar/defaultversion.txtar b/encoding/jsonschema/testdata/txtar/defaultversion.txtar new file mode 100644 index 000000000..8a398963b --- /dev/null +++ b/encoding/jsonschema/testdata/txtar/defaultversion.txtar @@ -0,0 +1,11 @@ +#version: http://json-schema.org/draft-04/schema# +#strict +-- schema.json -- +{ + "$id": "http://example.test", + "type": "string" +} +-- out/decode/extract -- +ERROR: +constraint "$id" is not supported in JSON schema version http://json-schema.org/draft-04/schema#: + schema.json:2:3 diff --git a/encoding/jsonschema/version.go b/encoding/jsonschema/version.go index 43a0e8df6..fe8bb477f 100644 --- a/encoding/jsonschema/version.go +++ b/encoding/jsonschema/version.go @@ -4,59 +4,58 @@ import ( "fmt" ) -//go:generate go run golang.org/x/tools/cmd/stringer -type=schemaVersion -linecomment +//go:generate go run golang.org/x/tools/cmd/stringer -type=Version -linecomment -type schemaVersion int +type Version int const ( - versionUnknown schemaVersion = iota // unknown - versionDraft04 // http://json-schema.org/draft-04/schema# - // Note: draft 05 never existed and should not be used. - versionDraft06 // http://json-schema.org/draft-06/schema# - versionDraft07 // http://json-schema.org/draft-07/schema# - version2019_09 // https://json-schema.org/draft/2019-09/schema - version2020_12 // https://json-schema.org/draft/2020-12/schema + VersionUnknown Version = iota // unknown + VersionDraft4 // http://json-schema.org/draft-04/schema# + // Note: draft 5 never existed and should not be used. + VersionDraft6 // http://json-schema.org/draft-06/schema# + VersionDraft7 // http://json-schema.org/draft-07/schema# + VersionDraft2019_09 // https://json-schema.org/draft/2019-09/schema + VersionDraft2020_12 // https://json-schema.org/draft/2020-12/schema numVersions // unknown ) -const defaultVersion = versionDraft07 - type versionSet int -const allVersions = versionSet(1<= Version(len(_Version_index)-1) { + return "Version(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Version_name[_Version_index[i]:_Version_index[i+1]] +} diff --git a/encoding/jsonschema/version_test.go b/encoding/jsonschema/version_test.go index 0c2dbc46f..fe1a14362 100644 --- a/encoding/jsonschema/version_test.go +++ b/encoding/jsonschema/version_test.go @@ -7,28 +7,28 @@ import ( ) func TestVFrom(t *testing.T) { - qt.Assert(t, qt.IsTrue(vfrom(versionDraft04).contains(versionDraft04))) - qt.Assert(t, qt.IsTrue(vfrom(versionDraft04).contains(versionDraft06))) - qt.Assert(t, qt.IsTrue(vfrom(versionDraft04).contains(version2020_12))) - qt.Assert(t, qt.IsFalse(vfrom(versionDraft06).contains(versionDraft04))) + qt.Assert(t, qt.IsTrue(vfrom(VersionDraft4).contains(VersionDraft4))) + qt.Assert(t, qt.IsTrue(vfrom(VersionDraft4).contains(VersionDraft6))) + qt.Assert(t, qt.IsTrue(vfrom(VersionDraft4).contains(VersionDraft2020_12))) + qt.Assert(t, qt.IsFalse(vfrom(VersionDraft6).contains(VersionDraft4))) } func TestVTo(t *testing.T) { - qt.Assert(t, qt.IsTrue(vto(versionDraft04).contains(versionDraft04))) - qt.Assert(t, qt.IsFalse(vto(versionDraft04).contains(versionDraft06))) - qt.Assert(t, qt.IsTrue(vto(versionDraft06).contains(versionDraft04))) - qt.Assert(t, qt.IsFalse(vto(versionDraft06).contains(versionDraft07))) + qt.Assert(t, qt.IsTrue(vto(VersionDraft4).contains(VersionDraft4))) + qt.Assert(t, qt.IsFalse(vto(VersionDraft4).contains(VersionDraft6))) + qt.Assert(t, qt.IsTrue(vto(VersionDraft6).contains(VersionDraft4))) + qt.Assert(t, qt.IsFalse(vto(VersionDraft6).contains(VersionDraft7))) } func TestVBetween(t *testing.T) { - qt.Assert(t, qt.IsFalse(vbetween(versionDraft06, version2019_09).contains(versionDraft04))) - qt.Assert(t, qt.IsTrue(vbetween(versionDraft06, version2019_09).contains(versionDraft06))) - qt.Assert(t, qt.IsTrue(vbetween(versionDraft06, version2019_09).contains(version2019_09))) - qt.Assert(t, qt.IsFalse(vbetween(versionDraft06, version2019_09).contains(version2020_12))) + qt.Assert(t, qt.IsFalse(vbetween(VersionDraft6, VersionDraft2019_09).contains(VersionDraft4))) + qt.Assert(t, qt.IsTrue(vbetween(VersionDraft6, VersionDraft2019_09).contains(VersionDraft6))) + qt.Assert(t, qt.IsTrue(vbetween(VersionDraft6, VersionDraft2019_09).contains(VersionDraft2019_09))) + qt.Assert(t, qt.IsFalse(vbetween(VersionDraft6, VersionDraft2019_09).contains(VersionDraft2020_12))) } func TestVSet(t *testing.T) { - qt.Assert(t, qt.IsTrue(vset(versionDraft06).contains(versionDraft06))) - qt.Assert(t, qt.IsFalse(vset(versionDraft06).contains(versionDraft04))) - qt.Assert(t, qt.IsFalse(vset(versionDraft06).contains(versionDraft07))) + qt.Assert(t, qt.IsTrue(vset(VersionDraft6).contains(VersionDraft6))) + qt.Assert(t, qt.IsFalse(vset(VersionDraft6).contains(VersionDraft4))) + qt.Assert(t, qt.IsFalse(vset(VersionDraft6).contains(VersionDraft7))) }