diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index a4de3ed..b5220c2 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -12,15 +12,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v4 - with: - go-version: "1.18" - cache: false - - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" - name: Lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.55 + version: v1.62 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0930332..099d883 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,19 +1,24 @@ name: Test Go -on: [push, pull_request] +on: + push: + tags: + - v* + branches: + - main + pull_request: jobs: - lint-test-build: - name: Lint, Test + test: + name: Test runs-on: ubuntu-latest steps: + - name: Check out code + uses: actions/checkout@v3 + - name: Set up Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v4 with: - go-version: "1.18" - id: go - - - name: Check out code - uses: actions/checkout@v2 + go-version-file: "go.mod" - name: Install Dependencies env: @@ -21,4 +26,9 @@ jobs: run: go mod download - name: Test - run: go test -tags unit -race ./... + run: go test -race -coverprofile=coverage.out -covermode=atomic ./... + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.golangci.yml b/.golangci.yml index 3dac8a3..b89b2e1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,11 +1,6 @@ run: tests: true max-same-issues: 50 - skip-dirs: - - resources - - old - skip-files: - - cmd/protopkg/main.go output: print-issued-lines: false @@ -19,13 +14,12 @@ linters: - unconvert - goimports - unused - - vetshadow + - govet - nakedret - errcheck - revive - ineffassign - goconst - - vet - unparam - gofmt @@ -45,14 +39,19 @@ linters-settings: - ifElseChain gofmt: rewrite-rules: - - pattern: 'interface{}' - replacement: 'any' - - pattern: 'a[b:len(a)]' - replacement: 'a[b:]' + - pattern: "interface{}" + replacement: "any" + - pattern: "a[b:len(a)]" + replacement: "a[b:]" issues: max-per-linter: 0 max-same: 0 + exclude-dirs: + - resources + - old + exclude-files: + - cmd/protopkg/main.go exclude-use-default: false exclude: # Captured by errcheck. diff --git a/README.md b/README.md index 1a68a09..3e73b7f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,108 @@ # Go JSON Schema Reflection +## My Fork Changes: +* Commit `c7e4988` support manual require: +```go +package main + +import ( + "fmt" + "reflect" + + "github.com/funte/jsonschema" +) + +type Base struct { + BA int `json:"ba" jsonschema:"required"` + BB string `json:"bb" jsonschema:"required"` +} + +type Child struct { + *Base + + CA bool `json:"ca" jsonschema:"required"` +} + +func main() { + // Enable pretty marshal. + jsonschema.MarshalWithIndent = true + jsonschema.MarshalIndent = " " + + r := jsonschema.Reflector{} + r.ExpandedStruct = true + r.RequiredFromJSONSchemaTags = true + + // Test Reflector.Require. + r.Require = func(f reflect.StructField) bool { + return f.Name == "CA" + } + if s, err := r.Reflect(Child{}).MarshalJSON(); err != nil { + fmt.Println("failed to marshal Child json schema, err=", err) + } else { + // Output should only quired "ca". + fmt.Println(string(s)) + } +} + + +``` +* Commit `bfefafd` support manual ignore: +```go +package main + +import ( + "fmt" + "reflect" + + "github.com/funte/jsonschema" +) + +type Base struct { + BA int `json:"ba" jsonschema:"required"` + BB string `json:"bb" jsonschema:"required"` +} + +type Child struct { + *Base + + CA bool `json:"ca" jsonschema:"required"` +} + +func main() { + // Enable pretty marshal. + jsonschema.MarshalWithIndent = true + jsonschema.MarshalIndent = " " + + r := jsonschema.Reflector{} + r.ExpandedStruct = true + r.RequiredFromJSONSchemaTags = true + + // Test Reflector.Ignore. + r.Ignore = func(f reflect.StructField) bool { + return f.Name != "CA" + } + if s, err := r.Reflect(Child{}).MarshalJSON(); err != nil { + fmt.Println("failed to marshal Child json schema, err=", err) + } else { + // Output should only has field "ca". + fmt.Println(string(s)) + } +} + +``` +* Commit `7f1f647` support pretty marshal: +```go +jsonschema.MarshalWithIndent = true +jsonschema.MarshalIndent = " " +``` + +## introduction + [![Lint](https://github.com/invopop/jsonschema/actions/workflows/lint.yaml/badge.svg)](https://github.com/invopop/jsonschema/actions/workflows/lint.yaml) [![Test Go](https://github.com/invopop/jsonschema/actions/workflows/test.yaml/badge.svg)](https://github.com/invopop/jsonschema/actions/workflows/test.yaml) [![Go Report Card](https://goreportcard.com/badge/github.com/invopop/jsonschema)](https://goreportcard.com/report/github.com/invopop/jsonschema) [![GoDoc](https://godoc.org/github.com/invopop/jsonschema?status.svg)](https://godoc.org/github.com/invopop/jsonschema) +[![codecov](https://codecov.io/gh/invopop/jsonschema/graph/badge.svg?token=JMEB8W8GNZ)](https://codecov.io/gh/invopop/jsonschema) ![Latest Tag](https://img.shields.io/github/v/tag/invopop/jsonschema) This package can be used to generate [JSON Schemas](http://json-schema.org/latest/json-schema-validation.html) from Go types through reflection. @@ -52,10 +151,10 @@ jsonschema.Reflect(&TestUser{}) ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/invopop/jsonschema_test/sample-user", - "$ref": "#/$defs/SampleUser", + "$id": "https://github.com/invopop/jsonschema_test/test-user", + "$ref": "#/$defs/TestUser", "$defs": { - "SampleUser": { + "TestUser": { "oneOf": [ { "required": ["birth_date"], diff --git a/comment_extractor.go b/comment_extractor.go deleted file mode 100644 index e157837..0000000 --- a/comment_extractor.go +++ /dev/null @@ -1,93 +0,0 @@ -package jsonschema - -import ( - "fmt" - "io/fs" - gopath "path" - "path/filepath" - "strings" - - "go/ast" - "go/doc" - "go/parser" - "go/token" -) - -// ExtractGoComments will read all the go files contained in the provided path, -// including sub-directories, in order to generate a dictionary of comments -// associated with Types and Fields. The results will be added to the `commentsMap` -// provided in the parameters and expected to be used for Schema "description" fields. -// -// The `go/parser` library is used to extract all the comments and unfortunately doesn't -// have a built-in way to determine the fully qualified name of a package. The `base` paremeter, -// the URL used to import that package, is thus required to be able to match reflected types. -// -// When parsing type comments, we use the `go/doc`'s Synopsis method to extract the first phrase -// only. Field comments, which tend to be much shorter, will include everything. -func ExtractGoComments(base, path string, commentMap map[string]string) error { - fset := token.NewFileSet() - dict := make(map[string][]*ast.Package) - err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - d, err := parser.ParseDir(fset, path, nil, parser.ParseComments) - if err != nil { - return err - } - for _, v := range d { - // paths may have multiple packages, like for tests - k := gopath.Join(base, path) - dict[k] = append(dict[k], v) - } - } - return nil - }) - if err != nil { - return err - } - - for pkg, p := range dict { - for _, f := range p { - gtxt := "" - typ := "" - ast.Inspect(f, func(n ast.Node) bool { - switch x := n.(type) { - case *ast.TypeSpec: - typ = x.Name.String() - if !ast.IsExported(typ) { - typ = "" - } else { - txt := x.Doc.Text() - if txt == "" && gtxt != "" { - txt = gtxt - gtxt = "" - } - txt = doc.Synopsis(txt) - commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt) - } - case *ast.Field: - txt := x.Doc.Text() - if txt == "" { - txt = x.Comment.Text() - } - if typ != "" && txt != "" { - for _, n := range x.Names { - if ast.IsExported(n.String()) { - k := fmt.Sprintf("%s.%s.%s", pkg, typ, n) - commentMap[k] = strings.TrimSpace(txt) - } - } - } - case *ast.GenDecl: - // remember for the next type - gtxt = x.Doc.Text() - } - return true - }) - } - } - - return nil -} diff --git a/fixtures/custom_comments.json b/fixtures/custom_comments.json new file mode 100644 index 0000000..db4eec4 --- /dev/null +++ b/fixtures/custom_comments.json @@ -0,0 +1,114 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/invopop/jsonschema/examples/user", + "$ref": "#/$defs/User", + "$defs": { + "NamedPets": { + "additionalProperties": { + "$ref": "#/$defs/Pet" + }, + "type": "object", + "description": "NamedPets is a map of animal names to pets." + }, + "Pet": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Name of the animal." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ], + "description": "Pet defines the user's fury friend." + }, + "Pets": { + "items": { + "$ref": "#/$defs/Pet" + }, + "type": "array", + "description": "Pets is a collection of Pet objects." + }, + "Plant": { + "properties": { + "variant": { + "type": "string", + "title": "Variant", + "description": "This comment will be used" + }, + "multicellular": { + "type": "boolean", + "title": "Multicellular", + "description": "Multicellular is true if the plant is multicellular" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "variant" + ], + "description": "Plant represents the plants the user might have and serves as a test of structs inside a `type` set." + }, + "User": { + "properties": { + "id": { + "type": "integer", + "description": "Field ID of Go type github.com/invopop/jsonschema/examples.User." + }, + "name": { + "type": "string", + "maxLength": 20, + "minLength": 1, + "pattern": ".*", + "title": "the name", + "description": "this is a property", + "default": "alex", + "examples": [ + "joe", + "lucy" + ] + }, + "friends": { + "items": { + "type": "integer" + }, + "type": "array", + "description": "list of IDs, omitted when empty" + }, + "tags": { + "type": "object", + "description": "Field Tags of Go type github.com/invopop/jsonschema/examples.User." + }, + "pets": { + "$ref": "#/$defs/Pets", + "description": "Field Pets of Go type github.com/invopop/jsonschema/examples.User." + }, + "named_pets": { + "$ref": "#/$defs/NamedPets", + "description": "Field NamedPets of Go type github.com/invopop/jsonschema/examples.User." + }, + "plants": { + "items": { + "$ref": "#/$defs/Plant" + }, + "type": "array", + "title": "Plants", + "description": "Field Plants of Go type github.com/invopop/jsonschema/examples.User." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "name", + "pets", + "named_pets", + "plants" + ], + "description": "Go type User, defined in package github.com/invopop/jsonschema/examples." + } + } +} \ No newline at end of file diff --git a/fixtures/go_comments_full.json b/fixtures/go_comments_full.json new file mode 100644 index 0000000..d1a5219 --- /dev/null +++ b/fixtures/go_comments_full.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/invopop/jsonschema/examples/user", + "$ref": "#/$defs/User", + "$defs": { + "NamedPets": { + "additionalProperties": { + "$ref": "#/$defs/Pet" + }, + "type": "object", + "description": "NamedPets is a map of animal names to pets." + }, + "Pet": { + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Name of the animal." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ], + "description": "Pet defines the user's fury friend." + }, + "Pets": { + "items": { + "$ref": "#/$defs/Pet" + }, + "type": "array", + "description": "Pets is a collection of Pet objects." + }, + "Plant": { + "properties": { + "variant": { + "type": "string", + "title": "Variant", + "description": "This comment will be used" + }, + "multicellular": { + "type": "boolean", + "title": "Multicellular", + "description": "Multicellular is true if the plant is multicellular" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "variant" + ], + "description": "Plant represents the plants the user might have and serves as a test\nof structs inside a `type` set." + }, + "User": { + "properties": { + "id": { + "type": "integer", + "description": "Unique sequential identifier." + }, + "name": { + "type": "string", + "maxLength": 20, + "minLength": 1, + "pattern": ".*", + "title": "the name", + "description": "this is a property", + "default": "alex", + "examples": [ + "joe", + "lucy" + ] + }, + "friends": { + "items": { + "type": "integer" + }, + "type": "array", + "description": "list of IDs, omitted when empty" + }, + "tags": { + "type": "object" + }, + "pets": { + "$ref": "#/$defs/Pets", + "description": "An array of pets the user cares for." + }, + "named_pets": { + "$ref": "#/$defs/NamedPets", + "description": "Set of animal names to pets" + }, + "plants": { + "items": { + "$ref": "#/$defs/Plant" + }, + "type": "array", + "title": "Plants", + "description": "Set of plants that the user likes" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "name", + "pets", + "named_pets", + "plants" + ], + "description": "User is used as a base to provide tests for comments.\nDon't forget to checkout the nested path." + } + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 49c0e35..33f878d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/invopop/jsonschema +module github.com/funte/jsonschema go 1.18 diff --git a/reflect.go b/reflect.go index 0be6cfe..6a09e36 100644 --- a/reflect.go +++ b/reflect.go @@ -106,6 +106,9 @@ type Reflector struct { // default of requiring any key *not* tagged with `json:,omitempty`. RequiredFromJSONSchemaTags bool + // Manual require specific struct required. + Require func(reflect.StructField) bool + // Do not reference definitions. This will remove the top-level $defs map and // instead cause the entire structure of types to be output in one tree. The // list of type definitions (`$defs`) will not be included. @@ -122,6 +125,9 @@ type Reflector struct { // switching to just allowing additional properties instead. IgnoredTypes []any + // Ignore specific struct field, enable dynamic generate different schemas from on struct. + Ignore func(reflect.StructField) bool + // Lookup allows a function to be defined that will provide a custom mapping of // types to Schema IDs. This allows existing schema documents to be referenced // by their ID instead of being embedded into the current schema definitions. @@ -143,6 +149,16 @@ type Reflector struct { // AdditionalFields allows adding structfields for a given type AdditionalFields func(reflect.Type) []reflect.StructField + // LookupComment allows customizing comment lookup. Given a reflect.Type and optionally + // a field name, it should return the comment string associated with this type or field. + // + // If the field name is empty, it should return the type's comment; otherwise, the field's + // comment should be returned. If no comment is found, an empty string should be returned. + // + // When set, this function is called before the below CommentMap lookup mechanism. However, + // if it returns an empty string, the CommentMap is still consulted. + LookupComment func(reflect.Type, string) string + // CommentMap is a dictionary of fully qualified go types and fields to comment // strings that will be used if a description has not already been provided in // the tags. Types and fields are added to the package path using "." as a @@ -156,7 +172,7 @@ type Reflector struct { // // map[string]string{"github.com/invopop/jsonschema.Reflector.DoNotReference": "Do not reference definitions."} // - // See also: AddGoComments + // See also: AddGoComments, LookupComment CommentMap map[string]string } @@ -492,6 +508,10 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r } handleField := func(f reflect.StructField) { + if !f.Anonymous && r.Ignore != nil && r.Ignore(f) { + return + } + name, shouldEmbed, required, nullable := r.reflectFieldName(f) // if anonymous and exported type should be processed recursively // current type should inherit properties of anonymous one @@ -558,19 +578,6 @@ func appendUniqueString(base []string, value string) []string { return append(base, value) } -func (r *Reflector) lookupComment(t reflect.Type, name string) string { - if r.CommentMap == nil { - return "" - } - - n := fullyQualifiedTypeName(t) - if name != "" { - n = n + "." + name - } - - return r.CommentMap[n] -} - // addDefinition will append the provided schema. If needed, an ID and anchor will also be added. func (r *Reflector) addDefinition(definitions Definitions, t reflect.Type, s *Schema) { name := r.typeName(t) @@ -1024,10 +1031,15 @@ func (r *Reflector) reflectFieldName(f reflect.StructField) (string, bool, bool, } var required bool - if !r.RequiredFromJSONSchemaTags { - requiredFromJSONTags(jsonTags, &required) + if r.Require != nil { + // Manual require. + required = r.Require(f) + } else { + if !r.RequiredFromJSONSchemaTags { + requiredFromJSONTags(jsonTags, &required) + } + requiredFromJSONSchemaTags(schemaTags, &required) } - requiredFromJSONSchemaTags(schemaTags, &required) nullable := nullableFromJSONSchemaTags(schemaTags) @@ -1081,6 +1093,11 @@ func (t *Schema) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, aux) } +// If true, marshal json with linebreak and indent. +var MarshalWithIndent = false +var MarshalPrefix = "" +var MarshalIndent = "\t" + // MarshalJSON is used to serialize a schema object or boolean. func (t *Schema) MarshalJSON() ([]byte, error) { if t.boolean != nil { @@ -1094,14 +1111,25 @@ func (t *Schema) MarshalJSON() ([]byte, error) { return []byte("true"), nil } type SchemaAlt Schema - b, err := json.Marshal((*SchemaAlt)(t)) + var b []byte + var err error + if MarshalWithIndent { + b, err = json.MarshalIndent((*SchemaAlt)(t), MarshalPrefix, MarshalIndent) + } else { + b, err = json.Marshal((*SchemaAlt)(t)) + } if err != nil { return nil, err } - if t.Extras == nil || len(t.Extras) == 0 { + if len(t.Extras) == 0 { return b, nil } - m, err := json.Marshal(t.Extras) + var m []byte + if MarshalWithIndent { + m, err = json.MarshalIndent(t.Extras, MarshalPrefix, MarshalIndent) + } else { + m, err = json.Marshal(t.Extras) + } if err != nil { return nil, err } @@ -1149,13 +1177,3 @@ func splitOnUnescapedCommas(tagString string) []string { func fullyQualifiedTypeName(t reflect.Type) string { return t.PkgPath() + "." + t.Name() } - -// AddGoComments will update the reflectors comment map with all the comments -// found in the provided source directories. See the #ExtractGoComments method -// for more details. -func (r *Reflector) AddGoComments(base, path string) error { - if r.CommentMap == nil { - r.CommentMap = make(map[string]string) - } - return ExtractGoComments(base, path, r.CommentMap) -} diff --git a/reflect_comments.go b/reflect_comments.go new file mode 100644 index 0000000..ff374c7 --- /dev/null +++ b/reflect_comments.go @@ -0,0 +1,146 @@ +package jsonschema + +import ( + "fmt" + "io/fs" + gopath "path" + "path/filepath" + "reflect" + "strings" + + "go/ast" + "go/doc" + "go/parser" + "go/token" +) + +type commentOptions struct { + fullObjectText bool // use the first sentence only? +} + +// CommentOption allows for special configuration options when preparing Go +// source files for comment extraction. +type CommentOption func(*commentOptions) + +// WithFullComment will configure the comment extraction to process to use an +// object type's full comment text instead of just the synopsis. +func WithFullComment() CommentOption { + return func(o *commentOptions) { + o.fullObjectText = true + } +} + +// AddGoComments will update the reflectors comment map with all the comments +// found in the provided source directories including sub-directories, in order to +// generate a dictionary of comments associated with Types and Fields. The results +// will be added to the `Reflect.CommentMap` ready to use with Schema "description" +// fields. +// +// The `go/parser` library is used to extract all the comments and unfortunately doesn't +// have a built-in way to determine the fully qualified name of a package. The `base` +// parameter, the URL used to import that package, is thus required to be able to match +// reflected types. +// +// When parsing type comments, by default we use the `go/doc`'s Synopsis method to extract +// the first phrase only. Field comments, which tend to be much shorter, will include everything. +// This behavior can be changed by using the `WithFullComment` option. +func (r *Reflector) AddGoComments(base, path string, opts ...CommentOption) error { + if r.CommentMap == nil { + r.CommentMap = make(map[string]string) + } + co := new(commentOptions) + for _, opt := range opts { + opt(co) + } + + return r.extractGoComments(base, path, r.CommentMap, co) +} + +func (r *Reflector) extractGoComments(base, path string, commentMap map[string]string, opts *commentOptions) error { + fset := token.NewFileSet() + dict := make(map[string][]*ast.Package) + err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + d, err := parser.ParseDir(fset, path, nil, parser.ParseComments) + if err != nil { + return err + } + for _, v := range d { + // paths may have multiple packages, like for tests + k := gopath.Join(base, path) + dict[k] = append(dict[k], v) + } + } + return nil + }) + if err != nil { + return err + } + + for pkg, p := range dict { + for _, f := range p { + gtxt := "" + typ := "" + ast.Inspect(f, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.TypeSpec: + typ = x.Name.String() + if !ast.IsExported(typ) { + typ = "" + } else { + txt := x.Doc.Text() + if txt == "" && gtxt != "" { + txt = gtxt + gtxt = "" + } + if !opts.fullObjectText { + txt = doc.Synopsis(txt) + } + commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt) + } + case *ast.Field: + txt := x.Doc.Text() + if txt == "" { + txt = x.Comment.Text() + } + if typ != "" && txt != "" { + for _, n := range x.Names { + if ast.IsExported(n.String()) { + k := fmt.Sprintf("%s.%s.%s", pkg, typ, n) + commentMap[k] = strings.TrimSpace(txt) + } + } + } + case *ast.GenDecl: + // remember for the next type + gtxt = x.Doc.Text() + } + return true + }) + } + } + + return nil +} + +func (r *Reflector) lookupComment(t reflect.Type, name string) string { + if r.LookupComment != nil { + if comment := r.LookupComment(t, name); comment != "" { + return comment + } + } + + if r.CommentMap == nil { + return "" + } + + n := fullyQualifiedTypeName(t) + if name != "" { + n = n + "." + name + } + + return r.CommentMap[n] +} diff --git a/reflect_comments_test.go b/reflect_comments_test.go new file mode 100644 index 0000000..3e752bb --- /dev/null +++ b/reflect_comments_test.go @@ -0,0 +1,61 @@ +package jsonschema + +import ( + "fmt" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/invopop/jsonschema/examples" +) + +func TestCommentsSchemaGeneration(t *testing.T) { + tests := []struct { + typ any + reflector *Reflector + fixture string + }{ + {&examples.User{}, prepareCommentReflector(t), "fixtures/go_comments.json"}, + {&examples.User{}, prepareCommentReflector(t, WithFullComment()), "fixtures/go_comments_full.json"}, + {&examples.User{}, prepareCustomCommentReflector(t), "fixtures/custom_comments.json"}, + } + for _, tt := range tests { + name := strings.TrimSuffix(filepath.Base(tt.fixture), ".json") + t.Run(name, func(t *testing.T) { + compareSchemaOutput(t, + tt.fixture, tt.reflector, tt.typ, + ) + }) + } +} + +func prepareCommentReflector(t *testing.T, opts ...CommentOption) *Reflector { + t.Helper() + r := new(Reflector) + err := r.AddGoComments("github.com/invopop/jsonschema", "./examples", opts...) + require.NoError(t, err, "did not expect error while adding comments") + return r +} + +func prepareCustomCommentReflector(t *testing.T) *Reflector { + t.Helper() + r := new(Reflector) + r.LookupComment = func(t reflect.Type, f string) string { + if t != reflect.TypeOf(examples.User{}) { + // To test the interaction between a custom LookupComment function and the + // AddGoComments function, we only override comments for the User type. + return "" + } + if f == "" { + return fmt.Sprintf("Go type %s, defined in package %s.", t.Name(), t.PkgPath()) + } + return fmt.Sprintf("Field %s of Go type %s.%s.", f, t.PkgPath(), t.Name()) + } + // Also add the Go comments. + err := r.AddGoComments("github.com/invopop/jsonschema", "./examples") + require.NoError(t, err, "did not expect error while adding comments") + return r +} diff --git a/reflect_test.go b/reflect_test.go index 37ea18a..93bee67 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -13,8 +13,6 @@ import ( "testing" "time" - "github.com/invopop/jsonschema/examples" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -429,7 +427,7 @@ func TestSchemaGeneration(t *testing.T) { {&MinValue{}, &Reflector{}, "fixtures/schema_with_minimum.json"}, {&TestNullable{}, &Reflector{}, "fixtures/nullable.json"}, {&GrandfatherType{}, &Reflector{ - AdditionalFields: func(r reflect.Type) []reflect.StructField { + AdditionalFields: func(_ reflect.Type) []reflect.StructField { return []reflect.StructField{ { Name: "Addr", @@ -446,7 +444,6 @@ func TestSchemaGeneration(t *testing.T) { {&CustomMapOuter{}, &Reflector{}, "fixtures/custom_map_type.json"}, {&CustomTypeFieldWithInterface{}, &Reflector{}, "fixtures/custom_type_with_interface.json"}, {&PatternTest{}, &Reflector{}, "fixtures/commas_in_pattern.json"}, - {&examples.User{}, prepareCommentReflector(t), "fixtures/go_comments.json"}, {&RecursiveExample{}, &Reflector{}, "fixtures/recursive.json"}, {&KeyNamed{}, &Reflector{ KeyNamer: func(s string) string { @@ -488,14 +485,6 @@ func TestSchemaGeneration(t *testing.T) { } } -func prepareCommentReflector(t *testing.T) *Reflector { - t.Helper() - r := new(Reflector) - err := r.AddGoComments("github.com/invopop/jsonschema", "./examples") - require.NoError(t, err, "did not expect error while adding comments") - return r -} - func TestBaselineUnmarshal(t *testing.T) { r := &Reflector{} compareSchemaOutput(t, "fixtures/test_user.json", r, &TestUser{})