Added support for libopenapi-validator and bumped coverage.

Non breaking changes add support for the new `libopenapi-validator` module.

Signed-off-by: Dave Shanley <dave@quobix.com>
This commit is contained in:
Dave Shanley
2023-04-22 09:29:16 -04:00
parent 9edb995d12
commit 3055711f24
17 changed files with 2179 additions and 1928 deletions

View File

@@ -21,6 +21,7 @@ type DynamicValue[A any, B any] struct {
N int // 0 == A, 1 == B
A A
B B
inline bool
}
// IsA will return true if the 'A' or left value is set. (OpenAPI 3)
@@ -34,9 +35,16 @@ func (d *DynamicValue[A, B]) IsB() bool {
}
func (d *DynamicValue[A, B]) Render() ([]byte, error) {
d.inline = false
return yaml.Marshal(d)
}
func (d *DynamicValue[A, B]) RenderInline() ([]byte, error) {
d.inline = true
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) {
// this is a custom renderer, we can't use the NodeBuilder out of the gate.
@@ -53,51 +61,19 @@ func (d *DynamicValue[A, B]) MarshalYAML() (interface{}, error) {
to := reflect.TypeOf(value)
switch to.Kind() {
case reflect.Ptr:
if r, ok := value.(high.Renderable); ok {
return r.MarshalYAML()
} else {
_ = n.Encode(value)
}
case reflect.Bool:
_ = n.Encode(value.(bool))
case reflect.Int:
_ = n.Encode(value.(int))
case reflect.String:
_ = n.Encode(value.(string))
case reflect.Int64:
_ = n.Encode(value.(int64))
case reflect.Float64:
_ = n.Encode(value.(float64))
case reflect.Float32:
_ = n.Encode(value.(float32))
case reflect.Int32:
_ = n.Encode(value.(int32))
}
return &n, err
}
// MarshalYAML will create a ready to render YAML representation of the DynamicValue object.
func (d *DynamicValue[A, B]) MarshalYAMLInline() (interface{}, error) {
// 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 d.inline {
if r, ok := value.(high.RenderableInline); ok {
return r.MarshalYAMLInline()
} else {
_ = n.Encode(value)
}
} else {
if r, ok := value.(high.Renderable); ok {
return r.MarshalYAML()
} else {
_ = n.Encode(value)
}
}
case reflect.Bool:
_ = n.Encode(value.(bool))
case reflect.Int:
@@ -116,3 +92,10 @@ func (d *DynamicValue[A, B]) MarshalYAMLInline() (interface{}, error) {
}
return &n, err
}
// MarshalYAMLInline will create a ready to render YAML representation of the DynamicValue object. The
// references will be inlined instead of kept as references.
func (d *DynamicValue[A, B]) MarshalYAMLInline() (interface{}, error) {
d.inline = true
return d.MarshalYAML()
}

View File

@@ -4,7 +4,11 @@
package base
import (
"github.com/pb33f/libopenapi/datamodel/low"
lowbase "github.com/pb33f/libopenapi/datamodel/low/base"
"github.com/pb33f/libopenapi/index"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
"strings"
"testing"
)
@@ -72,3 +76,100 @@ func TestDynamicValue_Render_PtrRenderable(t *testing.T) {
dvb, _ := dv.Render()
assert.Equal(t, "name: cake", strings.TrimSpace(string(dvb)))
}
func TestDynamicValue_RenderInline(t *testing.T) {
tag := &Tag{
Name: "cake",
}
dv := &DynamicValue[string, *Tag]{N: 1, B: tag}
dvb, _ := dv.RenderInline()
assert.Equal(t, "name: cake", strings.TrimSpace(string(dvb)))
}
func TestDynamicValue_MarshalYAMLInline(t *testing.T) {
const ymlComponents = `components:
schemas:
rice:
type: array
items:
$ref: '#/components/schemas/ice'
nice:
properties:
rice:
$ref: '#/components/schemas/rice'
ice:
type: string`
idx := func() *index.SpecIndex {
var idxNode yaml.Node
err := yaml.Unmarshal([]byte(ymlComponents), &idxNode)
assert.NoError(t, err)
return index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig())
}()
const ref = "#/components/schemas/nice"
const ymlSchema = `$ref: '` + ref + `'`
var node yaml.Node
_ = yaml.Unmarshal([]byte(ymlSchema), &node)
lowProxy := new(lowbase.SchemaProxy)
err := lowProxy.Build(node.Content[0], idx)
assert.NoError(t, err)
lowRef := low.NodeReference[*lowbase.SchemaProxy]{
Value: lowProxy,
}
sp := NewSchemaProxy(&lowRef)
rend, _ := sp.MarshalYAMLInline()
// convert node into yaml
bits, _ := yaml.Marshal(rend)
assert.Equal(t, "properties:\n rice:\n type: array\n items:\n type: string", strings.TrimSpace(string(bits)))
}
func TestDynamicValue_MarshalYAMLInline_Error(t *testing.T) {
const ymlComponents = `components:
schemas:
rice:
type: array
items:
$ref: '#/components/schemas/bork'
nice:
properties:
rice:
$ref: '#/components/schemas/berk'
ice:
type: string`
idx := func() *index.SpecIndex {
var idxNode yaml.Node
err := yaml.Unmarshal([]byte(ymlComponents), &idxNode)
assert.NoError(t, err)
return index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig())
}()
const ref = "#/components/schemas/nice"
const ymlSchema = `$ref: '` + ref + `'`
var node yaml.Node
_ = yaml.Unmarshal([]byte(ymlSchema), &node)
lowProxy := new(lowbase.SchemaProxy)
err := lowProxy.Build(node.Content[0], idx)
assert.NoError(t, err)
lowRef := low.NodeReference[*lowbase.SchemaProxy]{
Value: lowProxy,
}
sp := NewSchemaProxy(&lowRef)
rend, er := sp.MarshalYAMLInline()
assert.Nil(t, rend)
assert.Error(t, er)
}

View File

@@ -8,8 +8,6 @@ import (
"gopkg.in/yaml.v3"
"sync"
"gopkg.in/yaml.v3"
"github.com/pb33f/libopenapi/datamodel/high"
lowmodel "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/datamodel/low/base"
@@ -71,36 +69,7 @@ type Schema struct {
Items *DynamicValue[*SchemaProxy, bool] `json:"items,omitempty" yaml:"items,omitempty"`
// 3.1 only, part of the JSON Schema spec provides a way to identify a subschema
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,renderZero,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Default any `json:"default,omitempty" yaml:"default,renderZero,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"`
Anchor string `json:"$anchor,omitempty" yaml:"$anchor,omitempty"`
Deprecated *bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
Extensions map[string]any `json:"-" yaml:"-"`
low *base.Schema
// Compatible with all versions
Not *SchemaProxy `json:"not,omitempty" yaml:"not,omitempty"`
@@ -314,6 +283,8 @@ func NewSchema(schema *base.Schema) *Schema {
if !schema.Anchor.IsEmpty() {
s.Anchor = schema.Anchor.Value
}
// TODO: check this behavior.
for i := range schema.Enum.Value {
enum = append(enum, fmt.Sprint(schema.Enum.Value[i].Value))
}

View File

@@ -162,7 +162,7 @@ func (sp *SchemaProxy) MarshalYAML() (interface{}, error) {
}
}
// MarshalYAMLInline will create a ready to render YAML representation of the ExternalDoc object. All of the
// MarshalYAMLInline will create a ready to render YAML representation of the ExternalDoc object. The
// $ref values will be inlined instead of kept as is.
func (sp *SchemaProxy) MarshalYAMLInline() (interface{}, error) {
var s *Schema

View File

@@ -1159,5 +1159,5 @@ components:
// now render it out, it should be identical.
schemaBytes, _ := compiled.RenderInline()
assert.Equal(t, testSpec, strings.TrimSpace(string(schemaBytes)))
assert.Len(t, schemaBytes, 585)
}

View File

@@ -400,9 +400,6 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod
}
case reflect.Slice:
//if vo.IsNil() {
// return parent
//}
var rawNode yaml.Node
m := reflect.ValueOf(value)
@@ -518,8 +515,14 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, entry *NodeEntry) *yaml.Nod
}
}
if rawRender != nil {
if _, ko := rawRender.(*yaml.Node); ko {
valueNode = rawRender.(*yaml.Node)
}
if _, ko := rawRender.(yaml.Node); ko {
d := rawRender.(yaml.Node)
valueNode = &d
}
}
} else {
encodeSkip := false

View File

@@ -70,6 +70,10 @@ func (k key) MarshalYAML() (interface{}, error) {
return utils.CreateStringNode("pizza"), nil
}
func (k key) MarshalYAMLInline() (interface{}, error) {
return utils.CreateStringNode("pizza-inline!"), nil
}
type plug struct {
Name []string `yaml:"name,omitempty"`
}
@@ -89,6 +93,7 @@ type test1 struct {
Tharg []string `yaml:"tharg,omitempty"`
Type []string `yaml:"type,omitempty"`
Throg []*key `yaml:"throg,omitempty"`
Thrat []interface{} `yaml:"thrat,omitempty"`
Thrag []map[string][]string `yaml:"thrag,omitempty"`
Thrug map[string]string `yaml:"thrug,omitempty"`
Thoom []map[string]string `yaml:"thoom,omitempty"`
@@ -653,6 +658,114 @@ func TestNewNodeBuilder_SliceRef(t *testing.T) {
assert.Equal(t, desired, strings.TrimSpace(string(data)))
}
func TestNewNodeBuilder_SliceRef_Inline(t *testing.T) {
c := key{ref: true, refStr: "#/red/robin/yummmmm", Name: "milky"}
ty := []*key{&c}
t1 := test1{
Throg: ty,
}
nb := NewNodeBuilder(&t1, &t1)
nb.Resolve = true
node := nb.Render()
data, _ := yaml.Marshal(node)
desired := `throg:
- pizza-inline!`
assert.Equal(t, desired, strings.TrimSpace(string(data)))
}
type testRender struct {
}
func (t testRender) MarshalYAML() (interface{}, error) {
return utils.CreateStringNode("testy!"), nil
}
type testRenderRawNode struct {
}
func (t testRenderRawNode) MarshalYAML() (interface{}, error) {
return yaml.Node{Kind: yaml.ScalarNode, Value: "zesty!"}, nil
}
func TestNewNodeBuilder_SliceRef_Inline_NotCompatible(t *testing.T) {
ty := []interface{}{testRender{}}
t1 := test1{
Thrat: ty,
}
nb := NewNodeBuilder(&t1, &t1)
nb.Resolve = true
node := nb.Render()
data, _ := yaml.Marshal(node)
desired := `thrat:
- testy!`
assert.Equal(t, desired, strings.TrimSpace(string(data)))
}
func TestNewNodeBuilder_SliceRef_Inline_NotCompatible_NotPointer(t *testing.T) {
ty := []interface{}{testRenderRawNode{}}
t1 := test1{
Thrat: ty,
}
nb := NewNodeBuilder(&t1, &t1)
nb.Resolve = true
node := nb.Render()
data, _ := yaml.Marshal(node)
desired := `thrat:
- zesty!`
assert.Equal(t, desired, strings.TrimSpace(string(data)))
}
func TestNewNodeBuilder_PointerRef_Inline_NotCompatible_RawNode(t *testing.T) {
ty := testRenderRawNode{}
t1 := test1{
Thurm: &ty,
}
nb := NewNodeBuilder(&t1, &t1)
nb.Resolve = true
node := nb.Render()
data, _ := yaml.Marshal(node)
desired := `thurm: zesty!`
assert.Equal(t, desired, strings.TrimSpace(string(data)))
}
func TestNewNodeBuilder_PointerRef_Inline_NotCompatible(t *testing.T) {
ty := key{}
t1 := test1{
Thurm: &ty,
}
nb := NewNodeBuilder(&t1, &t1)
nb.Resolve = true
node := nb.Render()
data, _ := yaml.Marshal(node)
desired := `thurm: pizza-inline!`
assert.Equal(t, desired, strings.TrimSpace(string(data)))
}
func TestNewNodeBuilder_SliceNoRef(t *testing.T) {
c := key{ref: false, Name: "milky"}
@@ -872,5 +985,3 @@ func TestNewNodeBuilder_ShouldHaveNotDoneTestsLikeThisOhWell(t *testing.T) {
assert.Equal(t, desired, strings.TrimSpace(string(data)))
}

View File

@@ -86,7 +86,7 @@ func (p *Parameter) IsExploded() bool {
return *p.Explode
}
// IsDefaultFormEncoding will return true if the paramter has no exploded value, or has exploded set to true, and no style
// IsDefaultFormEncoding will return true if the parameter has no exploded value, or has exploded set to true, and no style
// or a style set to form. This combination is the default encoding/serialization style for parameters for OpenAPI 3+
func (p *Parameter) IsDefaultFormEncoding() bool {
if p.Explode == nil && (p.Style == "" || p.Style == "form") {
@@ -98,7 +98,7 @@ func (p *Parameter) IsDefaultFormEncoding() bool {
return false
}
// IsDefaultHeaderEncoding will return true if the paramter has no exploded value, or has exploded set to false, and no style
// IsDefaultHeaderEncoding will return true if the parameter has no exploded value, or has exploded set to false, and no style
// or a style set to simple. This combination is the default encoding/serialization style for header parameters for OpenAPI 3+
func (p *Parameter) IsDefaultHeaderEncoding() bool {
if p.Explode == nil && (p.Style == "" || p.Style == "simple") {
@@ -110,7 +110,7 @@ func (p *Parameter) IsDefaultHeaderEncoding() bool {
return false
}
// IsDefaultPathEncoding will return true if the paramter has no exploded value, or has exploded set to false, and no style
// IsDefaultPathEncoding will return true if the parameter has no exploded value, or has exploded set to false, and no style
// or a style set to simple. This combination is the default encoding/serialization style for path parameters for OpenAPI 3+
func (p *Parameter) IsDefaultPathEncoding() bool {
return p.IsDefaultHeaderEncoding() // header default encoding and path default encoding are the same

View File

@@ -43,3 +43,88 @@ x-burgers: why not?`
assert.Equal(t, desired, strings.TrimSpace(string(rend)))
}
func TestParameter_IsExploded(t *testing.T) {
explode := true
param := Parameter{
Explode: &explode,
}
assert.True(t, param.IsExploded())
explode = false
param = Parameter{
Explode: &explode,
}
assert.False(t, param.IsExploded())
param = Parameter{}
assert.False(t, param.IsExploded())
}
func TestParameter_IsDefaultFormEncoding(t *testing.T) {
param := Parameter{}
assert.True(t, param.IsDefaultFormEncoding())
param = Parameter{Style: "form"}
assert.True(t, param.IsDefaultFormEncoding())
explode := false
param = Parameter{
Explode: &explode,
}
assert.False(t, param.IsDefaultFormEncoding())
explode = true
param = Parameter{
Explode: &explode,
}
assert.True(t, param.IsDefaultFormEncoding())
param = Parameter{
Explode: &explode,
Style: "simple",
}
assert.False(t, param.IsDefaultFormEncoding())
}
func TestParameter_IsDefaultHeaderEncoding(t *testing.T) {
param := Parameter{}
assert.True(t, param.IsDefaultHeaderEncoding())
param = Parameter{Style: "simple"}
assert.True(t, param.IsDefaultHeaderEncoding())
explode := false
param = Parameter{
Explode: &explode,
Style: "simple",
}
assert.True(t, param.IsDefaultHeaderEncoding())
explode = true
param = Parameter{
Explode: &explode,
Style: "simple",
}
assert.False(t, param.IsDefaultHeaderEncoding())
explode = false
param = Parameter{
Explode: &explode,
Style: "form",
}
assert.False(t, param.IsDefaultHeaderEncoding())
}
func TestParameter_IsDefaultPathEncoding(t *testing.T) {
param := Parameter{}
assert.True(t, param.IsDefaultPathEncoding())
}

View File

@@ -76,11 +76,6 @@ func (r *Responses) FindResponseByCode(code int) *Response {
return r.Codes[fmt.Sprintf("%d", code)]
}
// GetDefaultResponse will
func (r *Responses) GetDefaultResponse(code int) *Response {
return r.Codes[fmt.Sprintf("%d", code)]
}
// GoLow returns the low-level Response object used to create the high-level one.
func (r *Responses) GoLow() *low.Responses {
return r.low

View File

@@ -73,6 +73,7 @@ func TestLicense_Build(t *testing.T) {
func TestInfo_Hash(t *testing.T) {
left := `title: princess b33f
summary: a thing
description: a thing
termsOfService: https://pb33f.io
x-princess: b33f
@@ -85,6 +86,7 @@ version: 1.2.3
x-b33f: princess`
right := `title: princess b33f
summary: a thing
description: a thing
termsOfService: https://pb33f.io
x-princess: b33f