diff --git a/datamodel/high/node_builder.go b/datamodel/high/node_builder.go index f8d1c5c..158f603 100644 --- a/datamodel/high/node_builder.go +++ b/datamodel/high/node_builder.go @@ -148,9 +148,13 @@ func (n *NodeBuilder) add(key string) { value = reflect.ValueOf(fLow) switch value.Kind() { case reflect.Struct: - nb := value.Interface().(low.HasValueNodeUntyped).GetValueNode() - if nb != nil { - nodeEntry.Line = nb.Line + y := value.Interface() + if nb, ok := y.(low.HasValueNodeUntyped); ok { + if nb.GetValueNode() != nil { + nodeEntry.Line = nb.GetValueNode().Line + } + } else { + panic("not supported yet") } default: // everything else, weight it to the bottom of the rendered object. @@ -265,10 +269,12 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, tag, key string, value any) // extract low level key line number if pr, ok := gh.(low.HasValueUnTyped); ok { fg := reflect.ValueOf(pr.GetValueUntyped()) + found := false for _, ky := range fg.MapKeys() { if we, wok := ky.Interface().(low.HasKeyNode); wok { er := we.GetKeyNode().Value if er == x { + found = true orderedCollection = append(orderedCollection, &NodeEntry{ Tag: x, Key: x, @@ -278,6 +284,7 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, tag, key string, value any) } } else { // this is a map, without any low level details available + found = true orderedCollection = append(orderedCollection, &NodeEntry{ Tag: x, Key: x, @@ -286,20 +293,38 @@ func (n *NodeBuilder) AddYAMLNode(parent *yaml.Node, tag, key string, value any) }) } } + if found != true { + // this is something new, add it. + orderedCollection = append(orderedCollection, &NodeEntry{ + Tag: x, + Key: x, + Line: 9999, + Value: m.MapIndex(k).Interface(), + }) + } } else { - // this is a map, without an enclosing struct + // this is a map, but it may be wrapped still. bj := reflect.ValueOf(gh) - for _, ky := range bj.MapKeys() { - er := ky.Interface().(low.HasKeyNode).GetKeyNode().Value - if er == x { - orderedCollection = append(orderedCollection, &NodeEntry{ - Tag: x, - Key: x, - Line: ky.Interface().(low.HasKeyNode).GetKeyNode().Line, - Value: m.MapIndex(k).Interface(), - }) + yh := bj.Interface() + calc := func(iu reflect.Value) { + for _, ky := range iu.MapKeys() { + er := ky.Interface().(low.HasKeyNode).GetKeyNode().Value + if er == x { + orderedCollection = append(orderedCollection, &NodeEntry{ + Tag: x, + Key: x, + Line: ky.Interface().(low.HasKeyNode).GetKeyNode().Line, + Value: m.MapIndex(k).Interface(), + }) + } } } + if vg, jo := yh.(low.HasKeyNode); jo { + fv := reflect.ValueOf(vg.GetKeyNode()) + calc(fv) + } else { + calc(bj) + } } } else { // this is a map, without any low level details available (probably an extension map). diff --git a/datamodel/high/v3/header.go b/datamodel/high/v3/header.go index 7afee6c..4e02a95 100644 --- a/datamodel/high/v3/header.go +++ b/datamodel/high/v3/header.go @@ -13,18 +13,18 @@ import ( // Header represents a high-level OpenAPI 3+ Header object that is backed by a low-level one. // - https://spec.openapis.org/oas/v3.1.0#header-object type Header struct { - Description string - Required bool - Deprecated bool - AllowEmptyValue bool - Style string - Explode bool - AllowReserved bool - Schema *highbase.SchemaProxy - Example any - Examples map[string]*highbase.Example - Content map[string]*MediaType - Extensions map[string]any + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + Schema *highbase.SchemaProxy `json:"schema,omitempty" yaml:"schema,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` + Examples map[string]*highbase.Example `json:"examples,omitempty" yaml:"examples,omitempty"` + Content map[string]*MediaType `json:"content,omitempty" yaml:"content,omitempty"` + Extensions map[string]any `json:"-" yaml:"-"` low *low.Header } diff --git a/datamodel/high/v3/oauth_flows.go b/datamodel/high/v3/oauth_flows.go index 64c3e46..5d0f5d7 100644 --- a/datamodel/high/v3/oauth_flows.go +++ b/datamodel/high/v3/oauth_flows.go @@ -6,16 +6,17 @@ package v3 import ( "github.com/pb33f/libopenapi/datamodel/high" low "github.com/pb33f/libopenapi/datamodel/low/v3" + "gopkg.in/yaml.v3" ) // OAuthFlows represents a high-level OpenAPI 3+ OAuthFlows object that is backed by a low-level one. // - https://spec.openapis.org/oas/v3.1.0#oauth-flows-object type OAuthFlows struct { - Implicit *OAuthFlow - Password *OAuthFlow - ClientCredentials *OAuthFlow - AuthorizationCode *OAuthFlow - Extensions map[string]any + Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` + Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` + ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` + AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` + Extensions map[string]any `json:"-" yaml:"-"` low *low.OAuthFlows } @@ -46,3 +47,18 @@ func NewOAuthFlows(flows *low.OAuthFlows) *OAuthFlows { func (o *OAuthFlows) GoLow() *low.OAuthFlows { return o.low } + +// Render will return a YAML representation of the OAuthFlows object as a byte slice. +func (o *OAuthFlows) Render() ([]byte, error) { + return yaml.Marshal(o) +} + +// MarshalYAML will create a ready to render YAML representation of the OAuthFlows object. +func (o *OAuthFlows) MarshalYAML() (interface{}, error) { + if o == nil { + return nil, nil + } + nb := high.NewNodeBuilder(o, o.low) + return nb.Render(), nil +} + diff --git a/datamodel/high/v3/oauth_flows_test.go b/datamodel/high/v3/oauth_flows_test.go index 074cb29..5e0e2db 100644 --- a/datamodel/high/v3/oauth_flows_test.go +++ b/datamodel/high/v3/oauth_flows_test.go @@ -9,32 +9,33 @@ import ( "github.com/pb33f/libopenapi/index" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" + "strings" "testing" ) func TestNewOAuthFlows(t *testing.T) { yml := `implicit: - authorizationUrl: https://pb33f.io/oauth - scopes: - write:burgers: modify and add new burgers - read:burgers: read all burgers + authorizationUrl: https://pb33f.io/oauth/implicit + scopes: + write:burgers: modify and add new burgers implicitly + 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 + authorizationUrl: https://pb33f.io/oauth/authCode + tokenUrl: https://api.pb33f.io/oauth/token + scopes: + write:burgers: modify burgers and stuff with a code + read:burgers: read all the burgers password: - authorizationUrl: https://pb33f.io/oauth - scopes: - write:burgers: modify and add new burgers - read:burgers: read all burgers + authorizationUrl: https://pb33f.io/oauth/password + scopes: + write:burgers: modify and add new burgers with a password + read:burgers: read all burgers clientCredentials: - authorizationUrl: https://pb33f.io/oauth - scopes: - write:burgers: modify burgers and stuff - read:burgers: read all the burgers ` + authorizationUrl: https://pb33f.io/oauth/clientCreds + scopes: + write:burgers: modify burgers and stuff with creds + read:burgers: read all the burgers` var idxNode yaml.Node _ = yaml.Unmarshal([]byte(yml), &idxNode) @@ -52,4 +53,36 @@ clientCredentials: assert.Len(t, r.ClientCredentials.Scopes, 2) assert.Equal(t, 2, r.GoLow().Implicit.Value.AuthorizationUrl.KeyNode.Line) + // now render it back out, and it should be identical! + rBytes, _ := r.Render() + assert.Equal(t, yml, strings.TrimSpace(string(rBytes))) + + modified := `implicit: + authorizationUrl: https://pb33f.io/oauth/implicit + scopes: + write:burgers: modify and add new burgers implicitly + read:burgers: read all burgers +authorizationCode: + authorizationUrl: https://pb33f.io/oauth/authCode + tokenUrl: https://api.pb33f.io/oauth/token + scopes: + write:burgers: modify burgers and stuff with a code + read:burgers: read all the burgers +password: + authorizationUrl: https://pb33f.io/oauth/password + scopes: + write:burgers: modify and add new burgers with a password + read:burgers: read all burgers +clientCredentials: + authorizationUrl: https://pb33f.io/oauth/clientCreds + scopes: + write:burgers: modify burgers and stuff with creds + read:burgers: read all the burgers + CHIP:CHOP: microwave a sock` + + // now modify it and render it back out, and it should be identical! + r.ClientCredentials.Scopes["CHIP:CHOP"] = "microwave a sock" + rBytes, _ = r.Render() + assert.Equal(t, modified, strings.TrimSpace(string(rBytes))) + } diff --git a/datamodel/low/model_builder.go b/datamodel/low/model_builder.go index a6fced8..e797194 100644 --- a/datamodel/low/model_builder.go +++ b/datamodel/low/model_builder.go @@ -386,7 +386,32 @@ func SetField(field *reflect.Value, valueNode *yaml.Node, keyNode *yaml.Node) er field.Set(reflect.ValueOf(ref)) } } - + case reflect.TypeOf(NodeReference[map[KeyReference[string]]ValueReference[string]]{}): + if utils.IsNodeMap(valueNode) { + if field.CanSet() { + items := make(map[KeyReference[string]]ValueReference[string]) + var cf *yaml.Node + for i, sliceItem := range valueNode.Content { + if i%2 == 0 { + cf = sliceItem + continue + } + items[KeyReference[string]{ + Value: cf.Value, + KeyNode: cf, + }] = ValueReference[string]{ + Value: sliceItem.Value, + ValueNode: sliceItem, + } + } + ref := NodeReference[map[KeyReference[string]]ValueReference[string]]{ + Value: items, + KeyNode: keyNode, + ValueNode: valueNode, + } + field.Set(reflect.ValueOf(ref)) + } + } case reflect.TypeOf(NodeReference[[]ValueReference[string]]{}): if utils.IsNodeArray(valueNode) { diff --git a/datamodel/low/v3/oauth_flows.go b/datamodel/low/v3/oauth_flows.go index a2196e1..e91b5d3 100644 --- a/datamodel/low/v3/oauth_flows.go +++ b/datamodel/low/v3/oauth_flows.go @@ -90,7 +90,7 @@ type OAuthFlow struct { AuthorizationUrl low.NodeReference[string] TokenUrl low.NodeReference[string] RefreshUrl low.NodeReference[string] - Scopes low.KeyReference[map[low.KeyReference[string]]low.ValueReference[string]] + Scopes low.NodeReference[map[low.KeyReference[string]]low.ValueReference[string]] Extensions map[low.KeyReference[string]]low.ValueReference[any] }