mirror of https://github.com/knative/func.git
Descriptions in JSON Schema (#1633)
* chore: update schema generation library Signed-off-by: Matej Vasek <mvasek@redhat.com> * feat: description in JSON schema from doc comments Signed-off-by: Matej Vasek <mvasek@redhat.com> --------- Signed-off-by: Matej Vasek <mvasek@redhat.com>
This commit is contained in:
parent
99de349e59
commit
7c724bc2f6
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
|
@ -1,5 +0,0 @@
|
|||
sudo: false
|
||||
language: go
|
||||
install: go get -t -v ./...
|
||||
go:
|
||||
- 1.15
|
|
@ -1,312 +1 @@
|
|||
# Go JSON Schema Reflection
|
||||
|
||||
[](https://travis-ci.org/alecthomas/jsonschema)
|
||||
[](https://gitter.im/alecthomas/Lobby)
|
||||
[](https://goreportcard.com/report/github.com/alecthomas/jsonschema)
|
||||
[](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).
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
st := &Type{
|
||||
Type: "object",
|
||||
Properties: orderedmap.New(),
|
||||
AdditionalProperties: []byte("false"),
|
||||
Description: r.lookupComment(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)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue