diff --git a/datamodel/high/base/dynamic_value.go b/datamodel/high/base/dynamic_value.go new file mode 100644 index 0000000..b561a8d --- /dev/null +++ b/datamodel/high/base/dynamic_value.go @@ -0,0 +1,69 @@ +// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package base + +import ( + "github.com/pb33f/libopenapi/datamodel/high" + "gopkg.in/yaml.v3" + "reflect" +) + +// DynamicValue is used to hold multiple possible values for a schema property. There are two values, a left +// value (A) and a right value (B). The left value (A) is a 3.0 schema property value, the right value (B) is a 3.1 +// schema value. +// +// OpenAPI 3.1 treats a Schema as a real JSON schema, which means some properties become incompatible, or others +// now support more than one primitive type or structure. +// The N value is a bit to make it each to know which value (A or B) is used, this prevents having to +// if/else on the value to determine which one is set. +type DynamicValue[A any, B any] struct { + N int // 0 == A, 1 == B + A A + B B +} + +// IsA will return true if the 'A' or left value is set. (OpenAPI 3) +func (d *DynamicValue[A, B]) IsA() bool { + return d.N == 0 +} + +// IsB will return true if the 'B' or right value is set (OpenAPI 3.1) +func (d *DynamicValue[A, B]) IsB() bool { + return d.N == 1 +} + +func (d *DynamicValue[A, B]) Render() ([]byte, error) { + return yaml.Marshal(d) +} + +// MarshalYAML will create a ready to render YAML representation of the DynamicValue object. +func (d *DynamicValue[A, B]) MarshalYAML() (interface{}, error) { + if d == nil { + return nil, nil + } + // this is a custom renderer, we can't use the NodeBuilder out of the gate. + var n yaml.Node + var err error + var value any + + if d.IsA() { + value = d.A + } + if d.IsB() { + value = d.B + } + to := reflect.TypeOf(value) + switch to.Kind() { + + case reflect.Ptr: + if r, ok := value.(high.Renderable); ok { + return r.MarshalYAML() + } + case reflect.Bool: + _ = n.Encode(value.(bool)) + case reflect.Int64: + _ = n.Encode(value.(int64)) + } + return n, err +} diff --git a/datamodel/high/base/schema.go b/datamodel/high/base/schema.go index 1bf2f35..ace8d94 100644 --- a/datamodel/high/base/schema.go +++ b/datamodel/high/base/schema.go @@ -5,6 +5,7 @@ package base import ( "fmt" + "gopkg.in/yaml.v3" "sync" "github.com/pb33f/libopenapi/datamodel/high" @@ -12,29 +13,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low/base" ) -// DynamicValue is used to hold multiple possible values for a schema property. There are two values, a left -// value (A) and a right value (B). The left value (A) is a 3.0 schema property value, the right value (B) is a 3.1 -// schema value. -// -// OpenAPI 3.1 treats a Schema as a real JSON schema, which means some properties become incompatible, or others -// now support more than one primitive type or structure. -// The N value is a bit to make it each to know which value (A or B) is used, this prevents having to -// if/else on the value to determine which one is set. -type DynamicValue[A any, B any] struct { - N int // 0 == A, 1 == B - A A - B B -} -// IsA will return true if the 'A' or left value is set. (OpenAPI 3) -func (s *DynamicValue[A, B]) IsA() bool { - return s.N == 0 -} - -// IsB will return true if the 'B' or right value is set (OpenAPI 3.1) -func (s *DynamicValue[A, B]) IsB() bool { - return s.N == 1 -} // Schema represents a JSON Schema that support Swagger, OpenAPI 3 and OpenAPI 3.1 // @@ -47,83 +26,83 @@ func (s *DynamicValue[A, B]) IsB() bool { // - v3.1 schema: https://spec.openapis.org/oas/v3.1.0#schema-object type Schema struct { // 3.1 only, used to define a dialect for this schema, label is '$schema'. - SchemaTypeRef string + SchemaTypeRef string `json:"$schema,omitempty" yaml:"$schema,omitempty"` // In versions 2 and 3.0, this ExclusiveMaximum can only be a boolean. // In version 3.1, ExclusiveMaximum is an integer. - ExclusiveMaximum *DynamicValue[bool, int64] + ExclusiveMaximum *DynamicValue[bool, int64] `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` // In versions 2 and 3.0, this ExclusiveMinimum can only be a boolean. // In version 3.1, ExclusiveMinimum is an integer. - ExclusiveMinimum *DynamicValue[bool, int64] + ExclusiveMinimum *DynamicValue[bool, int64] `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` // In versions 2 and 3.0, this Type is a single value, so array will only ever have one value // in version 3.1, Type can be multiple values - Type []string + Type []string `json:"type,omitempty" yaml:"type,omitempty"` // Schemas are resolved on demand using a SchemaProxy - AllOf []*SchemaProxy + AllOf []*SchemaProxy `json:"allOf,omitempty" yaml:"allOf,omitempty"` // Polymorphic Schemas are only available in version 3+ - OneOf []*SchemaProxy - AnyOf []*SchemaProxy - Discriminator *Discriminator + OneOf []*SchemaProxy `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` + AnyOf []*SchemaProxy `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` + Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` // in 3.1 examples can be an array (which is recommended) - Examples []any + Examples []any `json:"examples,omitempty" yaml:"examples,omitempty"` // in 3.1 prefixItems provides tuple validation support. - PrefixItems []*SchemaProxy + PrefixItems []*SchemaProxy `json:"prefixItems,omitempty" yaml:"prefixItems,omitempty"` // 3.1 Specific properties - Contains *SchemaProxy - MinContains *int64 - MaxContains *int64 - If *SchemaProxy - Else *SchemaProxy - Then *SchemaProxy - DependentSchemas map[string]*SchemaProxy - PatternProperties map[string]*SchemaProxy - PropertyNames *SchemaProxy - UnevaluatedItems *SchemaProxy - UnevaluatedProperties *SchemaProxy + Contains *SchemaProxy `json:"contains,omitempty" yaml:"contains,omitempty"` + MinContains *int64 `json:"minContains,omitempty" yaml:"minContains,omitempty"` + MaxContains *int64 `json:"maxContains,omitempty" yaml:"maxContains,omitempty"` + If *SchemaProxy `json:"if,omitempty" yaml:"if,omitempty"` + Else *SchemaProxy `json:"else,omitempty" yaml:"else,omitempty"` + Then *SchemaProxy `json:"then,omitempty" yaml:"then,omitempty"` + DependentSchemas map[string]*SchemaProxy `json:"dependentSchemas,omitempty" yaml:"dependentSchemas,omitempty"` + PatternProperties map[string]*SchemaProxy `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"` + PropertyNames *SchemaProxy `json:"propertyNames,omitempty" yaml:"propertyNames,omitempty"` + UnevaluatedItems *SchemaProxy `json:"unevaluatedItems,omitempty" yaml:"unevaluatedItems,omitempty"` + UnevaluatedProperties *SchemaProxy `json:"unevaluatedProperties,omitempty" yaml:"unevaluatedProperties,omitempty"` // in 3.1 Items can be a Schema or a boolean - Items *DynamicValue[*SchemaProxy, bool] + Items *DynamicValue[*SchemaProxy, bool] `json:"items,omitempty" yaml:"items,omitempty"` // Compatible with all versions - Not *SchemaProxy - Properties map[string]*SchemaProxy - Title string - MultipleOf *int64 - Maximum *int64 - Minimum *int64 - MaxLength *int64 - MinLength *int64 - Pattern string - Format string - MaxItems *int64 - MinItems *int64 - UniqueItems *int64 - MaxProperties *int64 - MinProperties *int64 - Required []string - Enum []any - AdditionalProperties any - Description string - Default any - Nullable *bool - ReadOnly bool // https://github.com/pb33f/libopenapi/issues/30 - WriteOnly bool // https://github.com/pb33f/libopenapi/issues/30 - XML *XML - ExternalDocs *ExternalDoc - Example any - Deprecated *bool - Extensions map[string]any + Not *SchemaProxy `json:"not,omitempty" yaml:"not,omitempty"` + Properties map[string]*SchemaProxy `json:"properties,omitempty" yaml:"properties,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + MultipleOf *int64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + Maximum *int64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + Minimum *int64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + MaxLength *int64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + MinLength *int64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + MaxItems *int64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + MinItems *int64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + UniqueItems *int64 `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + MaxProperties *int64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` + MinProperties *int64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` + AdditionalProperties any `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` + Nullable *bool `json:"nullable,omitempty" yaml:"nullable,omitempty"` + ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` // https://github.com/pb33f/libopenapi/issues/30 + WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` // https://github.com/pb33f/libopenapi/issues/30 + XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"` + ExternalDocs *ExternalDoc `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` + Deprecated *bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Extensions map[string]any `json:"-" yaml:"-"` low *base.Schema // Parent Proxy refers back to the low level SchemaProxy that is proxying this schema. - ParentProxy *SchemaProxy + ParentProxy *SchemaProxy `json:"-" yaml:"-"` } // NewSchema will create a new high-level schema from a low-level one. @@ -462,3 +441,17 @@ func NewSchema(schema *base.Schema) *Schema { func (s *Schema) GoLow() *base.Schema { return s.low } + +// Render will return a YAML representation of the Schema object as a byte slice. +func (s *Schema) Render() ([]byte, error) { + return yaml.Marshal(s) +} + +// MarshalYAML will create a ready to render YAML representation of the ExternalDoc object. +func (s *Schema) MarshalYAML() (interface{}, error) { + if s == nil { + return nil, nil + } + nb := high.NewNodeBuilder(s, s.low) + return nb.Render(), nil +} diff --git a/datamodel/high/base/schema_proxy.go b/datamodel/high/base/schema_proxy.go index 7939f0f..829892f 100644 --- a/datamodel/high/base/schema_proxy.go +++ b/datamodel/high/base/schema_proxy.go @@ -4,8 +4,10 @@ package base import ( + "github.com/pb33f/libopenapi/datamodel/high" "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" + "gopkg.in/yaml.v3" ) // SchemaProxy exists as a stub that will create a Schema once (and only once) the Schema() method is called. An @@ -83,3 +85,23 @@ func (sp *SchemaProxy) GoLow() *base.SchemaProxy { } return sp.schema.Value } + +// Render will return a YAML representation of the Schema object as a byte slice. +func (sp *SchemaProxy) Render() ([]byte, error) { + return yaml.Marshal(sp) +} + +// MarshalYAML will create a ready to render YAML representation of the ExternalDoc object. +func (sp *SchemaProxy) MarshalYAML() (interface{}, error) { + if sp == nil { + return nil, nil + } + s, err := sp.BuildSchema() + if err != nil { + return nil, err + } + nb := high.NewNodeBuilder(s, s.low) + return nb.Render(), nil + +} + diff --git a/datamodel/high/base/schema_test.go b/datamodel/high/base/schema_test.go index f5209ae..a47fe5d 100644 --- a/datamodel/high/base/schema_test.go +++ b/datamodel/high/base/schema_test.go @@ -257,6 +257,11 @@ unevaluatedProperties: wentLow := compiled.GoLow() assert.Equal(t, 114, wentLow.AdditionalProperties.ValueNode.Line) + + // now render it out! + schemaBytes, _ := compiled.Render() + assert.Equal(t, testSpec, string(schemaBytes)) + } func TestSchemaObjectWithAllOfSequenceOrder(t *testing.T) { @@ -701,3 +706,114 @@ properties: type: number ` } + +func TestNewSchemaProxy_RenderSchema(t *testing.T) { + testSpec := `type: object +description: something object +discriminator: + propertyName: athing + mapping: + log: cat + pizza: party +allOf: + - type: object + description: an allof thing + properties: + allOfA: + type: string + description: allOfA description + example: allOfAExp + allOfB: + type: string + description: allOfB description + example: allOfBExp +` + + var compNode yaml.Node + _ = yaml.Unmarshal([]byte(testSpec), &compNode) + + sp := new(lowbase.SchemaProxy) + err := sp.Build(compNode.Content[0], nil) + assert.NoError(t, err) + + lowproxy := low.NodeReference[*lowbase.SchemaProxy]{ + Value: sp, + ValueNode: compNode.Content[0], + } + + schemaProxy := NewSchemaProxy(&lowproxy) + compiled := schemaProxy.Schema() + + assert.Equal(t, schemaProxy, compiled.ParentProxy) + + assert.NotNil(t, compiled) + assert.Nil(t, schemaProxy.GetBuildError()) + + // now render it out, it should be identical. + schemaBytes, _ := compiled.Render() + assert.Equal(t, testSpec, string(schemaBytes)) + +} + +func TestNewSchemaProxy_RenderSchemaWithMultipleObjectTypes(t *testing.T) { + testSpec := `type: object +description: something object +oneOf: + - type: object + description: a oneof thing + properties: + oneOfA: + type: string + example: oneOfAExp +anyOf: + - type: object + description: an anyOf thing + properties: + anyOfA: + type: string + example: anyOfAExp +not: + type: object + description: a not thing + properties: + notA: + type: string + example: notAExp +items: + type: object + description: an items thing + properties: + itemsA: + type: string + description: itemsA description + example: itemsAExp + itemsB: + type: string + description: itemsB description + example: itemsBExp +` + + var compNode yaml.Node + _ = yaml.Unmarshal([]byte(testSpec), &compNode) + + sp := new(lowbase.SchemaProxy) + err := sp.Build(compNode.Content[0], nil) + assert.NoError(t, err) + + lowproxy := low.NodeReference[*lowbase.SchemaProxy]{ + Value: sp, + ValueNode: compNode.Content[0], + } + + schemaProxy := NewSchemaProxy(&lowproxy) + compiled := schemaProxy.Schema() + + assert.Equal(t, schemaProxy, compiled.ParentProxy) + + assert.NotNil(t, compiled) + assert.Nil(t, schemaProxy.GetBuildError()) + + // now render it out, it should be identical. + schemaBytes, _ := compiled.Render() + assert.Equal(t, testSpec, string(schemaBytes)) +} diff --git a/datamodel/high/shared.go b/datamodel/high/shared.go index 274a1fb..63167e4 100644 --- a/datamodel/high/shared.go +++ b/datamodel/high/shared.go @@ -14,7 +14,9 @@ package high import ( + "fmt" "github.com/pb33f/libopenapi/datamodel/low" + v3 "github.com/pb33f/libopenapi/datamodel/low/v3" "gopkg.in/yaml.v3" "reflect" "sort" @@ -153,12 +155,23 @@ func (n *NodeBuilder) add(key string) { field, _ := reflect.TypeOf(n.High).Elem().FieldByName(key) tag := string(field.Tag.Get("yaml")) tagName := strings.Split(tag, ",")[0] + if tag == "-" { + return + } // extract the value of the field fieldValue := reflect.ValueOf(n.High).Elem().FieldByName(key) f := fieldValue.Interface() value := reflect.ValueOf(f) + if tag == "additionalProperties" { + fmt.Printf("woo") + } + + if f == nil || value.IsZero() { + return + } + // create a new node entry nodeEntry := &NodeEntry{Key: tagName} @@ -169,12 +182,22 @@ func (n *NodeBuilder) add(key string) { nodeEntry.Value = value.String() case reflect.Bool: nodeEntry.Value = value.Bool() + case reflect.Slice: + if tagName == v3.TypeLabel { + if value.Len() == 1 { + nodeEntry.Value = value.Index(0).String() + } + } else { + if !value.IsNil() { + nodeEntry.Value = f + } + } case reflect.Ptr: - nodeEntry.Value = f - case reflect.Map: - nodeEntry.Value = f + if !value.IsNil() { + nodeEntry.Value = f + } default: - panic("not supported yet") + nodeEntry.Value = f } // if there is no low level object, then we cannot extract line numbers, @@ -196,7 +219,9 @@ func (n *NodeBuilder) add(key string) { nodeEntry.Line = 9999 } } - n.Nodes = append(n.Nodes, nodeEntry) + if nodeEntry.Value != nil { + n.Nodes = append(n.Nodes, nodeEntry) + } } func (n *NodeBuilder) Render() *yaml.Node { @@ -225,17 +250,75 @@ func AddYAMLNode(parent *yaml.Node, key string, value any) *yaml.Node { l = CreateStringNode(key) } var valueNode *yaml.Node + vo := reflect.ValueOf(value) switch t.Kind() { + + case reflect.String: + val := value.(string) + if val == "" { + return parent + } + valueNode = CreateStringNode(val) + break + + case reflect.Bool: + val := value.(bool) + if !val { + return parent + } + valueNode = CreateBoolNode("true") + break + + case reflect.Slice: + if vo.IsNil() { + return parent + } + + // type is a case where it can be a single value, or a slice. + // so, if the key is 'type', then check if the slice contains a sigle value + // and if so, render it as a string, otherwise, proceed as normal. + skip := false + if key == v3.TypeLabel { + //if vo.Len() == 1 { + // valueNode = CreateStringNode(value.([]string)[0]) + // skip = true + //} + } + + if !skip { + var rawNode yaml.Node + err := rawNode.Encode(value) + if err != nil { + return parent + } else { + valueNode = &rawNode + } + } + case reflect.Struct: panic("no way dude, why?") case reflect.Ptr: - rawRender, _ := value.(Renderable).MarshalYAML() - if rawRender != nil { - valueNode = rawRender.(*yaml.Node) + if r, ok := value.(Renderable); ok { + rawRender, _ := r.MarshalYAML() + if rawRender != nil { + valueNode = rawRender.(*yaml.Node) + } else { + return parent + } } else { + var rawNode yaml.Node + err := rawNode.Encode(value) + if err != nil { + return parent + } else { + valueNode = &rawNode + } + } + + default: + if vo.IsNil() { return parent } - default: var rawNode yaml.Node err := rawNode.Encode(value) if err != nil { @@ -270,6 +353,15 @@ func CreateStringNode(str string) *yaml.Node { return n } +func CreateBoolNode(str string) *yaml.Node { + n := &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!bool", + Value: str, + } + return n +} + func CreateIntNode(val int) *yaml.Node { i := strconv.Itoa(val) n := &yaml.Node{