diff --git a/encoding/jsonschema/constraints.go b/encoding/jsonschema/constraints.go index 1caf7534c..6cf38fa2b 100644 --- a/encoding/jsonschema/constraints.go +++ b/encoding/jsonschema/constraints.go @@ -58,101 +58,100 @@ func init() { const numPhases = 5 +// Note: OpenAPI is excluded from version sets by default, as it does not fit in +// the linear progression of the rest of the JSON Schema versions. + var constraints = []*constraint{ - todo("$anchor", vfrom(VersionDraft2019_09)), - p2d("$comment", constraintComment, vfrom(VersionDraft7)), - p2("$defs", constraintAddDefinitions), - todo("$dynamicAnchor", vfrom(VersionDraft2020_12)), - todo("$dynamicRef", vfrom(VersionDraft2020_12)), - p1d("$id", constraintID, vfrom(VersionDraft6)), - todo("$recursiveAnchor", vbetween(VersionDraft2019_09, VersionDraft2020_12)), - todo("$recursiveRef", vbetween(VersionDraft2019_09, VersionDraft2020_12)), - p2("$ref", constraintRef), - p0("$schema", constraintSchema), - todo("$vocabulary", vfrom(VersionDraft2019_09)), - p2d("additionalItems", constraintAdditionalItems, vto(VersionDraft2019_09)), - p4("additionalProperties", constraintAdditionalProperties), - p3("allOf", constraintAllOf), - p3("anyOf", constraintAnyOf), - p2d("const", constraintConst, vfrom(VersionDraft6)), - p2d("contains", constraintContains, vfrom(VersionDraft6)), - p2d("contentEncoding", constraintContentEncoding, vfrom(VersionDraft7)), - p2d("contentMediaType", constraintContentMediaType, vfrom(VersionDraft7)), - todo("contentSchema", vfrom(VersionDraft2019_09)), - p2("default", constraintDefault), - p2("definitions", constraintAddDefinitions), - p2("dependencies", constraintDependencies), - todo("dependentRequired", vfrom(VersionDraft2019_09)), - todo("dependentSchemas", vfrom(VersionDraft2019_09)), - p2("deprecated", constraintDeprecated), - p2("description", constraintDescription), - todo("else", vfrom(VersionDraft7)), - p2("enum", constraintEnum), - p2d("examples", constraintExamples, vfrom(VersionDraft6)), - p2("exclusiveMaximum", constraintExclusiveMaximum), - p2("exclusiveMinimum", constraintExclusiveMinimum), - todo("format", allVersions), - p1d("id", constraintID, vto(VersionDraft4)), - todo("if", vfrom(VersionDraft7)), - p2("items", constraintItems), - p1d("maxContains", constraintMaxContains, vfrom(VersionDraft2019_09)), - p2("maxItems", constraintMaxItems), - p2("maxLength", constraintMaxLength), - p2("maxProperties", constraintMaxProperties), - p3("maximum", constraintMaximum), - p1d("minContains", constraintMinContains, vfrom(VersionDraft2019_09)), - p2("minItems", constraintMinItems), - p2("minLength", constraintMinLength), - todo("minProperties", allVersions), - p3("minimum", constraintMinimum), - p2("multipleOf", constraintMultipleOf), - p3("not", constraintNot), - p2("nullable", constraintNullable), - p3("oneOf", constraintOneOf), - p2("pattern", constraintPattern), - p3("patternProperties", constraintPatternProperties), - todo("prefixItems", vfrom(VersionDraft2020_12)), - p2("properties", constraintProperties), - p2d("propertyNames", constraintPropertyNames, vfrom(VersionDraft6)), - todo("readOnly", vfrom(VersionDraft7)), - p3("required", constraintRequired), - todo("then", vfrom(VersionDraft7)), - p2("title", constraintTitle), - p2("type", constraintType), - todo("unevaluatedItems", vfrom(VersionDraft2019_09)), - todo("unevaluatedProperties", vfrom(VersionDraft2019_09)), - p2("uniqueItems", constraintUniqueItems), - todo("writeOnly", vfrom(VersionDraft7)), + pTODO("$anchor", vfrom(VersionDraft2019_09)), + p2("$comment", constraintComment, vfrom(VersionDraft7)), + p2("$defs", constraintAddDefinitions, allVersions), + pTODO("$dynamicAnchor", vfrom(VersionDraft2020_12)), + pTODO("$dynamicRef", vfrom(VersionDraft2020_12)), + p1("$id", constraintID, vfrom(VersionDraft6)), + pTODO("$recursiveAnchor", vbetween(VersionDraft2019_09, VersionDraft2020_12)), + pTODO("$recursiveRef", vbetween(VersionDraft2019_09, VersionDraft2020_12)), + p2("$ref", constraintRef, allVersions|openAPI), + p0("$schema", constraintSchema, allVersions), + pTODO("$vocabulary", vfrom(VersionDraft2019_09)), + p2("additionalItems", constraintAdditionalItems, vto(VersionDraft2019_09)), + p4("additionalProperties", constraintAdditionalProperties, allVersions|openAPI), + p3("allOf", constraintAllOf, allVersions|openAPI), + p3("anyOf", constraintAnyOf, allVersions|openAPI), + p2("const", constraintConst, vfrom(VersionDraft6)), + p2("contains", constraintContains, vfrom(VersionDraft6)), + p2("contentEncoding", constraintContentEncoding, vfrom(VersionDraft7)), + p2("contentMediaType", constraintContentMediaType, vfrom(VersionDraft7)), + pTODO("contentSchema", vfrom(VersionDraft2019_09)), + p2("default", constraintDefault, allVersions|openAPI), + p2("definitions", constraintAddDefinitions, allVersions), + p2("dependencies", constraintDependencies, allVersions), + pTODO("dependentRequired", vfrom(VersionDraft2019_09)), + pTODO("dependentSchemas", vfrom(VersionDraft2019_09)), + p2("deprecated", constraintDeprecated, vfrom(VersionDraft2019_09)|openAPI), + p2("description", constraintDescription, allVersions|openAPI), + pTODO("discriminator", vset(VersionOpenAPI)), + pTODO("else", vfrom(VersionDraft7)), + p2("enum", constraintEnum, allVersions|openAPI), + pTODO("example", vset(VersionOpenAPI)), + p2("examples", constraintExamples, vfrom(VersionDraft6)), + p2("exclusiveMaximum", constraintExclusiveMaximum, allVersions|openAPI), + p2("exclusiveMinimum", constraintExclusiveMinimum, allVersions|openAPI), + pTODO("externalDocs", vset(VersionOpenAPI)), + pTODO("format", allVersions|openAPI), + p1("id", constraintID, vto(VersionDraft4)&^openAPI), + pTODO("if", vfrom(VersionDraft7)), + p2("items", constraintItems, allVersions|openAPI), + p1("maxContains", constraintMaxContains, vfrom(VersionDraft2019_09)), + p2("maxItems", constraintMaxItems, allVersions|openAPI), + p2("maxLength", constraintMaxLength, allVersions|openAPI), + p2("maxProperties", constraintMaxProperties, allVersions|openAPI), + p3("maximum", constraintMaximum, allVersions|openAPI), + p1("minContains", constraintMinContains, vfrom(VersionDraft2019_09)), + p2("minItems", constraintMinItems, allVersions|openAPI), + p2("minLength", constraintMinLength, allVersions|openAPI), + pTODO("minProperties", allVersions|openAPI), + p3("minimum", constraintMinimum, allVersions|openAPI), + p2("multipleOf", constraintMultipleOf, allVersions|openAPI), + p3("not", constraintNot, allVersions|openAPI), + p2("nullable", constraintNullable, vset(VersionOpenAPI)), + p3("oneOf", constraintOneOf, allVersions|openAPI), + p2("pattern", constraintPattern, allVersions|openAPI), + p3("patternProperties", constraintPatternProperties, allVersions), + pTODO("prefixItems", vfrom(VersionDraft2020_12)), + p2("properties", constraintProperties, allVersions|openAPI), + p2("propertyNames", constraintPropertyNames, vfrom(VersionDraft6)), + pTODO("readOnly", vfrom(VersionDraft7)|openAPI), + p3("required", constraintRequired, allVersions|openAPI), + pTODO("then", vfrom(VersionDraft7)), + p2("title", constraintTitle, allVersions|openAPI), + p2("type", constraintType, allVersions|openAPI), + pTODO("unevaluatedItems", vfrom(VersionDraft2019_09)), + pTODO("unevaluatedProperties", vfrom(VersionDraft2019_09)), + p2("uniqueItems", constraintUniqueItems, allVersions|openAPI), + pTODO("writeOnly", vfrom(VersionDraft7)|openAPI), + pTODO("xml", vset(VersionOpenAPI)), } -func todo(name string, versions versionSet) *constraint { +func pTODO(name string, versions versionSet) *constraint { return &constraint{key: name, phase: 1, versions: versions, fn: constraintTODO} } -func p0(name string, f constraintFunc) *constraint { - return &constraint{key: name, phase: 0, versions: allVersions, fn: f} -} - -func p1(name string, f constraintFunc) *constraint { - return &constraint{key: name, phase: 1, versions: allVersions, fn: f} -} - -func p2(name string, f constraintFunc) *constraint { - return &constraint{key: name, phase: 2, versions: allVersions, fn: f} +func p0(name string, f constraintFunc, versions versionSet) *constraint { + return &constraint{key: name, phase: 0, versions: versions, fn: f} } -func p3(name string, f constraintFunc) *constraint { - return &constraint{key: name, phase: 3, versions: allVersions, fn: f} +func p1(name string, f constraintFunc, versions versionSet) *constraint { + return &constraint{key: name, phase: 1, versions: versions, fn: f} } -func p4(name string, f constraintFunc) *constraint { - return &constraint{key: name, phase: 4, versions: allVersions, fn: f} +func p2(name string, f constraintFunc, versions versionSet) *constraint { + return &constraint{key: name, phase: 2, versions: versions, fn: f} } -func p1d(name string, f constraintFunc, versions versionSet) *constraint { - return &constraint{key: name, phase: 1, versions: versions, fn: f} +func p3(name string, f constraintFunc, versions versionSet) *constraint { + return &constraint{key: name, phase: 3, versions: versions, fn: f} } -func p2d(name string, f constraintFunc, versions versionSet) *constraint { - return &constraint{key: name, phase: 2, versions: versions, fn: f} +func p4(name string, f constraintFunc, versions versionSet) *constraint { + return &constraint{key: name, phase: 4, versions: versions, fn: f} } diff --git a/encoding/jsonschema/decode_test.go b/encoding/jsonschema/decode_test.go index e0c641526..282041fdc 100644 --- a/encoding/jsonschema/decode_test.go +++ b/encoding/jsonschema/decode_test.go @@ -58,7 +58,9 @@ import ( // The #noverify tag in the txtar header causes verification and // instance tests to be skipped. // -// The #openapi tag in the txtar header enables OpenAPI extraction mode. +// The #version: tag selects the default schema version URI to use. +// As a special case, when this is "openapi", OpenAPI extraction +// mode is enabled. func TestDecode(t *testing.T) { test := cuetxtar.TxTarTest{ Root: "./testdata/txtar", @@ -72,17 +74,20 @@ func TestDecode(t *testing.T) { t.Skip("skipping because test is broken under the v2 evaluator") } - if t.HasTag("openapi") { - cfg.Root = "#/components/schemas/" - cfg.Map = func(p token.Pos, a []string) ([]ast.Label, error) { - // Just for testing: does not validate the path. - 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 + if versStr == "openapi" { + // OpenAPI doesn't have a JSON Schema URI so it gets a special case. + cfg.DefaultVersion = jsonschema.VersionOpenAPI + cfg.Root = "#/components/schemas/" + cfg.Map = func(p token.Pos, a []string) ([]ast.Label, error) { + // Just for testing: does not validate the path. + return []ast.Label{ast.NewIdent("#" + a[len(a)-1])}, nil + } + } else { + vers, err := jsonschema.ParseVersion(versStr) + qt.Assert(t, qt.IsNil(err)) + cfg.DefaultVersion = vers + } } cfg.Strict = t.HasTag("strict") diff --git a/encoding/jsonschema/testdata/txtar/basic.txtar b/encoding/jsonschema/testdata/txtar/basic.txtar index 0521c5fc6..3569993df 100644 --- a/encoding/jsonschema/testdata/txtar/basic.txtar +++ b/encoding/jsonschema/testdata/txtar/basic.txtar @@ -1,6 +1,6 @@ -- schema.json -- { - "$schema": "http://json-schema.org/draft-07/schema#", + "$schema": "https://json-schema.org/draft/2019-09/schema", "type": "object", "title": "Main schema", @@ -43,7 +43,7 @@ import "strings" // Main schema // // Specify who you are and all. -@jsonschema(schema="http://json-schema.org/draft-07/schema#") +@jsonschema(schema="https://json-schema.org/draft/2019-09/schema") // A person is a human being. person?: { diff --git a/encoding/jsonschema/testdata/txtar/openapi.txtar b/encoding/jsonschema/testdata/txtar/openapi.txtar index 3f1287c4a..842397d6c 100644 --- a/encoding/jsonschema/testdata/txtar/openapi.txtar +++ b/encoding/jsonschema/testdata/txtar/openapi.txtar @@ -1,4 +1,4 @@ -#openapi +#version: openapi -- schema.yaml -- components: diff --git a/encoding/jsonschema/version.go b/encoding/jsonschema/version.go index 88656adbc..075b4b4bb 100644 --- a/encoding/jsonschema/version.go +++ b/encoding/jsonschema/version.go @@ -31,12 +31,22 @@ const ( VersionDraft2019_09 // https://json-schema.org/draft/2019-09/schema VersionDraft2020_12 // https://json-schema.org/draft/2020-12/schema + // Note: OpenAPI stands alone: it's not in the regular JSON Schema lineage. + VersionOpenAPI // OpenAPI 3.0 + numVersions // unknown ) +var ( + openAPI = vset(VersionOpenAPI) + notOpenAPI = allVersions &^ vset(VersionOpenAPI) +) + type versionSet int -const allVersions = versionSet(1<= Version(len(_Version_index)-1) { diff --git a/encoding/openapi/decode.go b/encoding/openapi/decode.go index d60e82d68..1c6f60bfa 100644 --- a/encoding/openapi/decode.go +++ b/encoding/openapi/decode.go @@ -15,6 +15,7 @@ package openapi import ( + "fmt" "strings" "cuelang.org/go/cue" @@ -41,15 +42,24 @@ func Extract(data cue.InstanceOrValue, c *Config) (*ast.File, error) { } } - js, err := jsonschema.Extract(data, &jsonschema.Config{ - Root: oapiSchemas, - Map: openAPIMapping, - }) + v := data.Value() + versionValue := v.LookupPath(cue.MakePath(cue.Str("openapi"))) + if versionValue.Err() != nil { + return nil, fmt.Errorf("openapi field is required but not found") + } + version, err := versionValue.String() if err != nil { - return nil, err + return nil, fmt.Errorf("invalid openapi field (must be string): %v", err) + } + var schemaVersion jsonschema.Version + switch { + case strings.HasPrefix(version, "3.0."): + schemaVersion = jsonschema.VersionOpenAPI + case strings.HasPrefix(version, "3.1."): + schemaVersion = jsonschema.VersionDraft2020_12 + default: + return nil, fmt.Errorf("unknown OpenAPI version %q", version) } - - v := data.Value() doc, _ := v.LookupPath(cue.MakePath(cue.Str("info"), cue.Str("title"))).String() // Required if s, _ := v.LookupPath(cue.MakePath(cue.Str("info"), cue.Str("description"))).String(); s != "" { @@ -65,6 +75,14 @@ func Extract(data cue.InstanceOrValue, c *Config) (*ast.File, error) { add(cg) } + js, err := jsonschema.Extract(data, &jsonschema.Config{ + Root: oapiSchemas, + Map: openAPIMapping, + DefaultVersion: schemaVersion, + }) + if err != nil { + return nil, err + } preamble := js.Preamble() body := js.Decls[len(preamble):] for _, d := range preamble {