mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-06 12:37:49 +00:00
196 lines
6.4 KiB
Go
196 lines
6.4 KiB
Go
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package base
|
|
|
|
import (
|
|
"github.com/pb33f/libopenapi/datamodel/high"
|
|
"github.com/pb33f/libopenapi/datamodel/low"
|
|
"github.com/pb33f/libopenapi/datamodel/low/base"
|
|
"github.com/pb33f/libopenapi/index"
|
|
"github.com/pb33f/libopenapi/utils"
|
|
"gopkg.in/yaml.v3"
|
|
"sync"
|
|
)
|
|
|
|
// 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 {
|
|
schema *low.NodeReference[*base.SchemaProxy]
|
|
buildError error
|
|
rendered *Schema
|
|
refStr string
|
|
lock *sync.Mutex
|
|
}
|
|
|
|
// NewSchemaProxy creates a new high-level SchemaProxy from a low-level one.
|
|
func NewSchemaProxy(schema *low.NodeReference[*base.SchemaProxy]) *SchemaProxy {
|
|
return &SchemaProxy{schema: schema, lock: &sync.Mutex{}}
|
|
}
|
|
|
|
// CreateSchemaProxy will create a new high-level SchemaProxy from a high-level Schema, this acts the same
|
|
// as if the SchemaProxy is pre-rendered.
|
|
func CreateSchemaProxy(schema *Schema) *SchemaProxy {
|
|
return &SchemaProxy{rendered: schema, lock: &sync.Mutex{}}
|
|
}
|
|
|
|
// CreateSchemaProxyRef will create a new high-level SchemaProxy from a reference string, this is used only when
|
|
// building out new models from scratch that require a reference rather than a schema implementation.
|
|
func CreateSchemaProxyRef(ref string) *SchemaProxy {
|
|
return &SchemaProxy{refStr: ref, lock: &sync.Mutex{}}
|
|
}
|
|
|
|
// 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 {
|
|
sp.lock.Lock()
|
|
if sp.rendered == nil {
|
|
|
|
s := sp.schema.Value.Schema()
|
|
if s == nil {
|
|
sp.buildError = sp.schema.Value.GetBuildError()
|
|
sp.lock.Unlock()
|
|
return nil
|
|
}
|
|
sch := NewSchema(s)
|
|
sch.ParentProxy = sp
|
|
|
|
sp.rendered = sch
|
|
sp.lock.Unlock()
|
|
return sch
|
|
} else {
|
|
sp.lock.Unlock()
|
|
return sp.rendered
|
|
}
|
|
}
|
|
|
|
// IsReference returns true if the SchemaProxy is a reference to another Schema.
|
|
func (sp *SchemaProxy) IsReference() bool {
|
|
if sp.refStr != "" {
|
|
return true
|
|
}
|
|
if sp.schema != nil {
|
|
return sp.schema.Value.IsSchemaReference()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetReference returns the location of the $ref if this SchemaProxy is a reference to another Schema.
|
|
func (sp *SchemaProxy) GetReference() string {
|
|
if sp.refStr != "" {
|
|
return sp.refStr
|
|
}
|
|
return sp.schema.Value.GetSchemaReference()
|
|
}
|
|
|
|
// GetReferenceOrigin returns a pointer to the index.NodeOrigin of the $ref if this SchemaProxy is a reference to another Schema.
|
|
// returns nil if the origin cannot be found (which, means there is a bug, and we need to fix it).
|
|
func (sp *SchemaProxy) GetReferenceOrigin() *index.NodeOrigin {
|
|
if sp.schema != nil {
|
|
return sp.schema.Value.GetSchemaReferenceLocation()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BuildSchema operates the same way as Schema, except it will return any error along with the *Schema
|
|
func (sp *SchemaProxy) BuildSchema() (*Schema, error) {
|
|
if sp.rendered != nil {
|
|
return sp.rendered, sp.buildError
|
|
}
|
|
schema := sp.Schema()
|
|
er := sp.buildError
|
|
return schema, er
|
|
}
|
|
|
|
// GetBuildError returns any error that was thrown when calling Schema()
|
|
func (sp *SchemaProxy) GetBuildError() error {
|
|
return sp.buildError
|
|
}
|
|
|
|
func (sp *SchemaProxy) GoLow() *base.SchemaProxy {
|
|
if sp.schema == nil {
|
|
return nil
|
|
}
|
|
return sp.schema.Value
|
|
}
|
|
|
|
func (sp *SchemaProxy) GoLowUntyped() any {
|
|
if sp.schema == nil {
|
|
return nil
|
|
}
|
|
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) {
|
|
var s *Schema
|
|
var err error
|
|
// if this schema isn't a reference, then build it out.
|
|
if !sp.IsReference() {
|
|
s, err = sp.BuildSchema()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nb := high.NewNodeBuilder(s, s.low)
|
|
return nb.Render(), nil
|
|
} else {
|
|
// do not build out a reference, just marshal the reference.
|
|
mp := utils.CreateEmptyMapNode()
|
|
mp.Content = append(mp.Content,
|
|
utils.CreateStringNode("$ref"),
|
|
utils.CreateStringNode(sp.GetReference()))
|
|
return mp, nil
|
|
}
|
|
}
|
|
|
|
// 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
|
|
var err error
|
|
s, err = sp.BuildSchema()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nb := high.NewNodeBuilder(s, s.low)
|
|
nb.Resolve = true
|
|
return nb.Render(), nil
|
|
}
|