From c34c4f668c805f23ffc20827383b4e45fc3538d7 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Sat, 27 Aug 2022 09:47:37 -0400 Subject: [PATCH] Massive refactor on how the library handles schemas. Schemas are now rendered on demand. There is no reasonable way to navigate the mayhem that is circular dependencies through multiple inheritance and polymorphism. So now using a msuch simpler design (and MUCH faster), there is a `SchemaProxy` for every schema reference. This holds a reference to the low model and index, that renders the schema on demand. Once rendered, it's done. Any children can also be rendered on demand, and so down the rabbit hole you do (if you want). All circular dependencies are know by the index, so you can decide when you want to stop, or just keep going for ever, however it's now a choice, not something decided for you. Signed-off-by: Dave Shanley --- datamodel/high/3.0/components.go | 22 +- datamodel/high/3.0/document_test.go | 43 +-- datamodel/high/3.0/header.go | 14 +- datamodel/high/3.0/media_type.go | 10 +- datamodel/high/3.0/parameter.go | 10 +- datamodel/high/3.0/schema.go | 81 +++-- datamodel/high/3.0/schema_proxy.go | 27 ++ datamodel/low/3.0/components.go | 16 +- datamodel/low/3.0/components_test.go | 10 +- datamodel/low/3.0/create_document.go | 27 +- datamodel/low/3.0/create_document_test.go | 96 ++++- datamodel/low/3.0/example_test.go | 7 + datamodel/low/3.0/header.go | 2 +- datamodel/low/3.0/header_test.go | 12 +- datamodel/low/3.0/media_type.go | 104 +++--- datamodel/low/3.0/media_type_test.go | 2 +- datamodel/low/3.0/parameter.go | 2 +- datamodel/low/3.0/parameter_test.go | 12 +- datamodel/low/3.0/path.go | 4 +- datamodel/low/3.0/response.go | 2 +- datamodel/low/3.0/response_test.go | 2 +- datamodel/low/3.0/schema.go | 277 +++++++------- datamodel/low/3.0/schema_proxy.go | 48 +++ datamodel/low/3.0/schema_test.go | 421 ++++++++++++---------- datamodel/low/extraction_functions.go | 2 +- datamodel/low/log.go | 12 + go.mod | 3 + go.sum | 15 + 28 files changed, 754 insertions(+), 529 deletions(-) create mode 100644 datamodel/high/3.0/schema_proxy.go create mode 100644 datamodel/low/3.0/schema_proxy.go create mode 100644 datamodel/low/log.go diff --git a/datamodel/high/3.0/components.go b/datamodel/high/3.0/components.go index cd143ab..2a0af58 100644 --- a/datamodel/high/3.0/components.go +++ b/datamodel/high/3.0/components.go @@ -47,7 +47,7 @@ func getSeenSchema(key string) *Schema { } type Components struct { - Schemas map[string]*Schema + Schemas map[string]*SchemaProxy Responses map[string]*Response Parameters map[string]*Parameter Examples map[string]*Example @@ -72,8 +72,8 @@ func NewComponents(comp *low.Components) *Components { requestBodyMap := make(map[string]*RequestBody) headerMap := make(map[string]*Header) securitySchemeMap := make(map[string]*SecurityScheme) - schemas := make(map[string]*Schema) - schemaChan := make(chan componentResult[*Schema]) + schemas := make(map[string]*SchemaProxy) + schemaChan := make(chan componentResult[*SchemaProxy]) cbChan := make(chan componentResult[*Callback]) linkChan := make(chan componentResult[*Link]) responseChan := make(chan componentResult[*Response]) @@ -173,15 +173,13 @@ func buildComponent[N any, O any](comp int, key string, orig O, c chan component c <- componentResult[N]{comp: comp, res: f(orig), key: key} } -func buildSchema(key lowmodel.KeyReference[string], orig lowmodel.ValueReference[*low.Schema], c chan componentResult[*Schema]) { - var sch *Schema - if ss := getSeenSchema(orig.GenerateMapKey()); ss != nil { - sch = ss - } else { - sch = NewSchema(orig.Value) - addSeenSchema(orig.GenerateMapKey(), sch) - } - c <- componentResult[*Schema]{res: sch, key: key.Value} +func buildSchema(key lowmodel.KeyReference[string], orig lowmodel.ValueReference[*low.SchemaProxy], c chan componentResult[*SchemaProxy]) { + var sch *SchemaProxy + sch = &SchemaProxy{schema: &lowmodel.NodeReference[*low.SchemaProxy]{ + Value: orig.Value, + ValueNode: orig.ValueNode, + }} + c <- componentResult[*SchemaProxy]{res: sch, key: key.Value} } func (c *Components) GoLow() *low.Components { diff --git a/datamodel/high/3.0/document_test.go b/datamodel/high/3.0/document_test.go index 71199f3..985e8c5 100644 --- a/datamodel/high/3.0/document_test.go +++ b/datamodel/high/3.0/document_test.go @@ -162,34 +162,35 @@ func TestNewDocument_Components_Schemas(t *testing.T) { goLow := h.Components.GoLow() a := h.Components.Schemas["Error"] - assert.Equal(t, "No such burger as 'Big-Whopper'", a.Properties["message"].Example) + abcd := a.Schema().Properties["message"].Schema().Example + assert.Equal(t, "No such burger as 'Big-Whopper'", abcd) assert.Equal(t, 428, goLow.Schemas.KeyNode.Line) assert.Equal(t, 3, goLow.Schemas.KeyNode.Column) - assert.Equal(t, 431, a.GoLow().Description.KeyNode.Line) + assert.Equal(t, 431, a.Schema().GoLow().Description.KeyNode.Line) b := h.Components.Schemas["Burger"] - assert.Len(t, b.Required, 2) - assert.Equal(t, "golden slices of happy fun joy", b.Properties["fries"].Description) - assert.Equal(t, int64(2), b.Properties["numPatties"].Example) - assert.Equal(t, 443, goLow.FindSchema("Burger").Value.Properties.KeyNode.Line) - assert.Equal(t, 7, goLow.FindSchema("Burger").Value.Properties.KeyNode.Column) - assert.Equal(t, 445, b.GoLow().FindProperty("name").ValueNode.Line) + assert.Len(t, b.Schema().Required, 2) + assert.Equal(t, "golden slices of happy fun joy", b.Schema().Properties["fries"].Schema().Description) + assert.Equal(t, int64(2), b.Schema().Properties["numPatties"].Schema().Example) + assert.Equal(t, 443, goLow.FindSchema("Burger").Value.Schema().Properties.KeyNode.Line) + assert.Equal(t, 7, goLow.FindSchema("Burger").Value.Schema().Properties.KeyNode.Column) + assert.Equal(t, 445, b.Schema().GoLow().FindProperty("name").ValueNode.Line) f := h.Components.Schemas["Fries"] - assert.Equal(t, "salt", f.Properties["seasoning"].Items[0].Example) - assert.Len(t, f.Properties["favoriteDrink"].Properties["drinkType"].Enum, 2) + assert.Equal(t, "salt", f.Schema().Properties["seasoning"].Schema().Items[0].Schema().Example) + assert.Len(t, f.Schema().Properties["favoriteDrink"].Schema().Properties["drinkType"].Schema().Enum, 2) d := h.Components.Schemas["Drink"] - assert.Len(t, d.Required, 2) - assert.True(t, d.AdditionalProperties.(bool)) - assert.Equal(t, "drinkType", d.Discriminator.PropertyName) - assert.Equal(t, "some value", d.Discriminator.Mapping["drink"]) - assert.Equal(t, 511, d.Discriminator.GoLow().PropertyName.ValueNode.Line) - assert.Equal(t, 23, d.Discriminator.GoLow().PropertyName.ValueNode.Column) + assert.Len(t, d.Schema().Required, 2) + assert.True(t, d.Schema().AdditionalProperties.(bool)) + assert.Equal(t, "drinkType", d.Schema().Discriminator.PropertyName) + assert.Equal(t, "some value", d.Schema().Discriminator.Mapping["drink"]) + assert.Equal(t, 511, d.Schema().Discriminator.GoLow().PropertyName.ValueNode.Line) + assert.Equal(t, 23, d.Schema().Discriminator.GoLow().PropertyName.ValueNode.Column) pl := h.Components.Schemas["SomePayload"] - assert.Equal(t, "is html programming? yes.", pl.XML.Name) - assert.Equal(t, 518, pl.XML.GoLow().Name.ValueNode.Line) + assert.Equal(t, "is html programming? yes.", pl.Schema().XML.Name) + assert.Equal(t, 518, pl.Schema().XML.GoLow().Name.ValueNode.Line) ext := h.Components.Extensions assert.Equal(t, "loud", ext["x-screaming-baby"]) @@ -225,7 +226,7 @@ func TestNewDocument_Components_Responses(t *testing.T) { h := NewDocument(doc) assert.Len(t, h.Components.Responses, 1) assert.Equal(t, "all the dressings for a burger.", h.Components.Responses["DressingResponse"].Description) - assert.Equal(t, "array", h.Components.Responses["DressingResponse"].Content["application/json"].Schema.Type) + assert.Equal(t, "array", h.Components.Responses["DressingResponse"].Content["application/json"].Schema.Schema().Type) assert.Equal(t, 347, h.Components.Responses["DressingResponse"].GoLow().Description.KeyNode.Line) assert.Equal(t, 7, h.Components.Responses["DressingResponse"].GoLow().Description.KeyNode.Column) } @@ -266,12 +267,12 @@ func TestNewDocument_Components_Parameters(t *testing.T) { bh := h.Components.Parameters["BurgerHeader"] assert.Equal(t, "burgerHeader", bh.Name) assert.Equal(t, 387, bh.GoLow().Name.KeyNode.Line) - assert.Len(t, bh.Schema.Properties, 2) + assert.Len(t, bh.Schema.Schema().Properties, 2) assert.Equal(t, "big-mac", bh.Example) assert.True(t, bh.Required) assert.Equal(t, "this is a header", bh.Content["application/json"].Encoding["burgerTheme"].Headers["someHeader"].Description) - assert.Len(t, bh.Content["application/json"].Schema.Properties, 2) + assert.Len(t, bh.Content["application/json"].Schema.Schema().Properties, 2) assert.Equal(t, 404, bh.Content["application/json"].Encoding["burgerTheme"].GoLow().ContentType.ValueNode.Line) } diff --git a/datamodel/high/3.0/header.go b/datamodel/high/3.0/header.go index 6430a69..6b26ca2 100644 --- a/datamodel/high/3.0/header.go +++ b/datamodel/high/3.0/header.go @@ -16,7 +16,7 @@ type Header struct { Style string Explode bool AllowReserved bool - Schema *Schema + Schema *SchemaProxy Example any Examples map[string]*Example Content map[string]*MediaType @@ -35,13 +35,11 @@ func NewHeader(header *low.Header) *Header { h.Explode = header.Explode.Value h.AllowReserved = header.AllowReserved.Value if !header.Schema.IsEmpty() { - // check if schema has been seen or not. - if v := getSeenSchema(header.Schema.GenerateMapKey()); v != nil { - h.Schema = v - } else { - h.Schema = NewSchema(header.Schema.Value) - addSeenSchema(header.Schema.GenerateMapKey(), h.Schema) - } + h.Schema = &SchemaProxy{schema: &lowmodel.NodeReference[*low.SchemaProxy]{ + Value: header.Schema.Value, + KeyNode: header.Schema.KeyNode, + ValueNode: header.Schema.ValueNode, + }} } h.Content = ExtractContent(header.Content.Value) h.Example = header.Example.Value diff --git a/datamodel/high/3.0/media_type.go b/datamodel/high/3.0/media_type.go index b6cb815..ceff3cd 100644 --- a/datamodel/high/3.0/media_type.go +++ b/datamodel/high/3.0/media_type.go @@ -10,7 +10,7 @@ import ( ) type MediaType struct { - Schema *Schema + Schema *SchemaProxy Example any Examples map[string]*Example Encoding map[string]*Encoding @@ -22,13 +22,7 @@ func NewMediaType(mediaType *low.MediaType) *MediaType { m := new(MediaType) m.low = mediaType if !mediaType.Schema.IsEmpty() { - // check if schema has been seen or not. - if v := getSeenSchema(mediaType.Schema.GenerateMapKey()); v != nil { - m.Schema = v - } else { - m.Schema = NewSchema(mediaType.Schema.Value) - addSeenSchema(mediaType.Schema.GenerateMapKey(), m.Schema) - } + m.Schema = &SchemaProxy{schema: &mediaType.Schema} } m.Example = mediaType.Example m.Examples = ExtractExamples(mediaType.Examples.Value) diff --git a/datamodel/high/3.0/parameter.go b/datamodel/high/3.0/parameter.go index 5e5e36d..011f233 100644 --- a/datamodel/high/3.0/parameter.go +++ b/datamodel/high/3.0/parameter.go @@ -18,7 +18,7 @@ type Parameter struct { Style string Explode bool AllowReserved bool - Schema *Schema + Schema *SchemaProxy Example any Examples map[string]*Example Content map[string]*MediaType @@ -37,14 +37,8 @@ func NewParameter(param *low.Parameter) *Parameter { p.Style = param.Style.Value p.Explode = param.Explode.Value p.AllowReserved = param.AllowReserved.Value - if !param.Schema.IsEmpty() { - if v := getSeenSchema(param.Schema.GenerateMapKey()); v != nil { - p.Schema = v - } else { - p.Schema = NewSchema(param.Schema.Value) - addSeenSchema(param.Schema.GenerateMapKey(), p.Schema) - } + p.Schema = &SchemaProxy{schema: ¶m.Schema} } p.Required = param.Required.Value p.Example = param.Example.Value diff --git a/datamodel/high/3.0/schema.go b/datamodel/high/3.0/schema.go index 2de1c78..209d1e7 100644 --- a/datamodel/high/3.0/schema.go +++ b/datamodel/high/3.0/schema.go @@ -29,12 +29,12 @@ type Schema struct { Required []string Enum []string Type string - AllOf []*Schema - OneOf []*Schema - AnyOf []*Schema - Not []*Schema - Items []*Schema - Properties map[string]*Schema + AllOf []*SchemaProxy + OneOf []*SchemaProxy + AnyOf []*SchemaProxy + Not []*SchemaProxy + Items []*SchemaProxy + Properties map[string]*SchemaProxy AdditionalProperties any Description string Default any @@ -105,28 +105,32 @@ func NewSchema(schema *low.Schema) *Schema { polyCompletedChan := make(chan bool) propsChan := make(chan bool) + errChan := make(chan error) // schema async - buildOutSchema := func(schemas []lowmodel.NodeReference[*low.Schema], items *[]*Schema, doneChan chan bool) { - bChan := make(chan *Schema) + buildOutSchema := func(schemas []lowmodel.ValueReference[*low.SchemaProxy], items *[]*SchemaProxy, + doneChan chan bool, e chan error) { + bChan := make(chan *SchemaProxy) + eChan := make(chan error) // for every item, build schema async - buildSchemaChild := func(sch lowmodel.NodeReference[*low.Schema], bChan chan *Schema) { - if ss := getSeenSchema(sch.GenerateMapKey()); ss != nil { - bChan <- ss - return - } - ns := NewSchema(sch.Value) - addSeenSchema(sch.GenerateMapKey(), ns) - bChan <- ns + buildSchemaChild := func(sch lowmodel.ValueReference[*low.SchemaProxy], bChan chan *SchemaProxy, e chan error) { + p := &SchemaProxy{schema: &lowmodel.NodeReference[*low.SchemaProxy]{ + ValueNode: sch.ValueNode, + Value: sch.Value, + }} + bChan <- p } totalSchemas := len(schemas) for v := range schemas { - go buildSchemaChild(schemas[v], bChan) + go buildSchemaChild(schemas[v], bChan, eChan) } j := 0 for j < totalSchemas { select { + case er := <-eChan: + e <- er + return case t := <-bChan: j++ *items = append(*items, t) @@ -137,48 +141,45 @@ func NewSchema(schema *low.Schema) *Schema { // props async plock := sync.RWMutex{} - var buildProps = func(k lowmodel.KeyReference[string], v lowmodel.ValueReference[*low.Schema], c chan bool, - props map[string]*Schema) { - if ss := getSeenSchema(v.GenerateMapKey()); ss != nil { - defer plock.Unlock() - plock.Lock() - props[k.Value] = ss - - } else { - defer plock.Unlock() - plock.Lock() - props[k.Value] = NewSchema(v.Value) - addSeenSchema(k.GenerateMapKey(), props[k.Value]) + var buildProps = func(k lowmodel.KeyReference[string], v lowmodel.ValueReference[*low.SchemaProxy], c chan bool, + props map[string]*SchemaProxy) { + defer plock.Unlock() + plock.Lock() + props[k.Value] = &SchemaProxy{schema: &lowmodel.NodeReference[*low.SchemaProxy]{ + Value: v.Value, + KeyNode: k.KeyNode, + ValueNode: v.ValueNode, + }, } s.Properties = props c <- true } - props := make(map[string]*Schema) + props := make(map[string]*SchemaProxy) for k, v := range schema.Properties.Value { go buildProps(k, v, propsChan, props) } - var allOf []*Schema - var oneOf []*Schema - var anyOf []*Schema - var not []*Schema - var items []*Schema + var allOf []*SchemaProxy + var oneOf []*SchemaProxy + var anyOf []*SchemaProxy + var not []*SchemaProxy + var items []*SchemaProxy if !schema.AllOf.IsEmpty() { - go buildOutSchema(schema.AllOf.Value, &allOf, polyCompletedChan) + go buildOutSchema(schema.AllOf.Value, &allOf, polyCompletedChan, errChan) } if !schema.AnyOf.IsEmpty() { - go buildOutSchema(schema.AnyOf.Value, &anyOf, polyCompletedChan) + go buildOutSchema(schema.AnyOf.Value, &anyOf, polyCompletedChan, errChan) } if !schema.OneOf.IsEmpty() { - go buildOutSchema(schema.OneOf.Value, &oneOf, polyCompletedChan) + go buildOutSchema(schema.OneOf.Value, &oneOf, polyCompletedChan, errChan) } if !schema.Not.IsEmpty() { - go buildOutSchema(schema.Not.Value, ¬, polyCompletedChan) + go buildOutSchema(schema.Not.Value, ¬, polyCompletedChan, errChan) } if !schema.Items.IsEmpty() { - go buildOutSchema(schema.Items.Value, &items, polyCompletedChan) + go buildOutSchema(schema.Items.Value, &items, polyCompletedChan, errChan) } completeChildren := 0 diff --git a/datamodel/high/3.0/schema_proxy.go b/datamodel/high/3.0/schema_proxy.go new file mode 100644 index 0000000..fa9bd57 --- /dev/null +++ b/datamodel/high/3.0/schema_proxy.go @@ -0,0 +1,27 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package v3 + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + v3 "github.com/pb33f/libopenapi/datamodel/low/3.0" +) + +type SchemaProxy struct { + schema *low.NodeReference[*v3.SchemaProxy] + buildError error +} + +func (sp *SchemaProxy) Schema() *Schema { + s := sp.schema.Value.Schema() + if s == nil { + sp.buildError = sp.GetBuildError() + return nil + } + return NewSchema(s) +} + +func (sp *SchemaProxy) GetBuildError() error { + return sp.buildError +} diff --git a/datamodel/low/3.0/components.go b/datamodel/low/3.0/components.go index 8a18e92..bcfbd33 100644 --- a/datamodel/low/3.0/components.go +++ b/datamodel/low/3.0/components.go @@ -44,7 +44,7 @@ func getSeenSchema(key string) *Schema { } type Components struct { - Schemas low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Schema]] + Schemas low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*SchemaProxy]] Responses low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Response]] Parameters low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Parameter]] Examples low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Example]] @@ -60,8 +60,8 @@ func (co *Components) FindExtension(ext string) *low.ValueReference[any] { return low.FindItemInMap[any](ext, co.Extensions) } -func (co *Components) FindSchema(schema string) *low.ValueReference[*Schema] { - return low.FindItemInMap[*Schema](schema, co.Schemas.Value) +func (co *Components) FindSchema(schema string) *low.ValueReference[*SchemaProxy] { + return low.FindItemInMap[*SchemaProxy](schema, co.Schemas.Value) } func (co *Components) FindResponse(response string) *low.ValueReference[*Response] { @@ -103,7 +103,7 @@ func (co *Components) Build(root *yaml.Node, idx *index.SpecIndex) error { skipChan := make(chan bool) errorChan := make(chan error) paramChan := make(chan low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Parameter]]) - schemaChan := make(chan low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Schema]]) + schemaChan := make(chan low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*SchemaProxy]]) responsesChan := make(chan low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Response]]) examplesChan := make(chan low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Example]]) requestBodiesChan := make(chan low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*RequestBody]]) @@ -112,7 +112,7 @@ func (co *Components) Build(root *yaml.Node, idx *index.SpecIndex) error { linkChan := make(chan low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Link]]) callbackChan := make(chan low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Callback]]) - go extractComponentValues[*Schema](SchemasLabel, root, skipChan, errorChan, schemaChan, idx) + go extractComponentValues[*SchemaProxy](SchemasLabel, root, skipChan, errorChan, schemaChan, idx) go extractComponentValues[*Parameter](ParametersLabel, root, skipChan, errorChan, paramChan, idx) go extractComponentValues[*Response](ResponsesLabel, root, skipChan, errorChan, responsesChan, idx) go extractComponentValues[*Example](ExamplesLabel, root, skipChan, errorChan, examplesChan, idx) @@ -136,7 +136,6 @@ func (co *Components) Build(root *yaml.Node, idx *index.SpecIndex) error { n++ case schemas := <-schemaChan: co.Schemas = schemas - cacheSchemas(co.Schemas.Value) n++ case responses := <-responsesChan: co.Responses = responses @@ -191,6 +190,7 @@ func extractComponentValues[T low.Buildable[N], N any](label string, root *yaml. // for every component, build in a new thread! bChan := make(chan componentBuildResult[T]) + eChan := make(chan error) var buildComponent = func(label *yaml.Node, value *yaml.Node, c chan componentBuildResult[T], ec chan<- error) { var n T = new(N) _ = low.BuildModel(value, n) @@ -221,12 +221,14 @@ func extractComponentValues[T low.Buildable[N], N any](label string, root *yaml. continue } totalComponents++ - go buildComponent(currentLabel, v, bChan, errorChan) + go buildComponent(currentLabel, v, bChan, eChan) } completedComponents := 0 for completedComponents < totalComponents { select { + case e := <-eChan: + errorChan <- e case r := <-bChan: componentValues[r.k] = r.v completedComponents++ diff --git a/datamodel/low/3.0/components_test.go b/datamodel/low/3.0/components_test.go index 46117c3..98069e9 100644 --- a/datamodel/low/3.0/components_test.go +++ b/datamodel/low/3.0/components_test.go @@ -76,8 +76,8 @@ func TestComponents_Build_Success(t *testing.T) { err = n.Build(idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "one of many", n.FindSchema("one").Value.Description.Value) - assert.Equal(t, "two of many", n.FindSchema("two").Value.Description.Value) + assert.Equal(t, "one of many", n.FindSchema("one").Value.Schema().Description.Value) + assert.Equal(t, "two of many", n.FindSchema("two").Value.Schema().Description.Value) assert.Equal(t, "three of many", n.FindResponse("three").Value.Description.Value) assert.Equal(t, "four of many", n.FindResponse("four").Value.Description.Value) assert.Equal(t, "five of many", n.FindParameter("five").Value.Description.Value) @@ -92,8 +92,10 @@ func TestComponents_Build_Success(t *testing.T) { assert.Equal(t, "fourteen of many", n.FindSecurityScheme("fourteen").Value.Description.Value) assert.Equal(t, "fifteen of many", n.FindLink("fifteen").Value.Description.Value) assert.Equal(t, "sixteen of many", n.FindLink("sixteen").Value.Description.Value) - assert.Equal(t, "seventeen of many", n.FindCallback("seventeen").Value.FindExpression("{reference}").Value.Description.Value) - assert.Equal(t, "eighteen of many", n.FindCallback("eighteen").Value.FindExpression("{raference}").Value.Description.Value) + assert.Equal(t, "seventeen of many", + n.FindCallback("seventeen").Value.FindExpression("{reference}").Value.Description.Value) + assert.Equal(t, "eighteen of many", + n.FindCallback("eighteen").Value.FindExpression("{raference}").Value.Description.Value) } diff --git a/datamodel/low/3.0/create_document.go b/datamodel/low/3.0/create_document.go index 38532cd..f0d556b 100644 --- a/datamodel/low/3.0/create_document.go +++ b/datamodel/low/3.0/create_document.go @@ -36,7 +36,6 @@ func CreateDocument(info *datamodel.SpecInfo) (*Document, []error) { if er := runFunc(info, doc, idx); er != nil { *ers = append(*ers, er) } - wg.Done() } extractionFuncs := []func(i *datamodel.SpecInfo, d *Document, idx *index.SpecIndex) error{ @@ -46,15 +45,14 @@ func CreateDocument(info *datamodel.SpecInfo) (*Document, []error) { extractComponents, extractSecurity, extractExternalDocs, + extractPaths, } wg.Add(len(extractionFuncs)) for _, f := range extractionFuncs { go runExtraction(info, &doc, idx, f, &errors, &wg) } - wg.Wait() - extractPaths(info, &doc, idx) return &doc, errors } @@ -62,11 +60,8 @@ func extractInfo(info *datamodel.SpecInfo, doc *Document, idx *index.SpecIndex) _, ln, vn := utils.FindKeyNodeFull(InfoLabel, info.RootNode.Content) if vn != nil { ir := Info{} - err := low.BuildModel(vn, &ir) - if err != nil { - return err - } - err = ir.Build(vn, idx) + _ = low.BuildModel(vn, &ir) + _ = ir.Build(vn, idx) nr := low.NodeReference[*Info]{Value: &ir, ValueNode: vn, KeyNode: ln} doc.Info = nr } @@ -95,11 +90,11 @@ func extractComponents(info *datamodel.SpecInfo, doc *Document, idx *index.SpecI _, ln, vn := utils.FindKeyNodeFull(ComponentsLabel, info.RootNode.Content) if vn != nil { ir := Components{} - err := low.BuildModel(vn, &ir) + _ = low.BuildModel(vn, &ir) + err := ir.Build(vn, idx) if err != nil { return err } - err = ir.Build(vn, idx) nr := low.NodeReference[*Components]{Value: &ir, ValueNode: vn, KeyNode: ln} doc.Components = nr } @@ -114,11 +109,8 @@ func extractServers(info *datamodel.SpecInfo, doc *Document, idx *index.SpecInde for _, srvN := range vn.Content { if utils.IsNodeMap(srvN) { srvr := Server{} - err := low.BuildModel(srvN, &srvr) - if err != nil { - return err - } - srvr.Build(srvN, idx) + _ = low.BuildModel(srvN, &srvr) + _ = srvr.Build(srvN, idx) servers = append(servers, low.ValueReference[*Server]{ Value: &srvr, ValueNode: srvN, @@ -143,11 +135,10 @@ func extractTags(info *datamodel.SpecInfo, doc *Document, idx *index.SpecIndex) for _, tagN := range vn.Content { if utils.IsNodeMap(tagN) { tag := Tag{} - err := low.BuildModel(tagN, &tag) - if err != nil { + _ = low.BuildModel(tagN, &tag) + if err := tag.Build(tagN, idx); err != nil { return err } - tag.Build(tagN, idx) tags = append(tags, low.ValueReference[*Tag]{ Value: &tag, ValueNode: tagN, diff --git a/datamodel/low/3.0/create_document_test.go b/datamodel/low/3.0/create_document_test.go index 31258ca..fd5f12a 100644 --- a/datamodel/low/3.0/create_document_test.go +++ b/datamodel/low/3.0/create_document_test.go @@ -56,10 +56,10 @@ func BenchmarkCreateDocument_k8s(b *testing.B) { } func BenchmarkCreateDocument_Stripe(b *testing.B) { + data, _ := ioutil.ReadFile("../../../test_specs/stripe.yaml") + info, _ := datamodel.ExtractSpecInfo(data) for i := 0; i < b.N; i++ { - data, _ := ioutil.ReadFile("../../../test_specs/stripe.yaml") - info, _ := datamodel.ExtractSpecInfo(data) _, err := CreateDocument(info) if err != nil { panic("this should not error") @@ -78,6 +78,20 @@ func BenchmarkCreateDocument_Petstore(b *testing.B) { } } +func TestCreateDocumentStripe(t *testing.T) { + + data, _ := ioutil.ReadFile("../../../test_specs/stripe.yaml") + info, _ := datamodel.ExtractSpecInfo(data) + d, err := CreateDocument(info) + if err != nil { + panic("broken something") + } + + assert.Equal(t, "3.0.0", d.Version.Value) + assert.Equal(t, "Stripe API", d.Info.Value.Title.Value) + assert.NotEmpty(t, d.Info.Value.Title.Value) +} + func TestCreateDocument(t *testing.T) { initTest() assert.Equal(t, "3.0.1", doc.Version.Value) @@ -174,8 +188,8 @@ func TestCreateDocument_Paths(t *testing.T) { assert.Len(t, burgerId.Value.Get.Value.Parameters.Value, 2) param := burgerId.Value.Get.Value.Parameters.Value[1] assert.Equal(t, "burgerHeader", param.Value.Name.Value) - prop := param.Value.Schema.Value.FindProperty("burgerTheme") - assert.Equal(t, "something about a theme?", prop.Value.Description.Value) + prop := param.Value.Schema.Value.Schema().FindProperty("burgerTheme") + assert.Equal(t, "something about a theme?", prop.Value.Schema().Description.Value) assert.Equal(t, "big-mac", param.Value.Example.Value) // check content @@ -189,7 +203,7 @@ func TestCreateDocument_Paths(t *testing.T) { header := encoding.Value.FindHeader("someHeader") assert.NotNil(t, header.Value) assert.Equal(t, "this is a header", header.Value.Description.Value) - assert.Equal(t, "string", header.Value.Schema.Value.Type.Value) + assert.Equal(t, "string", header.Value.Schema.Value.Schema().Type.Value) // check request body on operation burgers := doc.Paths.Value.FindPath("/burgers") @@ -206,7 +220,7 @@ func TestCreateDocument_Paths(t *testing.T) { content := requestBody.FindContent("application/json").Value assert.NotNil(t, content) - assert.Len(t, content.Schema.Value.Properties.Value, 4) + assert.Len(t, content.Schema.Value.Schema().Properties.Value, 4) assert.Len(t, content.GetAllExamples(), 2) ex := content.FindExample("pbjBurger") @@ -253,7 +267,7 @@ func TestCreateDocument_Paths(t *testing.T) { assert.NotNil(t, respContent) assert.NotNil(t, respContent.Schema.Value) - assert.Len(t, respContent.Schema.Value.Required.Value, 2) + assert.Len(t, respContent.Schema.Value.Schema().Required.Value, 2) respExample := respContent.FindExample("quarterPounder") assert.NotNil(t, respExample.Value) @@ -304,19 +318,20 @@ func TestCreateDocument_Components_Schemas(t *testing.T) { burger := components.FindSchema("Burger") assert.NotNil(t, burger.Value) - assert.Equal(t, "The tastiest food on the planet you would love to eat everyday", burger.Value.Description.Value) + assert.Equal(t, "The tastiest food on the planet you would love to eat everyday", burger.Value.Schema().Description.Value) er := components.FindSchema("Error") assert.NotNil(t, er.Value) - assert.Equal(t, "Error defining what went wrong when providing a specification. The message should help indicate the issue clearly.", er.Value.Description.Value) + assert.Equal(t, "Error defining what went wrong when providing a specification. The message should help "+ + "indicate the issue clearly.", er.Value.Schema().Description.Value) fries := components.FindSchema("Fries") assert.NotNil(t, fries.Value) - assert.Len(t, fries.Value.Properties.Value, 3) - p := fries.Value.FindProperty("favoriteDrink") + assert.Len(t, fries.Value.Schema().Properties.Value, 3) + p := fries.Value.Schema().FindProperty("favoriteDrink") assert.Equal(t, "a frosty cold beverage can be coke or sprite", - p.Value.Description.Value) + p.Value.Schema().Description.Value) } @@ -395,7 +410,7 @@ func TestCreateDocument_Components_Headers(t *testing.T) { useOil := components.FindHeader("UseOil") assert.NotNil(t, useOil.Value) assert.Equal(t, "this is a header", useOil.Value.Description.Value) - assert.Equal(t, "string", useOil.Value.Schema.Value.Type.Value) + assert.Equal(t, "string", useOil.Value.Schema.Value.Schema().Type.Value) } func TestCreateDocument_Components_Links(t *testing.T) { @@ -442,7 +457,7 @@ func TestCreateDocument_Component_Discriminator(t *testing.T) { initTest() components := doc.Components.Value - dsc := components.FindSchema("Drink").Value.Discriminator.Value + dsc := components.FindSchema("Drink").Value.Schema().Discriminator.Value assert.NotNil(t, dsc) assert.Equal(t, "drinkType", dsc.PropertyName.Value) assert.Equal(t, "some value", dsc.FindMappingValue("drink").Value) @@ -453,8 +468,8 @@ func TestCreateDocument_CheckAdditionalProperties_Schema(t *testing.T) { initTest() components := doc.Components.Value d := components.FindSchema("Dressing") - assert.NotNil(t, d.Value.AdditionalProperties.Value) - if n, ok := d.Value.AdditionalProperties.Value.(*Schema); ok { + assert.NotNil(t, d.Value.Schema().AdditionalProperties.Value) + if n, ok := d.Value.Schema().AdditionalProperties.Value.(*Schema); ok { assert.Equal(t, "something in here.", n.Description.Value) } else { assert.Fail(t, "should be a schema") @@ -465,8 +480,8 @@ func TestCreateDocument_CheckAdditionalProperties_Bool(t *testing.T) { initTest() components := doc.Components.Value d := components.FindSchema("Drink") - assert.NotNil(t, d.Value.AdditionalProperties.Value) - assert.True(t, d.Value.AdditionalProperties.Value.(bool)) + assert.NotNil(t, d.Value.Schema().AdditionalProperties.Value) + assert.True(t, d.Value.Schema().AdditionalProperties.Value.(bool)) } func TestCreateDocument_Components_Error(t *testing.T) { @@ -477,6 +492,51 @@ func TestCreateDocument_Components_Error(t *testing.T) { bark: $ref: #bork` + info, _ := datamodel.ExtractSpecInfo([]byte(yml)) + var err []error + doc, err = CreateDocument(info) + assert.Len(t, err, 0) + + ob := doc.Components.Value.FindSchema("bork").Value + ob.Schema() + assert.Error(t, ob.GetBuildError()) +} + +func TestCreateDocument_Paths_Errors(t *testing.T) { + yml := `paths: + /p: + $ref: #bork` + + info, _ := datamodel.ExtractSpecInfo([]byte(yml)) + var err []error + doc, err = CreateDocument(info) + assert.Len(t, err, 1) +} + +func TestCreateDocument_Tags_Errors(t *testing.T) { + yml := `tags: + - $ref: #bork` + + info, _ := datamodel.ExtractSpecInfo([]byte(yml)) + var err []error + doc, err = CreateDocument(info) + assert.Len(t, err, 1) +} + +func TestCreateDocument_Security_Error(t *testing.T) { + yml := `security: + $ref: #bork` + + info, _ := datamodel.ExtractSpecInfo([]byte(yml)) + var err []error + doc, err = CreateDocument(info) + assert.Len(t, err, 1) +} + +func TestCreateDocument_ExternalDoc_Error(t *testing.T) { + yml := `externalDocs: + $ref: #bork` + info, _ := datamodel.ExtractSpecInfo([]byte(yml)) var err []error doc, err = CreateDocument(info) diff --git a/datamodel/low/3.0/example_test.go b/datamodel/low/3.0/example_test.go index eff5d21..6ca62e2 100644 --- a/datamodel/low/3.0/example_test.go +++ b/datamodel/low/3.0/example_test.go @@ -123,3 +123,10 @@ value: } } + +func TestExtractExampleValue(t *testing.T) { + assert.True(t, ExtractExampleValue(&yaml.Node{Tag: "!!bool", Value: "true"}).(bool)) + assert.Equal(t, int64(10), ExtractExampleValue(&yaml.Node{Tag: "!!int", Value: "10"}).(int64)) + assert.Equal(t, 33.2, ExtractExampleValue(&yaml.Node{Tag: "!!float", Value: "33.2"}).(float64)) + +} diff --git a/datamodel/low/3.0/header.go b/datamodel/low/3.0/header.go index 35da25a..c154652 100644 --- a/datamodel/low/3.0/header.go +++ b/datamodel/low/3.0/header.go @@ -22,7 +22,7 @@ type Header struct { Style low.NodeReference[string] Explode low.NodeReference[bool] AllowReserved low.NodeReference[bool] - Schema low.NodeReference[*Schema] + Schema low.NodeReference[*SchemaProxy] Example low.NodeReference[any] Examples low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Example]] Content low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*MediaType]] diff --git a/datamodel/low/3.0/header_test.go b/datamodel/low/3.0/header_test.go index 2b31229..a16b499 100644 --- a/datamodel/low/3.0/header_test.go +++ b/datamodel/low/3.0/header_test.go @@ -60,11 +60,11 @@ content: assert.True(t, n.Required.Value) assert.False(t, n.Deprecated.Value) assert.NotNil(t, n.Schema.Value) - assert.Equal(t, "my triple M, my loves", n.Schema.Value.Description.Value) - assert.NotNil(t, n.Schema.Value.Properties.Value) - assert.Equal(t, "she is my heart.", n.Schema.Value.FindProperty("michelle").Value.Description.Value) - assert.Equal(t, "she is my song.", n.Schema.Value.FindProperty("meddy").Value.Description.Value) - assert.Equal(t, "he is my champion.", n.Schema.Value.FindProperty("maddy").Value.Description.Value) + assert.Equal(t, "my triple M, my loves", n.Schema.Value.Schema().Description.Value) + assert.NotNil(t, n.Schema.Value.Schema().Properties.Value) + assert.Equal(t, "she is my heart.", n.Schema.Value.Schema().FindProperty("michelle").Value.Schema().Description.Value) + assert.Equal(t, "she is my song.", n.Schema.Value.Schema().FindProperty("meddy").Value.Schema().Description.Value) + assert.Equal(t, "he is my champion.", n.Schema.Value.Schema().FindProperty("maddy").Value.Schema().Description.Value) if v, ok := n.Example.Value.(map[string]interface{}); ok { assert.Equal(t, "my love.", v["michelle"]) @@ -76,7 +76,7 @@ content: con := n.FindContent("family/love").Value assert.NotNil(t, con) - assert.Equal(t, "family love.", con.Schema.Value.Description.Value) + assert.Equal(t, "family love.", con.Schema.Value.Schema().Description.Value) assert.Nil(t, n.FindContent("unknown")) ext := n.FindExtension("x-family-love").Value diff --git a/datamodel/low/3.0/media_type.go b/datamodel/low/3.0/media_type.go index 64128ae..6845985 100644 --- a/datamodel/low/3.0/media_type.go +++ b/datamodel/low/3.0/media_type.go @@ -4,78 +4,78 @@ package v3 import ( - "github.com/pb33f/libopenapi/datamodel/low" - "github.com/pb33f/libopenapi/index" - "github.com/pb33f/libopenapi/utils" - "gopkg.in/yaml.v3" + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/index" + "github.com/pb33f/libopenapi/utils" + "gopkg.in/yaml.v3" ) type MediaType struct { - Schema low.NodeReference[*Schema] - Example low.NodeReference[any] - Examples low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Example]] - Encoding low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Encoding]] - Extensions map[low.KeyReference[string]]low.ValueReference[any] + Schema low.NodeReference[*SchemaProxy] + Example low.NodeReference[any] + Examples low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Example]] + Encoding low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Encoding]] + Extensions map[low.KeyReference[string]]low.ValueReference[any] } func (mt *MediaType) FindExtension(ext string) *low.ValueReference[any] { - return low.FindItemInMap[any](ext, mt.Extensions) + return low.FindItemInMap[any](ext, mt.Extensions) } func (mt *MediaType) FindPropertyEncoding(eType string) *low.ValueReference[*Encoding] { - return low.FindItemInMap[*Encoding](eType, mt.Encoding.Value) + return low.FindItemInMap[*Encoding](eType, mt.Encoding.Value) } func (mt *MediaType) FindExample(eType string) *low.ValueReference[*Example] { - return low.FindItemInMap[*Example](eType, mt.Examples.Value) + return low.FindItemInMap[*Example](eType, mt.Examples.Value) } func (mt *MediaType) GetAllExamples() map[low.KeyReference[string]]low.ValueReference[*Example] { - return mt.Examples.Value + return mt.Examples.Value } func (mt *MediaType) Build(root *yaml.Node, idx *index.SpecIndex) error { - mt.Extensions = low.ExtractExtensions(root) + mt.Extensions = low.ExtractExtensions(root) - // handle example if set. - _, expLabel, expNode := utils.FindKeyNodeFull(ExampleLabel, root.Content) - if expNode != nil { - mt.Example = low.NodeReference[any]{Value: expNode.Value, KeyNode: expLabel, ValueNode: expNode} - } + // handle example if set. + _, expLabel, expNode := utils.FindKeyNodeFull(ExampleLabel, root.Content) + if expNode != nil { + mt.Example = low.NodeReference[any]{Value: expNode.Value, KeyNode: expLabel, ValueNode: expNode} + } - //handle schema - sch, sErr := ExtractSchema(root, idx) - if sErr != nil { - return sErr - } - if sch != nil { - mt.Schema = *sch - } + //handle schema + sch, sErr := ExtractSchema(root, idx) + if sErr != nil { + return sErr + } + if sch != nil { + mt.Schema = *sch + } - // handle examples if set. - exps, expsL, expsN, eErr := low.ExtractMapFlat[*Example](ExamplesLabel, root, idx) - if eErr != nil { - return eErr - } - if exps != nil { - mt.Examples = low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Example]]{ - Value: exps, - KeyNode: expsL, - ValueNode: expsN, - } - } + // handle examples if set. + exps, expsL, expsN, eErr := low.ExtractMapFlat[*Example](ExamplesLabel, root, idx) + if eErr != nil { + return eErr + } + if exps != nil { + mt.Examples = low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Example]]{ + Value: exps, + KeyNode: expsL, + ValueNode: expsN, + } + } - // handle encoding - encs, encsL, encsN, encErr := low.ExtractMapFlat[*Encoding](EncodingLabel, root, idx) - if encErr != nil { - return encErr - } - if encs != nil { - mt.Encoding = low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Encoding]]{ - Value: encs, - KeyNode: encsL, - ValueNode: encsN, - } - } - return nil + // handle encoding + encs, encsL, encsN, encErr := low.ExtractMapFlat[*Encoding](EncodingLabel, root, idx) + if encErr != nil { + return encErr + } + if encs != nil { + mt.Encoding = low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Encoding]]{ + Value: encs, + KeyNode: encsL, + ValueNode: encsN, + } + } + return nil } diff --git a/datamodel/low/3.0/media_type_test.go b/datamodel/low/3.0/media_type_test.go index f3b1def..757f9ee 100644 --- a/datamodel/low/3.0/media_type_test.go +++ b/datamodel/low/3.0/media_type_test.go @@ -36,7 +36,7 @@ x-rock: and roll` err = n.Build(idxNode.Content[0], idx) assert.NoError(t, err) assert.Equal(t, "and roll", n.FindExtension("x-rock").Value) - assert.Equal(t, "string", n.Schema.Value.Type.Value) + assert.Equal(t, "string", n.Schema.Value.Schema().Type.Value) assert.Equal(t, "hello", n.Example.Value) assert.Equal(t, "why?", n.FindExample("what").Value.Value.Value) assert.Equal(t, "there?", n.FindExample("where").Value.Value.Value) diff --git a/datamodel/low/3.0/parameter.go b/datamodel/low/3.0/parameter.go index 4916a98..53ffff6 100644 --- a/datamodel/low/3.0/parameter.go +++ b/datamodel/low/3.0/parameter.go @@ -25,7 +25,7 @@ type Parameter struct { Style low.NodeReference[string] Explode low.NodeReference[bool] AllowReserved low.NodeReference[bool] - Schema low.NodeReference[*Schema] + Schema low.NodeReference[*SchemaProxy] Example low.NodeReference[any] Examples low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Example]] Content low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*MediaType]] diff --git a/datamodel/low/3.0/parameter_test.go b/datamodel/low/3.0/parameter_test.go index 047c7c1..d72eae5 100644 --- a/datamodel/low/3.0/parameter_test.go +++ b/datamodel/low/3.0/parameter_test.go @@ -64,11 +64,11 @@ content: assert.Equal(t, "happy", n.Name.Value) assert.Equal(t, "path", n.In.Value) assert.NotNil(t, n.Schema.Value) - assert.Equal(t, "my triple M, my loves", n.Schema.Value.Description.Value) - assert.NotNil(t, n.Schema.Value.Properties.Value) - assert.Equal(t, "she is my heart.", n.Schema.Value.FindProperty("michelle").Value.Description.Value) - assert.Equal(t, "she is my song.", n.Schema.Value.FindProperty("meddy").Value.Description.Value) - assert.Equal(t, "he is my champion.", n.Schema.Value.FindProperty("maddy").Value.Description.Value) + assert.Equal(t, "my triple M, my loves", n.Schema.Value.Schema().Description.Value) + assert.NotNil(t, n.Schema.Value.Schema().Properties.Value) + assert.Equal(t, "she is my heart.", n.Schema.Value.Schema().FindProperty("michelle").Value.Schema().Description.Value) + assert.Equal(t, "she is my song.", n.Schema.Value.Schema().FindProperty("meddy").Value.Schema().Description.Value) + assert.Equal(t, "he is my champion.", n.Schema.Value.Schema().FindProperty("maddy").Value.Schema().Description.Value) if v, ok := n.Example.Value.(map[string]interface{}); ok { assert.Equal(t, "my love.", v["michelle"]) @@ -80,7 +80,7 @@ content: con := n.FindContent("family/love").Value assert.NotNil(t, con) - assert.Equal(t, "family love.", con.Schema.Value.Description.Value) + assert.Equal(t, "family love.", con.Schema.Value.Schema().Description.Value) assert.Nil(t, n.FindContent("unknown")) ext := n.FindExtension("x-family-love").Value diff --git a/datamodel/low/3.0/path.go b/datamodel/low/3.0/path.go index 9a7112f..d581bf6 100644 --- a/datamodel/low/3.0/path.go +++ b/datamodel/low/3.0/path.go @@ -30,9 +30,9 @@ type Paths struct { } func (p *Paths) FindPath(path string) *low.ValueReference[*PathItem] { - for k, p := range p.PathItems { + for k, j := range p.PathItems { if k.Value == path { - return &p + return &j } } return nil diff --git a/datamodel/low/3.0/response.go b/datamodel/low/3.0/response.go index 6817015..32f6726 100644 --- a/datamodel/low/3.0/response.go +++ b/datamodel/low/3.0/response.go @@ -39,7 +39,7 @@ func (r *Responses) Build(root *yaml.Node, idx *index.SpecIndex) error { r.Default = def } } else { - return fmt.Errorf("responses build failed: root node is not a map! line %d, col %d", root.Line, root.Column) + return fmt.Errorf("responses build failed: vn node is not a map! line %d, col %d", root.Line, root.Column) } return nil } diff --git a/datamodel/low/3.0/response_test.go b/datamodel/low/3.0/response_test.go index db902a0..fae717a 100644 --- a/datamodel/low/3.0/response_test.go +++ b/datamodel/low/3.0/response_test.go @@ -49,7 +49,7 @@ default: con := ok.Value.FindContent("nice/rice") assert.NotNil(t, con.Value) - assert.Equal(t, "this is some content.", con.Value.Schema.Value.Description.Value) + assert.Equal(t, "this is some content.", con.Value.Schema.Value.Schema().Description.Value) head := ok.Value.FindHeader("header1") assert.NotNil(t, head.Value) diff --git a/datamodel/low/3.0/schema.go b/datamodel/low/3.0/schema.go index b58d8de..e85330e 100644 --- a/datamodel/low/3.0/schema.go +++ b/datamodel/low/3.0/schema.go @@ -40,12 +40,12 @@ type Schema struct { Required low.NodeReference[[]low.ValueReference[string]] Enum low.NodeReference[[]low.ValueReference[string]] Type low.NodeReference[string] - AllOf low.NodeReference[[]low.NodeReference[*Schema]] - OneOf low.NodeReference[[]low.NodeReference[*Schema]] - AnyOf low.NodeReference[[]low.NodeReference[*Schema]] - Not low.NodeReference[[]low.NodeReference[*Schema]] - Items low.NodeReference[[]low.NodeReference[*Schema]] - Properties low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Schema]] + AllOf low.NodeReference[[]low.ValueReference[*SchemaProxy]] + OneOf low.NodeReference[[]low.ValueReference[*SchemaProxy]] + AnyOf low.NodeReference[[]low.ValueReference[*SchemaProxy]] + 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] Default low.NodeReference[any] @@ -60,8 +60,8 @@ type Schema struct { Extensions map[low.KeyReference[string]]low.ValueReference[any] } -func (s *Schema) FindProperty(name string) *low.ValueReference[*Schema] { - return low.FindItemInMap[*Schema](name, s.Properties.Value) +func (s *Schema) FindProperty(name string) *low.ValueReference[*SchemaProxy] { + return low.FindItemInMap[*SchemaProxy](name, s.Properties.Value) } func (s *Schema) Build(root *yaml.Node, idx *index.SpecIndex) error { @@ -83,7 +83,7 @@ func (s *Schema) BuildLevel(root *yaml.Node, idx *index.SpecIndex, level int) er if ref != nil { root = ref } else { - return fmt.Errorf("build schema failed: reference cannot be found: %s, line %d, col %d", + 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) } } @@ -140,39 +140,16 @@ func (s *Schema) BuildLevel(root *yaml.Node, idx *index.SpecIndex, level int) er } // for property, build in a new thread! - bChan := make(chan schemaBuildResult) - eChan := make(chan error) + bChan := make(chan schemaProxyBuildResult) - var buildProperty = func(label *yaml.Node, value *yaml.Node, c chan schemaBuildResult, ec chan<- error) { - // have we seen this before? - seen := getSeenSchema(fmt.Sprintf("%d:%d", value.Line, value.Column)) - if seen != nil { - c <- schemaBuildResult{ - k: low.KeyReference[string]{ - KeyNode: label, - Value: label.Value, - }, - v: low.ValueReference[*Schema]{ - Value: seen, - ValueNode: value, - }, - } - return - } - p := new(Schema) - _ = low.BuildModel(value, p) - err := p.BuildLevel(value, idx, level) - if err != nil { - ec <- err - return - } - c <- schemaBuildResult{ + 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[*Schema]{ - Value: p, + v: low.ValueReference[*SchemaProxy]{ + Value: &SchemaProxy{kn: label, vn: value, idx: idx}, ValueNode: value, }, } @@ -181,7 +158,7 @@ func (s *Schema) BuildLevel(root *yaml.Node, idx *index.SpecIndex, level int) er // handle properties _, propLabel, propsNode := utils.FindKeyNodeFull(PropertiesLabel, root.Content) if propsNode != nil { - propertyMap := make(map[low.KeyReference[string]]low.ValueReference[*Schema]) + propertyMap := make(map[low.KeyReference[string]]low.ValueReference[*SchemaProxy]) var currentProp *yaml.Node totalProps := 0 for i, prop := range propsNode.Content { @@ -201,64 +178,106 @@ func (s *Schema) BuildLevel(root *yaml.Node, idx *index.SpecIndex, level int) er } } totalProps++ - go buildProperty(currentProp, prop, bChan, eChan) + go buildProperty(currentProp, prop, bChan) } completedProps := 0 for completedProps < totalProps { select { - case err := <-eChan: - return err case res := <-bChan: completedProps++ propertyMap[res.k] = res.v } } - s.Properties = low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Schema]]{ + s.Properties = low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*SchemaProxy]]{ Value: propertyMap, KeyNode: propLabel, ValueNode: propsNode, } } - // extract all sub-schemas - var errors []error + var allOf, anyOf, oneOf, not, items []low.ValueReference[*SchemaProxy] - var allOf, anyOf, oneOf, not, items []low.NodeReference[*Schema] + _, allOfLabel, allOfValue := utils.FindKeyNodeFull(AllOfLabel, root.Content) + _, anyOfLabel, anyOfValue := utils.FindKeyNodeFull(AnyOfLabel, root.Content) + _, oneOfLabel, oneOfValue := utils.FindKeyNodeFull(OneOfLabel, root.Content) + _, notLabel, notValue := utils.FindKeyNodeFull(NotLabel, root.Content) + _, itemsLabel, itemsValue := utils.FindKeyNodeFull(ItemsLabel, root.Content) - // make this async at some point to speed things up. - allOfLabel, allOfValue := buildSchema(&allOf, AllOfLabel, root, level, &errors, idx) - anyOfLabel, anyOfValue := buildSchema(&anyOf, AnyOfLabel, root, level, &errors, idx) - oneOfLabel, oneOfValue := buildSchema(&oneOf, OneOfLabel, root, level, &errors, idx) - notLabel, notValue := buildSchema(¬, NotLabel, root, level, &errors, idx) - itemsLabel, itemsValue := buildSchema(&items, ItemsLabel, root, level, &errors, idx) + errorChan := make(chan error) + allOfChan := make(chan schemaProxyBuildResult) + anyOfChan := make(chan schemaProxyBuildResult) + oneOfChan := make(chan schemaProxyBuildResult) + itemsChan := make(chan schemaProxyBuildResult) + notChan := make(chan schemaProxyBuildResult) - if len(errors) > 0 { - // todo fix this - return errors[0] + 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.NodeReference[*Schema]]{ + s.AnyOf = low.NodeReference[[]low.ValueReference[*SchemaProxy]]{ Value: anyOf, KeyNode: anyOfLabel, ValueNode: anyOfValue, } } if len(oneOf) > 0 { - s.OneOf = low.NodeReference[[]low.NodeReference[*Schema]]{ + s.OneOf = low.NodeReference[[]low.ValueReference[*SchemaProxy]]{ Value: oneOf, KeyNode: oneOfLabel, ValueNode: oneOfValue, } } if len(allOf) > 0 { - s.AllOf = low.NodeReference[[]low.NodeReference[*Schema]]{ + s.AllOf = low.NodeReference[[]low.ValueReference[*SchemaProxy]]{ Value: allOf, KeyNode: allOfLabel, ValueNode: allOfValue, } } if len(not) > 0 { - s.Not = low.NodeReference[[]low.NodeReference[*Schema]]{ + s.Not = low.NodeReference[[]low.ValueReference[*SchemaProxy]]{ Value: not, KeyNode: notLabel, ValueNode: notValue, @@ -266,74 +285,74 @@ func (s *Schema) BuildLevel(root *yaml.Node, idx *index.SpecIndex, level int) er } if len(items) > 0 { - s.Items = low.NodeReference[[]low.NodeReference[*Schema]]{ + s.Items = low.NodeReference[[]low.ValueReference[*SchemaProxy]]{ Value: items, KeyNode: itemsLabel, ValueNode: itemsValue, } } - return nil } +func countSubSchemaItems(node *yaml.Node) int { + if utils.IsNodeMap(node) { + return 1 + } + if utils.IsNodeArray(node) { + return len(node.Content) + } + return 0 +} + type schemaBuildResult struct { k low.KeyReference[string] v low.ValueReference[*Schema] } +type schemaProxyBuildResult struct { + k low.KeyReference[string] + v low.ValueReference[*SchemaProxy] +} + func (s *Schema) extractExtensions(root *yaml.Node) { s.Extensions = low.ExtractExtensions(root) } -func buildSchema(schemas *[]low.NodeReference[*Schema], attribute string, rootNode *yaml.Node, level int, - errors *[]error, idx *index.SpecIndex) (labelNode *yaml.Node, valueNode *yaml.Node) { - - _, labelNode, valueNode = utils.FindKeyNodeFull(attribute, rootNode.Content) - //wg.Add(1) - if valueNode == nil { - return nil, nil - } +func buildSchema(schemas chan schemaProxyBuildResult, labelNode, valueNode *yaml.Node, errors chan error, idx *index.SpecIndex) { if valueNode != nil { + syncChan := make(chan *low.ValueReference[*SchemaProxy]) + errorChan := make(chan error) - build := func(kn *yaml.Node, vn *yaml.Node) *low.NodeReference[*Schema] { - schema := new(Schema) + // build out a SchemaProxy for every sub-schema. + build := func(kn *yaml.Node, vn *yaml.Node, c chan *low.ValueReference[*SchemaProxy], e chan error) { if h, _, _ := utils.IsNodeRefValue(vn); h { ref := low.LocateRefNode(vn, idx) if ref != nil { vn = ref } else { - *errors = append(*errors, 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)) - return nil + 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) + e <- err } } - seen := getSeenSchema(fmt.Sprintf("%d:%d", vn.Line, vn.Column)) - if seen != nil { - return &low.NodeReference[*Schema]{ - Value: seen, - KeyNode: kn, - ValueNode: vn, - } - } + // 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 - _ = low.BuildModel(vn, schema) - - // add schema before we build, so it doesn't get stuck in an infinite loop. - addSeenSchema(fmt.Sprintf("%d:%d", vn.Line, vn.Column), schema) - - err := schema.BuildLevel(vn, idx, level) - if err != nil { - *errors = append(*errors, err) - return nil - } - - return &low.NodeReference[*Schema]{ - Value: schema, - KeyNode: kn, + res := &low.ValueReference[*SchemaProxy]{ + Value: sp, ValueNode: vn, } + c <- res } if utils.IsNodeMap(valueNode) { @@ -342,42 +361,66 @@ func buildSchema(schemas *[]low.NodeReference[*Schema], attribute string, rootNo if ref != nil { valueNode = ref } else { - *errors = append(*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)) + 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) return } } - schema := build(labelNode, valueNode) - if schema != nil { - *schemas = append(*schemas, *schema) + // 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, errorChan) + select { + case e := <-errorChan: + errors <- e + break + case r := <-syncChan: + schemas <- schemaProxyBuildResult{ + k: low.KeyReference[string]{ + KeyNode: labelNode, + Value: labelNode.Value, + }, + v: *r, + } } } if utils.IsNodeArray(valueNode) { - //fmt.Println("polymorphic looping sucks dude.") + refBuilds := 0 for _, vn := range valueNode.Content { if h, _, _ := utils.IsNodeRefValue(vn); h { ref := low.LocateRefNode(vn, idx) if ref != nil { vn = ref } else { - *errors = append(*errors, 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)) + 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 } } - - schema := build(vn, vn) - if schema != nil { - *schemas = append(*schemas, *schema) + refBuilds++ + go build(vn, vn, syncChan, errorChan) + } + completedBuilds := 0 + for completedBuilds < refBuilds { + select { + case res := <-syncChan: + completedBuilds++ + schemas <- schemaProxyBuildResult{ + k: low.KeyReference[string]{ + KeyNode: labelNode, + Value: labelNode.Value, + }, + v: *res, + } } } } } - return labelNode, valueNode } -func ExtractSchema(root *yaml.Node, idx *index.SpecIndex) (*low.NodeReference[*Schema], error) { +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" if rf, rl, _ := utils.IsNodeRefValue(root); rf { @@ -407,20 +450,8 @@ func ExtractSchema(root *yaml.Node, idx *index.SpecIndex) (*low.NodeReference[*S if schNode != nil { // check if schema has already been built. - seen := getSeenSchema(fmt.Sprintf("%d:%d", schNode.Line, schNode.Column)) - if seen != nil { - return &low.NodeReference[*Schema]{Value: seen, KeyNode: schLabel, ValueNode: schNode}, nil - } - - var schema Schema - _ = low.BuildModel(schNode, &schema) - err := schema.Build(schNode, idx) - addSeenSchema(fmt.Sprintf("%d:%d", schNode.Line, schNode.Column), &schema) - if err != nil { - return nil, err - } - - return &low.NodeReference[*Schema]{Value: &schema, KeyNode: schLabel, ValueNode: schNode}, nil + schema := &SchemaProxy{kn: schLabel, vn: schNode, idx: idx} + return &low.NodeReference[*SchemaProxy]{Value: schema, KeyNode: schLabel, ValueNode: schNode}, nil } return nil, nil } diff --git a/datamodel/low/3.0/schema_proxy.go b/datamodel/low/3.0/schema_proxy.go new file mode 100644 index 0000000..2db50df --- /dev/null +++ b/datamodel/low/3.0/schema_proxy.go @@ -0,0 +1,48 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package v3 + +import ( + "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/index" + "go.uber.org/zap" + "gopkg.in/yaml.v3" +) + +type SchemaProxy struct { + kn *yaml.Node + vn *yaml.Node + idx *index.SpecIndex + rendered *Schema + buildError error +} + +func (sp *SchemaProxy) Build(root *yaml.Node, idx *index.SpecIndex) error { + sp.vn = root + sp.idx = idx + return nil +} + +func (sp *SchemaProxy) Schema() *Schema { + if sp.rendered != nil { + return sp.rendered + } + schema := new(Schema) + _ = low.BuildModel(sp.vn, schema) + err := schema.Build(sp.vn, sp.idx) + if err != nil { + low.Log.Error("unable to build schema", + zap.Int("line", sp.vn.Line), + zap.Int("column", sp.vn.Column), + zap.String("error", err.Error())) + sp.buildError = err + return nil + } + sp.rendered = schema + return schema +} + +func (sp *SchemaProxy) GetBuildError() error { + return sp.buildError +} diff --git a/datamodel/low/3.0/schema_test.go b/datamodel/low/3.0/schema_test.go index cc7acb8..2c1c7da 100644 --- a/datamodel/low/3.0/schema_test.go +++ b/datamodel/low/3.0/schema_test.go @@ -121,94 +121,98 @@ additionalProperties: true ` assert.Len(t, sch.Properties.Value, 2) v := sch.FindProperty("somethingB") - assert.Equal(t, "https://pb33f.io", v.Value.ExternalDocs.Value.URL.Value) - assert.Equal(t, "the best docs", v.Value.ExternalDocs.Value.Description.Value) + assert.Equal(t, "https://pb33f.io", v.Value.Schema().ExternalDocs.Value.URL.Value) + assert.Equal(t, "the best docs", v.Value.Schema().ExternalDocs.Value.Description.Value) - j := v.Value.FindProperty("somethingBProp") - assert.NotNil(t, j.Value) - assert.NotNil(t, j.Value.XML.Value) - assert.Equal(t, "an xml thing", j.Value.XML.Value.Name.Value) - assert.Equal(t, "an xml namespace", j.Value.XML.Value.Namespace.Value) - assert.Equal(t, "a prefix", j.Value.XML.Value.Prefix.Value) - assert.Equal(t, true, j.Value.XML.Value.Attribute.Value) - assert.Len(t, j.Value.XML.Value.Extensions, 1) + j := v.Value.Schema().FindProperty("somethingBProp").Value.Schema() + assert.NotNil(t, j) + assert.NotNil(t, j.XML.Value) + assert.Equal(t, "an xml thing", j.XML.Value.Name.Value) + assert.Equal(t, "an xml namespace", j.XML.Value.Namespace.Value) + assert.Equal(t, "a prefix", j.XML.Value.Prefix.Value) + assert.Equal(t, true, j.XML.Value.Attribute.Value) + assert.Len(t, j.XML.Value.Extensions, 1) - assert.NotNil(t, v.Value.AdditionalProperties.Value) + assert.NotNil(t, v.Value.Schema().AdditionalProperties.Value) var addProps map[string]interface{} - v.Value.AdditionalProperties.ValueNode.Decode(&addProps) + v.Value.Schema().AdditionalProperties.ValueNode.Decode(&addProps) assert.Equal(t, "yes", addProps["why"]) assert.Equal(t, true, addProps["thatIs"]) // check polymorphic values allOf - assert.Equal(t, "an allof thing", sch.AllOf.Value[0].Value.Description.Value) - assert.Len(t, sch.AllOf.Value[0].Value.Properties.Value, 2) + f := sch.AllOf.Value[0].Value.Schema() + assert.Equal(t, "an allof thing", f.Description.Value) + assert.Len(t, f.Properties.Value, 2) - v = sch.AllOf.Value[0].Value.FindProperty("allOfA") + v = f.FindProperty("allOfA") assert.NotNil(t, v) - assert.Equal(t, "allOfA description", v.Value.Description.Value) - assert.Equal(t, "allOfAExp", v.Value.Example.Value) - v = sch.AllOf.Value[0].Value.FindProperty("allOfB") + io := v.Value.Schema() + + assert.Equal(t, "allOfA description", io.Description.Value) + assert.Equal(t, "allOfAExp", io.Example.Value) + + qw := f.FindProperty("allOfB").Value.Schema() assert.NotNil(t, v) - assert.Equal(t, "allOfB description", v.Value.Description.Value) - assert.Equal(t, "allOfBExp", v.Value.Example.Value) + assert.Equal(t, "allOfB description", qw.Description.Value) + assert.Equal(t, "allOfBExp", qw.Example.Value) // check polymorphic values anyOf - assert.Equal(t, "an anyOf thing", sch.AnyOf.Value[0].Value.Description.Value) - assert.Len(t, sch.AnyOf.Value[0].Value.Properties.Value, 2) + assert.Equal(t, "an anyOf thing", sch.AnyOf.Value[0].Value.Schema().Description.Value) + assert.Len(t, sch.AnyOf.Value[0].Value.Schema().Properties.Value, 2) - v = sch.AnyOf.Value[0].Value.FindProperty("anyOfA") + v = sch.AnyOf.Value[0].Value.Schema().FindProperty("anyOfA") assert.NotNil(t, v) - assert.Equal(t, "anyOfA description", v.Value.Description.Value) - assert.Equal(t, "anyOfAExp", v.Value.Example.Value) + assert.Equal(t, "anyOfA description", v.Value.Schema().Description.Value) + assert.Equal(t, "anyOfAExp", v.Value.Schema().Example.Value) - v = sch.AnyOf.Value[0].Value.FindProperty("anyOfB") + v = sch.AnyOf.Value[0].Value.Schema().FindProperty("anyOfB") assert.NotNil(t, v) - assert.Equal(t, "anyOfB description", v.Value.Description.Value) - assert.Equal(t, "anyOfBExp", v.Value.Example.Value) + assert.Equal(t, "anyOfB description", v.Value.Schema().Description.Value) + assert.Equal(t, "anyOfBExp", v.Value.Schema().Example.Value) // check polymorphic values oneOf - assert.Equal(t, "a oneof thing", sch.OneOf.Value[0].Value.Description.Value) - assert.Len(t, sch.OneOf.Value[0].Value.Properties.Value, 2) + assert.Equal(t, "a oneof thing", sch.OneOf.Value[0].Value.Schema().Description.Value) + assert.Len(t, sch.OneOf.Value[0].Value.Schema().Properties.Value, 2) - v = sch.OneOf.Value[0].Value.FindProperty("oneOfA") + v = sch.OneOf.Value[0].Value.Schema().FindProperty("oneOfA") assert.NotNil(t, v) - assert.Equal(t, "oneOfA description", v.Value.Description.Value) - assert.Equal(t, "oneOfAExp", v.Value.Example.Value) + assert.Equal(t, "oneOfA description", v.Value.Schema().Description.Value) + assert.Equal(t, "oneOfAExp", v.Value.Schema().Example.Value) - v = sch.OneOf.Value[0].Value.FindProperty("oneOfB") + v = sch.OneOf.Value[0].Value.Schema().FindProperty("oneOfB") assert.NotNil(t, v) - assert.Equal(t, "oneOfB description", v.Value.Description.Value) - assert.Equal(t, "oneOfBExp", v.Value.Example.Value) + assert.Equal(t, "oneOfB description", v.Value.Schema().Description.Value) + assert.Equal(t, "oneOfBExp", v.Value.Schema().Example.Value) // check values NOT - assert.Equal(t, "a not thing", sch.Not.Value[0].Value.Description.Value) - assert.Len(t, sch.Not.Value[0].Value.Properties.Value, 2) + assert.Equal(t, "a not thing", sch.Not.Value[0].Value.Schema().Description.Value) + assert.Len(t, sch.Not.Value[0].Value.Schema().Properties.Value, 2) - v = sch.Not.Value[0].Value.FindProperty("notA") + v = sch.Not.Value[0].Value.Schema().FindProperty("notA") assert.NotNil(t, v) - assert.Equal(t, "notA description", v.Value.Description.Value) - assert.Equal(t, "notAExp", v.Value.Example.Value) + assert.Equal(t, "notA description", v.Value.Schema().Description.Value) + assert.Equal(t, "notAExp", v.Value.Schema().Example.Value) - v = sch.Not.Value[0].Value.FindProperty("notB") + v = sch.Not.Value[0].Value.Schema().FindProperty("notB") assert.NotNil(t, v) - assert.Equal(t, "notB description", v.Value.Description.Value) - assert.Equal(t, "notBExp", v.Value.Example.Value) + assert.Equal(t, "notB description", v.Value.Schema().Description.Value) + assert.Equal(t, "notBExp", v.Value.Schema().Example.Value) // check values Items - assert.Equal(t, "an items thing", sch.Items.Value[0].Value.Description.Value) - assert.Len(t, sch.Items.Value[0].Value.Properties.Value, 2) + assert.Equal(t, "an items thing", sch.Items.Value[0].Value.Schema().Description.Value) + assert.Len(t, sch.Items.Value[0].Value.Schema().Properties.Value, 2) - v = sch.Items.Value[0].Value.FindProperty("itemsA") + v = sch.Items.Value[0].Value.Schema().FindProperty("itemsA") assert.NotNil(t, v) - assert.Equal(t, "itemsA description", v.Value.Description.Value) - assert.Equal(t, "itemsAExp", v.Value.Example.Value) + assert.Equal(t, "itemsA description", v.Value.Schema().Description.Value) + assert.Equal(t, "itemsAExp", v.Value.Schema().Example.Value) - v = sch.Items.Value[0].Value.FindProperty("itemsB") + v = sch.Items.Value[0].Value.Schema().FindProperty("itemsB") assert.NotNil(t, v) - assert.Equal(t, "itemsB description", v.Value.Description.Value) - assert.Equal(t, "itemsBExp", v.Value.Example.Value) + assert.Equal(t, "itemsB description", v.Value.Schema().Description.Value) + assert.Equal(t, "itemsBExp", v.Value.Schema().Example.Value) // check discriminator assert.NotNil(t, sch.Discriminator.Value) @@ -220,123 +224,123 @@ additionalProperties: true ` assert.Equal(t, "party", mv.Value) } -func TestSchema_BuildLevel_TooDeep(t *testing.T) { - clearSchemas() - // if you design data models like this, you're doing it fucking wrong. Seriously. why, what is so complex about a model - // that it needs to be 30+ levels deep? I have seen this shit in the wild, it's unreadable, un-parsable garbage. - yml := `type: object -properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object - properties: - aValue: - type: object` - - var idxNode yaml.Node - _ = yaml.Unmarshal([]byte(yml), &idxNode) - idx := index.NewSpecIndex(&idxNode) - - var n Schema - err := low.BuildModel(&idxNode, &n) - assert.NoError(t, err) - - err = n.Build(idxNode.Content[0], idx) - assert.Error(t, err) - -} +//func TestSchema_BuildLevel_TooDeep(t *testing.T) { +// clearSchemas() +// // if you design data models like this, you're doing it fucking wrong. Seriously. why, what is so complex about a model +// // that it needs to be 30+ levels deep? I have seen this shit in the wild, it's unreadable, un-parsable garbage. +// yml := `type: object +//properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object +// properties: +// aValue: +// type: object` +// +// var idxNode yaml.Node +// _ = yaml.Unmarshal([]byte(yml), &idxNode) +// idx := index.NewSpecIndex(&idxNode) +// +// var n Schema +// err := low.BuildModel(&idxNode, &n) +// assert.NoError(t, err) +// +// err = n.Build(idxNode.Content[0], idx) +// assert.Error(t, err) +// +//} func TestSchema_Build_ErrorAdditionalProps(t *testing.T) { clearSchemas() @@ -380,7 +384,7 @@ properties: var n Schema err := n.Build(idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "this is something", n.FindProperty("aValue").Value.Description.Value) + assert.Equal(t, "this is something", n.FindProperty("aValue").Value.Schema().Description.Value) } @@ -452,11 +456,11 @@ items: assert.NoError(t, schErr) desc := "poly thing" - assert.Equal(t, desc, sch.OneOf.Value[0].Value.Description.Value) - assert.Equal(t, desc, sch.AnyOf.Value[0].Value.Description.Value) - assert.Equal(t, desc, sch.AllOf.Value[0].Value.Description.Value) - assert.Equal(t, desc, sch.Not.Value[0].Value.Description.Value) - assert.Equal(t, desc, sch.Items.Value[0].Value.Description.Value) + assert.Equal(t, desc, sch.OneOf.Value[0].Value.Schema().Description.Value) + assert.Equal(t, desc, sch.AnyOf.Value[0].Value.Schema().Description.Value) + assert.Equal(t, desc, sch.AllOf.Value[0].Value.Schema().Description.Value) + assert.Equal(t, desc, sch.Not.Value[0].Value.Schema().Description.Value) + assert.Equal(t, desc, sch.Items.Value[0].Value.Schema().Description.Value) } func Test_Schema_Polymorphism_Array_Ref_Fail(t *testing.T) { @@ -542,11 +546,11 @@ items: assert.NoError(t, schErr) desc := "poly thing" - assert.Equal(t, desc, sch.OneOf.Value[0].Value.Description.Value) - assert.Equal(t, desc, sch.AnyOf.Value[0].Value.Description.Value) - assert.Equal(t, desc, sch.AllOf.Value[0].Value.Description.Value) - assert.Equal(t, desc, sch.Not.Value[0].Value.Description.Value) - assert.Equal(t, desc, sch.Items.Value[0].Value.Description.Value) + assert.Equal(t, desc, sch.OneOf.Value[0].Value.Schema().Description.Value) + assert.Equal(t, desc, sch.AnyOf.Value[0].Value.Schema().Description.Value) + assert.Equal(t, desc, sch.AllOf.Value[0].Value.Schema().Description.Value) + assert.Equal(t, desc, sch.Not.Value[0].Value.Schema().Description.Value) + assert.Equal(t, desc, sch.Items.Value[0].Value.Schema().Description.Value) } func Test_Schema_Polymorphism_Map_Ref_Fail(t *testing.T) { @@ -649,6 +653,39 @@ allOf: } +func Test_Schema_Polymorphism_BorkChild_Array(t *testing.T) { + clearSchemas() + + yml := `components: + schemas: + Something: + $ref: #borko` + + var iNode yaml.Node + mErr := yaml.Unmarshal([]byte(yml), &iNode) + assert.NoError(t, mErr) + idx := index.NewSpecIndex(&iNode) + + yml = `type: object +allOf: + - type: object + allOf: + - $ref: #bork'` + + var sch Schema + var idxNode yaml.Node + _ = yaml.Unmarshal([]byte(yml), &idxNode) + + err := low.BuildModel(&idxNode, &sch) + assert.NoError(t, err) + + schErr := sch.Build(idxNode.Content[0], idx) + assert.NoError(t, schErr) + assert.Nil(t, sch.AllOf.Value[0].Value.Schema()) // child can't be resolved, so this will be nil. + assert.Error(t, sch.AllOf.Value[0].Value.GetBuildError()) + +} + func Test_Schema_Polymorphism_RefMadness(t *testing.T) { clearSchemas() @@ -679,7 +716,7 @@ allOf: assert.NoError(t, schErr) desc := "madness" - assert.Equal(t, desc, sch.AllOf.Value[0].Value.Description.Value) + assert.Equal(t, desc, sch.AllOf.Value[0].Value.Schema().Description.Value) } @@ -709,8 +746,8 @@ allOf: err := low.BuildModel(&idxNode, &sch) assert.NoError(t, err) - schErr := sch.Build(idxNode.Content[0], idx) - assert.Error(t, schErr) + _ = sch.Build(idxNode.Content[0], idx) + assert.Nil(t, sch.AllOf.Value[0].Value.Schema()) } @@ -771,8 +808,8 @@ func TestExtractSchema(t *testing.T) { res, err := ExtractSchema(idxNode.Content[0], idx) assert.NoError(t, err) assert.NotNil(t, res.Value) - aValue := res.Value.FindProperty("aValue") - assert.Equal(t, "this is something", aValue.Value.Description.Value) + aValue := res.Value.Schema().FindProperty("aValue") + assert.Equal(t, "this is something", aValue.Value.Schema().Description.Value) } func TestExtractSchema_Ref(t *testing.T) { @@ -798,7 +835,7 @@ func TestExtractSchema_Ref(t *testing.T) { res, err := ExtractSchema(idxNode.Content[0], idx) assert.NoError(t, err) assert.NotNil(t, res.Value) - assert.Equal(t, "this is something", res.Value.Description.Value) + assert.Equal(t, "this is something", res.Value.Schema().Description.Value) } func TestExtractSchema_Ref_Fail(t *testing.T) { @@ -847,7 +884,7 @@ func TestExtractSchema_RefRoot(t *testing.T) { res, err := ExtractSchema(idxNode.Content[0], idx) assert.NoError(t, err) assert.NotNil(t, res.Value) - assert.Equal(t, "this is something", res.Value.Description.Value) + assert.Equal(t, "this is something", res.Value.Schema().Description.Value) } func TestExtractSchema_RefRoot_Fail(t *testing.T) { @@ -892,8 +929,11 @@ func TestExtractSchema_RefRoot_Child_Fail(t *testing.T) { var idxNode yaml.Node _ = yaml.Unmarshal([]byte(yml), &idxNode) - _, err := ExtractSchema(idxNode.Content[0], idx) - assert.Error(t, err) + s, _ := ExtractSchema(idxNode.Content[0], idx) + + b := s.Value.Schema() + assert.Nil(t, b) + assert.Error(t, s.Value.GetBuildError()) } @@ -1036,6 +1076,7 @@ func TestExtractSchema_OneOfRef(t *testing.T) { res, err := ExtractSchema(idxNode.Content[0], idx) assert.NoError(t, err) - assert.Equal(t, "a frosty cold beverage can be coke or sprite", res.Value.OneOf.Value[0].Value.Description.Value) + assert.Equal(t, "a frosty cold beverage can be coke or sprite", + res.Value.Schema().OneOf.Value[0].Value.Schema().Description.Value) } diff --git a/datamodel/low/extraction_functions.go b/datamodel/low/extraction_functions.go index d490b57..a2819d1 100644 --- a/datamodel/low/extraction_functions.go +++ b/datamodel/low/extraction_functions.go @@ -137,7 +137,7 @@ func ExtractObject[T Buildable[N], N any](label string, root *yaml.Node, idx *in vn = ref } else { return NodeReference[T]{}, fmt.Errorf("object build failed: reference cannot be found: %s", - root.Content[1].Value) + vn.Content[1].Value) } } } diff --git a/datamodel/low/log.go b/datamodel/low/log.go new file mode 100644 index 0000000..4659712 --- /dev/null +++ b/datamodel/low/log.go @@ -0,0 +1,12 @@ +// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley +// SPDX-License-Identifier: MIT + +package low + +import "go.uber.org/zap" + +var Log *zap.Logger + +func init() { + Log, _ = zap.NewProduction() +} diff --git a/go.mod b/go.mod index d522fd3..2d5cfe2 100644 --- a/go.mod +++ b/go.mod @@ -13,4 +13,7 @@ require ( github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/iancoleman/strcase v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + go.uber.org/zap v1.23.0 // indirect ) diff --git a/go.sum b/go.sum index f91b384..3e598c9 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,10 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= +github.com/francoispqt/gojay v0.0.0-20181220093123-f2cc13a668ca h1:F2BD6Vhei4w0rtm4eNpzylNsB07CcCbpYA+xlqMx3mA= +github.com/francoispqt/gojay v0.0.0-20181220093123-f2cc13a668ca/go.mod h1:H8Wgri1Asi1VevY3ySdpIK5+KCpqzToVswNq8g2xZj4= +github.com/francoispqt/onelog v0.0.0-20190306043706-8c2bb31b10a4 h1:N9eG+1y9e3tnNPXKjssLMa8MumIBDWWoJQWM7htGWUc= +github.com/francoispqt/onelog v0.0.0-20190306043706-8c2bb31b10a4/go.mod h1:v1Il1fkBpjiYPpEJcGxqgrPUPcHuTC7eHh9zBV3CLBE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -57,14 +61,24 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -131,5 +145,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=