Skip to content

Commit cdc4123

Browse files
committed
encoding/jsonschema: support single schema not at root
Currently it's possible to ask encoding/jsonschema to decode a set of named schemas not at the root of the data document passed in by using `Config.Root`. However, it's not possible to ask for a single schema not at the root. Since that schema might refer to other schemas within the same JSON data, it's not quite as simple as extracting the sub-schema and then passing it to encoding/jsonschema. Support this mode by adding `Config.SingleRoot` to enable it. This will help us to translate Kubernetes CRD files. Also in passing make `jsonschema.Config.Root` work with integer indexes, as the schema in CRD files is inside an array. There's still an ambiguity but it seems much less likely that there will be named roots inside integerlike string fields than inside arrays. For #2691 Signed-off-by: Roger Peppe <[email protected]> Change-Id: I0870632d7261f37a460a161b722af64539534314 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1209501 TryBot-Result: CUEcueckoo <[email protected]> Unity-Result: CUE porcuepine <[email protected]> Reviewed-by: Daniel Martí <[email protected]>
1 parent 7cc1de3 commit cdc4123

File tree

6 files changed

+75
-13
lines changed

6 files changed

+75
-13
lines changed

encoding/jsonschema/decode.go

+21-7
Original file line numberDiff line numberDiff line change
@@ -122,19 +122,33 @@ func (d *decoder) addImport(n cue.Value, pkg string) *ast.Ident {
122122

123123
func (d *decoder) decode(v cue.Value) *ast.File {
124124
var defsRoot cue.Value
125+
// docRoot represents the root of the actual data, by contrast
126+
// with the "root" value as specified in [Config.Root] which
127+
// represents the root of the schemas to be decoded.
128+
docRoot := v
125129
if d.cfg.Root != "" {
126-
defsPath, err := parseRootRef(d.cfg.Root)
130+
rootPath, err := parseRootRef(d.cfg.Root)
127131
if err != nil {
128132
d.errf(cue.Value{}, "invalid Config.Root value %q: %v", d.cfg.Root, err)
129133
return nil
130134
}
131-
defsRoot = v.LookupPath(defsPath)
132-
if !defsRoot.Exists() && d.cfg.AllowNonExistentRoot {
133-
defsRoot = v.Context().CompileString("{}")
134-
} else if defsRoot.Kind() != cue.StructKind {
135-
d.errf(defsRoot, "value at path %v must be struct containing definitions but is actually %v", d.cfg.Root, defsRoot)
135+
root := v.LookupPath(rootPath)
136+
if !root.Exists() && !d.cfg.AllowNonExistentRoot {
137+
d.errf(v, "root value at path %v does not exist", d.cfg.Root)
136138
return nil
137139
}
140+
if d.cfg.SingleRoot {
141+
v = root
142+
} else {
143+
if !root.Exists() {
144+
root = v.Context().CompileString("{}")
145+
}
146+
if root.Kind() != cue.StructKind {
147+
d.errf(root, "value at path %v must be struct containing definitions but is actually %v", d.cfg.Root, root)
148+
return nil
149+
}
150+
defsRoot = root
151+
}
138152
}
139153

140154
var rootInfo schemaInfo
@@ -160,7 +174,7 @@ func (d *decoder) decode(v cue.Value) *ast.File {
160174
id: d.rootID,
161175
},
162176
isRoot: true,
163-
pos: v,
177+
pos: docRoot,
164178
}
165179

166180
if defsRoot.Exists() {

encoding/jsonschema/decode_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,14 @@ func TestDecode(t *testing.T) {
9898
cfg.DefaultVersion = vers
9999
}
100100
}
101+
if root, ok := t.Value("root"); ok {
102+
cfg.Root = root
103+
}
101104
cfg.Strict = t.HasTag("strict")
102105
cfg.StrictKeywords = cfg.StrictKeywords || t.HasTag("strictKeywords")
103106
cfg.AllowNonExistentRoot = t.HasTag("allowNonExistentRoot")
104107
cfg.StrictFeatures = t.HasTag("strictFeatures")
108+
cfg.SingleRoot = t.HasTag("singleRoot")
105109
cfg.PkgName, _ = t.Value("pkgName")
106110

107111
ctx := t.CueContext()

encoding/jsonschema/jsonschema.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ type Config struct {
113113
// JSON reference of location containing schemas. The empty string indicates
114114
// that there is a single schema at the root. If this is non-empty,
115115
// the referred-to location should be an object, and each member
116-
// is taken to be a schema.
116+
// is taken to be a schema (by default: see [Config.SingleRoot])
117117
//
118118
// Examples:
119119
// "#/" or "#" top-level fields are schemas.
@@ -124,6 +124,11 @@ type Config struct {
124124
// only. Just `#` is preferred.
125125
Root string
126126

127+
// SingleRoot is consulted only when Root is non-empty.
128+
// If Root is non-empty and SingleRoot is true, then
129+
// Root should specify the location of a single schema to extract.
130+
SingleRoot bool
131+
127132
// AllowNonExistentRoot prevents an error when there is no value at
128133
// the above Root path. Such an error can be useful to signal that
129134
// the data may not be a JSON Schema, but is not always a good idea.

encoding/jsonschema/ref.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,14 @@ func parseRootRef(str string) (cue.Path, error) {
4848
fragmentParts := slices.Collect(jsonPointerTokens(u.Fragment))
4949
var selectors []cue.Selector
5050
for _, r := range fragmentParts {
51-
// Technically this is incorrect because a numeric
52-
// element could also index into a list, but the
53-
// resulting CUE path will not allow that.
54-
selectors = append(selectors, cue.Str(r))
51+
if i, err := strconv.ParseUint(r, 10, 64); err == nil && strconv.FormatUint(i, 10) == r {
52+
// Technically this is incorrect because a numeric element
53+
// could also be a string selector and the resulting path
54+
// will not allow that.
55+
selectors = append(selectors, cue.Index(int64(i)))
56+
} else {
57+
selectors = append(selectors, cue.Str(r))
58+
}
5559
}
5660
return cue.MakePath(selectors...), nil
5761
}

encoding/jsonschema/testdata/txtar/openapi_nonexistent_error.txtar

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ Test what happens when there's an OpenAPI schema that has no
66
-- schema.yaml --
77
-- out/decode/extract --
88
ERROR:
9-
value at path #/components/schemas/ must be struct containing definitions but is actually _|_ // field not found: components
9+
root value at path #/components/schemas/ does not exist
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#root: #/spec/versions/0/schema/openAPIV3Schema
2+
#singleRoot
3+
4+
-- schema.yaml --
5+
---
6+
apiVersion: apiextensions.k8s.io/v1
7+
kind: CustomResourceDefinition
8+
spec:
9+
group: example.io
10+
scope: Namespaced
11+
versions:
12+
- name: v2
13+
schema:
14+
openAPIV3Schema:
15+
description: Something about a CRD.
16+
type: object
17+
properties:
18+
foo:
19+
description: description of foo
20+
type: string
21+
bar:
22+
description: description of bar
23+
allOf:
24+
- $ref: '#/spec/versions/0/schema/openAPIV3Schema/properties/foo'
25+
-- out/decode/extract --
26+
// Something about a CRD.
27+
// description of foo
28+
foo?: _#defs."/spec/versions/0/schema/openAPIV3Schema/properties/foo"
29+
30+
// description of bar
31+
bar?: _#defs."/spec/versions/0/schema/openAPIV3Schema/properties/foo"
32+
33+
// description of foo
34+
_#defs: "/spec/versions/0/schema/openAPIV3Schema/properties/foo": string
35+
...

0 commit comments

Comments
 (0)