Skip to content

Commit

Permalink
encoding/jsonschema: export Version
Browse files Browse the repository at this point in the history
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 <rogpeppe@gmail.com>
Change-Id: I442e7eb4d8f26c2709458f7a733180acfc804c97
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1200162
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
  • Loading branch information
rogpeppe committed Aug 29, 2024
1 parent df2c869 commit de41586
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 82 deletions.
22 changes: 11 additions & 11 deletions encoding/jsonschema/constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion encoding/jsonschema/constraints_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions encoding/jsonschema/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -379,7 +379,7 @@ type state struct {
minContains *uint64
maxContains *uint64

schemaVersion schemaVersion
schemaVersion Version
schemaVersionPresent bool

id *url.URL // base URI for $ref
Expand Down Expand Up @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions encoding/jsonschema/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
21 changes: 18 additions & 3 deletions encoding/jsonschema/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
}
29 changes: 0 additions & 29 deletions encoding/jsonschema/schemaversion_string.go

This file was deleted.

11 changes: 11 additions & 0 deletions encoding/jsonschema/testdata/txtar/defaultversion.txtar
Original file line number Diff line number Diff line change
@@ -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
37 changes: 18 additions & 19 deletions encoding/jsonschema/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<<numVersions-1) &^ (1 << versionUnknown)
const allVersions = versionSet(1<<numVersions-1) &^ (1 << VersionUnknown)

// contains reports whether m contains the version v.
func (m versionSet) contains(v schemaVersion) bool {
func (m versionSet) contains(v Version) bool {
return (m & vset(v)) != 0
}

// vset returns the version set containing exactly v.
func vset(v schemaVersion) versionSet {
func vset(v Version) versionSet {
return 1 << v
}

// vfrom returns the set of all versions starting at v.
func vfrom(v schemaVersion) versionSet {
func vfrom(v Version) versionSet {
return allVersions &^ (vset(v) - 1)
}

// vbetween returns the set of all versions between
// v0 and v1 inclusive.
func vbetween(v0, v1 schemaVersion) versionSet {
func vbetween(v0, v1 Version) versionSet {
return vfrom(v0) & vto(v1)
}

// vto returns the set of all versions up to
// and including v.
func vto(v schemaVersion) versionSet {
func vto(v Version) versionSet {
return allVersions & (vset(v+1) - 1)
}

func parseSchemaVersion(sv string) (schemaVersion, error) {
// ParseVersion parses a version URI that defines a JSON Schema version.
func ParseVersion(sv string) (Version, error) {
// If this linear search is ever a performance issue, we could
// build a map, but it doesn't seem worthwhile for now.
for i := schemaVersion(1); i < numVersions; i++ {
for i := Version(1); i < numVersions; i++ {
if sv == i.String() {
return i, nil
}
Expand Down
29 changes: 29 additions & 0 deletions encoding/jsonschema/version_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 15 additions & 15 deletions encoding/jsonschema/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}

0 comments on commit de41586

Please sign in to comment.