mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-13 21:05:30 +02:00
1196 lines
34 KiB
Go
1196 lines
34 KiB
Go
// Package jsonschema uses reflection to generate JSON Schemas from Go types [1].
|
|
//
|
|
// If json tags are present on struct fields, they will be used to infer
|
|
// property names and if a property is required (omitempty is present).
|
|
//
|
|
// [1] http://json-schema.org/latest/json-schema-validation.html
|
|
package jsonschema
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net"
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
|
)
|
|
|
|
// Version is the JSON Schema version.
|
|
var Version = "https://json-schema.org/draft/2020-12/schema"
|
|
|
|
// Schema represents a JSON Schema object type.
|
|
// RFC draft-bhutton-json-schema-00 section 4.3
|
|
type Schema struct {
|
|
// RFC draft-bhutton-json-schema-00
|
|
Version string `json:"$schema,omitempty"` // section 8.1.1
|
|
ID ID `json:"$id,omitempty"` // section 8.2.1
|
|
Anchor string `json:"$anchor,omitempty"` // section 8.2.2
|
|
Ref string `json:"$ref,omitempty"` // section 8.2.3.1
|
|
DynamicRef string `json:"$dynamicRef,omitempty"` // section 8.2.3.2
|
|
Definitions Definitions `json:"$defs,omitempty"` // section 8.2.4
|
|
Comments string `json:"$comment,omitempty"` // section 8.3
|
|
// RFC draft-bhutton-json-schema-00 section 10.2.1 (Sub-schemas with logic)
|
|
AllOf []*Schema `json:"allOf,omitempty"` // section 10.2.1.1
|
|
AnyOf []*Schema `json:"anyOf,omitempty"` // section 10.2.1.2
|
|
OneOf []*Schema `json:"oneOf,omitempty"` // section 10.2.1.3
|
|
Not *Schema `json:"not,omitempty"` // section 10.2.1.4
|
|
// RFC draft-bhutton-json-schema-00 section 10.2.2 (Apply sub-schemas conditionally)
|
|
If *Schema `json:"if,omitempty"` // section 10.2.2.1
|
|
Then *Schema `json:"then,omitempty"` // section 10.2.2.2
|
|
Else *Schema `json:"else,omitempty"` // section 10.2.2.3
|
|
DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"` // section 10.2.2.4
|
|
// RFC draft-bhutton-json-schema-00 section 10.3.1 (arrays)
|
|
PrefixItems []*Schema `json:"prefixItems,omitempty"` // section 10.3.1.1
|
|
Items *Schema `json:"items,omitempty"` // section 10.3.1.2 (replaces additionalItems)
|
|
Contains *Schema `json:"contains,omitempty"` // section 10.3.1.3
|
|
// RFC draft-bhutton-json-schema-00 section 10.3.2 (sub-schemas)
|
|
Properties *orderedmap.OrderedMap[string, *Schema] `json:"properties,omitempty"` // section 10.3.2.1
|
|
OriginalPropertiesMapping map[string]string `json:"-"`
|
|
PatternProperties map[string]*Schema `json:"patternProperties,omitempty"` // section 10.3.2.2
|
|
AdditionalProperties *Schema `json:"additionalProperties,omitempty"` // section 10.3.2.3
|
|
PropertyNames *Schema `json:"propertyNames,omitempty"` // section 10.3.2.4
|
|
// RFC draft-bhutton-json-schema-validation-00, section 6
|
|
Type string `json:"type,omitempty"` // section 6.1.1
|
|
Enum []any `json:"enum,omitempty"` // section 6.1.2
|
|
Const any `json:"const,omitempty"` // section 6.1.3
|
|
MultipleOf json.Number `json:"multipleOf,omitempty"` // section 6.2.1
|
|
Maximum json.Number `json:"maximum,omitempty"` // section 6.2.2
|
|
ExclusiveMaximum json.Number `json:"exclusiveMaximum,omitempty"` // section 6.2.3
|
|
Minimum json.Number `json:"minimum,omitempty"` // section 6.2.4
|
|
ExclusiveMinimum json.Number `json:"exclusiveMinimum,omitempty"` // section 6.2.5
|
|
MaxLength int `json:"maxLength,omitempty"` // section 6.3.1
|
|
MinLength int `json:"minLength,omitempty"` // section 6.3.2
|
|
Pattern string `json:"pattern,omitempty"` // section 6.3.3
|
|
MaxItems int `json:"maxItems,omitempty"` // section 6.4.1
|
|
MinItems int `json:"minItems,omitempty"` // section 6.4.2
|
|
UniqueItems bool `json:"uniqueItems,omitempty"` // section 6.4.3
|
|
MaxContains uint `json:"maxContains,omitempty"` // section 6.4.4
|
|
MinContains uint `json:"minContains,omitempty"` // section 6.4.5
|
|
MaxProperties int `json:"maxProperties,omitempty"` // section 6.5.1
|
|
MinProperties int `json:"minProperties,omitempty"` // section 6.5.2
|
|
Required []string `json:"required,omitempty"` // section 6.5.3
|
|
DependentRequired map[string][]string `json:"dependentRequired,omitempty"` // section 6.5.4
|
|
// RFC draft-bhutton-json-schema-validation-00, section 7
|
|
Format string `json:"format,omitempty"`
|
|
// RFC draft-bhutton-json-schema-validation-00, section 8
|
|
ContentEncoding string `json:"contentEncoding,omitempty"` // section 8.3
|
|
ContentMediaType string `json:"contentMediaType,omitempty"` // section 8.4
|
|
ContentSchema *Schema `json:"contentSchema,omitempty"` // section 8.5
|
|
// RFC draft-bhutton-json-schema-validation-00, section 9
|
|
Title string `json:"title,omitempty"` // section 9.1
|
|
Description string `json:"description,omitempty"` // section 9.1
|
|
Default any `json:"default,omitempty"` // section 9.2
|
|
Deprecated bool `json:"deprecated,omitempty"` // section 9.3
|
|
ReadOnly bool `json:"readOnly,omitempty"` // section 9.4
|
|
WriteOnly bool `json:"writeOnly,omitempty"` // section 9.4
|
|
Examples []any `json:"examples,omitempty"` // section 9.5
|
|
|
|
Extras map[string]any `json:"-"`
|
|
|
|
// Special boolean representation of the Schema - section 4.3.2
|
|
boolean *bool
|
|
}
|
|
|
|
var (
|
|
// TrueSchema defines a schema with a true value
|
|
TrueSchema = &Schema{boolean: &[]bool{true}[0]}
|
|
// FalseSchema defines a schema with a false value
|
|
FalseSchema = &Schema{boolean: &[]bool{false}[0]}
|
|
)
|
|
|
|
// customSchemaImpl 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 customSchemaImpl interface {
|
|
JSONSchema() *Schema
|
|
}
|
|
|
|
// Function to be run after the schema has been generated.
|
|
// this will let you modify a schema afterwards
|
|
type extendSchemaImpl interface {
|
|
JSONSchemaExtend(*Schema)
|
|
}
|
|
|
|
var customType = reflect.TypeOf((*customSchemaImpl)(nil)).Elem()
|
|
var extendType = reflect.TypeOf((*extendSchemaImpl)(nil)).Elem()
|
|
|
|
// customSchemaGetFieldDocString
|
|
type customSchemaGetFieldDocString interface {
|
|
GetFieldDocString(fieldName string) string
|
|
}
|
|
|
|
type customGetFieldDocString func(fieldName string) string
|
|
|
|
var customStructGetFieldDocString = reflect.TypeOf((*customSchemaGetFieldDocString)(nil)).Elem()
|
|
|
|
// Reflect reflects to Schema from a value using the default Reflector
|
|
func Reflect(v any) *Schema {
|
|
return ReflectFromType(reflect.TypeOf(v))
|
|
}
|
|
|
|
// ReflectFromType generates root schema using the default Reflector
|
|
func ReflectFromType(t reflect.Type) *Schema {
|
|
r := &Reflector{}
|
|
return r.ReflectFromType(t)
|
|
}
|
|
|
|
// A Reflector reflects values into a Schema.
|
|
type Reflector struct {
|
|
// BaseSchemaID defines the URI that will be used as a base to determine Schema
|
|
// IDs for models. For example, a base Schema ID of `https://invopop.com/schemas`
|
|
// when defined with a struct called `User{}`, will result in a schema with an
|
|
// ID set to `https://invopop.com/schemas/user`.
|
|
//
|
|
// If no `BaseSchemaID` is provided, we'll take the type's complete package path
|
|
// and use that as a base instead. Set `Anonymous` to try if you do not want to
|
|
// include a schema ID.
|
|
BaseSchemaID ID
|
|
|
|
// Anonymous when true will hide the auto-generated Schema ID and provide what is
|
|
// known as an "anonymous schema". As a rule, this is not recommended.
|
|
Anonymous bool
|
|
|
|
// AssignAnchor when true will use the original struct's name as an anchor inside
|
|
// every definition, including the root schema. These can be useful for having a
|
|
// reference to the original struct's name in CamelCase instead of the snake-case used
|
|
// by default for URI compatibility.
|
|
//
|
|
// Anchors do not appear to be widely used out in the wild, so at this time the
|
|
// anchors themselves will not be used inside generated schema.
|
|
AssignAnchor bool
|
|
|
|
// AllowAdditionalProperties will cause the Reflector to generate a schema
|
|
// without additionalProperties set to 'false' for all struct types. This means
|
|
// the presence of additional keys in JSON objects will not cause validation
|
|
// to fail. Note said additional keys will simply be dropped when the
|
|
// validated JSON is unmarshaled.
|
|
AllowAdditionalProperties bool
|
|
|
|
// RequiredFromJSONSchemaTags will cause the Reflector to generate a schema
|
|
// that requires any key tagged with `jsonschema:required`, overriding the
|
|
// default of requiring any key *not* tagged with `json:,omitempty`.
|
|
RequiredFromJSONSchemaTags 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.
|
|
DoNotReference bool
|
|
|
|
// ExpandedStruct when true will include the reflected type's definition in the
|
|
// root as opposed to a definition with a reference.
|
|
ExpandedStruct bool
|
|
|
|
// FieldNameTag will change the tag used to get field names. json tags are used by default.
|
|
FieldNameTag string
|
|
|
|
// IgnoredTypes defines a slice of types that should be ignored in the schema,
|
|
// switching to just allowing additional properties instead.
|
|
IgnoredTypes []any
|
|
|
|
// 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.
|
|
// Reflected types will never be pointers, only underlying elements.
|
|
Lookup func(reflect.Type) ID
|
|
|
|
// Mapper is a function that can be used to map custom Go types to jsonschema schemas.
|
|
Mapper func(reflect.Type) *Schema
|
|
|
|
// Namer allows customizing of type names. The default is to use the type's name
|
|
// provided by the reflect package.
|
|
Namer func(reflect.Type) string
|
|
|
|
// KeyNamer allows customizing of key names.
|
|
// The default is to use the key's name as is, or the json tag if present.
|
|
// If a json tag is present, KeyNamer will receive the tag's name as an argument, not the original key name.
|
|
KeyNamer func(string) string
|
|
|
|
// 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/invopop/jsonschema.Reflector": "A Reflector reflects values into a Schema."}
|
|
//
|
|
// And Fields defined as:
|
|
//
|
|
// map[string]string{"github.com/invopop/jsonschema.Reflector.DoNotReference": "Do not reference definitions."}
|
|
//
|
|
// See also: AddGoComments
|
|
CommentMap map[string]string
|
|
}
|
|
|
|
// Reflect reflects to Schema from a value.
|
|
func (r *Reflector) Reflect(v any) *Schema {
|
|
return r.ReflectFromType(reflect.TypeOf(v))
|
|
}
|
|
|
|
// ReflectFromType generates root schema
|
|
func (r *Reflector) ReflectFromType(t reflect.Type) *Schema {
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem() // re-assign from pointer
|
|
}
|
|
|
|
name := r.typeName(t)
|
|
|
|
s := new(Schema)
|
|
definitions := Definitions{}
|
|
s.Definitions = definitions
|
|
bs := r.reflectTypeToSchemaWithID(definitions, t)
|
|
if r.ExpandedStruct {
|
|
*s = *definitions[name]
|
|
delete(definitions, name)
|
|
} else {
|
|
*s = *bs
|
|
}
|
|
|
|
// Attempt to set the schema ID
|
|
if !r.Anonymous && s.ID == EmptyID {
|
|
baseSchemaID := r.BaseSchemaID
|
|
if baseSchemaID == EmptyID {
|
|
id := ID("https://" + t.PkgPath())
|
|
if err := id.Validate(); err == nil {
|
|
// it's okay to silently ignore URL errors
|
|
baseSchemaID = id
|
|
}
|
|
}
|
|
if baseSchemaID != EmptyID {
|
|
s.ID = baseSchemaID.Add(ToSnakeCase(name))
|
|
}
|
|
}
|
|
|
|
s.Version = Version
|
|
if !r.DoNotReference {
|
|
s.Definitions = definitions
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// Definitions hold schema definitions.
|
|
// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26
|
|
// RFC draft-wright-json-schema-validation-00, section 5.26
|
|
type Definitions map[string]*Schema
|
|
|
|
// Available Go defined types for JSON Schema Validation.
|
|
// RFC draft-wright-json-schema-validation-00, section 7.3
|
|
var (
|
|
timeType = reflect.TypeOf(time.Time{}) // date-time RFC section 7.3.1
|
|
ipType = reflect.TypeOf(net.IP{}) // ipv4 and ipv6 RFC section 7.3.4, 7.3.5
|
|
uriType = reflect.TypeOf(url.URL{}) // uri RFC section 7.3.6
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
|
|
var protoEnumType = reflect.TypeOf((*protoEnum)(nil)).Elem()
|
|
|
|
// SetBaseSchemaID is a helper use to be able to set the reflectors base
|
|
// schema ID from a string as opposed to then ID instance.
|
|
func (r *Reflector) SetBaseSchemaID(id string) {
|
|
r.BaseSchemaID = ID(id)
|
|
}
|
|
|
|
func (r *Reflector) refOrReflectTypeToSchema(definitions Definitions, t reflect.Type) *Schema {
|
|
id := r.lookupID(t)
|
|
if id != EmptyID {
|
|
return &Schema{
|
|
Ref: id.String(),
|
|
}
|
|
}
|
|
|
|
// Already added to definitions?
|
|
if def := r.refDefinition(definitions, t); def != nil {
|
|
return def
|
|
}
|
|
|
|
return r.reflectTypeToSchemaWithID(definitions, t)
|
|
}
|
|
|
|
func (r *Reflector) reflectTypeToSchemaWithID(defs Definitions, t reflect.Type) *Schema {
|
|
s := r.reflectTypeToSchema(defs, t)
|
|
if s != nil {
|
|
if r.Lookup != nil {
|
|
id := r.Lookup(t)
|
|
if id != EmptyID {
|
|
s.ID = id
|
|
}
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) *Schema {
|
|
// only try to reflect non-pointers
|
|
if t.Kind() == reflect.Ptr {
|
|
return r.refOrReflectTypeToSchema(definitions, t.Elem())
|
|
}
|
|
|
|
// Do any pre-definitions exist?
|
|
if r.Mapper != nil {
|
|
if t := r.Mapper(t); t != nil {
|
|
return t
|
|
}
|
|
}
|
|
if rt := r.reflectCustomSchema(definitions, t); rt != nil {
|
|
return rt
|
|
}
|
|
|
|
// Prepare a base to which details can be added
|
|
st := new(Schema)
|
|
|
|
// jsonpb will marshal protobuf enum options as either strings or integers.
|
|
// It will unmarshal either.
|
|
if t.Implements(protoEnumType) {
|
|
st.OneOf = []*Schema{
|
|
{Type: "string"},
|
|
{Type: "integer"},
|
|
}
|
|
return st
|
|
}
|
|
|
|
// 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
|
|
if t == ipType {
|
|
// TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5
|
|
st.Type = "string"
|
|
st.Format = "ipv4"
|
|
return st
|
|
}
|
|
|
|
switch t.Kind() {
|
|
case reflect.Struct:
|
|
r.reflectStruct(definitions, t, st)
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
r.reflectSliceOrArray(definitions, t, st)
|
|
|
|
case reflect.Map:
|
|
r.reflectMap(definitions, t, st)
|
|
|
|
case reflect.Interface:
|
|
// empty
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
st.Type = "integer"
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
st.Type = "number"
|
|
|
|
case reflect.Bool:
|
|
st.Type = "boolean"
|
|
|
|
case reflect.String:
|
|
st.Type = "string"
|
|
|
|
default:
|
|
panic("unsupported type " + t.String())
|
|
}
|
|
|
|
r.reflectSchemaExtend(definitions, t, st)
|
|
|
|
// Always try to reference the definition which may have just been created
|
|
if def := r.refDefinition(definitions, t); def != nil {
|
|
return def
|
|
}
|
|
|
|
return st
|
|
}
|
|
|
|
func (r *Reflector) reflectCustomSchema(definitions Definitions, t reflect.Type) *Schema {
|
|
if t.Kind() == reflect.Ptr {
|
|
return r.reflectCustomSchema(definitions, t.Elem())
|
|
}
|
|
|
|
if t.Implements(customType) {
|
|
v := reflect.New(t)
|
|
o := v.Interface().(customSchemaImpl)
|
|
st := o.JSONSchema()
|
|
r.addDefinition(definitions, t, st)
|
|
if ref := r.refDefinition(definitions, t); ref != nil {
|
|
return ref
|
|
}
|
|
return st
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Reflector) reflectSchemaExtend(definitions Definitions, t reflect.Type, s *Schema) *Schema {
|
|
if t.Implements(extendType) {
|
|
v := reflect.New(t)
|
|
o := v.Interface().(extendSchemaImpl)
|
|
o.JSONSchemaExtend(s)
|
|
if ref := r.refDefinition(definitions, t); ref != nil {
|
|
return ref
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type, st *Schema) {
|
|
if t == rawMessageType {
|
|
return
|
|
}
|
|
|
|
r.addDefinition(definitions, t, st)
|
|
|
|
if st.Description == "" {
|
|
st.Description = r.lookupComment(t, "")
|
|
}
|
|
|
|
if t.Kind() == reflect.Array {
|
|
st.MinItems = t.Len()
|
|
st.MaxItems = st.MinItems
|
|
}
|
|
if t.Kind() == reflect.Slice && t.Elem() == byteSliceType.Elem() {
|
|
st.Type = "string"
|
|
// NOTE: ContentMediaType is not set here
|
|
st.ContentEncoding = "base64"
|
|
} else {
|
|
st.Type = "array"
|
|
st.Items = r.refOrReflectTypeToSchema(definitions, t.Elem())
|
|
}
|
|
}
|
|
|
|
func (r *Reflector) reflectMap(definitions Definitions, t reflect.Type, st *Schema) {
|
|
r.addDefinition(definitions, t, st)
|
|
|
|
st.Type = "object"
|
|
if st.Description == "" {
|
|
st.Description = r.lookupComment(t, "")
|
|
}
|
|
|
|
switch t.Key().Kind() {
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
st.PatternProperties = map[string]*Schema{
|
|
"^[0-9]+$": r.refOrReflectTypeToSchema(definitions, t.Elem()),
|
|
}
|
|
st.AdditionalProperties = FalseSchema
|
|
return
|
|
}
|
|
if t.Elem().Kind() != reflect.Interface {
|
|
st.AdditionalProperties = r.refOrReflectTypeToSchema(definitions, t.Elem())
|
|
}
|
|
}
|
|
|
|
// Reflects a struct to a JSON Schema type.
|
|
func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Schema) {
|
|
// Handle special types
|
|
switch t {
|
|
case timeType: // date-time RFC section 7.3.1
|
|
s.Type = "string"
|
|
s.Format = "date-time"
|
|
return
|
|
case uriType: // uri RFC section 7.3.6
|
|
s.Type = "string"
|
|
s.Format = "uri"
|
|
return
|
|
}
|
|
|
|
r.addDefinition(definitions, t, s)
|
|
s.Type = "object"
|
|
s.Properties = NewProperties()
|
|
s.OriginalPropertiesMapping = make(map[string]string)
|
|
s.Description = r.lookupComment(t, "")
|
|
if r.AssignAnchor {
|
|
s.Anchor = t.Name()
|
|
}
|
|
if !r.AllowAdditionalProperties && s.AdditionalProperties == nil {
|
|
s.AdditionalProperties = FalseSchema
|
|
}
|
|
|
|
ignored := false
|
|
for _, it := range r.IgnoredTypes {
|
|
if reflect.TypeOf(it) == t {
|
|
ignored = true
|
|
break
|
|
}
|
|
}
|
|
if !ignored {
|
|
r.reflectStructFields(s, definitions, t)
|
|
}
|
|
}
|
|
|
|
func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t reflect.Type) {
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
if t.Kind() != reflect.Struct {
|
|
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, originalName, shouldEmbed, required, nullable := r.reflectFieldName(f)
|
|
// if anonymous and exported type should be processed recursively
|
|
// current type should inherit properties of anonymous one
|
|
if name == "" {
|
|
if shouldEmbed {
|
|
r.reflectStructFields(st, definitions, f.Type)
|
|
}
|
|
return
|
|
}
|
|
|
|
property := r.refOrReflectTypeToSchema(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 = &Schema{
|
|
OneOf: []*Schema{
|
|
property,
|
|
{
|
|
Type: "null",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
st.Properties.Set(name, property)
|
|
st.OriginalPropertiesMapping[originalName] = name
|
|
if required {
|
|
st.Required = appendUniqueString(st.Required, name)
|
|
}
|
|
}
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
f := t.Field(i)
|
|
handleField(f)
|
|
}
|
|
if r.AdditionalFields != nil {
|
|
if af := r.AdditionalFields(t); af != nil {
|
|
for _, sf := range af {
|
|
handleField(sf)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func appendUniqueString(base []string, value string) []string {
|
|
for _, v := range base {
|
|
if v == value {
|
|
return base
|
|
}
|
|
}
|
|
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)
|
|
if name == "" {
|
|
return
|
|
}
|
|
definitions[name] = s
|
|
}
|
|
|
|
// refDefinition will provide a schema with a reference to an existing definition.
|
|
func (r *Reflector) refDefinition(definitions Definitions, t reflect.Type) *Schema {
|
|
if r.DoNotReference {
|
|
return nil
|
|
}
|
|
name := r.typeName(t)
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
if _, ok := definitions[name]; !ok {
|
|
return nil
|
|
}
|
|
return &Schema{
|
|
Ref: "#/$defs/" + name,
|
|
}
|
|
}
|
|
|
|
func (r *Reflector) lookupID(t reflect.Type) ID {
|
|
if r.Lookup != nil {
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
return r.Lookup(t)
|
|
|
|
}
|
|
return EmptyID
|
|
}
|
|
|
|
func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, propertyName string) {
|
|
t.Description = f.Tag.Get("jsonschema_description")
|
|
|
|
tags := splitOnUnescapedCommas(f.Tag.Get("jsonschema"))
|
|
tags = t.genericKeywords(tags, parent, propertyName)
|
|
|
|
switch t.Type {
|
|
case "string":
|
|
t.stringKeywords(tags)
|
|
case "number":
|
|
t.numericalKeywords(tags)
|
|
case "integer":
|
|
t.numericalKeywords(tags)
|
|
case "array":
|
|
t.arrayKeywords(tags)
|
|
case "boolean":
|
|
t.booleanKeywords(tags)
|
|
}
|
|
extras := strings.Split(f.Tag.Get("jsonschema_extras"), ",")
|
|
t.extraKeywords(extras)
|
|
}
|
|
|
|
// read struct tags for generic keywords
|
|
func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName string) []string { //nolint:gocyclo
|
|
unprocessed := make([]string, 0, len(tags))
|
|
for _, tag := range tags {
|
|
nameValue := strings.Split(tag, "=")
|
|
if len(nameValue) == 2 {
|
|
name, val := nameValue[0], nameValue[1]
|
|
switch name {
|
|
case "title":
|
|
t.Title = val
|
|
case "description":
|
|
t.Description = val
|
|
case "type":
|
|
t.Type = val
|
|
case "anchor":
|
|
t.Anchor = val
|
|
case "oneof_required":
|
|
var typeFound *Schema
|
|
for i := range parent.OneOf {
|
|
if parent.OneOf[i].Title == nameValue[1] {
|
|
typeFound = parent.OneOf[i]
|
|
}
|
|
}
|
|
if typeFound == nil {
|
|
typeFound = &Schema{
|
|
Title: nameValue[1],
|
|
Required: []string{},
|
|
}
|
|
parent.OneOf = append(parent.OneOf, typeFound)
|
|
}
|
|
typeFound.Required = append(typeFound.Required, propertyName)
|
|
case "anyof_required":
|
|
var typeFound *Schema
|
|
for i := range parent.AnyOf {
|
|
if parent.AnyOf[i].Title == nameValue[1] {
|
|
typeFound = parent.AnyOf[i]
|
|
}
|
|
}
|
|
if typeFound == nil {
|
|
typeFound = &Schema{
|
|
Title: nameValue[1],
|
|
Required: []string{},
|
|
}
|
|
parent.AnyOf = append(parent.AnyOf, typeFound)
|
|
}
|
|
typeFound.Required = append(typeFound.Required, propertyName)
|
|
case "oneof_ref":
|
|
subSchema := t
|
|
if t.Items != nil {
|
|
subSchema = t.Items
|
|
}
|
|
if subSchema.OneOf == nil {
|
|
subSchema.OneOf = make([]*Schema, 0, 1)
|
|
}
|
|
subSchema.Ref = ""
|
|
refs := strings.Split(nameValue[1], ";")
|
|
for _, r := range refs {
|
|
subSchema.OneOf = append(subSchema.OneOf, &Schema{
|
|
Ref: r,
|
|
})
|
|
}
|
|
case "oneof_type":
|
|
if t.OneOf == nil {
|
|
t.OneOf = make([]*Schema, 0, 1)
|
|
}
|
|
t.Type = ""
|
|
types := strings.Split(nameValue[1], ";")
|
|
for _, ty := range types {
|
|
t.OneOf = append(t.OneOf, &Schema{
|
|
Type: ty,
|
|
})
|
|
}
|
|
case "anyof_ref":
|
|
subSchema := t
|
|
if t.Items != nil {
|
|
subSchema = t.Items
|
|
}
|
|
if subSchema.AnyOf == nil {
|
|
subSchema.AnyOf = make([]*Schema, 0, 1)
|
|
}
|
|
subSchema.Ref = ""
|
|
refs := strings.Split(nameValue[1], ";")
|
|
for _, r := range refs {
|
|
subSchema.AnyOf = append(subSchema.AnyOf, &Schema{
|
|
Ref: r,
|
|
})
|
|
}
|
|
case "anyof_type":
|
|
if t.AnyOf == nil {
|
|
t.AnyOf = make([]*Schema, 0, 1)
|
|
}
|
|
t.Type = ""
|
|
types := strings.Split(nameValue[1], ";")
|
|
for _, ty := range types {
|
|
t.AnyOf = append(t.AnyOf, &Schema{
|
|
Type: ty,
|
|
})
|
|
}
|
|
default:
|
|
unprocessed = append(unprocessed, tag)
|
|
}
|
|
}
|
|
}
|
|
return unprocessed
|
|
}
|
|
|
|
// read struct tags for boolean type keywords
|
|
func (t *Schema) booleanKeywords(tags []string) {
|
|
for _, tag := range tags {
|
|
nameValue := strings.Split(tag, "=")
|
|
if len(nameValue) != 2 {
|
|
continue
|
|
}
|
|
name, val := nameValue[0], nameValue[1]
|
|
if name == "default" {
|
|
if val == "true" {
|
|
t.Default = true
|
|
} else if val == "false" {
|
|
t.Default = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// read struct tags for string type keywords
|
|
func (t *Schema) stringKeywords(tags []string) {
|
|
for _, tag := range tags {
|
|
nameValue := strings.Split(tag, "=")
|
|
if len(nameValue) == 2 {
|
|
name, val := nameValue[0], nameValue[1]
|
|
switch name {
|
|
case "minLength":
|
|
i, _ := strconv.Atoi(val)
|
|
t.MinLength = i
|
|
case "maxLength":
|
|
i, _ := strconv.Atoi(val)
|
|
t.MaxLength = i
|
|
case "pattern":
|
|
t.Pattern = val
|
|
case "format":
|
|
switch val {
|
|
case "date-time", "email", "hostname", "ipv4", "ipv6", "uri", "uuid":
|
|
t.Format = val
|
|
}
|
|
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":
|
|
t.Examples = append(t.Examples, val)
|
|
case "enum":
|
|
t.Enum = append(t.Enum, val)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// read struct tags for numerical type keywords
|
|
func (t *Schema) numericalKeywords(tags []string) {
|
|
for _, tag := range tags {
|
|
nameValue := strings.Split(tag, "=")
|
|
if len(nameValue) == 2 {
|
|
name, val := nameValue[0], nameValue[1]
|
|
switch name {
|
|
case "multipleOf":
|
|
t.MultipleOf, _ = toJSONNumber(val)
|
|
case "minimum":
|
|
t.Minimum, _ = toJSONNumber(val)
|
|
case "maximum":
|
|
t.Maximum, _ = toJSONNumber(val)
|
|
case "exclusiveMaximum":
|
|
t.ExclusiveMaximum, _ = toJSONNumber(val)
|
|
case "exclusiveMinimum":
|
|
t.ExclusiveMinimum, _ = toJSONNumber(val)
|
|
case "default":
|
|
if num, ok := toJSONNumber(val); ok {
|
|
t.Default = num
|
|
}
|
|
case "example":
|
|
if num, ok := toJSONNumber(val); ok {
|
|
t.Examples = append(t.Examples, num)
|
|
}
|
|
case "enum":
|
|
if num, ok := toJSONNumber(val); ok {
|
|
t.Enum = append(t.Enum, num)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// read struct tags for object type keywords
|
|
// func (t *Type) objectKeywords(tags []string) {
|
|
// for _, tag := range tags{
|
|
// nameValue := strings.Split(tag, "=")
|
|
// name, val := nameValue[0], nameValue[1]
|
|
// switch name{
|
|
// case "dependencies":
|
|
// t.Dependencies = val
|
|
// break;
|
|
// case "patternProperties":
|
|
// t.PatternProperties = val
|
|
// break;
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// read struct tags for array type keywords
|
|
func (t *Schema) arrayKeywords(tags []string) {
|
|
var defaultValues []any
|
|
|
|
unprocessed := make([]string, 0, len(tags))
|
|
for _, tag := range tags {
|
|
nameValue := strings.Split(tag, "=")
|
|
if len(nameValue) == 2 {
|
|
name, val := nameValue[0], nameValue[1]
|
|
switch name {
|
|
case "minItems":
|
|
i, _ := strconv.Atoi(val)
|
|
t.MinItems = i
|
|
case "maxItems":
|
|
i, _ := strconv.Atoi(val)
|
|
t.MaxItems = i
|
|
case "uniqueItems":
|
|
t.UniqueItems = true
|
|
case "default":
|
|
defaultValues = append(defaultValues, val)
|
|
case "format":
|
|
t.Items.Format = val
|
|
case "pattern":
|
|
t.Items.Pattern = val
|
|
default:
|
|
unprocessed = append(unprocessed, tag) // left for further processing by underlying type
|
|
}
|
|
}
|
|
}
|
|
if len(defaultValues) > 0 {
|
|
t.Default = defaultValues
|
|
}
|
|
|
|
if len(unprocessed) == 0 {
|
|
// we don't have anything else to process
|
|
return
|
|
}
|
|
|
|
switch t.Items.Type {
|
|
case "string":
|
|
t.Items.stringKeywords(unprocessed)
|
|
case "number":
|
|
t.Items.numericalKeywords(unprocessed)
|
|
case "integer":
|
|
t.Items.numericalKeywords(unprocessed)
|
|
case "array":
|
|
// explicitly don't support traversal for the [][]..., as it's unclear where the array tags belong
|
|
case "boolean":
|
|
t.Items.booleanKeywords(unprocessed)
|
|
}
|
|
}
|
|
|
|
func (t *Schema) extraKeywords(tags []string) {
|
|
for _, tag := range tags {
|
|
nameValue := strings.SplitN(tag, "=", 2)
|
|
if len(nameValue) == 2 {
|
|
t.setExtra(nameValue[0], nameValue[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Schema) setExtra(key, val string) {
|
|
if t.Extras == nil {
|
|
t.Extras = map[string]any{}
|
|
}
|
|
if existingVal, ok := t.Extras[key]; ok {
|
|
switch existingVal := existingVal.(type) {
|
|
case string:
|
|
t.Extras[key] = []string{existingVal, val}
|
|
case []string:
|
|
t.Extras[key] = append(existingVal, val)
|
|
case int:
|
|
t.Extras[key], _ = strconv.Atoi(val)
|
|
case bool:
|
|
t.Extras[key] = (val == "true" || val == "t")
|
|
}
|
|
} else {
|
|
switch key {
|
|
case "minimum":
|
|
t.Extras[key], _ = strconv.Atoi(val)
|
|
default:
|
|
var x any
|
|
if val == "true" {
|
|
x = true
|
|
} else if val == "false" {
|
|
x = false
|
|
} else {
|
|
x = val
|
|
}
|
|
t.Extras[key] = x
|
|
}
|
|
}
|
|
}
|
|
|
|
func requiredFromJSONTags(tags []string, val *bool) {
|
|
if ignoredByJSONTags(tags) {
|
|
return
|
|
}
|
|
|
|
for _, tag := range tags[1:] {
|
|
if tag == "omitempty" {
|
|
*val = false
|
|
return
|
|
}
|
|
}
|
|
*val = true
|
|
}
|
|
|
|
func requiredFromJSONSchemaTags(tags []string, val *bool) {
|
|
if ignoredByJSONSchemaTags(tags) {
|
|
return
|
|
}
|
|
for _, tag := range tags {
|
|
if tag == "required" {
|
|
*val = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func nullableFromJSONSchemaTags(tags []string) bool {
|
|
if ignoredByJSONSchemaTags(tags) {
|
|
return false
|
|
}
|
|
for _, tag := range tags {
|
|
if tag == "nullable" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func ignoredByJSONTags(tags []string) bool {
|
|
return tags[0] == "-"
|
|
}
|
|
|
|
func ignoredByJSONSchemaTags(tags []string) bool {
|
|
return tags[0] == "-"
|
|
}
|
|
|
|
// toJSONNumber converts string to *json.Number.
|
|
// It'll aso return whether the number is valid.
|
|
func toJSONNumber(s string) (json.Number, bool) {
|
|
num := json.Number(s)
|
|
if _, err := num.Int64(); err == nil {
|
|
return num, true
|
|
}
|
|
if _, err := num.Float64(); err == nil {
|
|
return num, true
|
|
}
|
|
return json.Number(""), false
|
|
}
|
|
|
|
func (r *Reflector) fieldNameTag() string {
|
|
if r.FieldNameTag != "" {
|
|
return r.FieldNameTag
|
|
}
|
|
return "json"
|
|
}
|
|
|
|
func (r *Reflector) reflectFieldName(f reflect.StructField) (string, string, bool, bool, bool) {
|
|
jsonTagString := f.Tag.Get(r.fieldNameTag())
|
|
jsonTags := strings.Split(jsonTagString, ",")
|
|
|
|
if ignoredByJSONTags(jsonTags) {
|
|
return "", "", false, false, false
|
|
}
|
|
|
|
schemaTags := strings.Split(f.Tag.Get("jsonschema"), ",")
|
|
if ignoredByJSONSchemaTags(schemaTags) {
|
|
return "", "", false, false, false
|
|
}
|
|
|
|
var required bool
|
|
if !r.RequiredFromJSONSchemaTags {
|
|
requiredFromJSONTags(jsonTags, &required)
|
|
}
|
|
requiredFromJSONSchemaTags(schemaTags, &required)
|
|
|
|
nullable := nullableFromJSONSchemaTags(schemaTags)
|
|
|
|
if f.Anonymous && jsonTags[0] == "" {
|
|
// As per JSON Marshal rules, anonymous structs are inherited
|
|
if f.Type.Kind() == reflect.Struct {
|
|
return "", "", true, false, false
|
|
}
|
|
|
|
// As per JSON Marshal rules, anonymous pointer to structs are inherited
|
|
if f.Type.Kind() == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct {
|
|
return "", "", true, false, false
|
|
}
|
|
}
|
|
|
|
// Try to determine the name from the different combos
|
|
name := f.Name
|
|
originalName := f.Name
|
|
if jsonTags[0] != "" {
|
|
name = jsonTags[0]
|
|
}
|
|
if !f.Anonymous && f.PkgPath != "" {
|
|
// field not anonymous and not export has no export name
|
|
name = ""
|
|
} else if r.KeyNamer != nil {
|
|
name = r.KeyNamer(name)
|
|
}
|
|
|
|
return name, originalName, false, required, nullable
|
|
}
|
|
|
|
// UnmarshalJSON is used to parse a schema object or boolean.
|
|
func (t *Schema) UnmarshalJSON(data []byte) error {
|
|
if bytes.Equal(data, []byte("true")) {
|
|
*t = *TrueSchema
|
|
return nil
|
|
} else if bytes.Equal(data, []byte("false")) {
|
|
*t = *FalseSchema
|
|
return nil
|
|
}
|
|
type SchemaAlt Schema
|
|
aux := &struct {
|
|
*SchemaAlt
|
|
}{
|
|
SchemaAlt: (*SchemaAlt)(t),
|
|
}
|
|
return json.Unmarshal(data, aux)
|
|
}
|
|
|
|
// MarshalJSON is used to serialize a schema object or boolean.
|
|
func (t *Schema) MarshalJSON() ([]byte, error) {
|
|
if t.boolean != nil {
|
|
if *t.boolean {
|
|
return []byte("true"), nil
|
|
}
|
|
return []byte("false"), nil
|
|
}
|
|
if reflect.DeepEqual(&Schema{}, t) {
|
|
// Don't bother returning empty schemas
|
|
return []byte("true"), nil
|
|
}
|
|
type SchemaAlt Schema
|
|
b, err := json.Marshal((*SchemaAlt)(t))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if t.Extras == nil || len(t.Extras) == 0 {
|
|
return b, nil
|
|
}
|
|
m, err := json.Marshal(t.Extras)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(b) == 2 {
|
|
return m, nil
|
|
}
|
|
b[len(b)-1] = ','
|
|
return append(b, m[1:]...), nil
|
|
}
|
|
|
|
func (r *Reflector) typeName(t reflect.Type) string {
|
|
if r.Namer != nil {
|
|
if name := r.Namer(t); name != "" {
|
|
return name
|
|
}
|
|
}
|
|
return t.Name()
|
|
}
|
|
|
|
// Split on commas that are not preceded by `\`.
|
|
// This way, we prevent splitting regexes
|
|
func splitOnUnescapedCommas(tagString string) []string {
|
|
ret := make([]string, 0)
|
|
separated := strings.Split(tagString, ",")
|
|
ret = append(ret, separated[0])
|
|
i := 0
|
|
for _, nextTag := range separated[1:] {
|
|
if len(ret[i]) == 0 {
|
|
ret = append(ret, nextTag)
|
|
i++
|
|
continue
|
|
}
|
|
|
|
if ret[i][len(ret[i])-1] == '\\' {
|
|
ret[i] = ret[i][:len(ret[i])-1] + "," + nextTag
|
|
} else {
|
|
ret = append(ret, nextTag)
|
|
i++
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
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)
|
|
}
|