High-level base documentation is complete.

Examples and every model completed, 1/6th of the way through models.
This commit is contained in:
Dave Shanley
2022-09-15 11:13:54 -04:00
parent b036982212
commit 849074d0bc
9 changed files with 1030 additions and 818 deletions

View File

@@ -10,17 +10,55 @@ import (
"sync" "sync"
) )
// Schema represents a // Schema represents a JSON Schema that support Swagger, OpenAPI 3 and OpenAPI 3.1
//
// Until 3.1 OpenAPI had a strange relationship with JSON Schema. It's been a super-set/sub-set
// mix, which has been confusing. So, instead of building a bunch of different models, we have compressed
// all variations into a single model that makes it easy to support multiple spec types.
//
// - v2 schema: https://swagger.io/specification/v2/#schemaObject
// - v3 schema: https://swagger.io/specification/#schema-object
// - v3.1 schema: https://spec.openapis.org/oas/v3.1.0#schema-object
type Schema struct { type Schema struct {
// 3.1 only, used to define a dialect for this schema, label is '$schema'.
SchemaTypeRef string SchemaTypeRef string
// In versions 2 and 3.0, this ExclusiveMaximum can only be a boolean.
ExclusiveMaximumBool bool
// In version 3.1, ExclusiveMaximum is an integer.
ExclusiveMaximum int64
// In versions 2 and 3.0, this ExclusiveMinimum can only be a boolean.
ExclusiveMinimum int64
// In version 3.1, ExclusiveMinimum is an integer.
ExclusiveMinimumBool bool
// 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
// Schemas are resolved on demand using a SchemaProxy
AllOf []*SchemaProxy
// Polymorphic Schemas are only available in version 3+
OneOf []*SchemaProxy
AnyOf []*SchemaProxy
Discriminator *Discriminator
// in 3.1 examples can be an array (which is recommended)
Examples []any
// Compatible with all versions
Not []*SchemaProxy
Items []*SchemaProxy
Properties map[string]*SchemaProxy
Title string Title string
MultipleOf int64 MultipleOf int64
Maximum int64 Maximum int64
ExclusiveMaximumBool bool
ExclusiveMaximum int64
Minimum int64 Minimum int64
ExclusiveMinimum int64
ExclusiveMinimumBool bool
MaxLength int64 MaxLength int64
MinLength int64 MinLength int64
Pattern string Pattern string
@@ -32,29 +70,21 @@ type Schema struct {
MinProperties int64 MinProperties int64
Required []string Required []string
Enum []string Enum []string
Type []string
AllOf []*SchemaProxy
OneOf []*SchemaProxy
AnyOf []*SchemaProxy
Not []*SchemaProxy
Items []*SchemaProxy
Properties map[string]*SchemaProxy
AdditionalProperties any AdditionalProperties any
Description string Description string
Default any Default any
Nullable bool Nullable bool
Discriminator *Discriminator
ReadOnly bool ReadOnly bool
WriteOnly bool WriteOnly bool
XML *XML XML *XML
ExternalDocs *ExternalDoc ExternalDocs *ExternalDoc
Example any Example any
Examples []any
Deprecated bool Deprecated bool
Extensions map[string]any Extensions map[string]any
low *base.Schema low *base.Schema
} }
// NewSchema will create a new high-level schema from a low-level one.
func NewSchema(schema *base.Schema) *Schema { func NewSchema(schema *base.Schema) *Schema {
s := new(Schema) s := new(Schema)
s.low = schema s.low = schema
@@ -237,6 +267,7 @@ func NewSchema(schema *base.Schema) *Schema {
return s return s
} }
// GoLow will return the low-level instance of Schema that was used to create the high level one.
func (s *Schema) GoLow() *base.Schema { func (s *Schema) GoLow() *base.Schema {
return s.low return s.low
} }

View File

@@ -8,15 +8,52 @@ import (
"github.com/pb33f/libopenapi/datamodel/low/base" "github.com/pb33f/libopenapi/datamodel/low/base"
) )
// SchemaProxy exists as a stub that will create a Schema once (and only once) the Schema() method is called. An
// underlying low-level SchemaProxy backs this high-level one.
//
// Why use a Proxy design?
//
// There are three reasons.
//
// 1. Circular References and Endless Loops.
//
// JSON Schema allows for references to be used. This means references can loop around and create infinite recursive
// structures, These 'Circular references' technically mean a schema can NEVER be resolved, not without breaking the
// loop somewhere along the chain.
//
// Polymorphism in the form of 'oneOf' and 'anyOf' in version 3+ only exacerbates the problem.
//
// These circular traps can be discovered using the resolver, however it's still not enough to stop endless loops and
// endless goroutine spawning. A proxy design means that resolving occurs on demand and runs down a single level only.
// preventing any run-away loops.
//
// 2. Performance
//
// Even without circular references, Polymorphism creates large additional resolving chains that take a long time
// and slow things down when building. By preventing recursion through every polymorphic item, building models is kept
// fast and snappy, which is desired for realtime processing of specs.
//
// - Q: Yeah, but, why not just use state to avoiding re-visiting seen polymorphic nodes?
// - A: It's slow, takes up memory and still has runaway potential in very, very long chains.
//
// 3. Short Circuit Errors.
//
// Schemas are where things can get messy, mainly because the Schema standard changes between versions, and
// it's not actually JSONSchema until 3.1, so lots of times a bad schema will break parsing. Errors are only found
// when a schema is needed, so the rest of the document is parsed and ready to use.
type SchemaProxy struct { type SchemaProxy struct {
schema *low.NodeReference[*base.SchemaProxy] schema *low.NodeReference[*base.SchemaProxy]
buildError error buildError error
} }
// NewSchemaProxy creates a new high-level SchemaProxy from a low-level one.
func NewSchemaProxy(schema *low.NodeReference[*base.SchemaProxy]) *SchemaProxy { func NewSchemaProxy(schema *low.NodeReference[*base.SchemaProxy]) *SchemaProxy {
return &SchemaProxy{schema: schema} return &SchemaProxy{schema: schema}
} }
// Schema will create a new Schema instance using NewSchema from the low-level SchemaProxy backing this high-level one.
// If there is a problem building the Schema, then this method will return nil. Use GetBuildError to gain access
// to that building error.
func (sp *SchemaProxy) Schema() *Schema { func (sp *SchemaProxy) Schema() *Schema {
s := sp.schema.Value.Schema() s := sp.schema.Value.Schema()
if s == nil { if s == nil {
@@ -26,6 +63,7 @@ func (sp *SchemaProxy) Schema() *Schema {
return NewSchema(s) return NewSchema(s)
} }
// GetBuildError returns any error that was thrown when calling Schema()
func (sp *SchemaProxy) GetBuildError() error { func (sp *SchemaProxy) GetBuildError() error {
return sp.buildError return sp.buildError
} }

View File

@@ -4,6 +4,7 @@
package base package base
import ( import (
"fmt"
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
lowbase "github.com/pb33f/libopenapi/datamodel/low/base" lowbase "github.com/pb33f/libopenapi/datamodel/low/base"
"github.com/pb33f/libopenapi/index" "github.com/pb33f/libopenapi/index"
@@ -326,3 +327,67 @@ required: [cake, fish]`
assert.Equal(t, 100, wentLower.Name.ValueNode.Line) assert.Equal(t, 100, wentLower.Name.ValueNode.Line)
} }
func ExampleNewSchema() {
// create an example schema object
// this can be either JSON or YAML.
yml := `
title: this is a schema
type: object
properties:
aProperty:
description: this is an integer property
type: integer
format: int64`
// unmarshal raw bytes
var node yaml.Node
_ = yaml.Unmarshal([]byte(yml), &node)
// build out the low-level model
var lowSchema lowbase.Schema
_ = low.BuildModel(&node, &lowSchema)
_ = lowSchema.Build(node.Content[0], nil)
// build the high level model
highSchema := NewSchema(&lowSchema)
// print out the description of 'aProperty'
fmt.Print(highSchema.Properties["aProperty"].Schema().Description)
// Output: this is an integer property
}
func ExampleNewSchemaProxy() {
// create an example schema object
// this can be either JSON or YAML.
yml := `
title: this is a schema
type: object
properties:
aProperty:
description: this is an integer property
type: integer
format: int64`
// unmarshal raw bytes
var node yaml.Node
_ = yaml.Unmarshal([]byte(yml), &node)
// build out the low-level model
var lowSchema lowbase.SchemaProxy
_ = low.BuildModel(&node, &lowSchema)
_ = lowSchema.Build(node.Content[0], nil)
// build the high level schema proxy
highSchema := NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{
Value: &lowSchema,
})
// print out the description of 'aProperty'
fmt.Print(highSchema.Schema().Properties["aProperty"].Schema().Description)
// Output: this is an integer property
}

View File

@@ -8,6 +8,9 @@ import (
low "github.com/pb33f/libopenapi/datamodel/low/base" low "github.com/pb33f/libopenapi/datamodel/low/base"
) )
// Tag represents a high-level Tag instance that is backed by a low-level one.
// - v2: https://swagger.io/specification/v2/#tagObject
// - v3: https://swagger.io/specification/#tag-object
type Tag struct { type Tag struct {
Name string Name string
Description string Description string
@@ -16,6 +19,7 @@ type Tag struct {
low *low.Tag low *low.Tag
} }
// NewTag creates a new high-level Tag instance that is backed by a low-level one.
func NewTag(tag *low.Tag) *Tag { func NewTag(tag *low.Tag) *Tag {
t := new(Tag) t := new(Tag)
t.low = tag t.low = tag
@@ -32,10 +36,12 @@ func NewTag(tag *low.Tag) *Tag {
return t return t
} }
// GoLow returns the low-level Tag instance used to create the high-level one.
func (t *Tag) GoLow() *low.Tag { func (t *Tag) GoLow() *low.Tag {
return t.low return t.low
} }
// Experimental mutation API.
//func (t *Tag) SetName(value string) { //func (t *Tag) SetName(value string) {
// t.GoLow().Name.ValueNode.Value = value // t.GoLow().Name.ValueNode.Value = value
//} //}

View File

@@ -4,6 +4,7 @@
package base package base
import ( import (
"fmt"
lowmodel "github.com/pb33f/libopenapi/datamodel/low" lowmodel "github.com/pb33f/libopenapi/datamodel/low"
lowbase "github.com/pb33f/libopenapi/datamodel/low/base" lowbase "github.com/pb33f/libopenapi/datamodel/low/base"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -38,3 +39,31 @@ x-hack: code`
assert.Equal(t, 5, wentLow.FindExtension("x-hack").ValueNode.Line) assert.Equal(t, 5, wentLow.FindExtension("x-hack").ValueNode.Line)
} }
func ExampleNewTag() {
// create an example schema object
// this can be either JSON or YAML.
yml := `
name: Purchases
description: All kinds of purchase related operations
externalDocs:
url: https://pb33f.io/purchases
x-hack: code`
// unmarshal raw bytes
var node yaml.Node
_ = yaml.Unmarshal([]byte(yml), &node)
// build out the low-level model
var lowTag lowbase.Tag
_ = lowmodel.BuildModel(&node, &lowTag)
_ = lowTag.Build(node.Content[0], nil)
// build the high level tag
highTag := NewTag(&lowTag)
// print out the tag name
fmt.Print(highTag.Name)
// Output: Purchases
}

View File

@@ -8,6 +8,10 @@ import (
low "github.com/pb33f/libopenapi/datamodel/low/base" low "github.com/pb33f/libopenapi/datamodel/low/base"
) )
// XML represents a high-level representation of an XML object defined by all versions of OpenAPI and backed by
// low-level XML object.
// v2 - https://swagger.io/specification/v2/#xmlObject
// v3 - https://swagger.io/specification/#xml-object
type XML struct { type XML struct {
Name string Name string
Namespace string Namespace string
@@ -18,6 +22,7 @@ type XML struct {
low *low.XML low *low.XML
} }
// NewXML creates a new high-level XML instance from a low-level one.
func NewXML(xml *low.XML) *XML { func NewXML(xml *low.XML) *XML {
x := new(XML) x := new(XML)
x.low = xml x.low = xml
@@ -30,6 +35,7 @@ func NewXML(xml *low.XML) *XML {
return x return x
} }
// GoLow returns the low level XML reference used to create the high level one.
func (x *XML) GoLow() *low.XML { func (x *XML) GoLow() *low.XML {
return x.low return x.low
} }

View File

@@ -0,0 +1,36 @@
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
package base
import (
"fmt"
lowmodel "github.com/pb33f/libopenapi/datamodel/low"
lowbase "github.com/pb33f/libopenapi/datamodel/low/base"
"gopkg.in/yaml.v3"
)
func ExampleNewXML() {
// create an example schema object
// this can be either JSON or YAML.
yml := `
namespace: https://pb33f.io/schema
prefix: sample`
// unmarshal raw bytes
var node yaml.Node
_ = yaml.Unmarshal([]byte(yml), &node)
// build out the low-level model
var lowXML lowbase.XML
_ = lowmodel.BuildModel(&node, &lowXML)
_ = lowXML.Build(node.Content[0], nil)
// build the high level tag
highXML := NewXML(&lowXML)
// print out the XML namespace
fmt.Print(highXML.Namespace)
// Output: https://pb33f.io/schema
}

View File

@@ -241,7 +241,7 @@ func (s *Schema) Build(root *yaml.Node, idx *index.SpecIndex) error {
var xml XML var xml XML
_ = low.BuildModel(xmlNode, &xml) _ = low.BuildModel(xmlNode, &xml)
// extract extensions if set. // extract extensions if set.
_ = xml.Build(xmlNode) // returns no errors, can't check for one. _ = xml.Build(xmlNode, idx) // returns no errors, can't check for one.
s.XML = low.NodeReference[*XML]{Value: &xml, KeyNode: xmlLabel, ValueNode: xmlNode} s.XML = low.NodeReference[*XML]{Value: &xml, KeyNode: xmlLabel, ValueNode: xmlNode}
} }

View File

@@ -2,6 +2,7 @@ package base
import ( import (
"github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -14,7 +15,7 @@ type XML struct {
Extensions map[low.KeyReference[string]]low.ValueReference[any] Extensions map[low.KeyReference[string]]low.ValueReference[any]
} }
func (x *XML) Build(root *yaml.Node) error { func (x *XML) Build(root *yaml.Node, _ *index.SpecIndex) error {
x.Extensions = low.ExtractExtensions(root) x.Extensions = low.ExtractExtensions(root)
return nil return nil
} }