diff --git a/encoding/jsonschema/decode_test.go b/encoding/jsonschema/decode_test.go index 16f2f9fd0d9..977cad6c7fd 100644 --- a/encoding/jsonschema/decode_test.go +++ b/encoding/jsonschema/decode_test.go @@ -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)) @@ -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:])) } @@ -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), ` diff --git a/encoding/jsonschema/jsonschema.go b/encoding/jsonschema/jsonschema.go index a286e7413a2..653bb1b96f4 100644 --- a/encoding/jsonschema/jsonschema.go +++ b/encoding/jsonschema/jsonschema.go @@ -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. diff --git a/encoding/jsonschema/ref.go b/encoding/jsonschema/ref.go index 98b3d7a0058..e6840317c06 100644 --- a/encoding/jsonschema/ref.go +++ b/encoding/jsonschema/ref.go @@ -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 { @@ -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. @@ -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. @@ -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. @@ -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 @@ -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) { @@ -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 }