From 30d044f1d24d8196572bdb175770dfa8873077ec Mon Sep 17 00:00:00 2001 From: Claudio Beatrice Date: Wed, 4 Oct 2023 13:12:23 +0200 Subject: [PATCH] feat: add support for file:// prefix in refs --- pkg/generator/generate.go | 81 +++++++++++-------- pkg/schemas/reference.go | 45 +++++++++++ tests/data/core/refExternalFileWithScheme.go | 25 ++++++ .../data/core/refExternalFileWithScheme.json | 13 +++ 4 files changed, 131 insertions(+), 33 deletions(-) create mode 100644 pkg/schemas/reference.go create mode 100644 tests/data/core/refExternalFileWithScheme.go create mode 100644 tests/data/core/refExternalFileWithScheme.json diff --git a/pkg/generator/generate.go b/pkg/generator/generate.go index f3ae082..7bc78c5 100644 --- a/pkg/generator/generate.go +++ b/pkg/generator/generate.go @@ -60,6 +60,7 @@ var ( errUnsupportedRefFormat = errors.New("unsupported $ref format") errConflictSameFile = errors.New("conflict: same file") errDefinitionDoesNotExistInSchema = errors.New("definition does not exist in schema") + errCannotGenerateReferencedType = errors.New("cannot generate referenced type") ) func New(config Config) (*Generator, error) { @@ -273,13 +274,27 @@ func (g *schemaGenerator) generateRootType() error { } func (g *schemaGenerator) generateReferencedType(ref string) (codegen.Type, error) { - var fileName, scope, defName string - if i := strings.IndexRune(ref, '#'); i == -1 { - fileName = ref - } else { - fileName, scope = ref[0:i], ref[i+1:] + refType, err := schemas.GetRefType(ref) + if err != nil { + return nil, fmt.Errorf("%w: %w", errCannotGenerateReferencedType, err) + } + + if refType != schemas.RefTypeFile { + return nil, fmt.Errorf("%w: %w '%s'", errCannotGenerateReferencedType, errUnsupportedRefFormat, ref) + } + + ref = strings.TrimPrefix(ref, "file://") + + fileName := ref + + var scope, defName string + + if i := strings.IndexRune(ref, '#'); i != -1 { var prefix string + + fileName, scope = ref[0:i], ref[i+1:] lowercaseScope := strings.ToLower(scope) + for _, currentPrefix := range []string{ "/$defs/", // Draft-handrews-json-schema-validation-02. "/definitions/", // Legacy. @@ -292,28 +307,46 @@ func (g *schemaGenerator) generateReferencedType(ref string) (codegen.Type, erro } if len(prefix) == 0 { - return nil, fmt.Errorf("%w; must point to definition within file: %q", errUnsupportedRefFormat, ref) + return nil, fmt.Errorf( + "%w: value must point to definition within file: '%s'", + errCannotGenerateReferencedType, + ref, + ) } + defName = scope[len(prefix):] } schema := g.schema + sg := g if fileName != "" { - var err error + var serr error - schema, err = g.fileLoader.Load(fileName, g.schemaFileName) - if err != nil { - return nil, fmt.Errorf("could not follow $ref %q to file %q: %w", ref, fileName, err) + schema, serr = g.fileLoader.Load(fileName, g.schemaFileName) + if serr != nil { + return nil, fmt.Errorf("could not follow $ref %q to file %q: %w", ref, fileName, serr) } - qualified, err := schemas.QualifiedFileName(fileName, g.schemaFileName, g.config.ResolveExtensions) - if err != nil { - return nil, fmt.Errorf("could not resolve qualified file name for %s: %w", fileName, err) + qualified, qerr := schemas.QualifiedFileName(fileName, g.schemaFileName, g.config.ResolveExtensions) + if qerr != nil { + return nil, fmt.Errorf("could not resolve qualified file name for %s: %w", fileName, qerr) } - if err = g.addFile(qualified, schema); err != nil { - return nil, err + if ferr := g.addFile(qualified, schema); ferr != nil { + return nil, ferr + } + + output, oerr := g.findOutputFileForSchemaID(schema.ID) + if oerr != nil { + return nil, oerr + } + + sg = &schemaGenerator{ + Generator: g.Generator, + schema: schema, + schemaFileName: fileName, + output: output, } } @@ -355,24 +388,6 @@ func (g *schemaGenerator) generateReferencedType(ref string) (codegen.Type, erro }() } - var sg *schemaGenerator - - if fileName != "" { - output, err := g.findOutputFileForSchemaID(schema.ID) - if err != nil { - return nil, err - } - - sg = &schemaGenerator{ - Generator: g.Generator, - schema: schema, - schemaFileName: fileName, - output: output, - } - } else { - sg = g - } - t, err := sg.generateDeclaredType(def, newNameScope(defName)) if err != nil { return nil, err diff --git a/pkg/schemas/reference.go b/pkg/schemas/reference.go new file mode 100644 index 0000000..f096fd0 --- /dev/null +++ b/pkg/schemas/reference.go @@ -0,0 +1,45 @@ +package schemas + +import ( + "errors" + "fmt" + "net/url" +) + +var ( + ErrEmptyReference = errors.New("reference is empty") + ErrUnsupportedRefFormat = errors.New("unsupported $ref format") + ErrCannotParseRef = errors.New("cannot parse $ref") + ErrUnsupportedRefSchema = errors.New("unsupported $ref schema") + ErrGetRefType = errors.New("cannot get $ref type") +) + +type RefType string + +const ( + RefTypeFile RefType = "file" + RefTypeHTTP RefType = "http" + RefTypeHTTPS RefType = "https" + RefTypeUnknown RefType = "unknown" +) + +func GetRefType(ref string) (RefType, error) { + urlRef, err := url.Parse(ref) + if err != nil { + return RefTypeUnknown, fmt.Errorf("%w: %w", ErrGetRefType, err) + } + + switch urlRef.Scheme { + case "http": + return RefTypeHTTP, nil + + case "https": + return RefTypeHTTPS, nil + + case "file", "": + return RefTypeFile, nil + + default: + return RefTypeUnknown, fmt.Errorf("%w: %w", ErrGetRefType, ErrUnsupportedRefSchema) + } +} diff --git a/tests/data/core/refExternalFileWithScheme.go b/tests/data/core/refExternalFileWithScheme.go new file mode 100644 index 0000000..fa04dd6 --- /dev/null +++ b/tests/data/core/refExternalFileWithScheme.go @@ -0,0 +1,25 @@ +// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT. + +package test + +type Ref struct { + // MyThing corresponds to the JSON schema field "myThing". + MyThing *Thing `json:"myThing,omitempty" yaml:"myThing,omitempty" mapstructure:"myThing,omitempty"` + + // MyThing2 corresponds to the JSON schema field "myThing2". + MyThing2 *Thing `json:"myThing2,omitempty" yaml:"myThing2,omitempty" mapstructure:"myThing2,omitempty"` +} + +type RefExternalFileWithScheme struct { + // MyExternalThing corresponds to the JSON schema field "myExternalThing". + MyExternalThing *Thing `json:"myExternalThing,omitempty" yaml:"myExternalThing,omitempty" mapstructure:"myExternalThing,omitempty"` + + // SomeOtherExternalThing corresponds to the JSON schema field + // "someOtherExternalThing". + SomeOtherExternalThing *Thing `json:"someOtherExternalThing,omitempty" yaml:"someOtherExternalThing,omitempty" mapstructure:"someOtherExternalThing,omitempty"` +} + +type Thing struct { + // Name corresponds to the JSON schema field "name". + Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"` +} diff --git a/tests/data/core/refExternalFileWithScheme.json b/tests/data/core/refExternalFileWithScheme.json new file mode 100644 index 0000000..cf22a11 --- /dev/null +++ b/tests/data/core/refExternalFileWithScheme.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://example.com/refExternalFileWithScheme", + "type": "object", + "properties": { + "myExternalThing": { + "$ref": "file://./ref.json#/$defs/Thing" + }, + "someOtherExternalThing": { + "$ref": "file://./ref.json#/$defs/Thing" + } + } +}