Skip to content

Commit

Permalink
encoding/jsonschema: allow mapping URLs to values within a package
Browse files Browse the repository at this point in the history
A JSON Schema value might not be situated at the root of a CUE package.
This change allows `MapURL` to return not only the import path of
package in which a JSON Schema URI can be found in CUE, but also a
reference within that package.

This is technically a backwardly incompatible change to the `MapURL`
signature, but as this is a relatively new part of the API (added in
v0.10.0) and is unlikely to have many importers use it, I think that's
probably OK.

Signed-off-by: Roger Peppe <rogpeppe@gmail.com>
Change-Id: I3e44b2623fe3c5778afd1e6d2ad50948395a0145
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1201910
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
  • Loading branch information
rogpeppe committed Sep 30, 2024
1 parent 19eea7f commit 5cc6175
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 15 deletions.
10 changes: 5 additions & 5 deletions encoding/jsonschema/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,9 @@ properties: x: $ref: "https://something.test/foo#/definitions/blah"
`)
var calls []string
expr, err := jsonschema.Extract(v, &jsonschema.Config{
MapURL: func(u *url.URL) (string, error) {
MapURL: func(u *url.URL) (string, cue.Path, error) {
calls = append(calls, u.String())
return "other.test/something:blah", nil
return "other.test/something:blah", cue.ParsePath("#Foo.bar"), nil
},
})
qt.Assert(t, qt.IsNil(err))
Expand All @@ -230,7 +230,7 @@ properties: x: $ref: "https://something.test/foo#/definitions/blah"
qt.Assert(t, qt.Equals(string(b), `
import "other.test/something:blah"
x?: blah.#blah
x?: blah.#Foo.bar.#blah
...
`[1:]))
}
Expand All @@ -244,8 +244,8 @@ properties: {
}
`, cue.Filename("foo.cue"))
_, err := jsonschema.Extract(v, &jsonschema.Config{
MapURL: func(u *url.URL) (string, error) {
return "", fmt.Errorf("some error")
MapURL: func(u *url.URL) (string, cue.Path, error) {
return "", cue.Path{}, fmt.Errorf("some error")
},
})
qt.Assert(t, qt.Equals(errors.Details(err, nil), `
Expand Down
4 changes: 2 additions & 2 deletions encoding/jsonschema/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ type Config struct {
Map func(pos token.Pos, path []string) ([]ast.Label, error)

// MapURL maps a URL reference as found in $ref to
// an import path for a package.
// an import path for a package and a path within that package.
// If this is nil, [DefaultMapURL] will be used.
MapURL func(u *url.URL) (importPath string, err error)
MapURL func(u *url.URL) (importPath string, path cue.Path, err error)

// TODO: configurability to make it compatible with OpenAPI, such as
// - locations of definitions: #/components/schemas, for instance.
Expand Down
49 changes: 41 additions & 8 deletions encoding/jsonschema/ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func (s *state) makeCUERef(n cue.Value, u *url.URL, fragmentParts []string) (_e
return sel
}

var ident *ast.Ident
var refExpr ast.Expr

for ; ; s = s.up {
if s.up == nil {
Expand All @@ -138,7 +138,7 @@ func (s *state) makeCUERef(n cue.Value, u *url.URL, fragmentParts []string) (_e
return s.rootRef()
}

ident, fragmentParts = s.getNextIdent(n.Pos(), fragmentParts)
refExpr, fragmentParts = s.getNextIdent(n.Pos(), fragmentParts)

case u.Host != "":
// Reference not found within scope. Create an import reference.
Expand All @@ -147,7 +147,7 @@ func (s *state) makeCUERef(n cue.Value, u *url.URL, fragmentParts []string) (_e
// referenced. We could consider doing an extra pass to record
// all '$id's in a file to be able to link to them even if they
// are not in scope.
importPath, err := s.cfg.MapURL(u)
importPath, refPath, err := s.cfg.MapURL(u)
if err != nil {
ustr := u.String()
// Avoid producing many errors for the same URL.
Expand All @@ -162,8 +162,13 @@ func (s *state) makeCUERef(n cue.Value, u *url.URL, fragmentParts []string) (_e
s.errf(n, "cannot determine package name from import path %q", importPath)
return nil
}
ident = ast.NewIdent(ip.Qualifier)
ident := ast.NewIdent(ip.Qualifier)
ident.Node = &ast.ImportSpec{Path: ast.NewString(importPath)}
refExpr, err = pathRefSyntax(refPath, ident)
if err != nil {
s.errf(n, "invalid CUE path for URL %q: %v", u, err)
return nil
}

default:
// Just a path, not sure what that means.
Expand Down Expand Up @@ -206,13 +211,14 @@ func (s *state) makeCUERef(n cue.Value, u *url.URL, fragmentParts []string) (_e
}
return newSel(e, s.idRef[1])
}
ident, fragmentParts = s.getNextIdent(n.Pos(), fragmentParts)
ident, fragmentParts0 := s.getNextIdent(n.Pos(), fragmentParts)
ident.Node = s.obj
refExpr, fragmentParts = ident, fragmentParts0
break
}
}

return s.newSel(n.Pos(), ident, fragmentParts)
return s.newSel(n.Pos(), refExpr, fragmentParts)
}

// getNextSelector translates a JSON Reference path into a CUE path by consuming
Expand Down Expand Up @@ -424,7 +430,7 @@ func jsonSchemaRef(p token.Pos, a []string) ([]ast.Label, error) {
// path mapping. It trims off any ".json" suffix and uses the
// package name "schema" if the final component of the path
// isn't a valid CUE identifier.
func DefaultMapURL(u *url.URL) (importPath string, err error) {
func DefaultMapURL(u *url.URL) (string, cue.Path, error) {
p := u.Path
base := path.Base(p)
if !ast.IsValidIdent(base) {
Expand All @@ -436,5 +442,32 @@ func DefaultMapURL(u *url.URL) (importPath string, err error) {
}
p += ":" + base
}
return u.Host + p, nil
return u.Host + p, cue.Path{}, nil
}

// pathRefSyntax returns the syntax for an expression which
// looks up the path inside the given root expression's value.
// It returns an error if the path contains any elements with
// type [cue.OptionalConstraint], [cue.RequiredConstraint], or [cue.PatternConstraint],
// none of which are expressible as a CUE index expression.
//
// TODO implement this properly and move to a method on [cue.Path].
func pathRefSyntax(cuePath cue.Path, root ast.Expr) (ast.Expr, error) {
expr := root
for _, sel := range cuePath.Selectors() {
switch sel.LabelType() {
case cue.StringLabel, cue.DefinitionLabel:
ident := sel.String()
if !ast.IsValidIdent(ident) {
return nil, fmt.Errorf("cannot form expression for path %q", cuePath)
}
expr = &ast.SelectorExpr{
X: expr,
Sel: ast.NewIdent(sel.String()),
}
default:
return nil, fmt.Errorf("cannot form expression for path %q", cuePath)
}
}
return expr, nil
}

0 comments on commit 5cc6175

Please sign in to comment.