diff --git a/go.mod b/go.mod index 914eb97fa..3424664d0 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/Microsoft/go-winio v0.6.0 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 - github.com/alecthomas/jsonschema v0.0.0-20210526225647-edb03dcab7bc + github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b github.com/buildpacks/pack v0.28.0 github.com/cloudevents/sdk-go/v2 v2.13.0 github.com/containerd/containerd v1.6.10 diff --git a/go.sum b/go.sum index 98d57b59f..2e57663ad 100644 --- a/go.sum +++ b/go.sum @@ -210,8 +210,8 @@ github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/alecthomas/jsonschema v0.0.0-20210526225647-edb03dcab7bc h1:mT8qSzuyEAkxbv4GBln7yeuQZpBnfikr3PTuiPs6Z3k= -github.com/alecthomas/jsonschema v0.0.0-20210526225647-edb03dcab7bc/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60= +github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b h1:doCpXjVwui6HUN+xgNsNS3SZ0/jUZ68Eb+mJRNOZfog= +github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60= github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= diff --git a/schema/func_yaml-schema.json b/schema/func_yaml-schema.json index e61380b3e..5ee650151 100644 --- a/schema/func_yaml-schema.json +++ b/schema/func_yaml-schema.json @@ -11,7 +11,8 @@ "properties": { "git": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/Git" + "$ref": "#/definitions/Git", + "description": "Git stores information about an optionally associated git repository." }, "builderImages": { "patternProperties": { @@ -19,31 +20,36 @@ "type": "string" } }, - "type": "object" + "type": "object", + "description": "BuilderImages define optional explicit builder images to use by\nbuilder implementations in leau of the in-code defaults. They key\nis the builder's short name. For example:\nbuilderImages:\n pack: example.com/user/my-pack-node-builder\n s2i: example.com/user/my-s2i-node-builder" }, "buildpacks": { "items": { "type": "string" }, - "type": "array" + "type": "array", + "description": "Optional list of buildpacks to use when building the function" }, "builder": { "enum": [ "pack", "s2i" ], - "type": "string" + "type": "string", + "description": "Builder is the name of the subsystem that will complete the underlying\nbuild (pack, s2i, etc)" }, "buildEnvs": { "items": { "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/Env" }, - "type": "array" + "type": "array", + "description": "Build Env variables to be set" } }, "additionalProperties": false, - "type": "object" + "type": "object", + "description": "BuildSpec" }, "DeploySpec": { "required": [ @@ -56,10 +62,12 @@ ], "properties": { "namespace": { - "type": "string" + "type": "string", + "description": "Namespace into which the function is deployed on supported platforms." }, "remote": { - "type": "boolean" + "type": "boolean", + "description": "Remote indicates the deployment (and possibly build) process are to\nbe triggered in a remote environment rather than run locally." }, "annotations": { "patternProperties": { @@ -67,26 +75,31 @@ "type": "string" } }, - "type": "object" + "type": "object", + "description": "Map containing user-supplied annotations\nExample: { \"division\": \"finance\" }" }, "options": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/Options" + "$ref": "#/definitions/Options", + "description": "Options to be set on deployed function (scaling, etc.)" }, "labels": { "items": { "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/Label" }, - "type": "array" + "type": "array", + "description": "Map of user-supplied labels" }, "healthEndpoints": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/HealthEndpoints" + "$ref": "#/definitions/HealthEndpoints", + "description": "Health endpoints specified by the language pack" } }, "additionalProperties": false, - "type": "object" + "type": "object", + "description": "DeploySpec" }, "Env": { "required": [ @@ -119,46 +132,58 @@ ], "properties": { "specVersion": { - "type": "string" + "type": "string", + "description": "SpecVersion at which this function is known to be compatible.\nMore specifically, it is the highest migration which has been applied.\nFor details see the .Migrated() and .Migrate() methods." }, "name": { "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", - "type": "string" + "type": "string", + "description": "Name of the function." }, "runtime": { - "type": "string" + "type": "string", + "description": "Runtime is the language plus context. nodejs|go|quarkus|rust etc." }, "registry": { - "type": "string" + "type": "string", + "description": "Registry at which to store interstitial containers, in the form\n[registry]/[user]." }, "image": { - "type": "string" + "type": "string", + "description": "Optional full OCI image tag in form:\n [registry]/[namespace]/[name]:[tag]\nexample:\n quay.io/alice/my.function.name\nRegistry is optional and is defaulted to DefaultRegistry\nexample:\n alice/my.function.name\nIf Image is provided, it overrides the default of concatenating\n\"Registry+Name:latest\" to derive the Image." }, "imageDigest": { - "type": "string" + "type": "string", + "description": "SHA256 hash of the latest image that has been built" }, "created": { "type": "string", + "description": "Created time is the moment that creation was successfully completed\naccording to the client which is in charge of what constitutes being\nfully \"Created\" (aka initialized)", "format": "date-time" }, "invoke": { - "type": "string" + "type": "string", + "description": "Invoke defines hints for use when invoking this function.\nSee Client.Invoke for usage." }, "build": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/BuildSpec" + "$ref": "#/definitions/BuildSpec", + "description": "BuildSpec define the build properties for a function" }, "run": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/RunSpec" + "$ref": "#/definitions/RunSpec", + "description": "RunSpec define the runtime properties for a function" }, "deploy": { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/DeploySpec" + "$ref": "#/definitions/DeploySpec", + "description": "DeploySpec define the deployment properties for a function" } }, "additionalProperties": false, - "type": "object" + "type": "object", + "description": "Function" }, "Git": { "properties": { @@ -185,7 +210,8 @@ } }, "additionalProperties": false, - "type": "object" + "type": "object", + "description": "HealthEndpoints specify the liveness and readiness endpoints for a Runtime" }, "Label": { "required": [ @@ -194,7 +220,8 @@ "properties": { "key": { "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\\/)?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$", - "type": "string" + "type": "string", + "description": "Key consist of optional prefix part (ended by '/') and name part\nPrefix part validation pattern: [a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\nName part validation pattern: ([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]" }, "value": { "pattern": "^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$", @@ -275,17 +302,20 @@ "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/Volume" }, - "type": "array" + "type": "array", + "description": "List of volumes to be mounted to the function" }, "envs": { "items": { "$ref": "#/definitions/Env" }, - "type": "array" + "type": "array", + "description": "Env variables to be set" } }, "additionalProperties": false, - "type": "object" + "type": "object", + "description": "RunSpec" }, "ScaleOptions": { "properties": { diff --git a/schema/generator/main.go b/schema/generator/main.go index 854433e24..f2887954a 100644 --- a/schema/generator/main.go +++ b/schema/generator/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" "encoding/json" + "fmt" "os" "github.com/alecthomas/jsonschema" @@ -23,7 +24,15 @@ func main() { // Genereated schema is written into schema/func_yaml-schema.json file func generateFuncYamlSchema() error { // generate json schema for function struct - js := jsonschema.Reflect(&fn.Function{}) + r := &jsonschema.Reflector{} + + err := r.AddGoComments("knative.dev/func", "./pkg/functions/") + if err != nil { + return fmt.Errorf("cannot parse docstrings: %w", err) + } + + js := r.Reflect(&fn.Function{}) + schema, err := js.MarshalJSON() if err != nil { return err diff --git a/vendor/github.com/alecthomas/jsonschema/.golangci.yml b/vendor/github.com/alecthomas/jsonschema/.golangci.yml new file mode 100644 index 000000000..df2c4fe1f --- /dev/null +++ b/vendor/github.com/alecthomas/jsonschema/.golangci.yml @@ -0,0 +1,88 @@ +run: + tests: true + max-same-issues: 50 + skip-dirs: + - resources + - old + skip-files: + - cmd/protopkg/main.go + +output: + print-issued-lines: false + +linters: + enable-all: true + disable: + - maligned + - megacheck + - lll + - typecheck # `go build` catches this, and it doesn't currently work with Go 1.11 modules + - goimports # horrendously slow with go modules :( + - dupl # has never been actually useful + - gochecknoglobals + - gochecknoinits + - interfacer # author deprecated it because it provides bad suggestions + - funlen + - whitespace + - godox + - wsl + - dogsled + - gomnd + - gocognit + - gocyclo + - scopelint + - godot + - nestif + - testpackage + - goerr113 + - gci + - gofumpt + - exhaustivestruct + - nlreturn + - forbidigo + - cyclop + - paralleltest + - ifshort # so annoying + - golint + - tagliatelle + - forcetypeassert + - wrapcheck + - revive + - structcheck + - stylecheck + - exhaustive + +linters-settings: + govet: + check-shadowing: true + use-installed-packages: true + dupl: + threshold: 100 + goconst: + min-len: 8 + min-occurrences: 3 + gocyclo: + min-complexity: 20 + gocritic: + disabled-checks: + - ifElseChain + + +issues: + max-per-linter: 0 + max-same: 0 + exclude-use-default: false + exclude: + # Captured by errcheck. + - '^(G104|G204):' + # Very commonly not checked. + - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*Print(f|ln|)|os\.(Un)?Setenv). is not checked' + # Weird error only seen on Kochiku... + - 'internal error: no range for' + - 'exported method `.*\.(MarshalJSON|UnmarshalJSON|URN|Payload|GoString|Close|Provides|Requires|ExcludeFromHash|MarshalText|UnmarshalText|Description|Check|Poll|Severity)` should have comment or be unexported' + - 'composite literal uses unkeyed fields' + - 'declaration of "err" shadows declaration' + - 'by other packages, and that stutters' + - 'Potential file inclusion via variable' + - 'at least one file in a package should have a package comment' + - 'bad syntax for struct tag pair' diff --git a/vendor/github.com/alecthomas/jsonschema/.travis.yml b/vendor/github.com/alecthomas/jsonschema/.travis.yml deleted file mode 100644 index c056a1bc8..000000000 --- a/vendor/github.com/alecthomas/jsonschema/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -sudo: false -language: go -install: go get -t -v ./... -go: - - 1.15 diff --git a/vendor/github.com/alecthomas/jsonschema/README.md b/vendor/github.com/alecthomas/jsonschema/README.md index fdf3f48e0..c40d7b422 100644 --- a/vendor/github.com/alecthomas/jsonschema/README.md +++ b/vendor/github.com/alecthomas/jsonschema/README.md @@ -1,312 +1 @@ -# Go JSON Schema Reflection - -[![Build Status](https://travis-ci.org/alecthomas/jsonschema.png)](https://travis-ci.org/alecthomas/jsonschema) -[![Gitter chat](https://badges.gitter.im/alecthomas.png)](https://gitter.im/alecthomas/Lobby) -[![Go Report Card](https://goreportcard.com/badge/github.com/alecthomas/jsonschema)](https://goreportcard.com/report/github.com/alecthomas/jsonschema) -[![GoDoc](https://godoc.org/github.com/alecthomas/jsonschema?status.svg)](https://godoc.org/github.com/alecthomas/jsonschema) - -This package can be used to generate [JSON Schemas](http://json-schema.org/latest/json-schema-validation.html) from Go types through reflection. - -- Supports arbitrarily complex types, including `interface{}`, maps, slices, etc. -- Supports json-schema features such as minLength, maxLength, pattern, format, etc. -- Supports simple string and numeric enums. -- Supports custom property fields via the `jsonschema_extras` struct tag. - -## Example - -The following Go type: - -```go -type TestUser struct { - ID int `json:"id"` - Name string `json:"name" jsonschema:"title=the name,description=The name of a friend,example=joe,example=lucy,default=alex"` - Friends []int `json:"friends,omitempty" jsonschema_description:"The list of IDs, omitted when empty"` - Tags map[string]interface{} `json:"tags,omitempty" jsonschema_extras:"a=b,foo=bar,foo=bar1"` - BirthDate time.Time `json:"birth_date,omitempty" jsonschema:"oneof_required=date"` - YearOfBirth string `json:"year_of_birth,omitempty" jsonschema:"oneof_required=year"` - Metadata interface{} `json:"metadata,omitempty" jsonschema:"oneof_type=string;array"` - FavColor string `json:"fav_color,omitempty" jsonschema:"enum=red,enum=green,enum=blue"` -} -``` - -Results in following JSON Schema: - -```go -jsonschema.Reflect(&TestUser{}) -``` - -```json -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/TestUser", - "definitions": { - "TestUser": { - "type": "object", - "properties": { - "metadata": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array" - } - ] - }, - "birth_date": { - "type": "string", - "format": "date-time" - }, - "friends": { - "type": "array", - "items": { - "type": "integer" - }, - "description": "The list of IDs, omitted when empty" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string", - "title": "the name", - "description": "The name of a friend", - "default": "alex", - "examples": [ - "joe", - "lucy" - ] - }, - "tags": { - "type": "object", - "patternProperties": { - ".*": { - "additionalProperties": true - } - }, - "a": "b", - "foo": [ - "bar", - "bar1" - ] - }, - "fav_color": { - "type": "string", - "enum": [ - "red", - "green", - "blue" - ] - } - }, - "additionalProperties": false, - "required": ["id", "name"], - "oneOf": [ - { - "required": [ - "birth_date" - ], - "title": "date" - }, - { - "required": [ - "year_of_birth" - ], - "title": "year" - } - ] - } - } -} -``` -## Configurable behaviour - -The behaviour of the schema generator can be altered with parameters when a `jsonschema.Reflector` -instance is created. - -### ExpandedStruct - -If set to ```true```, makes the top level struct not to reference itself in the definitions. But type passed should be a struct type. - -eg. - -```go -type GrandfatherType struct { - FamilyName string `json:"family_name" jsonschema:"required"` -} - -type SomeBaseType struct { - SomeBaseProperty int `json:"some_base_property"` - // The jsonschema required tag is nonsensical for private and ignored properties. - // Their presence here tests that the fields *will not* be required in the output - // schema, even if they are tagged required. - somePrivateBaseProperty string `json:"i_am_private" jsonschema:"required"` - SomeIgnoredBaseProperty string `json:"-" jsonschema:"required"` - SomeSchemaIgnoredProperty string `jsonschema:"-,required"` - SomeUntaggedBaseProperty bool `jsonschema:"required"` - someUnexportedUntaggedBaseProperty bool - Grandfather GrandfatherType `json:"grand"` -} -``` - -will output: - -```json -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "required": [ - "some_base_property", - "grand", - "SomeUntaggedBaseProperty" - ], - "properties": { - "SomeUntaggedBaseProperty": { - "type": "boolean" - }, - "grand": { - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/GrandfatherType" - }, - "some_base_property": { - "type": "integer" - } - }, - "type": "object", - "definitions": { - "GrandfatherType": { - "required": [ - "family_name" - ], - "properties": { - "family_name": { - "type": "string" - } - }, - "additionalProperties": false, - "type": "object" - } - } -} -``` - -### PreferYAMLSchema - -JSON schemas can also be used to validate YAML, however YAML frequently uses -different identifiers to JSON indicated by the `yaml:` tag. The `Reflector` will -by default prefer `json:` tags over `yaml:` tags (and only use the latter if the -former are not present). This behavior can be changed via the `PreferYAMLSchema` -flag, that will switch this behavior: `yaml:` tags will be preferred over -`json:` tags. - -With `PreferYAMLSchema: true`, the following struct: -```go -type Person struct { - FirstName string `json:"FirstName" yaml:"first_name"` -} -``` - -would result in this schema: -```json -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/TestYamlAndJson", - "definitions": { - "Person": { - "required": ["first_name"], - "properties": { - "first_name": { - "type": "string" - } - }, - "additionalProperties": false, - "type": "object" - } - } -} -``` - -whereas without the flag one obtains: -```json -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/TestYamlAndJson", - "definitions": { - "Person": { - "required": ["FirstName"], - "properties": { - "first_name": { - "type": "string" - } - }, - "additionalProperties": false, - "type": "object" - } - } -} -``` - -### Custom Type Definitions - -Sometimes it can be useful to have custom JSON Marshal and Unmarshal methods in your structs that automatically convert for example a string into an object. - -To override auto-generating an object type for your struct, implement the `JSONSchemaType() *Type` method and whatever is defined will be provided in the schema definitions. - -Take the following simplified example of a `CompactDate` that only includes the Year and Month: - -```go -type CompactDate struct { - Year int - Month int -} - -func (d *CompactDate) UnmarshalJSON(data []byte) error { - if len(data) != 9 { - return errors.New("invalid compact date length") - } - var err error - d.Year, err = strconv.Atoi(string(data[1:5])) - if err != nil { - return err - } - d.Month, err = strconv.Atoi(string(data[7:8])) - if err != nil { - return err - } - return nil -} - -func (d *CompactDate) MarshalJSON() ([]byte, error) { - buf := new(bytes.Buffer) - buf.WriteByte('"') - buf.WriteString(fmt.Sprintf("%d-%02d", d.Year, d.Month)) - buf.WriteByte('"') - return buf.Bytes(), nil -} - -func (CompactDate) JSONSchemaType() *Type { - return &Type{ - Type: "string", - Title: "Compact Date", - Description: "Short date that only includes year and month", - Pattern: "^[0-9]{4}-[0-1][0-9]$", - } -} -``` - -The resulting schema generated for this struct would look like: - -```json -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/CompactDate", - "definitions": { - "CompactDate": { - "pattern": "^[0-9]{4}-[0-1][0-9]$", - "type": "string", - "title": "Compact Date", - "description": "Short date that only includes year and month" - } - } -} -``` - +# Maintenance of this project has moved to [invopop/jsonschema](https://github.com/invopop/jsonschema). diff --git a/vendor/github.com/alecthomas/jsonschema/comment_extractor.go b/vendor/github.com/alecthomas/jsonschema/comment_extractor.go new file mode 100644 index 000000000..0088b412a --- /dev/null +++ b/vendor/github.com/alecthomas/jsonschema/comment_extractor.go @@ -0,0 +1,90 @@ +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 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/vendor/github.com/alecthomas/jsonschema/reflect.go b/vendor/github.com/alecthomas/jsonschema/reflect.go index d332914f3..34ca4affd 100644 --- a/vendor/github.com/alecthomas/jsonschema/reflect.go +++ b/vendor/github.com/alecthomas/jsonschema/reflect.go @@ -30,14 +30,23 @@ type Schema struct { Definitions Definitions } -// customSchemaType is used to detect if the structure provides it's own +// customSchemaType is used to detect if the type provides it's own // custom Schema Type definition to use instead. Very useful for situations // where there are custom JSON Marshal and Unmarshal methods. type customSchemaType interface { JSONSchemaType() *Type } -var customStructType = reflect.TypeOf((*customSchemaType)(nil)).Elem() +var customType = reflect.TypeOf((*customSchemaType)(nil)).Elem() + +// customSchemaGetFieldDocString +type customSchemaGetFieldDocString interface { + GetFieldDocString(fieldName string) string +} + +type customGetFieldDocString func(fieldName string) string + +var customStructGetFieldDocString = reflect.TypeOf((*customSchemaGetFieldDocString)(nil)).Elem() // Type represents a JSON Schema object type. type Type struct { @@ -78,6 +87,9 @@ type Type struct { Default interface{} `json:"default,omitempty"` // section 6.2 Format string `json:"format,omitempty"` // section 7 Examples []interface{} `json:"examples,omitempty"` // section 7.4 + // RFC draft-handrews-json-schema-validation-02, section 9.4 + ReadOnly bool `json:"readOnly,omitempty"` + WriteOnly bool `json:"writeOnly,omitempty"` // RFC draft-wright-json-schema-hyperschema-00, section 4 Media *Type `json:"media,omitempty"` // section 4.3 BinaryEncoding string `json:"binaryEncoding,omitempty"` // section 4.3 @@ -141,7 +153,7 @@ type Reflector struct { // switching to just allowing additional properties instead. IgnoredTypes []interface{} - // TypeMapper is a function that can be used to map custom Go types to jsconschema types. + // TypeMapper is a function that can be used to map custom Go types to jsonschema types. TypeMapper func(reflect.Type) *Type // TypeNamer allows customizing of type names @@ -149,6 +161,22 @@ type Reflector struct { // AdditionalFields allows adding structfields for a given type AdditionalFields func(reflect.Type) []reflect.StructField + + // 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 + // separator. + // + // Type descriptions should be defined like: + // + // map[string]string{"github.com/alecthomas/jsonschema.Reflector": "A Reflector reflects values into a Schema."} + // + // And Fields defined as: + // + // map[string]string{"github.com/alecthomas/jsonschema.Reflector.DoNotReference": "Do not reference definitions."} + // + // See also: AddGoComments + CommentMap map[string]string } // Reflect reflects to Schema from a value. @@ -198,6 +226,9 @@ var ( // Byte slices will be encoded as base64 var byteSliceType = reflect.TypeOf([]byte(nil)) +// Except for json.RawMessage +var rawMessageType = reflect.TypeOf(json.RawMessage{}) + // Go code generated from protobuf enum types should fulfil this interface. type protoEnum interface { EnumDescriptor() ([]byte, []int) @@ -211,6 +242,16 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) return &Type{Ref: "#/definitions/" + r.typeName(t)} } + if r.TypeMapper != nil { + if t := r.TypeMapper(t); t != nil { + return t + } + } + + if rt := r.reflectCustomType(definitions, t); rt != nil { + return rt + } + // jsonpb will marshal protobuf enum options as either strings or integers. // It will unmarshal either. if t.Implements(protoEnumType) { @@ -220,24 +261,16 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) }} } - if r.TypeMapper != nil { - if t := r.TypeMapper(t); t != nil { - return t - } - } - // Defined format types for JSON Schema Validation // RFC draft-wright-json-schema-validation-00, section 7.3 // TODO email RFC section 7.3.2, hostname RFC section 7.3.3, uriref RFC section 7.3.7 - switch t { - case ipType: + if t == ipType { // TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5 return &Type{Type: "string", Format: "ipv4"} // ipv4 RFC section 7.3.4 } switch t.Kind() { case reflect.Struct: - switch t { case timeType: // date-time RFC section 7.3.1 return &Type{Type: "string", Format: "date-time"} @@ -248,6 +281,18 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) } case reflect.Map: + switch t.Key().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + rt := &Type{ + Type: "object", + PatternProperties: map[string]*Type{ + "^[0-9]+$": r.reflectTypeToSchema(definitions, t.Elem()), + }, + AdditionalProperties: []byte("false"), + } + return rt + } + rt := &Type{ Type: "object", PatternProperties: map[string]*Type{ @@ -259,6 +304,11 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) case reflect.Slice, reflect.Array: returnType := &Type{} + if t == rawMessageType { + return &Type{ + AdditionalProperties: []byte("true"), + } + } if t.Kind() == reflect.Array { returnType.MinItems = t.Len() returnType.MaxItems = returnType.MinItems @@ -296,8 +346,35 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) panic("unsupported type " + t.String()) } -// Refects a struct to a JSON Schema type. +func (r *Reflector) reflectCustomType(definitions Definitions, t reflect.Type) *Type { + if t.Kind() == reflect.Ptr { + return r.reflectCustomType(definitions, t.Elem()) + } + + if t.Implements(customType) { + v := reflect.New(t) + o := v.Interface().(customSchemaType) + st := o.JSONSchemaType() + definitions[r.typeName(t)] = st + if r.DoNotReference { + return st + } else { + return &Type{ + Version: Version, + Ref: "#/definitions/" + r.typeName(t), + } + } + } + + return nil +} + +// Reflects a struct to a JSON Schema type. func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type) *Type { + if st := r.reflectCustomType(definitions, t); st != nil { + return st + } + for _, ignored := range r.IgnoredTypes { if reflect.TypeOf(ignored) == t { st := &Type{ @@ -315,27 +392,20 @@ func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type) *Type Ref: "#/definitions/" + r.typeName(t), } } - } } - var st *Type - if t.Implements(customStructType) { - v := reflect.New(t) - o := v.Interface().(customSchemaType) - st = o.JSONSchemaType() - definitions[r.typeName(t)] = st - } else { - st = &Type{ - Type: "object", - Properties: orderedmap.New(), - AdditionalProperties: []byte("false"), - } - if r.AllowAdditionalProperties { - st.AdditionalProperties = []byte("true") - } - definitions[r.typeName(t)] = st - r.reflectStructFields(st, definitions, t) + + st := &Type{ + Type: "object", + Properties: orderedmap.New(), + AdditionalProperties: []byte("false"), + Description: r.lookupComment(t, ""), + } + if r.AllowAdditionalProperties { + st.AdditionalProperties = []byte("true") } + definitions[r.typeName(t)] = st + r.reflectStructFields(st, definitions, t) if r.DoNotReference { return st @@ -355,6 +425,13 @@ func (r *Reflector) reflectStructFields(st *Type, definitions Definitions, t ref return } + var getFieldDocString customGetFieldDocString + if t.Implements(customStructGetFieldDocString) { + v := reflect.New(t) + o := v.Interface().(customSchemaGetFieldDocString) + getFieldDocString = o.GetFieldDocString + } + handleField := func(f reflect.StructField) { name, shouldEmbed, required, nullable := r.reflectFieldName(f) // if anonymous and exported type should be processed recursively @@ -368,6 +445,12 @@ func (r *Reflector) reflectStructFields(st *Type, definitions Definitions, t ref property := r.reflectTypeToSchema(definitions, f.Type) property.structKeywordsFromTags(f, st, name) + if property.Description == "" { + property.Description = r.lookupComment(t, f.Name) + } + if getFieldDocString != nil { + property.Description = getFieldDocString(f.Name) + } if nullable { property = &Type{ @@ -399,6 +482,19 @@ func (r *Reflector) reflectStructFields(st *Type, definitions Definitions, t ref } } +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] +} + func (t *Type) structKeywordsFromTags(f reflect.StructField, parentType *Type, propertyName string) { t.Description = f.Tag.Get("jsonschema_description") tags := strings.Split(f.Tag.Get("jsonschema"), ",") @@ -493,6 +589,12 @@ func (t *Type) stringKeywords(tags []string) { t.Format = val break } + case "readOnly": + i, _ := strconv.ParseBool(val) + t.ReadOnly = i + case "writeOnly": + i, _ := strconv.ParseBool(val) + t.WriteOnly = i case "default": t.Default = val case "example": @@ -570,6 +672,17 @@ func (t *Type) arrayKeywords(tags []string) { t.UniqueItems = true case "default": defaultValues = append(defaultValues, val) + case "enum": + switch t.Items.Type { + case "string": + t.Items.Enum = append(t.Items.Enum, val) + case "integer": + i, _ := strconv.Atoi(val) + t.Items.Enum = append(t.Items.Enum, i) + case "number": + f, _ := strconv.ParseFloat(val, 64) + t.Items.Enum = append(t.Items.Enum, f) + } } } } @@ -592,11 +705,11 @@ func (t *Type) setExtra(key, val string) { t.Extras = map[string]interface{}{} } if existingVal, ok := t.Extras[key]; ok { - switch existingVal.(type) { + switch existingVal := existingVal.(type) { case string: - t.Extras[key] = []string{existingVal.(string), val} + t.Extras[key] = []string{existingVal, val} case []string: - t.Extras[key] = append(existingVal.([]string), val) + t.Extras[key] = append(existingVal, val) case int: t.Extras[key], _ = strconv.Atoi(val) } @@ -772,7 +885,21 @@ func (r *Reflector) typeName(t reflect.Type) string { } } if r.FullyQualifyTypeNames { - return t.PkgPath() + "." + t.Name() + return fullyQualifiedTypeName(t) } return t.Name() } + +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/vendor/modules.txt b/vendor/modules.txt index 024a4640b..c5d954c47 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -113,7 +113,7 @@ github.com/acomagu/bufpipe # github.com/agext/levenshtein v1.2.3 ## explicit github.com/agext/levenshtein -# github.com/alecthomas/jsonschema v0.0.0-20210526225647-edb03dcab7bc +# github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b ## explicit; go 1.12 github.com/alecthomas/jsonschema # github.com/antlr/antlr4/runtime/Go/antlr v1.4.10