mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-10 12:37:48 +00:00
Working through complex rendering edgecases.
This commit is contained in:
@@ -65,5 +65,5 @@ func (d *DynamicValue[A, B]) MarshalYAML() (interface{}, error) {
|
|||||||
case reflect.Int64:
|
case reflect.Int64:
|
||||||
_ = n.Encode(value.(int64))
|
_ = n.Encode(value.(int64))
|
||||||
}
|
}
|
||||||
return n, err
|
return &n, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package base
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
lowmodel "github.com/pb33f/libopenapi/datamodel/low"
|
lowmodel "github.com/pb33f/libopenapi/datamodel/low"
|
||||||
@@ -171,8 +172,7 @@ version: 1.2.3
|
|||||||
x-pizza: pepperoni
|
x-pizza: pepperoni
|
||||||
x-cake:
|
x-cake:
|
||||||
name: someone
|
name: someone
|
||||||
url: nowhere
|
url: nowhere`
|
||||||
`
|
|
||||||
|
|
||||||
// unmarshal yaml into a *yaml.Node instance
|
// unmarshal yaml into a *yaml.Node instance
|
||||||
var cNode yaml.Node
|
var cNode yaml.Node
|
||||||
@@ -198,7 +198,7 @@ x-cake:
|
|||||||
|
|
||||||
// marshal high back to yaml, should be the same as the original, in same order.
|
// marshal high back to yaml, should be the same as the original, in same order.
|
||||||
bytes, _ := highInfo.Render()
|
bytes, _ := highInfo.Render()
|
||||||
assert.Equal(t, yml, string(bytes))
|
assert.Equal(t, yml, strings.TrimSpace(string(bytes)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -235,6 +235,8 @@ func NewSchema(schema *base.Schema) *Schema {
|
|||||||
Value: addPropSchema,
|
Value: addPropSchema,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: check for slice and map types and unpack correctly.
|
||||||
|
|
||||||
s.AdditionalProperties = schema.AdditionalProperties.Value
|
s.AdditionalProperties = schema.AdditionalProperties.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package base
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/pb33f/libopenapi/datamodel/low"
|
"github.com/pb33f/libopenapi/datamodel/low"
|
||||||
@@ -68,138 +69,6 @@ func TestNewSchemaProxy(t *testing.T) {
|
|||||||
func TestNewSchemaProxy_WithObject(t *testing.T) {
|
func TestNewSchemaProxy_WithObject(t *testing.T) {
|
||||||
testSpec := `type: object
|
testSpec := `type: object
|
||||||
description: something 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'
|
|
||||||
oneOf:
|
|
||||||
type: object
|
|
||||||
description: a oneof thing
|
|
||||||
properties:
|
|
||||||
oneOfA:
|
|
||||||
type: string
|
|
||||||
description: oneOfA description
|
|
||||||
example: 'oneOfAExp'
|
|
||||||
oneOfB:
|
|
||||||
type: string
|
|
||||||
description: oneOfB description
|
|
||||||
example: 'oneOfBExp'
|
|
||||||
anyOf:
|
|
||||||
type: object
|
|
||||||
description: an anyOf thing
|
|
||||||
properties:
|
|
||||||
anyOfA:
|
|
||||||
type: string
|
|
||||||
description: anyOfA description
|
|
||||||
example: 'anyOfAExp'
|
|
||||||
anyOfB:
|
|
||||||
type: string
|
|
||||||
description: anyOfB description
|
|
||||||
example: 'anyOfBExp'
|
|
||||||
not:
|
|
||||||
type: object
|
|
||||||
description: a not thing
|
|
||||||
properties:
|
|
||||||
notA:
|
|
||||||
type: string
|
|
||||||
description: notA description
|
|
||||||
example: 'notAExp'
|
|
||||||
notB:
|
|
||||||
type: string
|
|
||||||
description: notB description
|
|
||||||
example: 'notBExp'
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
description: an items thing
|
|
||||||
properties:
|
|
||||||
itemsA:
|
|
||||||
type: string
|
|
||||||
description: itemsA description
|
|
||||||
example: 'itemsAExp'
|
|
||||||
itemsB:
|
|
||||||
type: string
|
|
||||||
description: itemsB description
|
|
||||||
example: 'itemsBExp'
|
|
||||||
prefixItems:
|
|
||||||
type: object
|
|
||||||
description: an items thing
|
|
||||||
properties:
|
|
||||||
itemsA:
|
|
||||||
type: string
|
|
||||||
description: itemsA description
|
|
||||||
example: 'itemsAExp'
|
|
||||||
itemsB:
|
|
||||||
type: string
|
|
||||||
description: itemsB description
|
|
||||||
example: 'itemsBExp'
|
|
||||||
properties:
|
|
||||||
somethingBee:
|
|
||||||
type: number
|
|
||||||
somethingThree:
|
|
||||||
type: number
|
|
||||||
somethingTwo:
|
|
||||||
type: number
|
|
||||||
somethingOne:
|
|
||||||
type: number
|
|
||||||
somethingA:
|
|
||||||
type: number
|
|
||||||
description: a number
|
|
||||||
example: 2
|
|
||||||
somethingB:
|
|
||||||
type: object
|
|
||||||
description: an object
|
|
||||||
externalDocs:
|
|
||||||
description: the best docs
|
|
||||||
url: https://pb33f.io
|
|
||||||
properties:
|
|
||||||
somethingBProp:
|
|
||||||
type: string
|
|
||||||
description: something b subprop
|
|
||||||
example: picnics are nice.
|
|
||||||
xml:
|
|
||||||
name: an xml thing
|
|
||||||
namespace: an xml namespace
|
|
||||||
prefix: a prefix
|
|
||||||
attribute: true
|
|
||||||
wrapped: false
|
|
||||||
x-pizza: love
|
|
||||||
additionalProperties:
|
|
||||||
why: yes
|
|
||||||
thatIs: true
|
|
||||||
additionalProperties: true
|
|
||||||
xml:
|
|
||||||
name: XML Thing
|
|
||||||
externalDocs:
|
|
||||||
url: https://pb33f.io/docs
|
|
||||||
enum: [fish, cake]
|
|
||||||
required: [cake, fish]
|
|
||||||
maxLength: 10
|
|
||||||
minLength: 1
|
|
||||||
maxItems: 10
|
|
||||||
minItems: 1
|
|
||||||
maxProperties: 10
|
|
||||||
minProperties: 1
|
|
||||||
nullable: true
|
|
||||||
readOnly: true
|
|
||||||
writeOnly: false
|
|
||||||
deprecated: true
|
|
||||||
contains:
|
|
||||||
type: int
|
|
||||||
minContains: 1
|
|
||||||
maxContains: 10
|
|
||||||
if:
|
if:
|
||||||
type: string
|
type: string
|
||||||
else:
|
else:
|
||||||
@@ -218,7 +87,128 @@ unevaluatedItems:
|
|||||||
type: boolean
|
type: boolean
|
||||||
unevaluatedProperties:
|
unevaluatedProperties:
|
||||||
type: integer
|
type: integer
|
||||||
`
|
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
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
description: a oneof thing
|
||||||
|
properties:
|
||||||
|
oneOfA:
|
||||||
|
type: string
|
||||||
|
description: oneOfA description
|
||||||
|
example: oneOfAExp
|
||||||
|
oneOfB:
|
||||||
|
type: string
|
||||||
|
description: oneOfB description
|
||||||
|
example: oneOfBExp
|
||||||
|
anyOf:
|
||||||
|
- type: object
|
||||||
|
description: an anyOf thing
|
||||||
|
properties:
|
||||||
|
anyOfA:
|
||||||
|
type: string
|
||||||
|
description: anyOfA description
|
||||||
|
example: anyOfAExp
|
||||||
|
anyOfB:
|
||||||
|
type: string
|
||||||
|
description: anyOfB description
|
||||||
|
example: anyOfBExp
|
||||||
|
not:
|
||||||
|
type: object
|
||||||
|
description: a not thing
|
||||||
|
properties:
|
||||||
|
notA:
|
||||||
|
type: string
|
||||||
|
description: notA description
|
||||||
|
example: notAExp
|
||||||
|
notB:
|
||||||
|
type: string
|
||||||
|
description: notB description
|
||||||
|
example: notBExp
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
description: an items thing
|
||||||
|
properties:
|
||||||
|
itemsA:
|
||||||
|
type: string
|
||||||
|
description: itemsA description
|
||||||
|
example: itemsAExp
|
||||||
|
itemsB:
|
||||||
|
type: string
|
||||||
|
description: itemsB description
|
||||||
|
example: itemsBExp
|
||||||
|
prefixItems:
|
||||||
|
- type: object
|
||||||
|
description: an items thing
|
||||||
|
properties:
|
||||||
|
itemsA:
|
||||||
|
type: string
|
||||||
|
description: itemsA description
|
||||||
|
example: itemsAExp
|
||||||
|
itemsB:
|
||||||
|
type: string
|
||||||
|
description: itemsB description
|
||||||
|
example: itemsBExp
|
||||||
|
properties:
|
||||||
|
somethingA:
|
||||||
|
type: number
|
||||||
|
description: a number
|
||||||
|
example: "2"
|
||||||
|
additionalProperties:
|
||||||
|
- chicken
|
||||||
|
- nugget
|
||||||
|
- soup
|
||||||
|
somethingB:
|
||||||
|
type: object
|
||||||
|
exclusiveMinimum: true
|
||||||
|
exclusiveMaximum: true
|
||||||
|
description: an object
|
||||||
|
externalDocs:
|
||||||
|
description: the best docs
|
||||||
|
url: https://pb33f.io
|
||||||
|
properties:
|
||||||
|
somethingBProp:
|
||||||
|
type: string
|
||||||
|
description: something b subprop
|
||||||
|
example: picnics are nice.
|
||||||
|
xml:
|
||||||
|
name: an xml thing
|
||||||
|
namespace: an xml namespace
|
||||||
|
prefix: a prefix
|
||||||
|
attribute: true
|
||||||
|
x-pizza: love
|
||||||
|
additionalProperties:
|
||||||
|
why: yes
|
||||||
|
thatIs: true
|
||||||
|
additionalProperties: true
|
||||||
|
required:
|
||||||
|
- them
|
||||||
|
enum:
|
||||||
|
- one
|
||||||
|
- two
|
||||||
|
x-pizza: tasty
|
||||||
|
examples:
|
||||||
|
- hey
|
||||||
|
- hi!
|
||||||
|
contains:
|
||||||
|
type: int
|
||||||
|
maxContains: 10
|
||||||
|
minContains: 1`
|
||||||
|
|
||||||
var compNode yaml.Node
|
var compNode yaml.Node
|
||||||
_ = yaml.Unmarshal([]byte(testSpec), &compNode)
|
_ = yaml.Unmarshal([]byte(testSpec), &compNode)
|
||||||
@@ -252,15 +242,13 @@ unevaluatedProperties:
|
|||||||
assert.Equal(t, "string", compiled.PropertyNames.Schema().Type[0])
|
assert.Equal(t, "string", compiled.PropertyNames.Schema().Type[0])
|
||||||
assert.Equal(t, "boolean", compiled.UnevaluatedItems.Schema().Type[0])
|
assert.Equal(t, "boolean", compiled.UnevaluatedItems.Schema().Type[0])
|
||||||
assert.Equal(t, "integer", compiled.UnevaluatedProperties.Schema().Type[0])
|
assert.Equal(t, "integer", compiled.UnevaluatedProperties.Schema().Type[0])
|
||||||
assert.NotNil(t, compiled.Nullable)
|
|
||||||
assert.True(t, *compiled.Nullable)
|
|
||||||
|
|
||||||
wentLow := compiled.GoLow()
|
wentLow := compiled.GoLow()
|
||||||
assert.Equal(t, 114, wentLow.AdditionalProperties.ValueNode.Line)
|
assert.Equal(t, 129, wentLow.AdditionalProperties.ValueNode.Line)
|
||||||
|
|
||||||
// now render it out!
|
// now render it out!
|
||||||
schemaBytes, _ := compiled.Render()
|
schemaBytes, _ := compiled.Render()
|
||||||
assert.Equal(t, testSpec, string(schemaBytes))
|
assert.Equal(t, testSpec, strings.TrimSpace(string(schemaBytes)))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,3 +805,278 @@ items:
|
|||||||
schemaBytes, _ := compiled.Render()
|
schemaBytes, _ := compiled.Render()
|
||||||
assert.Equal(t, testSpec, string(schemaBytes))
|
assert.Equal(t, testSpec, string(schemaBytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewSchemaProxy_RenderSchemaEnsurePropertyOrdering(t *testing.T) {
|
||||||
|
testSpec := `properties:
|
||||||
|
somethingBee:
|
||||||
|
type: number
|
||||||
|
somethingThree:
|
||||||
|
type: number
|
||||||
|
somethingTwo:
|
||||||
|
type: number
|
||||||
|
somethingOne:
|
||||||
|
type: number
|
||||||
|
somethingA:
|
||||||
|
type: number
|
||||||
|
description: a number
|
||||||
|
example: "2"
|
||||||
|
somethingB:
|
||||||
|
type: object
|
||||||
|
description: an object
|
||||||
|
externalDocs:
|
||||||
|
description: the best docs
|
||||||
|
url: https://pb33f.io
|
||||||
|
properties:
|
||||||
|
somethingBProp:
|
||||||
|
type: string
|
||||||
|
description: something b subprop
|
||||||
|
example: picnics are nice.
|
||||||
|
xml:
|
||||||
|
name: an xml thing
|
||||||
|
namespace: an xml namespace
|
||||||
|
prefix: a prefix
|
||||||
|
attribute: true
|
||||||
|
x-pizza: love
|
||||||
|
additionalProperties:
|
||||||
|
why: yes
|
||||||
|
thatIs: true
|
||||||
|
additionalProperties: true
|
||||||
|
xml:
|
||||||
|
name: XML Thing`
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
// now render it out, it should be identical.
|
||||||
|
schemaBytes, _ := compiled.Render()
|
||||||
|
assert.Equal(t, testSpec, strings.TrimSpace(string(schemaBytes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSchemaProxy_RenderSchemaCheckDiscriminatorMappingOrder(t *testing.T) {
|
||||||
|
testSpec := `discriminator:
|
||||||
|
mapping:
|
||||||
|
log: cat
|
||||||
|
pizza: party
|
||||||
|
chicken: nuggets
|
||||||
|
warm: soup
|
||||||
|
cold: heart`
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
// now render it out, it should be identical.
|
||||||
|
schemaBytes, _ := compiled.Render()
|
||||||
|
assert.Equal(t, testSpec, strings.TrimSpace(string(schemaBytes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSchemaProxy_RenderSchemaCheckAdditionalPropertiesSlice(t *testing.T) {
|
||||||
|
testSpec := `additionalProperties:
|
||||||
|
- one
|
||||||
|
- two
|
||||||
|
- miss a few
|
||||||
|
- ninety nine
|
||||||
|
- hundred`
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
compiled.low.Hash()
|
||||||
|
|
||||||
|
// now render it out, it should be identical.
|
||||||
|
schemaBytes, _ := compiled.Render()
|
||||||
|
assert.Equal(t, testSpec, strings.TrimSpace(string(schemaBytes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
oneOf:
|
||||||
|
type: object
|
||||||
|
description: a oneof thing
|
||||||
|
properties:
|
||||||
|
oneOfA:
|
||||||
|
type: string
|
||||||
|
description: oneOfA description
|
||||||
|
example: 'oneOfAExp'
|
||||||
|
oneOfB:
|
||||||
|
type: string
|
||||||
|
description: oneOfB description
|
||||||
|
example: 'oneOfBExp'
|
||||||
|
anyOf:
|
||||||
|
type: object
|
||||||
|
description: an anyOf thing
|
||||||
|
properties:
|
||||||
|
anyOfA:
|
||||||
|
type: string
|
||||||
|
description: anyOfA description
|
||||||
|
example: 'anyOfAExp'
|
||||||
|
anyOfB:
|
||||||
|
type: string
|
||||||
|
description: anyOfB description
|
||||||
|
example: 'anyOfBExp'
|
||||||
|
not:
|
||||||
|
type: object
|
||||||
|
description: a not thing
|
||||||
|
properties:
|
||||||
|
notA:
|
||||||
|
type: string
|
||||||
|
description: notA description
|
||||||
|
example: 'notAExp'
|
||||||
|
notB:
|
||||||
|
type: string
|
||||||
|
description: notB description
|
||||||
|
example: 'notBExp'
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
description: an items thing
|
||||||
|
properties:
|
||||||
|
itemsA:
|
||||||
|
type: string
|
||||||
|
description: itemsA description
|
||||||
|
example: 'itemsAExp'
|
||||||
|
itemsB:
|
||||||
|
type: string
|
||||||
|
description: itemsB description
|
||||||
|
example: 'itemsBExp'
|
||||||
|
prefixItems:
|
||||||
|
type: object
|
||||||
|
description: an items thing
|
||||||
|
properties:
|
||||||
|
itemsA:
|
||||||
|
type: string
|
||||||
|
description: itemsA description
|
||||||
|
example: 'itemsAExp'
|
||||||
|
itemsB:
|
||||||
|
type: string
|
||||||
|
description: itemsB description
|
||||||
|
example: 'itemsBExp'
|
||||||
|
properties:
|
||||||
|
somethingBee:
|
||||||
|
type: number
|
||||||
|
somethingThree:
|
||||||
|
type: number
|
||||||
|
somethingTwo:
|
||||||
|
type: number
|
||||||
|
somethingOne:
|
||||||
|
type: number
|
||||||
|
somethingA:
|
||||||
|
type: number
|
||||||
|
description: a number
|
||||||
|
example: 2
|
||||||
|
somethingB:
|
||||||
|
type: object
|
||||||
|
description: an object
|
||||||
|
externalDocs:
|
||||||
|
description: the best docs
|
||||||
|
url: https://pb33f.io
|
||||||
|
properties:
|
||||||
|
somethingBProp:
|
||||||
|
type: string
|
||||||
|
description: something b subprop
|
||||||
|
example: picnics are nice.
|
||||||
|
xml:
|
||||||
|
name: an xml thing
|
||||||
|
namespace: an xml namespace
|
||||||
|
prefix: a prefix
|
||||||
|
attribute: true
|
||||||
|
wrapped: false
|
||||||
|
x-pizza: love
|
||||||
|
additionalProperties:
|
||||||
|
why: yes
|
||||||
|
thatIs: true
|
||||||
|
additionalProperties: true
|
||||||
|
xml:
|
||||||
|
name: XML Thing
|
||||||
|
externalDocs:
|
||||||
|
url: https://pb33f.io/docs
|
||||||
|
enum: [fish, cake]
|
||||||
|
required: [cake, fish]
|
||||||
|
maxLength: 10
|
||||||
|
minLength: 1
|
||||||
|
maxItems: 10
|
||||||
|
minItems: 1
|
||||||
|
maxProperties: 10
|
||||||
|
minProperties: 1
|
||||||
|
nullable: true
|
||||||
|
readOnly: true
|
||||||
|
writeOnly: false
|
||||||
|
deprecated: true
|
||||||
|
contains:
|
||||||
|
type: int
|
||||||
|
minContains: 1
|
||||||
|
maxContains: 10
|
||||||
|
if:
|
||||||
|
type: string
|
||||||
|
else:
|
||||||
|
type: integer
|
||||||
|
then:
|
||||||
|
type: boolean
|
||||||
|
dependentSchemas:
|
||||||
|
schemaOne:
|
||||||
|
type: string
|
||||||
|
patternProperties:
|
||||||
|
patternOne:
|
||||||
|
type: string
|
||||||
|
propertyNames:
|
||||||
|
type: string
|
||||||
|
unevaluatedItems:
|
||||||
|
type: boolean
|
||||||
|
unevaluatedProperties:
|
||||||
|
type: integer
|
||||||
|
*/
|
||||||
388
datamodel/high/node_builder.go
Normal file
388
datamodel/high/node_builder.go
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package high
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pb33f/libopenapi/datamodel/low"
|
||||||
|
"github.com/pb33f/libopenapi/datamodel/low/v3"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NodeEntry represents a single node used by NodeBuilder.
|
||||||
|
type NodeEntry struct {
|
||||||
|
Tag string
|
||||||
|
Key string
|
||||||
|
Value any
|
||||||
|
Line int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeBuilder is a structure used by libopenapi high-level objects, to render themselves back to YAML.
|
||||||
|
// this allows high-level objects to be 'mutable' because all changes will be rendered out.
|
||||||
|
type NodeBuilder struct {
|
||||||
|
Nodes []*NodeEntry
|
||||||
|
High any
|
||||||
|
Low any
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNodeBuilder will create a new NodeBuilder instance, this is the only way to create a NodeBuilder.
|
||||||
|
// The function accepts a high level object and a low level object (need to be siblings/same type).
|
||||||
|
//
|
||||||
|
// Using reflection, a map of every field in the high level object is created, ready to be rendered.
|
||||||
|
func NewNodeBuilder(high any, low any) *NodeBuilder {
|
||||||
|
// create a new node builder
|
||||||
|
nb := &NodeBuilder{High: high, Low: low}
|
||||||
|
|
||||||
|
// extract fields from the high level object and add them into our node builder.
|
||||||
|
// this will allow us to extract the line numbers from the low level object as well.
|
||||||
|
v := reflect.ValueOf(high).Elem()
|
||||||
|
num := v.NumField()
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
nb.add(v.Type().Field(i).Name)
|
||||||
|
}
|
||||||
|
return nb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NodeBuilder) add(key string) {
|
||||||
|
|
||||||
|
// only operate on exported fields.
|
||||||
|
if unicode.IsLower(rune(key[0])) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the key is 'Extensions' then we need to extract the keys from the map
|
||||||
|
// and add them to the node builder.
|
||||||
|
if key == "Extensions" {
|
||||||
|
extensions := reflect.ValueOf(n.High).Elem().FieldByName(key)
|
||||||
|
for _, e := range extensions.MapKeys() {
|
||||||
|
v := extensions.MapIndex(e)
|
||||||
|
|
||||||
|
extKey := e.String()
|
||||||
|
extValue := v.Interface()
|
||||||
|
nodeEntry := &NodeEntry{Tag: extKey, Key: extKey, Value: extValue}
|
||||||
|
|
||||||
|
if !reflect.ValueOf(n.Low).IsZero() {
|
||||||
|
fieldValue := reflect.ValueOf(n.Low).Elem().FieldByName("Extensions")
|
||||||
|
f := fieldValue.Interface()
|
||||||
|
value := reflect.ValueOf(f)
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
if j, ok := n.Low.(low.HasExtensionsUntyped); ok {
|
||||||
|
originalExtensions := j.GetExtensions()
|
||||||
|
for k := range originalExtensions {
|
||||||
|
if k.Value == extKey {
|
||||||
|
nodeEntry.Line = originalExtensions[k].ValueNode.Line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("not supported yet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n.Nodes = append(n.Nodes, nodeEntry)
|
||||||
|
}
|
||||||
|
// done, extensions are handled separately.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the field with the tag supplied.
|
||||||
|
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 f == nil || value.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new node entry
|
||||||
|
nodeEntry := &NodeEntry{Tag: tagName, Key: key}
|
||||||
|
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
nodeEntry.Value = strconv.FormatInt(value.Int(), 10)
|
||||||
|
case reflect.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:
|
||||||
|
if !value.IsNil() {
|
||||||
|
nodeEntry.Value = f
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
if !value.IsNil() {
|
||||||
|
nodeEntry.Value = f
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
nodeEntry.Value = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is no low level object, then we cannot extract line numbers,
|
||||||
|
// so skip and default to 0, which means a new entry to the spec.
|
||||||
|
// this will place new content and the top of the rendered object.
|
||||||
|
if !reflect.ValueOf(n.Low).IsZero() {
|
||||||
|
lowFieldValue := reflect.ValueOf(n.Low).Elem().FieldByName(key)
|
||||||
|
fLow := lowFieldValue.Interface()
|
||||||
|
value = reflect.ValueOf(fLow)
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
nb := value.Interface().(low.HasValueNodeUntyped).GetValueNode()
|
||||||
|
if nb != nil {
|
||||||
|
nodeEntry.Line = nb.Line
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// everything else, weight it to the bottom of the rendered object.
|
||||||
|
// this is things that we have no way of knowing where they should be placed.
|
||||||
|
nodeEntry.Line = 9999
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nodeEntry.Value != nil {
|
||||||
|
n.Nodes = append(n.Nodes, nodeEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render will render the NodeBuilder back to a YAML node, iterating over every NodeEntry defined
|
||||||
|
func (n *NodeBuilder) Render() *yaml.Node {
|
||||||
|
// order nodes by line number, retain original order
|
||||||
|
sort.Slice(n.Nodes, func(i, j int) bool {
|
||||||
|
return n.Nodes[i].Line < n.Nodes[j].Line
|
||||||
|
})
|
||||||
|
m := CreateEmptyMapNode()
|
||||||
|
for i := range n.Nodes {
|
||||||
|
node := n.Nodes[i]
|
||||||
|
n.AddYAMLNode(m, node.Tag, node.Key, node.Value)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddYAMLNode will add a new *yaml.Node to the parent node, using the tag, key and value provided.
|
||||||
|
// If the value is nil, then the node will not be added. This method is recursive, so it will dig down
|
||||||
|
// into any non-scalar types.
|
||||||
|
func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, tag, key string, value any) *yaml.Node {
|
||||||
|
if value == nil {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the type
|
||||||
|
t := reflect.TypeOf(value)
|
||||||
|
var l *yaml.Node
|
||||||
|
if tag != "" {
|
||||||
|
l = CreateStringNode(tag)
|
||||||
|
}
|
||||||
|
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.Map:
|
||||||
|
|
||||||
|
// the keys will be rendered randomly, if we don't find out the original line
|
||||||
|
// number of the tag.
|
||||||
|
var orderedCollection []*NodeEntry
|
||||||
|
m := reflect.ValueOf(value)
|
||||||
|
for _, k := range m.MapKeys() {
|
||||||
|
|
||||||
|
var x string
|
||||||
|
// extract key
|
||||||
|
if o, ok := k.Interface().(low.HasKeyNode); ok {
|
||||||
|
x = o.GetKeyNode().Value
|
||||||
|
} else {
|
||||||
|
x = k.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// go low and pull out the line number.
|
||||||
|
lowProps := reflect.ValueOf(n.Low)
|
||||||
|
if !lowProps.IsZero() && !lowProps.IsNil() {
|
||||||
|
gu := lowProps.Elem()
|
||||||
|
gi := gu.FieldByName(key)
|
||||||
|
jl := reflect.ValueOf(gi)
|
||||||
|
if !jl.IsZero() && gi.Interface() != nil {
|
||||||
|
gh := gi.Interface()
|
||||||
|
// extract low level key line number
|
||||||
|
if pr, ok := gh.(low.HasValueUnTyped); ok {
|
||||||
|
fg := reflect.ValueOf(pr.GetValueUntyped())
|
||||||
|
for _, ky := range fg.MapKeys() {
|
||||||
|
er := ky.Interface().(low.HasKeyNode).GetKeyNode().Value
|
||||||
|
if er == x {
|
||||||
|
orderedCollection = append(orderedCollection, &NodeEntry{
|
||||||
|
Tag: x,
|
||||||
|
Key: x,
|
||||||
|
Line: ky.Interface().(low.HasKeyNode).GetKeyNode().Line,
|
||||||
|
Value: m.MapIndex(k).Interface(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this is a map, without an enclosing struct
|
||||||
|
bj := reflect.ValueOf(gh)
|
||||||
|
for _, ky := range bj.MapKeys() {
|
||||||
|
er := ky.Interface().(low.HasKeyNode).GetKeyNode().Value
|
||||||
|
if er == x {
|
||||||
|
orderedCollection = append(orderedCollection, &NodeEntry{
|
||||||
|
Tag: x,
|
||||||
|
Key: x,
|
||||||
|
Line: ky.Interface().(low.HasKeyNode).GetKeyNode().Line,
|
||||||
|
Value: m.MapIndex(k).Interface(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this is a map, without any low level details available (probably an extension map).
|
||||||
|
orderedCollection = append(orderedCollection, &NodeEntry{
|
||||||
|
Tag: x,
|
||||||
|
Key: x,
|
||||||
|
Line: 9999,
|
||||||
|
Value: m.MapIndex(k).Interface(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this is a map, without any low level details available (probably an extension map).
|
||||||
|
orderedCollection = append(orderedCollection, &NodeEntry{
|
||||||
|
Tag: x,
|
||||||
|
Key: x,
|
||||||
|
Line: 9999,
|
||||||
|
Value: m.MapIndex(k).Interface(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort the slice by line number to ensure everything is rendered in order.
|
||||||
|
sort.Slice(orderedCollection, func(i, j int) bool {
|
||||||
|
return orderedCollection[i].Line < orderedCollection[j].Line
|
||||||
|
})
|
||||||
|
|
||||||
|
// create an empty map.
|
||||||
|
p := CreateEmptyMapNode()
|
||||||
|
|
||||||
|
// build out each map node in original order.
|
||||||
|
for _, cv := range orderedCollection {
|
||||||
|
n.AddYAMLNode(p, cv.Tag, cv.Key, cv.Value)
|
||||||
|
}
|
||||||
|
valueNode = p
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
if vo.IsNil() {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawNode yaml.Node
|
||||||
|
err := rawNode.Encode(value)
|
||||||
|
if err != nil {
|
||||||
|
return parent
|
||||||
|
} else {
|
||||||
|
valueNode = &rawNode
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
if r, ok := value.(low.ValueReference[any]); ok {
|
||||||
|
valueNode = r.GetValueNode()
|
||||||
|
} else {
|
||||||
|
panic("not supported yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Ptr:
|
||||||
|
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
|
||||||
|
}
|
||||||
|
var rawNode yaml.Node
|
||||||
|
err := rawNode.Encode(value)
|
||||||
|
if err != nil {
|
||||||
|
return parent
|
||||||
|
} else {
|
||||||
|
valueNode = &rawNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l != nil {
|
||||||
|
parent.Content = append(parent.Content, l, valueNode)
|
||||||
|
} else {
|
||||||
|
parent.Content = valueNode.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateEmptyMapNode() *yaml.Node {
|
||||||
|
n := &yaml.Node{
|
||||||
|
Kind: yaml.MappingNode,
|
||||||
|
Tag: "!!map",
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateStringNode(str string) *yaml.Node {
|
||||||
|
n := &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "!!str",
|
||||||
|
Value: str,
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateBoolNode(str string) *yaml.Node {
|
||||||
|
n := &yaml.Node{
|
||||||
|
Kind: yaml.ScalarNode,
|
||||||
|
Tag: "!!bool",
|
||||||
|
Value: str,
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
type Renderable interface {
|
||||||
|
MarshalYAML() (interface{}, error)
|
||||||
|
}
|
||||||
@@ -14,15 +14,7 @@
|
|||||||
package high
|
package high
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/pb33f/libopenapi/datamodel/low"
|
"github.com/pb33f/libopenapi/datamodel/low"
|
||||||
v3 "github.com/pb33f/libopenapi/datamodel/low/v3"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GoesLow is used to represent any high-level model. All high level models meet this interface and can be used to
|
// GoesLow is used to represent any high-level model. All high level models meet this interface and can be used to
|
||||||
@@ -75,303 +67,3 @@ func UnpackExtensions[T any, R low.HasExtensions[T]](low GoesLow[R]) (map[string
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalExtensions is a convenience function that makes it easy and simple to marshal an objects extensions into a
|
|
||||||
// map that can then correctly rendered back down in to YAML.
|
|
||||||
func MarshalExtensions(parent *yaml.Node, extensions map[string]any) {
|
|
||||||
for k := range extensions {
|
|
||||||
AddYAMLNode(parent, k, extensions[k])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type NodeEntry struct {
|
|
||||||
Key string
|
|
||||||
Value any
|
|
||||||
Line int
|
|
||||||
}
|
|
||||||
|
|
||||||
type NodeBuilder struct {
|
|
||||||
Nodes []*NodeEntry
|
|
||||||
High any
|
|
||||||
Low any
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNodeBuilder(high any, low any) *NodeBuilder {
|
|
||||||
// create a new node builder
|
|
||||||
nb := &NodeBuilder{High: high, Low: low}
|
|
||||||
|
|
||||||
// extract fields from the high level object and add them into our node builder.
|
|
||||||
// this will allow us to extract the line numbers from the low level object as well.
|
|
||||||
v := reflect.ValueOf(high).Elem()
|
|
||||||
num := v.NumField()
|
|
||||||
for i := 0; i < num; i++ {
|
|
||||||
nb.add(v.Type().Field(i).Name)
|
|
||||||
}
|
|
||||||
return nb
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NodeBuilder) add(key string) {
|
|
||||||
|
|
||||||
// only operate on exported fields.
|
|
||||||
if unicode.IsLower(rune(key[0])) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the key is 'Extensions' then we need to extract the keys from the map
|
|
||||||
// and add them to the node builder.
|
|
||||||
if key == "Extensions" {
|
|
||||||
extensions := reflect.ValueOf(n.High).Elem().FieldByName(key)
|
|
||||||
for _, e := range extensions.MapKeys() {
|
|
||||||
v := extensions.MapIndex(e)
|
|
||||||
|
|
||||||
extKey := e.String()
|
|
||||||
extValue := v.Interface()
|
|
||||||
nodeEntry := &NodeEntry{Key: extKey, Value: extValue}
|
|
||||||
|
|
||||||
if !reflect.ValueOf(n.Low).IsZero() {
|
|
||||||
fieldValue := reflect.ValueOf(n.Low).Elem().FieldByName("Extensions")
|
|
||||||
f := fieldValue.Interface()
|
|
||||||
value := reflect.ValueOf(f)
|
|
||||||
switch value.Kind() {
|
|
||||||
case reflect.Map:
|
|
||||||
if j, ok := n.Low.(low.HasExtensionsUntyped); ok {
|
|
||||||
originalExtensions := j.GetExtensions()
|
|
||||||
for k := range originalExtensions {
|
|
||||||
if k.Value == extKey {
|
|
||||||
nodeEntry.Line = originalExtensions[k].ValueNode.Line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic("not supported yet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n.Nodes = append(n.Nodes, nodeEntry)
|
|
||||||
}
|
|
||||||
// done, extensions are handled separately.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the field with the tag supplied.
|
|
||||||
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}
|
|
||||||
|
|
||||||
switch value.Kind() {
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
nodeEntry.Value = strconv.FormatInt(value.Int(), 10)
|
|
||||||
case reflect.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:
|
|
||||||
if !value.IsNil() {
|
|
||||||
nodeEntry.Value = f
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
nodeEntry.Value = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is no low level object, then we cannot extract line numbers,
|
|
||||||
// so skip and default to 0, which means a new entry to the spec.
|
|
||||||
// this will place new content and the top of the rendered object.
|
|
||||||
if !reflect.ValueOf(n.Low).IsZero() {
|
|
||||||
lowFieldValue := reflect.ValueOf(n.Low).Elem().FieldByName(key)
|
|
||||||
fLow := lowFieldValue.Interface()
|
|
||||||
value = reflect.ValueOf(fLow)
|
|
||||||
switch value.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
nb := value.Interface().(low.HasValueNodeUntyped).GetValueNode()
|
|
||||||
if nb != nil {
|
|
||||||
nodeEntry.Line = nb.Line
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// everything else, weight it to the bottom of the rendered object.
|
|
||||||
// this is things that we have no way of knowing where they should be placed.
|
|
||||||
nodeEntry.Line = 9999
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if nodeEntry.Value != nil {
|
|
||||||
n.Nodes = append(n.Nodes, nodeEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NodeBuilder) Render() *yaml.Node {
|
|
||||||
// order nodes by line number, retain original order
|
|
||||||
sort.Slice(n.Nodes, func(i, j int) bool {
|
|
||||||
return n.Nodes[i].Line < n.Nodes[j].Line
|
|
||||||
})
|
|
||||||
m := CreateEmptyMapNode()
|
|
||||||
for i := range n.Nodes {
|
|
||||||
node := n.Nodes[i]
|
|
||||||
AddYAMLNode(m, node.Key, node.Value)
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddYAMLNode(parent *yaml.Node, key string, value any) *yaml.Node {
|
|
||||||
|
|
||||||
if value == nil {
|
|
||||||
return parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the type
|
|
||||||
t := reflect.TypeOf(value)
|
|
||||||
var l *yaml.Node
|
|
||||||
if key != "" {
|
|
||||||
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:
|
|
||||||
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
|
|
||||||
}
|
|
||||||
var rawNode yaml.Node
|
|
||||||
err := rawNode.Encode(value)
|
|
||||||
if err != nil {
|
|
||||||
return parent
|
|
||||||
} else {
|
|
||||||
valueNode = &rawNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if l != nil {
|
|
||||||
parent.Content = append(parent.Content, l, valueNode)
|
|
||||||
} else {
|
|
||||||
parent.Content = valueNode.Content
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateEmptyMapNode() *yaml.Node {
|
|
||||||
n := &yaml.Node{
|
|
||||||
Kind: yaml.MappingNode,
|
|
||||||
Tag: "!!map",
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateStringNode(str string) *yaml.Node {
|
|
||||||
n := &yaml.Node{
|
|
||||||
Kind: yaml.ScalarNode,
|
|
||||||
Tag: "!!str",
|
|
||||||
Value: str,
|
|
||||||
}
|
|
||||||
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{
|
|
||||||
Kind: yaml.ScalarNode,
|
|
||||||
Tag: "!!int",
|
|
||||||
Value: i,
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
type Renderable interface {
|
|
||||||
MarshalYAML() (interface{}, error)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -155,9 +155,9 @@ func (d *Document) Render() ([]byte, error) {
|
|||||||
// MarshalYAML will create a ready to render YAML representation of the Document object.
|
// MarshalYAML will create a ready to render YAML representation of the Document object.
|
||||||
func (d *Document) MarshalYAML() (interface{}, error) {
|
func (d *Document) MarshalYAML() (interface{}, error) {
|
||||||
n := high.CreateEmptyMapNode()
|
n := high.CreateEmptyMapNode()
|
||||||
high.AddYAMLNode(n, low.SchemaDialectLabel, d.JsonSchemaDialect)
|
//high.AddYAMLNode(n, low.SchemaDialectLabel, d.JsonSchemaDialect)
|
||||||
high.AddYAMLNode(n, low.OpenAPILabel, d.Version)
|
//high.AddYAMLNode(n, low.OpenAPILabel, d.Version)
|
||||||
high.AddYAMLNode(n, low.InfoLabel, d.Info)
|
//high.AddYAMLNode(n, low.InfoLabel, d.Info)
|
||||||
//high.AddYAMLNode(n, low.TagsLabel, d.Tags)
|
//high.AddYAMLNode(n, low.TagsLabel, d.Tags)
|
||||||
//high.AddYAMLNode(n, low.ServersLabel, d.Servers)
|
//high.AddYAMLNode(n, low.ServersLabel, d.Servers)
|
||||||
//high.AddYAMLNode(n, low.SecurityLabel, d.Security)
|
//high.AddYAMLNode(n, low.SecurityLabel, d.Security)
|
||||||
@@ -166,6 +166,6 @@ func (d *Document) MarshalYAML() (interface{}, error) {
|
|||||||
//high.AddYAMLNode(n, low.PathsLabel, d.Paths)
|
//high.AddYAMLNode(n, low.PathsLabel, d.Paths)
|
||||||
//high.AddYAMLNode(n, low.ComponentsLabel, d.Components)
|
//high.AddYAMLNode(n, low.ComponentsLabel, d.Components)
|
||||||
//high.AddYAMLNode(n, low.WebhooksLabel, d.Webhooks)
|
//high.AddYAMLNode(n, low.WebhooksLabel, d.Webhooks)
|
||||||
high.MarshalExtensions(n, d.Extensions)
|
//high.MarshalExtensions(n, d.Extensions)
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package base
|
|||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -174,8 +175,44 @@ func (s *Schema) Hash() [32]byte {
|
|||||||
d = append(d, fmt.Sprint(s.MinProperties.Value))
|
d = append(d, fmt.Sprint(s.MinProperties.Value))
|
||||||
}
|
}
|
||||||
if !s.AdditionalProperties.IsEmpty() {
|
if !s.AdditionalProperties.IsEmpty() {
|
||||||
|
|
||||||
|
// check type of properties, if we have a low level map, we need to hash the values in a repeatable
|
||||||
|
// order.
|
||||||
|
to := reflect.TypeOf(s.AdditionalProperties.Value)
|
||||||
|
vo := reflect.ValueOf(s.AdditionalProperties.Value)
|
||||||
|
var values []string
|
||||||
|
switch to.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
for i := 0; i < vo.Len(); i++ {
|
||||||
|
vn := vo.Index(i).Interface()
|
||||||
|
values = append(values, fmt.Sprintf("%d:%s", i, low.GenerateHashString(vn)))
|
||||||
|
}
|
||||||
|
sort.Strings(values)
|
||||||
|
d = append(d, strings.Join(values, "||"))
|
||||||
|
|
||||||
|
case reflect.Map:
|
||||||
|
for _, k := range vo.MapKeys() {
|
||||||
|
var x string
|
||||||
|
var l int
|
||||||
|
var v any
|
||||||
|
// extract key
|
||||||
|
if o, ok := k.Interface().(low.HasKeyNode); ok {
|
||||||
|
x = o.GetKeyNode().Value
|
||||||
|
l = o.GetKeyNode().Line
|
||||||
|
v = vo.MapIndex(k).Interface().(low.HasValueNodeUntyped).GetValueNode().Value
|
||||||
|
} else {
|
||||||
|
x = k.String()
|
||||||
|
l = 999
|
||||||
|
v = vo.MapIndex(k).Interface()
|
||||||
|
}
|
||||||
|
values = append(values, fmt.Sprintf("%d:%s:%s", l, x, low.GenerateHashString(v)))
|
||||||
|
}
|
||||||
|
sort.Strings(values)
|
||||||
|
d = append(d, strings.Join(values, "||"))
|
||||||
|
default:
|
||||||
d = append(d, low.GenerateHashString(s.AdditionalProperties.Value))
|
d = append(d, low.GenerateHashString(s.AdditionalProperties.Value))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if !s.Description.IsEmpty() {
|
if !s.Description.IsEmpty() {
|
||||||
d = append(d, fmt.Sprint(s.Description.Value))
|
d = append(d, fmt.Sprint(s.Description.Value))
|
||||||
}
|
}
|
||||||
@@ -593,17 +630,60 @@ func (s *Schema) Build(root *yaml.Node, idx *index.SpecIndex) error {
|
|||||||
if utils.IsNodeMap(addPNode) {
|
if utils.IsNodeMap(addPNode) {
|
||||||
// check if this is a reference, or an inline schema.
|
// check if this is a reference, or an inline schema.
|
||||||
isRef, _, _ := utils.IsNodeRefValue(addPNode)
|
isRef, _, _ := utils.IsNodeRefValue(addPNode)
|
||||||
sp := &SchemaProxy{
|
var sp *SchemaProxy
|
||||||
|
// now check if this object has a 'type' if so, it's a schema, if not... it's a random
|
||||||
|
// object, and we should treat it as a raw map.
|
||||||
|
if _, v := utils.FindKeyNodeTop(TypeLabel, addPNode.Content); v != nil {
|
||||||
|
sp = &SchemaProxy{
|
||||||
kn: addPLabel,
|
kn: addPLabel,
|
||||||
vn: addPNode,
|
vn: addPNode,
|
||||||
idx: idx,
|
idx: idx,
|
||||||
}
|
}
|
||||||
if isRef {
|
|
||||||
sp.isReference = true
|
|
||||||
_, vn := utils.FindKeyNodeTop("$ref", addPNode.Content)
|
|
||||||
sp.referenceLookup = vn.Value
|
|
||||||
}
|
}
|
||||||
|
if isRef {
|
||||||
|
_, vn := utils.FindKeyNodeTop("$ref", addPNode.Content)
|
||||||
|
sp = &SchemaProxy{
|
||||||
|
kn: addPLabel,
|
||||||
|
vn: addPNode,
|
||||||
|
idx: idx,
|
||||||
|
isReference: true,
|
||||||
|
referenceLookup: vn.Value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this is a reference, or a schema, we're done.
|
||||||
|
if sp != nil {
|
||||||
s.AdditionalProperties = low.NodeReference[any]{Value: sp, KeyNode: addPLabel, ValueNode: addPNode}
|
s.AdditionalProperties = low.NodeReference[any]{Value: sp, KeyNode: addPLabel, ValueNode: addPNode}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// if this is a map, collect all the keys and values.
|
||||||
|
if utils.IsNodeMap(addPNode) {
|
||||||
|
|
||||||
|
addProps := make(map[low.KeyReference[string]]low.ValueReference[any])
|
||||||
|
var label string
|
||||||
|
for g := range addPNode.Content {
|
||||||
|
if g%2 == 0 {
|
||||||
|
label = addPNode.Content[g].Value
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
addProps[low.KeyReference[string]{Value: label, KeyNode: addPNode.Content[g-1]}] =
|
||||||
|
low.ValueReference[any]{Value: addPNode.Content[g].Value, ValueNode: addPNode.Content[g]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.AdditionalProperties = low.NodeReference[any]{Value: addProps, KeyNode: addPLabel, ValueNode: addPNode}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the node is an array, extract everything into a trackable structure
|
||||||
|
if utils.IsNodeArray(addPNode) {
|
||||||
|
var addProps []low.ValueReference[any]
|
||||||
|
for i := range addPNode.Content {
|
||||||
|
addProps = append(addProps,
|
||||||
|
low.ValueReference[any]{Value: addPNode.Content[i].Value, ValueNode: addPNode.Content[i]})
|
||||||
|
}
|
||||||
|
s.AdditionalProperties =
|
||||||
|
low.NodeReference[any]{Value: addProps, KeyNode: addPLabel, ValueNode: addPNode}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if utils.IsNodeBoolValue(addPNode) {
|
if utils.IsNodeBoolValue(addPNode) {
|
||||||
b, _ := strconv.ParseBool(addPNode.Value)
|
b, _ := strconv.ParseBool(addPNode.Value)
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ type HasValue[T any] interface {
|
|||||||
*T
|
*T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasValueUnTyped is implemented by NodeReference and ValueReference to return the yaml.Node backing the value.
|
||||||
|
type HasValueUnTyped interface {
|
||||||
|
GetValueUntyped() any
|
||||||
|
GetValueNode() *yaml.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasKeyNode is implemented by KeyReference to return the yaml.Node backing the key.
|
||||||
|
type HasKeyNode interface {
|
||||||
|
GetKeyNode() *yaml.Node
|
||||||
|
}
|
||||||
|
|
||||||
// NodeReference is a low-level container for holding a Value of type T, as well as references to
|
// NodeReference is a low-level container for holding a Value of type T, as well as references to
|
||||||
// a key yaml.Node that points to the key node that contains the value node, and the value node that contains
|
// a key yaml.Node that points to the key node that contains the value node, and the value node that contains
|
||||||
// the actual value.
|
// the actual value.
|
||||||
@@ -158,6 +169,11 @@ func (n NodeReference[T]) GetValue() T {
|
|||||||
return n.Value
|
return n.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetValueUntyped will return the raw value of the node with no type
|
||||||
|
func (n NodeReference[T]) GetValueUntyped() any {
|
||||||
|
return n.Value
|
||||||
|
}
|
||||||
|
|
||||||
// IsEmpty will return true if this reference has no key or value nodes assigned (it's been ignored)
|
// IsEmpty will return true if this reference has no key or value nodes assigned (it's been ignored)
|
||||||
func (n ValueReference[T]) IsEmpty() bool {
|
func (n ValueReference[T]) IsEmpty() bool {
|
||||||
return n.ValueNode == nil
|
return n.ValueNode == nil
|
||||||
@@ -187,11 +203,26 @@ func (n ValueReference[T]) GetValue() T {
|
|||||||
return n.Value
|
return n.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetValueUntyped will return the raw value of the node with no type
|
||||||
|
func (n ValueReference[T]) GetValueUntyped() any {
|
||||||
|
return n.Value
|
||||||
|
}
|
||||||
|
|
||||||
// IsEmpty will return true if this reference has no key or value nodes assigned (it's been ignored)
|
// IsEmpty will return true if this reference has no key or value nodes assigned (it's been ignored)
|
||||||
func (n KeyReference[T]) IsEmpty() bool {
|
func (n KeyReference[T]) IsEmpty() bool {
|
||||||
return n.KeyNode == nil
|
return n.KeyNode == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetValueUntyped will return the raw value of the node with no type
|
||||||
|
func (n KeyReference[T]) GetValueUntyped() any {
|
||||||
|
return n.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeyNode will return the yaml.Node containing the reference key node.
|
||||||
|
func (n KeyReference[T]) GetKeyNode() *yaml.Node {
|
||||||
|
return n.KeyNode
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateMapKey will return a string based on the line and column number of the node, e.g. 33:56 for line 33, col 56.
|
// GenerateMapKey will return a string based on the line and column number of the node, e.g. 33:56 for line 33, col 56.
|
||||||
func (n KeyReference[T]) GenerateMapKey() string {
|
func (n KeyReference[T]) GenerateMapKey() string {
|
||||||
return fmt.Sprintf("%d:%d", n.KeyNode.Line, n.KeyNode.Column)
|
return fmt.Sprintf("%d:%d", n.KeyNode.Line, n.KeyNode.Column)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (e *ExtensionChanges) TotalBreakingChanges() int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompareExtensions will compare a left and right map of Key/ValueReference models for any changes to
|
// CompareExtensions will compare a left and right map of Tag/ValueReference models for any changes to
|
||||||
// anything. This function does not try and cast the value of an extension to perform checks, it
|
// anything. This function does not try and cast the value of an extension to perform checks, it
|
||||||
// will perform a basic value check.
|
// will perform a basic value check.
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user