diff --git a/datamodel/low/3.0/encoding.go b/datamodel/low/3.0/encoding.go index ecfd6fd..bea6ec8 100644 --- a/datamodel/low/3.0/encoding.go +++ b/datamodel/low/3.0/encoding.go @@ -17,7 +17,18 @@ type Encoding struct { AllowReserved low.NodeReference[bool] } -func (en Encoding) Build(root *yaml.Node) error { +func (en *Encoding) FindHeader(hType string) *low.ValueReference[*Header] { + for _, c := range en.Headers { + for n, o := range c { + if n.Value == hType { + return &o + } + } + } + return nil +} + +func (en *Encoding) Build(root *yaml.Node) error { headers, err := ExtractMap[*Header](HeadersLabel, root) if err != nil { diff --git a/datamodel/low/3.0/extraction_functions.go b/datamodel/low/3.0/extraction_functions.go index 9eb7647..13bf880 100644 --- a/datamodel/low/3.0/extraction_functions.go +++ b/datamodel/low/3.0/extraction_functions.go @@ -5,6 +5,7 @@ import ( "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" "strconv" + "strings" "sync" ) @@ -27,6 +28,44 @@ func ExtractSchema(root *yaml.Node) (*low.NodeReference[*Schema], error) { var mapLock sync.Mutex +func ExtractObject[T any](label string, root *yaml.Node) (low.NodeReference[*T], error) { + _, ln, vn := utils.FindKeyNodeFull(label, root.Content) + var n *T = new(T) + err := BuildModel(root, n) + if err != nil { + return low.NodeReference[*T]{}, err + } + return low.NodeReference[*T]{ + Value: n, + KeyNode: ln, + ValueNode: vn, + }, nil +} + +func ExtractArray[T low.Buildable[N], N any](label string, root *yaml.Node) ([]low.NodeReference[T], error) { + _, ln, vn := utils.FindKeyNodeFull(label, root.Content) + var items []low.NodeReference[T] + if vn != nil && ln != nil { + for _, node := range vn.Content { + var n T = new(N) + err := BuildModel(node, n) + if err != nil { + return []low.NodeReference[T]{}, err + } + berr := n.Build(node) + if berr != nil { + return nil, berr + } + items = append(items, low.NodeReference[T]{ + Value: n, + ValueNode: vn, + KeyNode: ln, + }) + } + } + return items, nil +} + func ExtractMap[PT low.Buildable[N], N any](label string, root *yaml.Node) (map[low.KeyReference[string]]map[low.KeyReference[string]]low.ValueReference[PT], error) { _, labelNode, valueNode := utils.FindKeyNodeFull(label, root.Content) if valueNode != nil { @@ -37,6 +76,9 @@ func ExtractMap[PT low.Buildable[N], N any](label string, root *yaml.Node) (map[ currentLabelNode = en continue } + if strings.HasPrefix(strings.ToLower(currentLabelNode.Value), "x-") { + continue // yo, don't pay any attention to extensions, not here anyway. + } var n PT = new(N) err := BuildModel(valueNode, n) if err != nil { @@ -54,7 +96,6 @@ func ExtractMap[PT low.Buildable[N], N any](label string, root *yaml.Node) (map[ ValueNode: en, } } - resMap := make(map[low.KeyReference[string]]map[low.KeyReference[string]]low.ValueReference[PT]) resMap[low.KeyReference[string]{ Value: labelNode.Value, diff --git a/datamodel/low/3.0/media_type.go b/datamodel/low/3.0/media_type.go index 3a3ed04..125b1d4 100644 --- a/datamodel/low/3.0/media_type.go +++ b/datamodel/low/3.0/media_type.go @@ -14,6 +14,17 @@ type MediaType struct { Extensions map[low.KeyReference[string]]low.ValueReference[any] } +func (mt *MediaType) FindPropertyEncoding(eType string) *low.ValueReference[*Encoding] { + for _, c := range mt.Encoding { + for n, o := range c { + if n.Value == eType { + return &o + } + } + } + return nil +} + func (mt *MediaType) Build(root *yaml.Node) error { // extract extensions diff --git a/datamodel/low/3.0/operation.go b/datamodel/low/3.0/operation.go index e13dd6b..3f43707 100644 --- a/datamodel/low/3.0/operation.go +++ b/datamodel/low/3.0/operation.go @@ -2,7 +2,6 @@ package v3 import ( "github.com/pb33f/libopenapi/datamodel/low" - "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) @@ -34,42 +33,19 @@ func (o *Operation) Build(root *yaml.Node) error { } o.Extensions = extensionMap - // extract external docs - _, ln, exDocs := utils.FindKeyNodeFull(ExternalDocsLabel, root.Content) - if exDocs != nil { - var externalDoc ExternalDoc - err = BuildModel(exDocs, &externalDoc) - if err != nil { - return err - } - o.ExternalDocs = low.NodeReference[*ExternalDoc]{ - Value: &externalDoc, - KeyNode: ln, - ValueNode: exDocs, - } + // extract externalDocs + extDocs, dErr := ExtractObject[ExternalDoc](ExternalDocsLabel, root) + if dErr != nil { + return dErr } + o.ExternalDocs = extDocs - // build parameters - _, paramLabel, paramNode := utils.FindKeyNodeFull(ParametersLabel, root.Content) - if paramNode != nil && paramLabel != nil { - var params []low.NodeReference[*Parameter] - - for _, pN := range paramNode.Content { - var param Parameter - err = BuildModel(pN, ¶m) - if err != nil { - return err - } - err = param.Build(pN) - if err != nil { - return err - } - params = append(params, low.NodeReference[*Parameter]{ - Value: ¶m, - ValueNode: paramNode, - KeyNode: paramLabel, - }) - } + // extract parameters + params, pErr := ExtractArray[*Parameter](ParametersLabel, root) + if pErr != nil { + return pErr + } + if params != nil { o.Parameters = params } diff --git a/datamodel/low/3.0/parameter.go b/datamodel/low/3.0/parameter.go index f36771d..cb9963e 100644 --- a/datamodel/low/3.0/parameter.go +++ b/datamodel/low/3.0/parameter.go @@ -28,7 +28,7 @@ type Parameter struct { Extensions map[low.KeyReference[string]]low.ValueReference[any] } -func (p *Parameter) GetContent(cType string) *low.ValueReference[*MediaType] { +func (p *Parameter) FindContent(cType string) *low.ValueReference[*MediaType] { for _, c := range p.Content { for n, o := range c { if n.Value == cType { @@ -61,6 +61,15 @@ func (p *Parameter) Build(root *yaml.Node) error { } p.Schema = *sch + // handle examples if set. + exps, eErr := ExtractMap[*Example](ExamplesLabel, root) + if eErr != nil { + return eErr + } + if exps != nil { + p.Examples = exps + } + // handle content, if set. con, cErr := ExtractMap[*MediaType](ContentLabel, root) if cErr != nil { diff --git a/datamodel/low/3.0/path.go b/datamodel/low/3.0/path.go index 771e6b6..88851fb 100644 --- a/datamodel/low/3.0/path.go +++ b/datamodel/low/3.0/path.go @@ -24,7 +24,7 @@ type Paths struct { Extensions map[low.KeyReference[string]]low.ValueReference[any] } -func (p *Paths) GetPath(path string) *low.ValueReference[*PathItem] { +func (p *Paths) FindPath(path string) *low.ValueReference[*PathItem] { for k, p := range p.PathItems { if k.Value == path { return &p @@ -87,16 +87,16 @@ func (p *Paths) Build(root *yaml.Node) error { type PathItem struct { Description low.NodeReference[string] Summary low.NodeReference[string] - Get *low.NodeReference[*Operation] - Put *low.NodeReference[*Operation] - Post *low.NodeReference[*Operation] - Delete *low.NodeReference[*Operation] - Options *low.NodeReference[*Operation] - Head *low.NodeReference[*Operation] - Patch *low.NodeReference[*Operation] - Trace *low.NodeReference[*Operation] - Servers []*low.NodeReference[*Server] - Parameters []*low.NodeReference[*Parameter] + Get low.NodeReference[*Operation] + Put low.NodeReference[*Operation] + Post low.NodeReference[*Operation] + Delete low.NodeReference[*Operation] + Options low.NodeReference[*Operation] + Head low.NodeReference[*Operation] + Patch low.NodeReference[*Operation] + Trace low.NodeReference[*Operation] + Servers []low.NodeReference[*Server] + Parameters []low.NodeReference[*Parameter] Extensions map[low.KeyReference[string]]low.ValueReference[any] } @@ -143,21 +143,21 @@ func (p *PathItem) Build(root *yaml.Node) error { switch currentNode.Value { case GetLabel: - p.Get = &opRef + p.Get = opRef case PostLabel: - p.Post = &opRef + p.Post = opRef case PutLabel: - p.Put = &opRef + p.Put = opRef case PatchLabel: - p.Patch = &opRef + p.Patch = opRef case DeleteLabel: - p.Delete = &opRef + p.Delete = opRef case HeadLabel: - p.Head = &opRef + p.Head = opRef case OptionsLabel: - p.Options = &opRef + p.Options = opRef case TraceLabel: - p.Trace = &opRef + p.Trace = opRef } } diff --git a/datamodel/low/3.0/request_body.go b/datamodel/low/3.0/request_body.go index 6d9d7e0..3b7ef79 100644 --- a/datamodel/low/3.0/request_body.go +++ b/datamodel/low/3.0/request_body.go @@ -6,9 +6,29 @@ import ( ) type RequestBody struct { - Node *yaml.Node Description low.NodeReference[string] - Content map[string]MediaType + Content map[low.KeyReference[string]]map[low.KeyReference[string]]low.ValueReference[*MediaType] Required low.NodeReference[bool] - Extensions map[string]low.ObjectReference + Extensions map[low.KeyReference[string]]low.ValueReference[any] +} + +func (rb *RequestBody) Build(root *yaml.Node) error { + // extract extensions + extensionMap, err := ExtractExtensions(root) + if err != nil { + return err + } + if extensionMap != nil { + rb.Extensions = extensionMap + } + + // handle content, if set. + con, cErr := ExtractMap[*MediaType](ContentLabel, root) + if cErr != nil { + return cErr + } + if con != nil { + rb.Content = con + } + return nil } diff --git a/datamodel/low/3.0/tag.go b/datamodel/low/3.0/tag.go index a4ce3b4..083a583 100644 --- a/datamodel/low/3.0/tag.go +++ b/datamodel/low/3.0/tag.go @@ -2,7 +2,6 @@ package v3 import ( "github.com/pb33f/libopenapi/datamodel/low" - "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) @@ -26,17 +25,12 @@ func (t *Tag) Build(root *yaml.Node) error { } t.Extensions = extensionMap - _, ln, exDocs := utils.FindKeyNodeFull(ExternalDocsLabel, root.Content) - // extract external docs - var externalDoc ExternalDoc - err = BuildModel(exDocs, &externalDoc) - if err != nil { - return err - } - t.ExternalDocs = low.NodeReference[*ExternalDoc]{ - Value: &externalDoc, - KeyNode: ln, - ValueNode: exDocs, + // extract externalDocs + extDocs, dErr := ExtractObject[ExternalDoc](ExternalDocsLabel, root) + if dErr != nil { + return dErr } + t.ExternalDocs = extDocs + return nil } diff --git a/openapi/create_document_test.go b/openapi/create_document_test.go index 16d37b9..92c6bc2 100644 --- a/openapi/create_document_test.go +++ b/openapi/create_document_test.go @@ -1,126 +1,138 @@ package openapi import ( - "github.com/pb33f/libopenapi/datamodel" - v3 "github.com/pb33f/libopenapi/datamodel/low/3.0" - "github.com/stretchr/testify/assert" - "io/ioutil" - "testing" + "github.com/pb33f/libopenapi/datamodel" + v3 "github.com/pb33f/libopenapi/datamodel/low/3.0" + "github.com/stretchr/testify/assert" + "io/ioutil" + "testing" ) var doc *v3.Document func init() { - data, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") - info, _ := datamodel.ExtractSpecInfo(data) - doc, _ = CreateDocument(info) + data, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") + info, _ := datamodel.ExtractSpecInfo(data) + doc, _ = CreateDocument(info) } func BenchmarkCreateDocument(b *testing.B) { - data, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") - info, _ := datamodel.ExtractSpecInfo(data) - for i := 0; i < b.N; i++ { - doc, _ = CreateDocument(info) - } + data, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") + info, _ := datamodel.ExtractSpecInfo(data) + for i := 0; i < b.N; i++ { + doc, _ = CreateDocument(info) + } } func TestCreateDocument(t *testing.T) { - assert.Equal(t, "3.0.1", doc.Version.Value) - assert.Equal(t, "Burger Shop", doc.Info.Value.Title.Value) - assert.NotEmpty(t, doc.Info.Value.Title.Value) + assert.Equal(t, "3.0.1", doc.Version.Value) + assert.Equal(t, "Burger Shop", doc.Info.Value.Title.Value) + assert.NotEmpty(t, doc.Info.Value.Title.Value) } func TestCreateDocument_Info(t *testing.T) { - assert.Equal(t, "https://pb33f.io", doc.Info.Value.TermsOfService.Value) - assert.Equal(t, "pb33f", doc.Info.Value.Contact.Value.Name.Value) - assert.Equal(t, "buckaroo@pb33f.io", doc.Info.Value.Contact.Value.Email.Value) - assert.Equal(t, "https://pb33f.io", doc.Info.Value.Contact.Value.URL.Value) - assert.Equal(t, "pb33f", doc.Info.Value.License.Value.Name.Value) - assert.Equal(t, "https://pb33f.io/made-up", doc.Info.Value.License.Value.URL.Value) + assert.Equal(t, "https://pb33f.io", doc.Info.Value.TermsOfService.Value) + assert.Equal(t, "pb33f", doc.Info.Value.Contact.Value.Name.Value) + assert.Equal(t, "buckaroo@pb33f.io", doc.Info.Value.Contact.Value.Email.Value) + assert.Equal(t, "https://pb33f.io", doc.Info.Value.Contact.Value.URL.Value) + assert.Equal(t, "pb33f", doc.Info.Value.License.Value.Name.Value) + assert.Equal(t, "https://pb33f.io/made-up", doc.Info.Value.License.Value.URL.Value) } func TestCreateDocument_Servers(t *testing.T) { - assert.Len(t, doc.Servers, 2) - server1 := doc.Servers[0] - server2 := doc.Servers[1] + assert.Len(t, doc.Servers, 2) + server1 := doc.Servers[0] + server2 := doc.Servers[1] - // server 1 - assert.Equal(t, "{scheme}://api.pb33f.io", server1.Value.URL.Value) - assert.NotEmpty(t, server1.Value.Description.Value) - assert.Len(t, server1.Value.Variables.Value, 1) - assert.Len(t, server1.Value.Variables.Value["scheme"].Value.Enum, 2) - assert.Equal(t, server1.Value.Variables.Value["scheme"].Value.Default.Value, "https") - assert.NotEmpty(t, server1.Value.Variables.Value["scheme"].Value.Description.Value) + // server 1 + assert.Equal(t, "{scheme}://api.pb33f.io", server1.Value.URL.Value) + assert.NotEmpty(t, server1.Value.Description.Value) + assert.Len(t, server1.Value.Variables.Value, 1) + assert.Len(t, server1.Value.Variables.Value["scheme"].Value.Enum, 2) + assert.Equal(t, server1.Value.Variables.Value["scheme"].Value.Default.Value, "https") + assert.NotEmpty(t, server1.Value.Variables.Value["scheme"].Value.Description.Value) - // server 2 - assert.Equal(t, "https://{domain}.{host}.com", server2.Value.URL.Value) - assert.NotEmpty(t, server2.Value.Description.Value) - assert.Len(t, server2.Value.Variables.Value, 2) - assert.Equal(t, server2.Value.Variables.Value["domain"].Value.Default.Value, "api") - assert.NotEmpty(t, server2.Value.Variables.Value["domain"].Value.Description.Value) - assert.NotEmpty(t, server2.Value.Variables.Value["host"].Value.Description.Value) - assert.Equal(t, server2.Value.Variables.Value["host"].Value.Default.Value, "pb33f.io") - assert.Equal(t, "1.2", doc.Info.Value.Version.Value) + // server 2 + assert.Equal(t, "https://{domain}.{host}.com", server2.Value.URL.Value) + assert.NotEmpty(t, server2.Value.Description.Value) + assert.Len(t, server2.Value.Variables.Value, 2) + assert.Equal(t, server2.Value.Variables.Value["domain"].Value.Default.Value, "api") + assert.NotEmpty(t, server2.Value.Variables.Value["domain"].Value.Description.Value) + assert.NotEmpty(t, server2.Value.Variables.Value["host"].Value.Description.Value) + assert.Equal(t, server2.Value.Variables.Value["host"].Value.Default.Value, "pb33f.io") + assert.Equal(t, "1.2", doc.Info.Value.Version.Value) } func TestCreateDocument_Tags(t *testing.T) { - assert.Len(t, doc.Tags, 2) + assert.Len(t, doc.Tags, 2) - // tag1 - assert.Equal(t, "Burgers", doc.Tags[0].Value.Name.Value) - assert.NotEmpty(t, doc.Tags[0].Value.Description.Value) - assert.NotNil(t, doc.Tags[0].Value.ExternalDocs.Value) - assert.Equal(t, "https://pb33f.io", doc.Tags[0].Value.ExternalDocs.Value.URL.Value) - assert.NotEmpty(t, doc.Tags[0].Value.ExternalDocs.Value.URL.Value) - assert.Len(t, doc.Tags[0].Value.Extensions, 7) + // tag1 + assert.Equal(t, "Burgers", doc.Tags[0].Value.Name.Value) + assert.NotEmpty(t, doc.Tags[0].Value.Description.Value) + assert.NotNil(t, doc.Tags[0].Value.ExternalDocs.Value) + assert.Equal(t, "https://pb33f.io", doc.Tags[0].Value.ExternalDocs.Value.URL.Value) + assert.NotEmpty(t, doc.Tags[0].Value.ExternalDocs.Value.URL.Value) + assert.Len(t, doc.Tags[0].Value.Extensions, 7) - for key, extension := range doc.Tags[0].Value.Extensions { - switch key.Value { - case "x-internal-ting": - assert.Equal(t, "somethingSpecial", extension.Value) - case "x-internal-tong": - assert.Equal(t, int64(1), extension.Value) - case "x-internal-tang": - assert.Equal(t, 1.2, extension.Value) - case "x-internal-tung": - assert.Equal(t, true, extension.Value) - case "x-internal-arr": - assert.Len(t, extension.Value, 2) - assert.Equal(t, "one", extension.Value.([]interface{})[0].(string)) - case "x-internal-arrmap": - assert.Len(t, extension.Value, 2) - assert.Equal(t, "now", extension.Value.([]interface{})[0].(map[string]interface{})["what"]) - case "x-something-else": - // crazy times in the upside down. this API should be avoided for the higher up use cases. - // this is why we will need a higher level API to this model, this looks cool and all, but dude. - assert.Equal(t, "now?", extension.Value.(map[string]interface{})["ok"].([]interface{})[0].(map[string]interface{})["what"]) - } + for key, extension := range doc.Tags[0].Value.Extensions { + switch key.Value { + case "x-internal-ting": + assert.Equal(t, "somethingSpecial", extension.Value) + case "x-internal-tong": + assert.Equal(t, int64(1), extension.Value) + case "x-internal-tang": + assert.Equal(t, 1.2, extension.Value) + case "x-internal-tung": + assert.Equal(t, true, extension.Value) + case "x-internal-arr": + assert.Len(t, extension.Value, 2) + assert.Equal(t, "one", extension.Value.([]interface{})[0].(string)) + case "x-internal-arrmap": + assert.Len(t, extension.Value, 2) + assert.Equal(t, "now", extension.Value.([]interface{})[0].(map[string]interface{})["what"]) + case "x-something-else": + // crazy times in the upside down. this API should be avoided for the higher up use cases. + // this is why we will need a higher level API to this model, this looks cool and all, but dude. + assert.Equal(t, "now?", extension.Value.(map[string]interface{})["ok"].([]interface{})[0].(map[string]interface{})["what"]) + } - } + } - /// tag2 - assert.Equal(t, "Dressing", doc.Tags[1].Value.Name.Value) - assert.NotEmpty(t, doc.Tags[1].Value.Description.Value) - assert.NotNil(t, doc.Tags[1].Value.ExternalDocs.Value) - assert.Equal(t, "https://pb33f.io", doc.Tags[1].Value.ExternalDocs.Value.URL.Value) - assert.NotEmpty(t, doc.Tags[1].Value.ExternalDocs.Value.URL.Value) - assert.Len(t, doc.Tags[1].Value.Extensions, 0) + /// tag2 + assert.Equal(t, "Dressing", doc.Tags[1].Value.Name.Value) + assert.NotEmpty(t, doc.Tags[1].Value.Description.Value) + assert.NotNil(t, doc.Tags[1].Value.ExternalDocs.Value) + assert.Equal(t, "https://pb33f.io", doc.Tags[1].Value.ExternalDocs.Value.URL.Value) + assert.NotEmpty(t, doc.Tags[1].Value.ExternalDocs.Value.URL.Value) + assert.Len(t, doc.Tags[1].Value.Extensions, 0) } func TestCreateDocument_Paths(t *testing.T) { - assert.Len(t, doc.Paths.Value.PathItems, 6) - burgerId := doc.Paths.Value.GetPath("/burgers/{burgerId}") - assert.NotNil(t, burgerId) - assert.Len(t, burgerId.Value.Get.Value.Parameters, 2) - param := burgerId.Value.Get.Value.Parameters[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) - assert.Equal(t, "big-mac", param.Value.Example.Value) + assert.Len(t, doc.Paths.Value.PathItems, 6) + burgerId := doc.Paths.Value.FindPath("/burgers/{burgerId}") + assert.NotNil(t, burgerId) + assert.Len(t, burgerId.Value.Get.Value.Parameters, 2) + param := burgerId.Value.Get.Value.Parameters[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) + assert.Equal(t, "big-mac", param.Value.Example.Value) - // check content - pContent := param.Value.GetContent("application/json") - assert.Equal(t, "somethingNice", pContent.Value.Example.Value) + // check content + pContent := param.Value.FindContent("application/json") + assert.Equal(t, "somethingNice", pContent.Value.Example.Value) + + encoding := pContent.Value.FindPropertyEncoding("burgerTheme") + assert.NotNil(t, encoding.Value) + assert.Len(t, encoding.Value.Headers, 1) + + 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) + + // check request body on operation + //burgers:= doc.Paths.Value.FindPath("/burgers/{burgerId}") }