Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a custom mechanism for looking up comments. #159

Merged
merged 2 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
name: Test Go
on: [push]
on:
push:
tags:
- v*
branches:
- main
pull_request:
jobs:
test:
name: Test
Expand Down
114 changes: 114 additions & 0 deletions fixtures/custom_comments.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
}
25 changes: 11 additions & 14 deletions reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,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
Expand All @@ -156,7 +166,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
}

Expand Down Expand Up @@ -558,19 +568,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)
Expand Down
20 changes: 20 additions & 0 deletions reflect_comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io/fs"
gopath "path"
"path/filepath"
"reflect"
"strings"

"go/ast"
Expand Down Expand Up @@ -124,3 +125,22 @@ func (r *Reflector) extractGoComments(base, path string, commentMap map[string]s

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]
}
26 changes: 25 additions & 1 deletion reflect_comments_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package jsonschema

import (
"fmt"
"path/filepath"
"reflect"
"strings"
"testing"

"github.com/invopop/jsonschema/examples"
"github.com/stretchr/testify/require"

"github.com/invopop/jsonschema/examples"
)

func TestCommentsSchemaGeneration(t *testing.T) {
Expand All @@ -17,6 +20,7 @@ func TestCommentsSchemaGeneration(t *testing.T) {
}{
{&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")
Expand All @@ -35,3 +39,23 @@ func prepareCommentReflector(t *testing.T, opts ...CommentOption) *Reflector {
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
}
Loading