Add JSON schema support
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type Builder interface {
|
||||
Context() context.Context
|
||||
Describe(valueType reflect.Type) (*Node, error)
|
||||
FlattenStruct(node *Node, structType reflect.Type) error
|
||||
Define(name string, build func() (*Node, error)) (*Node, error)
|
||||
}
|
||||
|
||||
type Describer interface {
|
||||
DescribeSchema(builder Builder) (*Node, error)
|
||||
}
|
||||
|
||||
type UnionVariant struct {
|
||||
Value any
|
||||
StructType reflect.Type
|
||||
TypeOptional bool
|
||||
}
|
||||
|
||||
func DiscriminatedUnion(builder Builder, discriminatorKey string, discriminatorRequired bool, variants []UnionVariant, buildBase func(variant *Node) error) (*Node, error) {
|
||||
variantNodes := make([]*Node, 0, len(variants))
|
||||
for _, variant := range variants {
|
||||
variantNode := StrictObject()
|
||||
if variant.TypeOptional {
|
||||
stringValue, isString := variant.Value.(string)
|
||||
if !isString {
|
||||
return nil, E.New("optional discriminator requires a string value")
|
||||
}
|
||||
variantNode.Properties.Put(discriminatorKey, StringEnum(stringValue, ""))
|
||||
} else {
|
||||
variantNode.Properties.Put(discriminatorKey, &Node{Const: variant.Value})
|
||||
}
|
||||
if discriminatorRequired || !variant.TypeOptional {
|
||||
variantNode.Required = append(variantNode.Required, discriminatorKey)
|
||||
}
|
||||
if buildBase != nil {
|
||||
err := buildBase(variantNode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if variant.StructType != nil {
|
||||
err := builder.FlattenStruct(variantNode, variant.StructType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
variantNodes = append(variantNodes, variantNode)
|
||||
}
|
||||
return OneOf(variantNodes...), nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdjson "encoding/json"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func Generate(ctx context.Context, rootType reflect.Type) ([]byte, error) {
|
||||
g := &generator{
|
||||
ctx: ctx,
|
||||
defs: make(map[string]*Node),
|
||||
defTypes: make(map[reflect.Type]string),
|
||||
}
|
||||
root, err := g.Describe(rootType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root.Defs = g.sortedDefs()
|
||||
content, err := stdjson.MarshalIndent(root, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(content, '\n'), nil
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
stdjson "encoding/json"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
var (
|
||||
jsonUnmarshalerType = reflect.TypeFor[stdjson.Unmarshaler]()
|
||||
contextUnmarshalerType = reflect.TypeFor[json.ContextUnmarshaler]()
|
||||
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
|
||||
|
||||
durationType = reflect.TypeFor[badoption.Duration]()
|
||||
addrType = reflect.TypeFor[badoption.Addr]()
|
||||
prefixType = reflect.TypeFor[badoption.Prefix]()
|
||||
prefixableType = reflect.TypeFor[badoption.Prefixable]()
|
||||
httpHeaderType = reflect.TypeFor[badoption.HTTPHeader]()
|
||||
memoryBytesType = reflect.TypeFor[byteformats.MemoryBytes]()
|
||||
networkBytesCompatType = reflect.TypeFor[byteformats.NetworkBytesCompat]()
|
||||
)
|
||||
|
||||
type generator struct {
|
||||
ctx context.Context
|
||||
defs map[string]*Node
|
||||
defTypes map[reflect.Type]string
|
||||
path []string
|
||||
}
|
||||
|
||||
func (g *generator) Context() context.Context {
|
||||
return g.ctx
|
||||
}
|
||||
|
||||
func (g *generator) Define(name string, build func() (*Node, error)) (*Node, error) {
|
||||
_, exists := g.defs[name]
|
||||
if exists {
|
||||
return RefNode(name), nil
|
||||
}
|
||||
g.defs[name] = nil
|
||||
node, err := build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.defs[name] = node
|
||||
return RefNode(name), nil
|
||||
}
|
||||
|
||||
func implementationOf[T any](valueType reflect.Type) (T, bool) {
|
||||
interfaceType := reflect.TypeFor[T]()
|
||||
if !valueType.Implements(interfaceType) && !reflect.PointerTo(valueType).Implements(interfaceType) {
|
||||
var zeroValue T
|
||||
return zeroValue, false
|
||||
}
|
||||
return reflect.New(valueType).Interface().(T), true
|
||||
}
|
||||
|
||||
func (g *generator) Describe(valueType reflect.Type) (*Node, error) {
|
||||
for valueType.Kind() == reflect.Pointer {
|
||||
valueType = valueType.Elem()
|
||||
}
|
||||
describer, described := implementationOf[Describer](valueType)
|
||||
if described {
|
||||
return describer.DescribeSchema(g)
|
||||
}
|
||||
switch valueType {
|
||||
case durationType:
|
||||
return g.Define("Duration", func() (*Node, error) {
|
||||
return DurationNode(), nil
|
||||
})
|
||||
case addrType, prefixType, prefixableType:
|
||||
return StringNode(), nil
|
||||
case httpHeaderType:
|
||||
return g.Define("HTTPHeader", func() (*Node, error) {
|
||||
return &Node{Type: "object", AdditionalProperties: ListableOf(StringNode())}, nil
|
||||
})
|
||||
case memoryBytesType, networkBytesCompatType:
|
||||
return AnyOf(UnsignedNode(64), StringNode()), nil
|
||||
}
|
||||
if isListable(valueType) {
|
||||
elementNode, err := g.Describe(valueType.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ListableOf(elementNode), nil
|
||||
}
|
||||
if isTypedMap(valueType) {
|
||||
return g.typedMapNode(valueType)
|
||||
}
|
||||
pointerType := reflect.PointerTo(valueType)
|
||||
if pointerType.Implements(jsonUnmarshalerType) || pointerType.Implements(contextUnmarshalerType) {
|
||||
return nil, E.New("unmapped custom JSON type ", valueType.String(), " at ", strings.Join(g.path, "."))
|
||||
}
|
||||
if pointerType.Implements(textUnmarshalerType) {
|
||||
return StringNode(), nil
|
||||
}
|
||||
switch valueType.Kind() {
|
||||
case reflect.Struct:
|
||||
if valueType.Name() == "" {
|
||||
node := StrictObject()
|
||||
err := g.FlattenStruct(node, valueType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
return g.Define(g.defNameFor(valueType), func() (*Node, error) {
|
||||
node := StrictObject()
|
||||
err := g.FlattenStruct(node, valueType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return node, nil
|
||||
})
|
||||
case reflect.Slice, reflect.Array:
|
||||
if valueType.Kind() == reflect.Slice && valueType.Elem().Kind() == reflect.Uint8 {
|
||||
// encoding/json accepts both base64 strings and number arrays.
|
||||
return AnyOf(StringNode(), &Node{Type: "array", Items: UnsignedNode(8)}), nil
|
||||
}
|
||||
elementNode, err := g.Describe(valueType.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Node{Type: "array", Items: elementNode}, nil
|
||||
case reflect.Map:
|
||||
if valueType.Key().Kind() != reflect.String {
|
||||
return nil, E.New("unsupported map key type ", valueType.String(), " at ", strings.Join(g.path, "."))
|
||||
}
|
||||
valueNode, err := g.Describe(valueType.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Node{Type: "object", AdditionalProperties: valueNode}, nil
|
||||
case reflect.Bool:
|
||||
return BooleanNode(), nil
|
||||
case reflect.String:
|
||||
return StringNode(), nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return IntegerNode(), nil
|
||||
case reflect.Uint, reflect.Uint64:
|
||||
return UnsignedNode(64), nil
|
||||
case reflect.Uint8:
|
||||
return UnsignedNode(8), nil
|
||||
case reflect.Uint16:
|
||||
return UnsignedNode(16), nil
|
||||
case reflect.Uint32:
|
||||
return UnsignedNode(32), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return &Node{Type: "number"}, nil
|
||||
default:
|
||||
return nil, E.New("unsupported kind ", valueType.Kind().String(), " for ", valueType.String(), " at ", strings.Join(g.path, "."))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generator) defNameFor(fieldType reflect.Type) string {
|
||||
existingName, loaded := g.defTypes[fieldType]
|
||||
if loaded {
|
||||
return existingName
|
||||
}
|
||||
name := fieldType.Name()
|
||||
for otherType, otherName := range g.defTypes {
|
||||
if otherName == name && otherType != fieldType {
|
||||
name = pathBase(fieldType.PkgPath()) + "." + name
|
||||
break
|
||||
}
|
||||
}
|
||||
g.defTypes[fieldType] = name
|
||||
return name
|
||||
}
|
||||
|
||||
func pathBase(packagePath string) string {
|
||||
index := strings.LastIndexByte(packagePath, '/')
|
||||
if index < 0 {
|
||||
return packagePath
|
||||
}
|
||||
return packagePath[index+1:]
|
||||
}
|
||||
|
||||
// FlattenStruct merges the JSON fields of structType into node, following the
|
||||
// same flattening semantics as badjson.MarshallObjects / anonymous embedding.
|
||||
func (g *generator) FlattenStruct(node *Node, structType reflect.Type) error {
|
||||
for structType.Kind() == reflect.Pointer {
|
||||
structType = structType.Elem()
|
||||
}
|
||||
if structType.Kind() != reflect.Struct {
|
||||
return E.New("cannot flatten non-struct type ", structType.String(), " at ", strings.Join(g.path, "."))
|
||||
}
|
||||
for i := range structType.NumField() {
|
||||
field := structType.Field(i)
|
||||
if !field.IsExported() && !field.Anonymous {
|
||||
continue
|
||||
}
|
||||
tagValue := field.Tag.Get("json")
|
||||
tagName, _, _ := strings.Cut(tagValue, ",")
|
||||
if tagName == "-" {
|
||||
continue
|
||||
}
|
||||
fieldType := field.Type
|
||||
for fieldType.Kind() == reflect.Pointer {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
if field.Tag.Get("schema") == "omit" {
|
||||
continue
|
||||
}
|
||||
if field.Anonymous && tagName == "" {
|
||||
err := g.FlattenStruct(node, fieldType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if tagName == "" {
|
||||
tagName = field.Name
|
||||
}
|
||||
enumTag := field.Tag.Get("enum")
|
||||
examplesTag := field.Tag.Get("examples")
|
||||
referenceTag := field.Tag.Get("reference")
|
||||
g.path = append(g.path, structType.Name()+"."+tagName)
|
||||
var fieldNode *Node
|
||||
var err error
|
||||
if enumTag != "" || examplesTag != "" || referenceTag != "" {
|
||||
fieldNode, err = taggedFieldNode(fieldType, enumTag, examplesTag, referenceTag)
|
||||
} else {
|
||||
fieldNode, err = g.Describe(fieldType)
|
||||
}
|
||||
g.path = g.path[:len(g.path)-1]
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node.Properties.Put(tagName, fieldNode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func taggedFieldNode(fieldType reflect.Type, enumTag string, examplesTag string, referenceTag string) (*Node, error) {
|
||||
elementType := fieldType
|
||||
for elementType.Kind() == reflect.Pointer {
|
||||
elementType = elementType.Elem()
|
||||
}
|
||||
listable := isListable(fieldType)
|
||||
plainSlice := !listable && elementType.Kind() == reflect.Slice && elementType.Elem().Kind() == reflect.String
|
||||
if listable {
|
||||
elementType = fieldType.Elem()
|
||||
} else if plainSlice {
|
||||
elementType = elementType.Elem()
|
||||
}
|
||||
var element *Node
|
||||
var err error
|
||||
if enumTag != "" {
|
||||
element, err = taggedValueNode(elementType, strings.Split(enumTag, ","))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
element, err = taggedValueNode(elementType, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if examplesTag != "" {
|
||||
examples, parseErr := taggedValues(elementType, strings.Split(examplesTag, ","))
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
element.Examples = examples
|
||||
}
|
||||
if referenceTag != "" {
|
||||
if elementType.Kind() != reflect.String {
|
||||
return nil, E.New("reference tags require a string field, got ", fieldType.String())
|
||||
}
|
||||
element.TagReference = referenceTag
|
||||
}
|
||||
if listable {
|
||||
return ListableOf(element), nil
|
||||
}
|
||||
if plainSlice {
|
||||
return &Node{Type: "array", Items: element}, nil
|
||||
}
|
||||
return element, nil
|
||||
}
|
||||
|
||||
func taggedValueNode(fieldType reflect.Type, values []string) (*Node, error) {
|
||||
switch fieldType.Kind() {
|
||||
case reflect.String:
|
||||
node := StringNode()
|
||||
for _, value := range values {
|
||||
node.Enum = append(node.Enum, value)
|
||||
}
|
||||
return node, nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
node := IntegerNode()
|
||||
enumValues, err := taggedValues(fieldType, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.Enum = enumValues
|
||||
return node, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
node := UnsignedNode(fieldType.Bits())
|
||||
enumValues, err := taggedValues(fieldType, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.Enum = enumValues
|
||||
return node, nil
|
||||
default:
|
||||
node := StringNode()
|
||||
for _, value := range values {
|
||||
err := unmarshalTaggedValue(fieldType, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.Enum = append(node.Enum, value)
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalTaggedValue(fieldType reflect.Type, value string) error {
|
||||
err := json.Unmarshal([]byte(strconv.Quote(value)), reflect.New(fieldType).Interface())
|
||||
if err != nil {
|
||||
return E.Cause(err, "unmarshal tagged value ", value, " as ", fieldType.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func taggedValues(fieldType reflect.Type, values []string) ([]any, error) {
|
||||
result := make([]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
switch fieldType.Kind() {
|
||||
case reflect.String:
|
||||
result = append(result, value)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
integerValue, err := strconv.ParseInt(value, 10, fieldType.Bits())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse enum value ", value, " for ", fieldType.String())
|
||||
}
|
||||
result = append(result, integerValue)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
unsignedValue, err := strconv.ParseUint(value, 10, fieldType.Bits())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse enum value ", value, " for ", fieldType.String())
|
||||
}
|
||||
result = append(result, unsignedValue)
|
||||
default:
|
||||
err := unmarshalTaggedValue(fieldType, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isListable(fieldType reflect.Type) bool {
|
||||
return fieldType.Kind() == reflect.Slice &&
|
||||
fieldType.PkgPath() == "github.com/sagernet/sing/common/json/badoption" &&
|
||||
strings.HasPrefix(fieldType.Name(), "Listable[")
|
||||
}
|
||||
|
||||
func isTypedMap(fieldType reflect.Type) bool {
|
||||
return fieldType.Kind() == reflect.Struct &&
|
||||
fieldType.PkgPath() == "github.com/sagernet/sing/common/json/badjson" &&
|
||||
strings.HasPrefix(fieldType.Name(), "TypedMap[")
|
||||
}
|
||||
|
||||
func (g *generator) typedMapNode(fieldType reflect.Type) (*Node, error) {
|
||||
mapField, found := fieldType.FieldByName("Map")
|
||||
if !found {
|
||||
return nil, E.New("unexpected TypedMap layout: missing Map in ", fieldType.String())
|
||||
}
|
||||
rawMapField, found := mapField.Type.FieldByName("rawMap")
|
||||
if !found {
|
||||
return nil, E.New("unexpected TypedMap layout: missing rawMap in ", fieldType.String())
|
||||
}
|
||||
keyType := rawMapField.Type.Key()
|
||||
elementValueField, found := rawMapField.Type.Elem().Elem().FieldByName("Value")
|
||||
if !found {
|
||||
return nil, E.New("unexpected TypedMap layout: missing element value in ", fieldType.String())
|
||||
}
|
||||
entryValueField, found := elementValueField.Type.FieldByName("Value")
|
||||
if !found {
|
||||
return nil, E.New("unexpected TypedMap layout: missing entry value in ", fieldType.String())
|
||||
}
|
||||
valueNode, err := g.Describe(entryValueField.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node := &Node{Type: "object", AdditionalProperties: valueNode}
|
||||
if keyType.Kind() != reflect.String || keyType.PkgPath() != "" {
|
||||
keyNode, keyErr := g.Describe(keyType)
|
||||
if keyErr != nil {
|
||||
return nil, keyErr
|
||||
}
|
||||
node.PropertyNames = keyNode
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (g *generator) sortedDefs() *badjson.TypedMap[string, *Node] {
|
||||
names := make([]string, 0, len(g.defs))
|
||||
for name := range g.defs {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.Sort(names)
|
||||
result := new(badjson.TypedMap[string, *Node])
|
||||
for _, name := range names {
|
||||
result.Put(name, g.defs[name])
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
SchemaURI string `json:"$schema,omitempty"`
|
||||
ID string `json:"$id,omitempty"`
|
||||
Ref string `json:"$ref,omitempty"`
|
||||
Type any `json:"type,omitempty"`
|
||||
Const any `json:"const,omitempty"`
|
||||
Enum []any `json:"enum,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
Minimum *int64 `json:"minimum,omitempty"`
|
||||
Maximum *uint64 `json:"maximum,omitempty"`
|
||||
Items *Node `json:"items,omitempty"`
|
||||
Properties *badjson.TypedMap[string, *Node] `json:"properties,omitempty"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
PropertyNames *Node `json:"propertyNames,omitempty"`
|
||||
AdditionalProperties any `json:"additionalProperties,omitempty"`
|
||||
UnevaluatedProperties any `json:"unevaluatedProperties,omitempty"`
|
||||
AllOf []*Node `json:"allOf,omitempty"`
|
||||
AnyOf []*Node `json:"anyOf,omitempty"`
|
||||
OneOf []*Node `json:"oneOf,omitempty"`
|
||||
Deprecated bool `json:"deprecated,omitempty"`
|
||||
Examples []any `json:"examples,omitempty"`
|
||||
TagReference string `json:"x-tag-reference,omitempty"`
|
||||
Defs *badjson.TypedMap[string, *Node] `json:"$defs,omitempty"`
|
||||
}
|
||||
|
||||
func StrictObject() *Node {
|
||||
return &Node{
|
||||
Type: "object",
|
||||
Properties: new(badjson.TypedMap[string, *Node]),
|
||||
AdditionalProperties: false,
|
||||
}
|
||||
}
|
||||
|
||||
func LooseObject() *Node {
|
||||
return &Node{
|
||||
Type: "object",
|
||||
Properties: new(badjson.TypedMap[string, *Node]),
|
||||
}
|
||||
}
|
||||
|
||||
func StringNode() *Node {
|
||||
return &Node{Type: "string"}
|
||||
}
|
||||
|
||||
func TagReferenceNode(kind string) *Node {
|
||||
return &Node{Type: "string", TagReference: kind}
|
||||
}
|
||||
|
||||
func BooleanNode() *Node {
|
||||
return &Node{Type: "boolean"}
|
||||
}
|
||||
|
||||
func IntegerNode() *Node {
|
||||
return &Node{Type: "integer"}
|
||||
}
|
||||
|
||||
func UnsignedNode(bits int) *Node {
|
||||
minimumValue := int64(0)
|
||||
node := &Node{Type: "integer", Minimum: &minimumValue}
|
||||
if bits < 64 {
|
||||
maximumValue := uint64(1)<<bits - 1
|
||||
node.Maximum = &maximumValue
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
const durationPattern = `^[-+]?(((\d+(\.\d*)?|\.\d+)(ns|us|µs|μs|ms|s|m|h|d))+|0)$`
|
||||
|
||||
func DurationNode() *Node {
|
||||
return &Node{Type: "string", Pattern: durationPattern}
|
||||
}
|
||||
|
||||
func StringEnum(values ...string) *Node {
|
||||
anyValues := make([]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
anyValues = append(anyValues, value)
|
||||
}
|
||||
return &Node{Type: "string", Enum: anyValues}
|
||||
}
|
||||
|
||||
func StringConst(value string) *Node {
|
||||
return &Node{Const: value}
|
||||
}
|
||||
|
||||
func AnyOf(nodes ...*Node) *Node {
|
||||
return &Node{AnyOf: nodes}
|
||||
}
|
||||
|
||||
func OneOf(nodes ...*Node) *Node {
|
||||
return &Node{OneOf: nodes}
|
||||
}
|
||||
|
||||
func ListableOf(element *Node) *Node {
|
||||
return AnyOf(element, &Node{Type: "array", Items: element})
|
||||
}
|
||||
|
||||
func RefNode(name string) *Node {
|
||||
return &Node{Ref: "#/$defs/" + name}
|
||||
}
|
||||
Reference in New Issue
Block a user