Files
libopenapi/datamodel/low/base/schema.go
Dave Shanley 61f99b8fd6 Replacing extensions hash code **breaking change**
This is a large update, I realized that extensions are not being hashed correctly, and because I have the same code everywhere, it means running back through the stack and cleaning up the invalid code that will break if multiple extensions are used in different positions in the raw spec.

At the same time, I realized that the v2 model has the same primitive/enum issues that are part cleaned up in v3. This is a breaking changhe because enums are now []any and not []string, as well as primitives for bool, int etc are all pointers now instead of the copied values.

This will break any consumers.
2022-11-18 11:00:34 -05:00

867 lines
27 KiB
Go

package base
import (
"crypto/sha256"
"fmt"
"github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/index"
"github.com/pb33f/libopenapi/utils"
"gopkg.in/yaml.v3"
"sort"
"strconv"
"strings"
)
// SchemaDynamicValue 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 SchemaDynamicValue[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 SchemaDynamicValue[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 SchemaDynamicValue[A, B]) IsB() bool {
return s.N == 1
}
// 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 {
// Reference to the '$schema' dialect setting (3.1 only)
SchemaTypeRef low.NodeReference[string]
// In versions 2 and 3.0, this ExclusiveMaximum can only be a boolean.
ExclusiveMaximum low.NodeReference[SchemaDynamicValue[bool, int64]]
// In versions 2 and 3.0, this ExclusiveMinimum can only be a boolean.
ExclusiveMinimum low.NodeReference[SchemaDynamicValue[bool, int64]]
// 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 low.NodeReference[SchemaDynamicValue[string, []low.ValueReference[string]]]
// Schemas are resolved on demand using a SchemaProxy
AllOf low.NodeReference[[]low.ValueReference[*SchemaProxy]]
// Polymorphic Schemas are only available in version 3+
OneOf low.NodeReference[[]low.ValueReference[*SchemaProxy]]
AnyOf low.NodeReference[[]low.ValueReference[*SchemaProxy]]
Discriminator low.NodeReference[*Discriminator]
// in 3.1 examples can be an array (which is recommended)
Examples low.NodeReference[[]low.ValueReference[any]]
// Compatible with all versions
Title low.NodeReference[string]
MultipleOf low.NodeReference[int64]
Maximum low.NodeReference[int64]
Minimum low.NodeReference[int64]
MaxLength low.NodeReference[int64]
MinLength low.NodeReference[int64]
Pattern low.NodeReference[string]
Format low.NodeReference[string]
MaxItems low.NodeReference[int64]
MinItems low.NodeReference[int64]
UniqueItems low.NodeReference[int64]
MaxProperties low.NodeReference[int64]
MinProperties low.NodeReference[int64]
Required low.NodeReference[[]low.ValueReference[string]]
Enum low.NodeReference[[]low.ValueReference[any]]
Not low.NodeReference[[]low.ValueReference[*SchemaProxy]]
Items low.NodeReference[[]low.ValueReference[*SchemaProxy]]
Properties low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*SchemaProxy]]
AdditionalProperties low.NodeReference[any]
Description low.NodeReference[string]
ContentEncoding low.NodeReference[string]
ContentMediaType low.NodeReference[string]
Default low.NodeReference[any]
Nullable low.NodeReference[bool]
ReadOnly low.NodeReference[bool]
WriteOnly low.NodeReference[bool]
XML low.NodeReference[*XML]
ExternalDocs low.NodeReference[*ExternalDoc]
Example low.NodeReference[any]
Deprecated low.NodeReference[bool]
Extensions map[low.KeyReference[string]]low.ValueReference[any]
}
// Hash will calculate a SHA256 hash from the values of the schema, This allows equality checking against
// Schemas defined inside an OpenAPI document. The only way to know if a schema has changed, is to hash it.
func (s *Schema) Hash() [32]byte {
// calculate a hash from every property in the schema.
var d []string
if !s.SchemaTypeRef.IsEmpty() {
d = append(d, fmt.Sprint(s.SchemaTypeRef.Value))
}
if !s.Title.IsEmpty() {
d = append(d, fmt.Sprint(s.Title.Value))
}
if !s.MultipleOf.IsEmpty() {
d = append(d, fmt.Sprint(s.MultipleOf.Value))
}
if !s.Maximum.IsEmpty() {
d = append(d, fmt.Sprint(s.Maximum.Value))
}
if !s.Minimum.IsEmpty() {
d = append(d, fmt.Sprint(s.Minimum.Value))
}
if !s.MaxLength.IsEmpty() {
d = append(d, fmt.Sprint(s.MaxLength.Value))
}
if !s.MinLength.IsEmpty() {
d = append(d, fmt.Sprint(s.MinLength.Value))
}
if !s.Pattern.IsEmpty() {
d = append(d, fmt.Sprint(s.Pattern.Value))
}
if !s.Format.IsEmpty() {
d = append(d, fmt.Sprint(s.Format.Value))
}
if !s.MaxItems.IsEmpty() {
d = append(d, fmt.Sprint(s.MaxItems.Value))
}
if !s.MinItems.IsEmpty() {
d = append(d, fmt.Sprint(s.MinItems.Value))
}
if !s.UniqueItems.IsEmpty() {
d = append(d, fmt.Sprint(s.UniqueItems.Value))
}
if !s.MaxProperties.IsEmpty() {
d = append(d, fmt.Sprint(s.MaxProperties.Value))
}
if !s.MinProperties.IsEmpty() {
d = append(d, fmt.Sprint(s.MinProperties.Value))
}
if !s.AdditionalProperties.IsEmpty() {
d = append(d, low.GenerateHashString(s.AdditionalProperties.Value))
}
if !s.Description.IsEmpty() {
d = append(d, fmt.Sprint(s.Description.Value))
}
if !s.ContentEncoding.IsEmpty() {
d = append(d, fmt.Sprint(s.ContentEncoding.Value))
}
if !s.ContentMediaType.IsEmpty() {
d = append(d, fmt.Sprint(s.ContentMediaType.Value))
}
if !s.Default.IsEmpty() {
d = append(d, low.GenerateHashString(s.Default.Value))
}
if !s.Nullable.IsEmpty() {
d = append(d, fmt.Sprint(s.Nullable.Value))
}
if !s.ReadOnly.IsEmpty() {
d = append(d, fmt.Sprint(s.ReadOnly.Value))
}
if !s.WriteOnly.IsEmpty() {
d = append(d, fmt.Sprint(s.WriteOnly.Value))
}
if !s.Deprecated.IsEmpty() {
d = append(d, fmt.Sprint(s.Deprecated.Value))
}
if !s.ExclusiveMaximum.IsEmpty() && s.ExclusiveMaximum.Value.IsA() {
d = append(d, fmt.Sprint(s.ExclusiveMaximum.Value.A))
}
if !s.ExclusiveMaximum.IsEmpty() && s.ExclusiveMaximum.Value.IsB() {
d = append(d, fmt.Sprint(s.ExclusiveMaximum.Value.B))
}
if !s.ExclusiveMinimum.IsEmpty() && s.ExclusiveMinimum.Value.IsA() {
d = append(d, fmt.Sprint(s.ExclusiveMinimum.Value.A))
}
if !s.ExclusiveMinimum.IsEmpty() && s.ExclusiveMinimum.Value.IsB() {
d = append(d, fmt.Sprint(s.ExclusiveMinimum.Value.B))
}
if !s.Type.IsEmpty() && s.Type.Value.IsA() {
d = append(d, fmt.Sprint(s.Type.Value.A))
}
if !s.Type.IsEmpty() && s.Type.Value.IsB() {
j := make([]string, len(s.Type.Value.B))
for h := range s.Type.Value.B {
j[h] = s.Type.Value.B[h].Value
}
sort.Strings(j)
d = append(d, strings.Join(j, "|"))
}
keys := make([]string, len(s.Required.Value))
for i := range s.Required.Value {
keys[i] = s.Required.Value[i].Value
}
sort.Strings(keys)
d = append(d, keys...)
keys = make([]string, len(s.Enum.Value))
for i := range s.Enum.Value {
keys[i] = fmt.Sprint(s.Enum.Value[i].Value)
}
sort.Strings(keys)
d = append(d, keys...)
for i := range s.Enum.Value {
d = append(d, fmt.Sprint(s.Enum.Value[i].Value))
}
propertyKeys := make([]string, len(s.Properties.Value))
z := 0
for i := range s.Properties.Value {
propertyKeys[z] = i.Value
z++
}
sort.Strings(propertyKeys)
for k := range propertyKeys {
prop := s.FindProperty(propertyKeys[k]).Value
if !prop.IsSchemaReference() {
d = append(d, low.GenerateHashString(prop.Schema()))
}
}
if s.XML.Value != nil {
d = append(d, low.GenerateHashString(s.XML.Value))
}
if s.ExternalDocs.Value != nil {
d = append(d, low.GenerateHashString(s.ExternalDocs.Value))
}
if s.Discriminator.Value != nil {
d = append(d, low.GenerateHashString(s.Discriminator.Value))
}
// hash polymorphic data
if len(s.OneOf.Value) > 0 {
oneOfKeys := make([]string, len(s.OneOf.Value))
oneOfEntities := make(map[string]*Schema)
z = 0
for i := range s.OneOf.Value {
g := s.OneOf.Value[i].Value
if !g.IsSchemaReference() {
k := g.Schema()
r := low.GenerateHashString(k)
oneOfEntities[r] = k
oneOfKeys[z] = r
z++
}
}
sort.Strings(oneOfKeys)
for k := range oneOfKeys {
d = append(d, low.GenerateHashString(oneOfEntities[oneOfKeys[k]]))
}
}
if len(s.AllOf.Value) > 0 {
allOfKeys := make([]string, len(s.AllOf.Value))
allOfEntities := make(map[string]*Schema)
z = 0
for i := range s.AllOf.Value {
g := s.AllOf.Value[i].Value
if !g.IsSchemaReference() {
k := g.Schema()
r := low.GenerateHashString(k)
allOfEntities[r] = k
allOfKeys[z] = r
z++
}
}
sort.Strings(allOfKeys)
for k := range allOfKeys {
d = append(d, low.GenerateHashString(allOfEntities[allOfKeys[k]]))
}
}
if len(s.AnyOf.Value) > 0 {
anyOfKeys := make([]string, len(s.AnyOf.Value))
anyOfEntities := make(map[string]*Schema)
z = 0
for i := range s.AnyOf.Value {
g := s.AnyOf.Value[i].Value
if !g.IsSchemaReference() {
k := g.Schema()
r := low.GenerateHashString(k)
anyOfEntities[r] = k
anyOfKeys[z] = r
z++
}
}
sort.Strings(anyOfKeys)
for k := range anyOfKeys {
d = append(d, low.GenerateHashString(anyOfEntities[anyOfKeys[k]]))
}
}
if len(s.Not.Value) > 0 {
notKeys := make([]string, len(s.Not.Value))
notEntities := make(map[string]*Schema)
z = 0
for i := range s.Not.Value {
g := s.Not.Value[i].Value
if !g.IsSchemaReference() {
k := g.Schema()
r := low.GenerateHashString(k)
notEntities[r] = k
notKeys[z] = r
z++
}
}
sort.Strings(notKeys)
for k := range notKeys {
d = append(d, low.GenerateHashString(notEntities[notKeys[k]]))
}
}
if len(s.Items.Value) > 0 {
itemsKeys := make([]string, len(s.Items.Value))
itemsEntities := make(map[string]*Schema)
z = 0
for i := range s.Items.Value {
g := s.Items.Value[i].Value
if !g.IsSchemaReference() {
k := g.Schema()
r := low.GenerateHashString(k)
itemsEntities[r] = k
itemsKeys[z] = r
z++
}
}
sort.Strings(itemsKeys)
for k := range itemsKeys {
d = append(d, low.GenerateHashString(itemsEntities[itemsKeys[k]]))
}
}
// add extensions to hash
keys = make([]string, len(s.Extensions))
z = 0
for k := range s.Extensions {
keys[z] = fmt.Sprintf("%s-%x", k.Value, sha256.Sum256([]byte(fmt.Sprint(s.Extensions[k].Value))))
z++
}
sort.Strings(keys)
d = append(d, keys...)
if s.Example.Value != nil {
d = append(d, low.GenerateHashString(s.Example.Value))
}
if !s.Examples.IsEmpty() {
var xph []string
for w := range s.Examples.Value {
xph = append(xph, low.GenerateHashString(s.Examples.Value[w].Value))
}
sort.Strings(xph)
d = append(d, strings.Join(xph, "|"))
}
return sha256.Sum256([]byte(strings.Join(d, "|")))
}
// FindProperty will return a ValueReference pointer containing a SchemaProxy pointer
// from a property key name. if found
func (s *Schema) FindProperty(name string) *low.ValueReference[*SchemaProxy] {
return low.FindItemInMap[*SchemaProxy](name, s.Properties.Value)
}
// Build will perform a number of operations.
// Extraction of the following happens in this method:
// - Extensions
// - Type
// - ExclusiveMinimum and ExclusiveMaximum
// - Examples
// - AdditionalProperties
// - Discriminator
// - ExternalDocs
// - XML
// - Properties
// - AllOf, OneOf, AnyOf
// - Not
// - Items
func (s *Schema) Build(root *yaml.Node, idx *index.SpecIndex) error {
if h, _, _ := utils.IsNodeRefValue(root); h {
ref, err := low.LocateRefNode(root, idx)
if ref != nil {
root = ref
if err != nil {
if !idx.AllowCircularReferenceResolving() {
return fmt.Errorf("build schema failed: %s", err.Error())
}
}
} else {
return fmt.Errorf("build schema failed: reference cannot be found: '%s', line %d, col %d",
root.Content[1].Value, root.Content[1].Line, root.Content[1].Column)
}
}
s.extractExtensions(root)
// determine schema type, singular (3.0) or multiple (3.1), use a variable value
_, typeLabel, typeValue := utils.FindKeyNodeFullTop(TypeLabel, root.Content)
if typeValue != nil {
if utils.IsNodeStringValue(typeValue) {
s.Type = low.NodeReference[SchemaDynamicValue[string, []low.ValueReference[string]]]{
KeyNode: typeLabel,
ValueNode: typeValue,
Value: SchemaDynamicValue[string, []low.ValueReference[string]]{N: 0, A: typeValue.Value},
}
}
if utils.IsNodeArray(typeValue) {
var refs []low.ValueReference[string]
for r := range typeValue.Content {
refs = append(refs, low.ValueReference[string]{
Value: typeValue.Content[r].Value,
ValueNode: typeValue.Content[r],
})
}
s.Type = low.NodeReference[SchemaDynamicValue[string, []low.ValueReference[string]]]{
KeyNode: typeLabel,
ValueNode: typeValue,
Value: SchemaDynamicValue[string, []low.ValueReference[string]]{N: 1, B: refs},
}
}
}
// determine exclusive minimum type, bool (3.0) or int (3.1)
_, exMinLabel, exMinValue := utils.FindKeyNodeFullTop(ExclusiveMinimumLabel, root.Content)
if exMinValue != nil {
if utils.IsNodeBoolValue(exMinValue) {
val, _ := strconv.ParseBool(exMinValue.Value)
s.ExclusiveMinimum = low.NodeReference[SchemaDynamicValue[bool, int64]]{
KeyNode: exMinLabel,
ValueNode: exMinValue,
Value: SchemaDynamicValue[bool, int64]{N: 0, A: val},
}
}
if utils.IsNodeIntValue(exMinValue) {
val, _ := strconv.ParseInt(exMinValue.Value, 10, 64)
s.ExclusiveMinimum = low.NodeReference[SchemaDynamicValue[bool, int64]]{
KeyNode: exMinLabel,
ValueNode: exMinValue,
Value: SchemaDynamicValue[bool, int64]{N: 1, B: val},
}
}
}
// determine exclusive maximum type, bool (3.0) or int (3.1)
_, exMaxLabel, exMaxValue := utils.FindKeyNodeFullTop(ExclusiveMaximumLabel, root.Content)
if exMaxValue != nil {
if utils.IsNodeBoolValue(exMaxValue) {
val, _ := strconv.ParseBool(exMaxValue.Value)
s.ExclusiveMaximum = low.NodeReference[SchemaDynamicValue[bool, int64]]{
KeyNode: exMaxLabel,
ValueNode: exMaxValue,
Value: SchemaDynamicValue[bool, int64]{N: 0, A: val},
}
}
if utils.IsNodeIntValue(exMaxValue) {
val, _ := strconv.ParseInt(exMaxValue.Value, 10, 64)
s.ExclusiveMaximum = low.NodeReference[SchemaDynamicValue[bool, int64]]{
KeyNode: exMaxLabel,
ValueNode: exMaxValue,
Value: SchemaDynamicValue[bool, int64]{N: 1, B: val},
}
}
}
// handle schema reference type if set. (3.1)
_, schemaRefLabel, schemaRefNode := utils.FindKeyNodeFullTop(SchemaTypeLabel, root.Content)
if schemaRefNode != nil {
s.SchemaTypeRef = low.NodeReference[string]{
Value: schemaRefNode.Value, KeyNode: schemaRefLabel, ValueNode: schemaRefLabel}
}
// handle example if set. (3.0)
_, expLabel, expNode := utils.FindKeyNodeFull(ExampleLabel, root.Content)
if expNode != nil {
s.Example = low.NodeReference[any]{Value: ExtractExampleValue(expNode), KeyNode: expLabel, ValueNode: expNode}
}
// handle examples if set.(3.1)
_, expArrLabel, expArrNode := utils.FindKeyNodeFullTop(ExamplesLabel, root.Content)
if expArrNode != nil {
if utils.IsNodeArray(expArrNode) {
var examples []low.ValueReference[any]
for i := range expArrNode.Content {
examples = append(examples, low.ValueReference[any]{Value: ExtractExampleValue(expArrNode.Content[i]), ValueNode: expArrNode.Content[i]})
}
s.Examples = low.NodeReference[[]low.ValueReference[any]]{
Value: examples,
ValueNode: expArrNode,
KeyNode: expArrLabel,
}
}
}
_, addPLabel, addPNode := utils.FindKeyNodeFullTop(AdditionalPropertiesLabel, root.Content)
if addPNode != nil {
if utils.IsNodeMap(addPNode) {
schema, serr := low.ExtractObjectRaw[*Schema](addPNode, idx)
if serr != nil {
return serr
}
s.AdditionalProperties = low.NodeReference[any]{Value: schema, KeyNode: addPLabel, ValueNode: addPNode}
}
if utils.IsNodeBoolValue(addPNode) {
b, _ := strconv.ParseBool(addPNode.Value)
s.AdditionalProperties = low.NodeReference[any]{Value: b, KeyNode: addPLabel, ValueNode: addPNode}
}
}
// handle discriminator if set.
_, discLabel, discNode := utils.FindKeyNodeFullTop(DiscriminatorLabel, root.Content)
if discNode != nil {
var discriminator Discriminator
_ = low.BuildModel(discNode, &discriminator)
s.Discriminator = low.NodeReference[*Discriminator]{Value: &discriminator, KeyNode: discLabel, ValueNode: discNode}
}
// handle externalDocs if set.
_, extDocLabel, extDocNode := utils.FindKeyNodeFullTop(ExternalDocsLabel, root.Content)
if extDocNode != nil {
var exDoc ExternalDoc
_ = low.BuildModel(extDocNode, &exDoc)
_ = exDoc.Build(extDocNode, idx) // throws no errors, can't check for one.
s.ExternalDocs = low.NodeReference[*ExternalDoc]{Value: &exDoc, KeyNode: extDocLabel, ValueNode: extDocNode}
}
// handle xml if set.
_, xmlLabel, xmlNode := utils.FindKeyNodeFullTop(XMLLabel, root.Content)
if xmlNode != nil {
var xml XML
_ = low.BuildModel(xmlNode, &xml)
// extract extensions if set.
_ = xml.Build(xmlNode, idx) // returns no errors, can't check for one.
s.XML = low.NodeReference[*XML]{Value: &xml, KeyNode: xmlLabel, ValueNode: xmlNode}
}
// for property, build in a new thread!
bChan := make(chan schemaProxyBuildResult)
var buildProperty = func(label *yaml.Node, value *yaml.Node, c chan schemaProxyBuildResult) {
c <- schemaProxyBuildResult{
k: low.KeyReference[string]{
KeyNode: label,
Value: label.Value,
},
v: low.ValueReference[*SchemaProxy]{
Value: &SchemaProxy{kn: label, vn: value, idx: idx},
ValueNode: value,
},
}
}
// handle properties
_, propLabel, propsNode := utils.FindKeyNodeFullTop(PropertiesLabel, root.Content)
if propsNode != nil {
propertyMap := make(map[low.KeyReference[string]]low.ValueReference[*SchemaProxy])
var currentProp *yaml.Node
totalProps := 0
for i, prop := range propsNode.Content {
if i%2 == 0 {
currentProp = prop
continue
}
// check our prop isn't reference
if h, _, _ := utils.IsNodeRefValue(prop); h {
ref, _ := low.LocateRefNode(prop, idx)
if ref != nil {
prop = ref
} else {
return fmt.Errorf("schema properties build failed: cannot find reference %s, line %d, col %d",
prop.Content[1].Value, prop.Content[1].Column, prop.Content[1].Line)
}
}
totalProps++
go buildProperty(currentProp, prop, bChan)
}
completedProps := 0
for completedProps < totalProps {
select {
case res := <-bChan:
completedProps++
propertyMap[res.k] = res.v
}
}
s.Properties = low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*SchemaProxy]]{
Value: propertyMap,
KeyNode: propLabel,
ValueNode: propsNode,
}
}
var allOf, anyOf, oneOf, not, items []low.ValueReference[*SchemaProxy]
_, allOfLabel, allOfValue := utils.FindKeyNodeFullTop(AllOfLabel, root.Content)
_, anyOfLabel, anyOfValue := utils.FindKeyNodeFullTop(AnyOfLabel, root.Content)
_, oneOfLabel, oneOfValue := utils.FindKeyNodeFullTop(OneOfLabel, root.Content)
_, notLabel, notValue := utils.FindKeyNodeFullTop(NotLabel, root.Content)
_, itemsLabel, itemsValue := utils.FindKeyNodeFullTop(ItemsLabel, root.Content)
errorChan := make(chan error)
allOfChan := make(chan schemaProxyBuildResult)
anyOfChan := make(chan schemaProxyBuildResult)
oneOfChan := make(chan schemaProxyBuildResult)
itemsChan := make(chan schemaProxyBuildResult)
notChan := make(chan schemaProxyBuildResult)
totalBuilds := countSubSchemaItems(allOfValue) +
countSubSchemaItems(anyOfValue) +
countSubSchemaItems(oneOfValue) +
countSubSchemaItems(notValue) +
countSubSchemaItems(itemsValue)
if allOfValue != nil {
go buildSchema(allOfChan, allOfLabel, allOfValue, errorChan, idx)
}
if anyOfValue != nil {
go buildSchema(anyOfChan, anyOfLabel, anyOfValue, errorChan, idx)
}
if oneOfValue != nil {
go buildSchema(oneOfChan, oneOfLabel, oneOfValue, errorChan, idx)
}
if itemsValue != nil {
go buildSchema(itemsChan, itemsLabel, itemsValue, errorChan, idx)
}
if notValue != nil {
go buildSchema(notChan, notLabel, notValue, errorChan, idx)
}
completeCount := 0
for completeCount < totalBuilds {
select {
case e := <-errorChan:
return e
case r := <-allOfChan:
completeCount++
allOf = append(allOf, r.v)
case r := <-anyOfChan:
completeCount++
anyOf = append(anyOf, r.v)
case r := <-oneOfChan:
completeCount++
oneOf = append(oneOf, r.v)
case r := <-itemsChan:
completeCount++
items = append(items, r.v)
case r := <-notChan:
completeCount++
not = append(not, r.v)
}
}
if len(anyOf) > 0 {
s.AnyOf = low.NodeReference[[]low.ValueReference[*SchemaProxy]]{
Value: anyOf,
KeyNode: anyOfLabel,
ValueNode: anyOfValue,
}
}
if len(oneOf) > 0 {
s.OneOf = low.NodeReference[[]low.ValueReference[*SchemaProxy]]{
Value: oneOf,
KeyNode: oneOfLabel,
ValueNode: oneOfValue,
}
}
if len(allOf) > 0 {
s.AllOf = low.NodeReference[[]low.ValueReference[*SchemaProxy]]{
Value: allOf,
KeyNode: allOfLabel,
ValueNode: allOfValue,
}
}
if len(not) > 0 {
s.Not = low.NodeReference[[]low.ValueReference[*SchemaProxy]]{
Value: not,
KeyNode: notLabel,
ValueNode: notValue,
}
}
if len(items) > 0 {
s.Items = low.NodeReference[[]low.ValueReference[*SchemaProxy]]{
Value: items,
KeyNode: itemsLabel,
ValueNode: itemsValue,
}
}
return nil
}
// count the number of sub-schemas in a node.
func countSubSchemaItems(node *yaml.Node) int {
if utils.IsNodeMap(node) {
return 1
}
if utils.IsNodeArray(node) {
return len(node.Content)
}
return 0
}
// schema build result container used for async building.
type schemaProxyBuildResult struct {
k low.KeyReference[string]
v low.ValueReference[*SchemaProxy]
}
// extract extensions from schema
func (s *Schema) extractExtensions(root *yaml.Node) {
s.Extensions = low.ExtractExtensions(root)
}
// build out a child schema for parent schema.
func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml.Node, errors chan error, idx *index.SpecIndex) {
if valueNode != nil {
syncChan := make(chan *low.ValueReference[*SchemaProxy])
// build out a SchemaProxy for every sub-schema.
build := func(kn *yaml.Node, vn *yaml.Node, c chan *low.ValueReference[*SchemaProxy],
isRef bool, refLocation string) {
// a proxy design works best here. polymorphism, pretty much guarantees that a sub-schema can
// take on circular references through polymorphism. Like the resolver, if we try and follow these
// journey's through hyperspace, we will end up creating endless amounts of threads, spinning off
// chasing down circles, that in turn spin up endless threads.
// In order to combat this, we need a schema proxy that will only resolve the schema when asked, and then
// it will only do it one level at a time.
sp := new(SchemaProxy)
sp.kn = kn
sp.vn = vn
sp.idx = idx
if isRef {
sp.referenceLookup = refLocation
sp.isReference = true
}
res := &low.ValueReference[*SchemaProxy]{
Value: sp,
ValueNode: vn,
}
c <- res
}
isRef := false
refLocation := ""
if utils.IsNodeMap(valueNode) {
h := false
if h, _, refLocation = utils.IsNodeRefValue(valueNode); h {
isRef = true
ref, _ := low.LocateRefNode(valueNode, idx)
if ref != nil {
valueNode = ref
} else {
errors <- fmt.Errorf("build schema failed: reference cannot be found: %s, line %d, col %d",
valueNode.Content[1].Value, valueNode.Content[1].Line, valueNode.Content[1].Column)
}
}
// this only runs once, however to keep things consistent, it makes sense to use the same async method
// that arrays will use.
go build(labelNode, valueNode, syncChan, isRef, refLocation)
select {
case r := <-syncChan:
schemas <- schemaProxyBuildResult{
k: low.KeyReference[string]{
KeyNode: labelNode,
Value: labelNode.Value,
},
v: *r,
}
}
}
if utils.IsNodeArray(valueNode) {
refBuilds := 0
for _, vn := range valueNode.Content {
isRef = false
h := false
if h, _, refLocation = utils.IsNodeRefValue(vn); h {
isRef = true
ref, _ := low.LocateRefNode(vn, idx)
if ref != nil {
vn = ref
} else {
err := fmt.Errorf("build schema failed: reference cannot be found: %s, line %d, col %d",
vn.Content[1].Value, vn.Content[1].Line, vn.Content[1].Column)
errors <- err
return
}
}
refBuilds++
go build(vn, vn, syncChan, isRef, refLocation)
}
completedBuilds := 0
for completedBuilds < refBuilds {
select {
case res := <-syncChan:
completedBuilds++
schemas <- schemaProxyBuildResult{
k: low.KeyReference[string]{
KeyNode: labelNode,
Value: labelNode.Value,
},
v: *res,
}
}
}
}
}
}
// ExtractSchema will return a pointer to a NodeReference that contains a *SchemaProxy if successful. The function
// will specifically look for a key node named 'schema' and extract the value mapped to that key. If the operation
// fails then no NodeReference is returned and an error is returned instead.
func ExtractSchema(root *yaml.Node, idx *index.SpecIndex) (*low.NodeReference[*SchemaProxy], error) {
var schLabel, schNode *yaml.Node
errStr := "schema build failed: reference '%s' cannot be found at line %d, col %d"
isRef := false
refLocation := ""
if rf, rl, _ := utils.IsNodeRefValue(root); rf {
// locate reference in index.
isRef = true
ref, _ := low.LocateRefNode(root, idx)
if ref != nil {
schNode = ref
schLabel = rl
} else {
return nil, fmt.Errorf(errStr,
root.Content[1].Value, root.Content[1].Line, root.Content[1].Column)
}
} else {
_, schLabel, schNode = utils.FindKeyNodeFull(SchemaLabel, root.Content)
if schNode != nil {
h := false
if h, _, refLocation = utils.IsNodeRefValue(schNode); h {
isRef = true
ref, _ := low.LocateRefNode(schNode, idx)
if ref != nil {
schNode = ref
} else {
return nil, fmt.Errorf(errStr,
schNode.Content[1].Value, schNode.Content[1].Line, schNode.Content[1].Column)
}
}
}
}
if schNode != nil {
// check if schema has already been built.
schema := &SchemaProxy{kn: schLabel, vn: schNode, idx: idx, isReference: isRef, referenceLookup: refLocation}
return &low.NodeReference[*SchemaProxy]{Value: schema, KeyNode: schLabel, ValueNode: schNode}, nil
}
return nil, nil
}