From ec87ddf8ccb4dad1630c5e30fc4755a99f5bb0a5 Mon Sep 17 00:00:00 2001 From: Dave Shanley Date: Tue, 9 Aug 2022 07:59:22 -0400 Subject: [PATCH] Operartion model is now complete! That should be the largest bulk of complexity, now onto components. --- datamodel/low/3.0/document.go | 4 +- datamodel/low/3.0/extraction_functions.go | 46 ++++++++++-- datamodel/low/3.0/model_builder.go | 3 + datamodel/low/3.0/oauth_flows.go | 32 ++++++++ datamodel/low/3.0/operation.go | 27 ++++++- datamodel/low/3.0/response.go | 15 ++-- datamodel/low/3.0/security_scheme.go | 91 ++++++++++++++++++++++- index/spec_index_test.go | 2 +- openapi/create_document.go | 11 ++- openapi/create_document_test.go | 46 ++++++++---- test_specs/burgershop.openapi.yaml | 35 +++++++++ 11 files changed, 271 insertions(+), 41 deletions(-) diff --git a/datamodel/low/3.0/document.go b/datamodel/low/3.0/document.go index 8d9e7fa..892a1e0 100644 --- a/datamodel/low/3.0/document.go +++ b/datamodel/low/3.0/document.go @@ -7,9 +7,9 @@ import ( type Document struct { Version low.NodeReference[string] Info low.NodeReference[*Info] - Servers []low.NodeReference[*Server] + Servers low.NodeReference[[]low.ValueReference[*Server]] Paths low.NodeReference[*Paths] - Components *Components + Components low.NodeReference[*Components] Security []*SecurityRequirement Tags []low.NodeReference[*Tag] ExternalDocs *ExternalDoc diff --git a/datamodel/low/3.0/extraction_functions.go b/datamodel/low/3.0/extraction_functions.go index 339056d..2de03e1 100644 --- a/datamodel/low/3.0/extraction_functions.go +++ b/datamodel/low/3.0/extraction_functions.go @@ -64,11 +64,14 @@ func ExtractObjectRaw[T low.Buildable[N], N any](root *yaml.Node) (T, error) { func ExtractObject[T low.Buildable[N], N any](label string, root *yaml.Node) (low.NodeReference[T], error) { _, ln, vn := utils.FindKeyNodeFull(label, root.Content) var n T = new(N) - err := BuildModel(root, n) + err := BuildModel(vn, n) if err != nil { return low.NodeReference[T]{}, err } - err = n.Build(root) + if ln == nil { + return low.NodeReference[T]{}, nil + } + err = n.Build(vn) if err != nil { return low.NodeReference[T]{}, err } @@ -79,30 +82,59 @@ func ExtractObject[T low.Buildable[N], N any](label string, root *yaml.Node) (lo }, nil } -func ExtractArray[T low.Buildable[N], N any](label string, root *yaml.Node) ([]low.NodeReference[T], *yaml.Node, *yaml.Node, error) { +func ExtractArray[T low.Buildable[N], N any](label string, root *yaml.Node) ([]low.ValueReference[T], *yaml.Node, *yaml.Node, error) { _, ln, vn := utils.FindKeyNodeFull(label, root.Content) - var items []low.NodeReference[T] + var items []low.ValueReference[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]{}, ln, vn, err + return []low.ValueReference[T]{}, ln, vn, err } berr := n.Build(node) if berr != nil { return nil, ln, vn, berr } - items = append(items, low.NodeReference[T]{ + items = append(items, low.ValueReference[T]{ Value: n, ValueNode: node, - KeyNode: ln, }) } } return items, ln, vn, nil } +func ExtractMapFlatNoLookup[PT low.Buildable[N], N any](root *yaml.Node) (map[low.KeyReference[string]]low.ValueReference[PT], error) { + valueMap := make(map[low.KeyReference[string]]low.ValueReference[PT]) + if utils.IsNodeMap(root) { + var currentKey *yaml.Node + for i, node := range root.Content { + if i%2 == 0 { + currentKey = node + continue + } + var n PT = new(N) + err := BuildModel(node, n) + if err != nil { + return nil, err + } + berr := n.Build(node) + if berr != nil { + return nil, berr + } + valueMap[low.KeyReference[string]{ + Value: currentKey.Value, + KeyNode: currentKey, + }] = low.ValueReference[PT]{ + Value: n, + ValueNode: node, + } + } + } + return valueMap, nil +} + func ExtractMapFlat[PT low.Buildable[N], N any](label string, root *yaml.Node) (map[low.KeyReference[string]]low.ValueReference[PT], *yaml.Node, *yaml.Node, error) { _, labelNode, valueNode := utils.FindKeyNodeFull(label, root.Content) if valueNode != nil { diff --git a/datamodel/low/3.0/model_builder.go b/datamodel/low/3.0/model_builder.go index f7b7680..3ec83cd 100644 --- a/datamodel/low/3.0/model_builder.go +++ b/datamodel/low/3.0/model_builder.go @@ -11,6 +11,9 @@ import ( ) func BuildModel(node *yaml.Node, model interface{}) error { + if node == nil { + return nil + } if reflect.ValueOf(model).Type().Kind() != reflect.Pointer { return fmt.Errorf("cannot build model on non-pointer: %v", reflect.ValueOf(model).Type().Kind()) diff --git a/datamodel/low/3.0/oauth_flows.go b/datamodel/low/3.0/oauth_flows.go index 84fa856..07b3dbd 100644 --- a/datamodel/low/3.0/oauth_flows.go +++ b/datamodel/low/3.0/oauth_flows.go @@ -5,6 +5,13 @@ import ( "gopkg.in/yaml.v3" ) +const ( + ImplicitLabel = "implicit" + PasswordLabel = "password" + ClientCredentialsLabel = "clientCredentials" + AuthorizationCodeLabel = "authorizationCode" +) + type OAuthFlows struct { Implicit low.NodeReference[*OAuthFlow] Password low.NodeReference[*OAuthFlow] @@ -19,7 +26,32 @@ func (o *OAuthFlows) Build(root *yaml.Node) error { return err } o.Extensions = extensionMap + + v, vErr := ExtractObject[*OAuthFlow](ImplicitLabel, root) + if vErr != nil { + return vErr + } + o.Implicit = v + + v, vErr = ExtractObject[*OAuthFlow](PasswordLabel, root) + if vErr != nil { + return vErr + } + o.Password = v + + v, vErr = ExtractObject[*OAuthFlow](ClientCredentialsLabel, root) + if vErr != nil { + return vErr + } + o.ClientCredentials = v + + v, vErr = ExtractObject[*OAuthFlow](AuthorizationCodeLabel, root) + if vErr != nil { + return vErr + } + o.AuthorizationCode = v return nil + } type OAuthFlow struct { diff --git a/datamodel/low/3.0/operation.go b/datamodel/low/3.0/operation.go index 45d1bfe..6586a2c 100644 --- a/datamodel/low/3.0/operation.go +++ b/datamodel/low/3.0/operation.go @@ -18,13 +18,13 @@ type Operation struct { Description low.NodeReference[string] ExternalDocs low.NodeReference[*ExternalDoc] OperationId low.NodeReference[string] - Parameters low.NodeReference[[]low.NodeReference[*Parameter]] + Parameters low.NodeReference[[]low.ValueReference[*Parameter]] RequestBody low.NodeReference[*RequestBody] Responses low.NodeReference[*Responses] Callbacks low.NodeReference[map[low.KeyReference[string]]low.ValueReference[*Callback]] Deprecated low.NodeReference[bool] - Security []low.NodeReference[*SecurityRequirement] - Servers []low.NodeReference[*Server] + Security low.NodeReference[*SecurityRequirement] + Servers low.NodeReference[[]low.ValueReference[*Server]] Extensions map[low.KeyReference[string]]low.ValueReference[any] } @@ -48,7 +48,7 @@ func (o *Operation) Build(root *yaml.Node) error { return pErr } if params != nil { - o.Parameters = low.NodeReference[[]low.NodeReference[*Parameter]]{ + o.Parameters = low.NodeReference[[]low.ValueReference[*Parameter]]{ Value: params, KeyNode: ln, ValueNode: vn, @@ -82,5 +82,24 @@ func (o *Operation) Build(root *yaml.Node) error { } } + // extract security + sec, sErr := ExtractObject[*SecurityRequirement](SecurityLabel, root) + if sErr != nil { + return sErr + } + o.Security = sec + + // extract servers + servers, sl, sn, serErr := ExtractArray[*Server](ServersLabel, root) + if serErr != nil { + return serErr + } + if servers != nil { + o.Servers = low.NodeReference[[]low.ValueReference[*Server]]{ + Value: servers, + KeyNode: sl, + ValueNode: sn, + } + } return nil } diff --git a/datamodel/low/3.0/response.go b/datamodel/low/3.0/response.go index 7c472f0..e5f4185 100644 --- a/datamodel/low/3.0/response.go +++ b/datamodel/low/3.0/response.go @@ -2,6 +2,7 @@ package v3 import ( "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) @@ -15,12 +16,14 @@ type Responses struct { } func (r *Responses) Build(root *yaml.Node) error { - codes, _, _, err := ExtractMapFlat[*Response](ResponsesLabel, root) - if err != nil { - return err - } - if codes != nil { - r.Codes = codes + if utils.IsNodeMap(root) { + codes, err := ExtractMapFlatNoLookup[*Response](root) + if err != nil { + return err + } + if codes != nil { + r.Codes = codes + } } return nil } diff --git a/datamodel/low/3.0/security_scheme.go b/datamodel/low/3.0/security_scheme.go index bf75231..9ac8a25 100644 --- a/datamodel/low/3.0/security_scheme.go +++ b/datamodel/low/3.0/security_scheme.go @@ -2,9 +2,14 @@ package v3 import ( "github.com/pb33f/libopenapi/datamodel/low" + "github.com/pb33f/libopenapi/utils" "gopkg.in/yaml.v3" ) +const ( + SecurityLabel = "security" +) + type SecurityScheme struct { Type low.NodeReference[string] Description low.NodeReference[string] @@ -27,5 +32,89 @@ func (ss *SecurityScheme) Build(root *yaml.Node) error { } type SecurityRequirement struct { - Value low.NodeReference[[]low.ValueReference[string]] + Value []low.ValueReference[map[low.KeyReference[string]][]low.ValueReference[string]] +} + +func (sr *SecurityRequirement) FindRequirement(name string) []low.ValueReference[string] { + for _, r := range sr.Value { + for k, v := range r.Value { + if k.Value == name { + return v + } + } + } + return nil +} + +func (sr *SecurityRequirement) Build(root *yaml.Node) error { + + //if utils.IsNodeArray(root) { + // var currSec *yaml.Node + // var requirements []low.ValueReference[map[low.KeyReference[string]][]low.ValueReference[string]] + // for i, n := range root.Content { + // if i%2 == 0 { + // currSec = n + // continue + // } + // if utils.IsNodeArray(n) { + // res := make(map[low.KeyReference[string]][]low.ValueReference[string]) + // var dat []low.ValueReference[string] + // for _, r := range n.Content { + // dat = append(dat, low.ValueReference[string]{ + // Value: r.Value, + // ValueNode: r, + // }) + // } + // res[low.KeyReference[string]{ + // Value: currSec.Value, + // KeyNode: currSec, + // }] = dat + // requirements = append(requirements, low.ValueReference[map[low.KeyReference[string]][]low.ValueReference[string]]{ + // Value: res, + // ValueNode: n, + // }) + // } + // } + // sr.Value = requirements + //} + + if utils.IsNodeArray(root) { + + var requirements []low.ValueReference[map[low.KeyReference[string]][]low.ValueReference[string]] + for _, n := range root.Content { + var currSec *yaml.Node + if utils.IsNodeMap(n) { + res := make(map[low.KeyReference[string]][]low.ValueReference[string]) + var dat []low.ValueReference[string] + for i, r := range n.Content { + if i%2 == 0 { + currSec = r + continue + } + if utils.IsNodeArray(r) { + // value (should be) an array of strings + var keyValues []low.ValueReference[string] + for _, strN := range r.Content { + keyValues = append(keyValues, low.ValueReference[string]{ + Value: strN.Value, + ValueNode: strN, + }) + } + dat = keyValues + } + } + res[low.KeyReference[string]{ + Value: currSec.Value, + KeyNode: currSec, + }] = dat + requirements = append(requirements, low.ValueReference[map[low.KeyReference[string]][]low.ValueReference[string]]{ + Value: res, + ValueNode: n, + }) + } + } + sr.Value = requirements + } + + return nil } diff --git a/index/spec_index_test.go b/index/spec_index_test.go index 0fecbf3..9a5bff6 100644 --- a/index/spec_index_test.go +++ b/index/spec_index_test.go @@ -221,7 +221,7 @@ func TestSpecIndex_BurgerShop(t *testing.T) { assert.NotNil(t, index.GetDiscoveredReferences()) assert.Equal(t, 0, len(index.GetPolyReferences())) assert.NotNil(t, index.GetOperationParameterReferences()) - assert.Equal(t, 0, len(index.GetAllSecuritySchemes())) + assert.Equal(t, 3, len(index.GetAllSecuritySchemes())) assert.Equal(t, 2, len(index.GetAllParameters())) assert.Equal(t, 0, len(index.GetAllResponses())) assert.Equal(t, 2, len(index.GetInlineOperationDuplicateParameters())) diff --git a/openapi/create_document.go b/openapi/create_document.go index dad34ec..3040b67 100644 --- a/openapi/create_document.go +++ b/openapi/create_document.go @@ -72,7 +72,7 @@ func extractServers(info *datamodel.SpecInfo, doc *v3.Document) error { _, ln, vn := utils.FindKeyNodeFull(v3.ServersLabel, info.RootNode.Content) if vn != nil { if utils.IsNodeArray(vn) { - var servers []low.NodeReference[*v3.Server] + var servers []low.ValueReference[*v3.Server] for _, srvN := range vn.Content { if utils.IsNodeMap(srvN) { srvr := v3.Server{} @@ -81,14 +81,17 @@ func extractServers(info *datamodel.SpecInfo, doc *v3.Document) error { return err } srvr.Build(srvN) - servers = append(servers, low.NodeReference[*v3.Server]{ + servers = append(servers, low.ValueReference[*v3.Server]{ Value: &srvr, ValueNode: srvN, - KeyNode: ln, }) } } - doc.Servers = servers + doc.Servers = low.NodeReference[[]low.ValueReference[*v3.Server]]{ + Value: servers, + KeyNode: ln, + ValueNode: vn, + } } } return nil diff --git a/openapi/create_document_test.go b/openapi/create_document_test.go index b91afe4..bdfe217 100644 --- a/openapi/create_document_test.go +++ b/openapi/create_document_test.go @@ -40,26 +40,26 @@ func TestCreateDocument_Info(t *testing.T) { } 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.Value, 2) + server1 := doc.Servers.Value[0].Value + server2 := doc.Servers.Value[1].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) + assert.Equal(t, "{scheme}://api.pb33f.io", server1.URL.Value) + assert.NotEmpty(t, server1.Description.Value) + assert.Len(t, server1.Variables.Value, 1) + assert.Len(t, server1.Variables.Value["scheme"].Value.Enum, 2) + assert.Equal(t, server1.Variables.Value["scheme"].Value.Default.Value, "https") + assert.NotEmpty(t, server1.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, "https://{domain}.{host}.com", server2.URL.Value) + assert.NotEmpty(t, server2.Description.Value) + assert.Len(t, server2.Variables.Value, 2) + assert.Equal(t, server2.Variables.Value["domain"].Value.Default.Value, "api") + assert.NotEmpty(t, server2.Variables.Value["domain"].Value.Description.Value) + assert.NotEmpty(t, server2.Variables.Value["host"].Value.Description.Value) + assert.Equal(t, server2.Variables.Value["host"].Value.Default.Value, "pb33f.io") assert.Equal(t, "1.2", doc.Info.Value.Version.Value) } @@ -220,4 +220,18 @@ func TestCreateDocument_Paths(t *testing.T) { assert.NotNil(t, burgerIdParam) assert.Equal(t, "$response.body#/id", burgerIdParam.Value) + // check security requirements + security := burgersPost.Security.Value + assert.NotNil(t, security) + assert.Len(t, security.Value, 1) + + oAuthReq := security.FindRequirement("OAuthScheme") + assert.Len(t, oAuthReq, 2) + assert.Equal(t, "read:burgers", oAuthReq[0].Value) + + servers := burgersPost.Servers.Value + assert.NotNil(t, servers) + assert.Len(t, servers, 1) + assert.Equal(t, "https://pb33f.io", servers[0].Value.URL.Value) + } diff --git a/test_specs/burgershop.openapi.yaml b/test_specs/burgershop.openapi.yaml index d857915..0c43708 100644 --- a/test_specs/burgershop.openapi.yaml +++ b/test_specs/burgershop.openapi.yaml @@ -138,6 +138,13 @@ paths: summary: invalid request value: message: unable to accept this request, looks bad, missing something. + security: + - OAuthScheme: + - read:burgers + - write:burgers + servers: + - url: https://pb33f.io + description: this is an alternative server for this operation. /burgers/{burgerId}: get: callbacks: @@ -324,6 +331,34 @@ paths: example: message: "failed looking up all dressings, something went wrong." components: + securitySchemes: + APIKeyScheme: + type: apiKey + description: an apiKey security scheme + name: apiKeyScheme + in: query + JWTScheme: + type: http + description: an JWT security scheme + name: aJWTThing + scheme: bearer + bearerFormat: JWT + OAuthScheme: + type: oauth2 + description: an oAuth security scheme + name: oAuthy + flows: + implicit: + authorizationUrl: https://pb33f.io/oauth + scopes: + write:burgers: modify and add new burgers + read:burgers: read all burgers + authorizationCode: + authorizationUrl: https://pb33f.io/oauth + tokenUrl: https://api.pb33f.io/oauth/token + scopes: + write:burgers: modify burgers and stuff + read:burgers: read all the burgers parameters: BurgerHeader: in: header