diff --git a/go.mod b/go.mod index 7592ed1de..c1dd37bc8 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.9.0 github.com/subosito/gotenv v1.4.0 + github.com/swaggest/go-asyncapi v0.8.0 github.com/tidwall/gjson v1.14.0 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 golang.org/x/net v0.24.0 diff --git a/go.sum b/go.sum index af92a6ed0..f7f69a1bf 100644 --- a/go.sum +++ b/go.sum @@ -371,6 +371,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= +github.com/swaggest/go-asyncapi v0.8.0 h1:aze7YL3o/4fkxx9ZL8ubKdyYCqFJy+9+JHpwLyF10H4= +github.com/swaggest/go-asyncapi v0.8.0/go.mod h1:s1urrZuYPcNPJrRUU/kF1eOnIBU02O4JfXCDS09wONI= github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= diff --git a/pkg/apic/asyncapi.go b/pkg/apic/asyncapi.go new file mode 100644 index 000000000..e6de79de2 --- /dev/null +++ b/pkg/apic/asyncapi.go @@ -0,0 +1,82 @@ +package apic + +import ( + "fmt" + "net/url" + "strconv" + "strings" + + management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1alpha1" + asyncSpec "github.com/swaggest/go-asyncapi/spec-2.4.0" +) + +const ( + protocol = "protocol" +) + +// custom parse URL to allow SASL_* schemes(url.Parse throws error) +func parseURL(strURL string) (scheme, host string, port int64, err error) { + urlElements := strings.Split(strURL, "://") + remaining := strURL + if len(urlElements) > 1 { + scheme = urlElements[0] + remaining = urlElements[1] + } + + strURL = fmt.Sprintf("tmp://%s", remaining) + u, e := url.Parse(strURL) + if e != nil { + err = e + return + } + + host = u.Hostname() + port, _ = strconv.ParseInt(u.Port(), 10, 32) + return +} + +type asyncApi struct { + spec *asyncSpec.AsyncAPI + raw []byte +} + +func (a *asyncApi) GetResourceType() string { + return AsyncAPI +} + +func (a *asyncApi) GetID() string { + return a.spec.ID +} + +func (a *asyncApi) GetTitle() string { + return a.spec.Info.Title +} + +func (a *asyncApi) GetVersion() string { + return a.spec.Info.Version +} + +func (a *asyncApi) GetEndpoints() ([]management.ApiServiceInstanceSpecEndpoint, error) { + endpoints := make([]management.ApiServiceInstanceSpecEndpoint, 0) + for _, server := range a.spec.Servers { + scheme, host, port, err := parseURL(server.Server.URL) + if err != nil { + return nil, err + } + endpoints = append(endpoints, management.ApiServiceInstanceSpecEndpoint{ + Host: host, + Protocol: scheme, + Port: int32(port), + Routing: management.ApiServiceInstanceSpecRouting{ + Details: map[string]interface{}{ + protocol: server.Server.Protocol, + }, + }, + }) + } + return endpoints, nil +} + +func (a *asyncApi) GetSpecBytes() []byte { + return a.raw +} diff --git a/pkg/apic/asyncapi_test.go b/pkg/apic/asyncapi_test.go new file mode 100644 index 000000000..5d8662041 --- /dev/null +++ b/pkg/apic/asyncapi_test.go @@ -0,0 +1,385 @@ +package apic + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type asyncServer struct { + name string + description string + url string + protocol string + protocolVersion string + useSaslScram bool + useSaslPlain bool +} + +type asyncChannel struct { + name string + publish bool + subscribe bool + publishMessage string + subscribeMessage string +} + +type asyncMessage struct { + name string + format string + contentType string + payload map[string]interface{} +} + +func TestAsyncAPIGenerator(t *testing.T) { + tests := map[string]struct { + expectBuilder bool + id string + title string + version string + servers []asyncServer + messages []asyncMessage + channels []asyncChannel + }{ + "no id in info": { + expectBuilder: true, + }, + "invalid id in info": { + id: "test", + expectBuilder: true, + }, + "no title in info": { + id: "kafka://test", + expectBuilder: true, + }, + "no version in info": { + id: "kafka://test", + title: "test", + expectBuilder: true, + }, + "no servers": { + id: "kafka://test", + title: "test", + version: "1.0.0", + expectBuilder: true, + }, + "servers with no url": { + id: "kafka://test", + title: "test", + version: "1.0.0", + servers: []asyncServer{ + { + name: "test", + description: "test", + url: "", + }, + }, + expectBuilder: true, + }, + "servers with no protocol": { + id: "kafka://test", + title: "test", + version: "1.0.0", + servers: []asyncServer{ + { + name: "test", + description: "test", + url: "PLAINTEXT://localhost", + }, + }, + expectBuilder: true, + }, + "servers with no protocol version": { + id: "kafka://test", + title: "test", + version: "1.0.0", + servers: []asyncServer{ + { + name: "test", + description: "test", + url: "PLAINTEXT://localhost", + protocol: "kafka", + }, + }, + expectBuilder: true, + }, + "no channels": { + id: "kafka://test", + title: "test", + version: "1.0.0", + servers: []asyncServer{ + { + name: "test", + description: "test", + url: "PLAINTEXT://localhost", + protocol: "kafka", + protocolVersion: "1.0.0", + }, + }, + expectBuilder: true, + }, + "servers with invalid component message message ref": { + id: "kafka://test", + title: "test", + version: "1.0.0", + servers: []asyncServer{ + { + name: "test", + description: "test", + url: "PLAINTEXT://localhost", + protocol: "kafka", + protocolVersion: "1.0.0", + }, + }, + channels: []asyncChannel{ + { + name: "test", + publish: true, + subscribe: true, + publishMessage: "", + }, + }, + expectBuilder: true, + }, + "servers with no component message for publish message ref": { + id: "kafka://test", + title: "test", + version: "1.0.0", + servers: []asyncServer{ + { + name: "test", + description: "test", + url: "PLAINTEXT://localhost", + protocol: "kafka", + protocolVersion: "1.0.0", + }, + }, + channels: []asyncChannel{ + { + name: "test", + publish: true, + subscribe: true, + publishMessage: "order", + subscribeMessage: "order", + }, + }, + expectBuilder: true, + }, + "servers with no component message for subscribe message ref": { + id: "kafka://test", + title: "test", + version: "1.0.0", + servers: []asyncServer{ + { + name: "test", + description: "test", + url: "PLAINTEXT://localhost", + protocol: "kafka", + protocolVersion: "1.0.0", + }, + }, + channels: []asyncChannel{ + { + name: "test", + publish: true, + subscribe: true, + publishMessage: "order-pub", + subscribeMessage: "order-sub", + }, + }, + messages: []asyncMessage{ + { + name: "order-pub", + format: "json", + payload: map[string]interface{}{"schema": map[string]interface{}{}}, + }, + }, + expectBuilder: true, + }, + "invalid component message name": { + id: "kafka://test", + title: "test", + version: "1.0.0", + servers: []asyncServer{ + { + name: "test", + description: "test", + url: "PLAINTEXT://localhost", + protocol: "kafka", + protocolVersion: "1.0.0", + }, + }, + channels: []asyncChannel{ + { + name: "test", + publish: true, + subscribe: true, + publishMessage: "order-pub", + subscribeMessage: "order-sub", + }, + }, + messages: []asyncMessage{ + { + name: "order-pub", + format: "json", + payload: map[string]interface{}{ + "schema": map[string]interface{}{}, + }, + }, + { + name: "order-sub", + format: "json", + payload: map[string]interface{}{ + "schema": map[string]interface{}{}, + }, + }, + { + name: "", + format: "json", + payload: map[string]interface{}{ + "schema": map[string]interface{}{}, + }, + }, + }, + expectBuilder: true, + }, + "invalid component message payload": { + id: "kafka://test", + title: "test", + version: "1.0.0", + servers: []asyncServer{ + { + name: "test", + description: "test", + url: "PLAINTEXT://localhost", + protocol: "kafka", + protocolVersion: "1.0.0", + }, + }, + channels: []asyncChannel{ + { + name: "test", + publish: true, + subscribe: true, + publishMessage: "order-pub", + subscribeMessage: "order-sub", + }, + }, + messages: []asyncMessage{ + { + name: "order-pub", + format: "json", + payload: map[string]interface{}{ + "schema": map[string]interface{}{}, + }, + }, + { + name: "order-sub", + format: "json", + payload: map[string]interface{}{ + "schema": map[string]interface{}{}, + }, + }, + { + name: "test", + format: "json", + }, + }, + expectBuilder: true, + }, + "valid async api spec": { + id: "kafka://test", + title: "test", + version: "1.0.0", + servers: []asyncServer{ + { + name: "test-plain", + description: "test", + url: "SASL_SSL://localhost:9092", + protocol: "kafka", + protocolVersion: "1.0.0", + useSaslScram: false, + useSaslPlain: true, + }, + }, + channels: []asyncChannel{ + { + name: "test", + publish: true, + subscribe: true, + publishMessage: "order", + subscribeMessage: "order", + }, + }, + messages: []asyncMessage{ + { + name: "order", + format: "application/vnd.oai.openapi+json;version=3.0.0", + contentType: "application/json", + payload: map[string]interface{}{ + "schema": map[string]interface{}{}, + }, + }, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + builder := CreateAsyncAPIBuilder() + for _, serverDetail := range tt.servers { + opts := make([]AsyncAPIServerOpts, 0) + if serverDetail.useSaslPlain { + opts = append(opts, WithSaslPlainSecurity("SASL_PLAIN")) + } + if serverDetail.useSaslScram { + opts = append(opts, WithSaslScramSecurity("SCRAM-SHA-512", "SASL_PLAINTEXT")) + } + opts = append(opts, WithProtocol(serverDetail.protocol, serverDetail.protocolVersion)) + builder.AddServer(serverDetail.name, serverDetail.description, serverDetail.url, opts...) + } + + for _, msg := range tt.messages { + builder.AddComponentMessage(msg.name, msg.format, msg.contentType, msg.payload) + } + for _, channel := range tt.channels { + opts := make([]asyncAPIChannelOpts, 0) + if channel.publish { + opts = append(opts, WithKafkaPublishOperationBinding(true, true)) + } + if channel.subscribe { + opts = append(opts, WithKafkaSubscribeOperationBinding(true, true)) + } + builder.AddChannel(channel.name, channel.name, opts...) + builder.SetPublishMessageRef(channel.name, channel.publishMessage) + builder.SetSubscribeMessageRef(channel.name, channel.subscribeMessage) + } + + spec, err := builder.Build(tt.id, tt.title, tt.title, tt.version) + if tt.expectBuilder { + assert.NotNil(t, err) + assert.Nil(t, spec) + return + } + assert.Nil(t, err) + assert.NotNil(t, spec) + + assert.Equal(t, "asyncapi", spec.GetResourceType()) + assert.Equal(t, "kafka://test", spec.GetID()) + assert.Equal(t, "test", spec.GetTitle()) + assert.Equal(t, "1.0.0", spec.GetVersion()) + + raw := spec.GetSpecBytes() + assert.NotEmpty(t, raw) + + endpoints, err := spec.GetEndpoints() + assert.Nil(t, err) + assert.NotEmpty(t, endpoints) + + assert.Equal(t, "localhost", endpoints[0].Host) + assert.Equal(t, int32(9092), endpoints[0].Port) + assert.Equal(t, "SASL_SSL", endpoints[0].Protocol) + assert.Equal(t, "kafka", endpoints[0].Routing.Details[protocol].(string)) + }) + } +} diff --git a/pkg/apic/asyncapibuilder.go b/pkg/apic/asyncapibuilder.go new file mode 100644 index 000000000..d1f1bb7ed --- /dev/null +++ b/pkg/apic/asyncapibuilder.go @@ -0,0 +1,349 @@ +package apic + +import ( + "fmt" + "net/url" + + "github.com/Axway/agent-sdk/pkg/util/exception" + "github.com/swaggest/go-asyncapi/spec-2.4.0" +) + +const ( + componentMessageRefTemplate = "#/components/messages/%s" +) + +type asyncAPIChannelOpts func(channel *spec.ChannelItem) + +func WithKafkaPublishOperationBinding(useGroupID, useClientID bool) asyncAPIChannelOpts { + return func(c *spec.ChannelItem) { + kafkaOp := &spec.KafkaOperation{} + if useGroupID { + kafkaOp.GroupID = &spec.KafkaOperationGroupID{ + Schema: map[string]interface{}{ + "type": "string", + }, + } + } + if useClientID { + kafkaOp.ClientID = &spec.KafkaOperationClientID{ + Schema: map[string]interface{}{ + "type": "string", + }, + } + } + c.Publish = &spec.Operation{ + Bindings: &spec.OperationBindingsObject{Kafka: kafkaOp}, + } + } +} + +func WithKafkaSubscribeOperationBinding(useGroupID, useClientID bool) asyncAPIChannelOpts { + return func(c *spec.ChannelItem) { + kafkaOp := &spec.KafkaOperation{} + if useGroupID { + kafkaOp.GroupID = &spec.KafkaOperationGroupID{ + Schema: map[string]interface{}{ + "type": "string", + }, + } + } + if useClientID { + kafkaOp.ClientID = &spec.KafkaOperationClientID{ + Schema: map[string]interface{}{ + "type": "string", + }, + } + } + c.Subscribe = &spec.Operation{ + Bindings: &spec.OperationBindingsObject{Kafka: kafkaOp}, + } + } +} + +// TBD ? +// func WithKafkaPublishBinding() asyncAPIChannelOpts {} + +type AsyncAPIServerOpts func(*asyncAPIBuilder, *spec.Server) + +func WithSaslPlainSecurity(description string) AsyncAPIServerOpts { + return func(b *asyncAPIBuilder, s *spec.Server) { + if s.Security == nil { + s.Security = make([]map[string][]string, 0) + } + s.Security = append(s.Security, map[string][]string{ + "saslPlainCreds": {}, + }) + b.securitySchemas["saslPlainCreds"] = &spec.SecurityScheme{ + SaslSecurityScheme: &spec.SaslSecurityScheme{ + SaslPlainSecurityScheme: &spec.SaslPlainSecurityScheme{ + Description: description, + }, + }, + } + } +} + +func WithSaslScramSecurity(scramMechanism, description string) AsyncAPIServerOpts { + return func(b *asyncAPIBuilder, s *spec.Server) { + if s.Security == nil { + s.Security = make([]map[string][]string, 0) + } + s.Security = append(s.Security, map[string][]string{ + "saslScramCreds": {}, + }) + scramType := spec.SaslScramSecuritySchemeTypeScramSha256 + if scramMechanism == "SCRAM-SHA-512" { + scramType = spec.SaslScramSecuritySchemeTypeScramSha512 + } + b.securitySchemas["saslScramCreds"] = &spec.SecurityScheme{ + SaslSecurityScheme: &spec.SaslSecurityScheme{ + SaslScramSecurityScheme: &spec.SaslScramSecurityScheme{ + Type: scramType, + Description: description, + }, + }, + } + } +} + +func WithProtocol(protocol, protocolVersion string) AsyncAPIServerOpts { + return func(b *asyncAPIBuilder, s *spec.Server) { + s.Protocol = protocol + s.ProtocolVersion = protocolVersion + } +} + +type AsyncAPIBuilder interface { + AddServer(name, description, url string, opts ...AsyncAPIServerOpts) AsyncAPIBuilder + AddChannel(name, description string, opts ...asyncAPIChannelOpts) AsyncAPIBuilder + SetPublishMessageRef(channelName, componentMessage string) AsyncAPIBuilder + SetSubscribeMessageRef(channelName, componentMessage string) AsyncAPIBuilder + AddComponentMessage(msgName, schemaFormat, contentType string, payload map[string]interface{}) AsyncAPIBuilder + Build(id, title, description, version string) (AsyncSpecProcessor, error) +} + +type asyncAPIBuilder struct { + servers map[string]spec.Server + channels map[string]spec.ChannelItem + channelPublishMessageRef map[string]string + channelSubscribeMessageRef map[string]string + componentMessages map[string]spec.MessageEntity + securitySchemas map[string]*spec.SecurityScheme +} + +func CreateAsyncAPIBuilder() AsyncAPIBuilder { + return &asyncAPIBuilder{ + servers: make(map[string]spec.Server), + channels: make(map[string]spec.ChannelItem), + channelPublishMessageRef: make(map[string]string), + channelSubscribeMessageRef: make(map[string]string), + componentMessages: make(map[string]spec.MessageEntity), + securitySchemas: make(map[string]*spec.SecurityScheme), + } +} + +func (b *asyncAPIBuilder) AddServer(name, description, url string, opts ...AsyncAPIServerOpts) AsyncAPIBuilder { + server := spec.Server{ + URL: url, + Description: description, + } + for _, o := range opts { + o(b, &server) + } + b.servers[name] = server + return b +} + +func (b *asyncAPIBuilder) AddChannel(name, description string, opts ...asyncAPIChannelOpts) AsyncAPIBuilder { + channel := spec.ChannelItem{ + Description: description, + } + + for _, o := range opts { + o(&channel) + } + + b.channels[name] = channel + return b +} + +func setChannelMessageRef(m map[string]string, channelName, messageSubject string) { + m[channelName] = messageSubject +} + +func (b *asyncAPIBuilder) SetPublishMessageRef(channelName, componentMessage string) AsyncAPIBuilder { + setChannelMessageRef(b.channelPublishMessageRef, channelName, componentMessage) + return b +} + +func (b *asyncAPIBuilder) SetSubscribeMessageRef(channelName, componentMessage string) AsyncAPIBuilder { + setChannelMessageRef(b.channelSubscribeMessageRef, channelName, componentMessage) + return b +} + +func (b *asyncAPIBuilder) AddComponentMessage(msgName, schemaFormat, contentType string, payload map[string]interface{}) AsyncAPIBuilder { + msg := spec.MessageEntity{ + Payload: payload, + ContentType: contentType, + SchemaFormat: schemaFormat, + } + + b.componentMessages[msgName] = msg + return b +} + +func (b *asyncAPIBuilder) validateInfo(id, title, description, version string) { + if id == "" { + exception.Throw(fmt.Errorf("no identifier defined")) + } + u, _ := url.Parse(id) + if u.Scheme == "" { + exception.Throw(fmt.Errorf("invalid api id, should be uri format")) + } + + if title == "" { + exception.Throw(fmt.Errorf("no title defined")) + } + if version == "" { + exception.Throw(fmt.Errorf("no version defined")) + } +} + +func (b *asyncAPIBuilder) validateServers() { + if len(b.servers) == 0 { + exception.Throw(fmt.Errorf("no server defined")) + } + for _, s := range b.servers { + if s.URL == "" { + exception.Throw(fmt.Errorf("invalid server URL")) + } + if s.Protocol == "" { + exception.Throw(fmt.Errorf("invalid server protocol")) + } + if s.ProtocolVersion == "" { + exception.Throw(fmt.Errorf("invalid server protocol version")) + } + } + +} +func validateMessageRef(channelOp string, channelMessageRef map[string]string, componentMessages map[string]spec.MessageEntity) { + for channel, message := range channelMessageRef { + if message == "" { + exception.Throw(fmt.Errorf("invalid message reference for %s operation in channel %s", channelOp, channel)) + } + if _, ok := componentMessages[message]; !ok { + exception.Throw(fmt.Errorf("invalid message reference %s for %s operation in channel %s", message, channelOp, channel)) + } + } +} + +func (b *asyncAPIBuilder) validateChannels() { + if len(b.channels) == 0 { + exception.Throw(fmt.Errorf("no channels defined")) + } + validateMessageRef("publish", b.channelPublishMessageRef, b.componentMessages) + validateMessageRef("subscribe", b.channelSubscribeMessageRef, b.componentMessages) +} + +func (b *asyncAPIBuilder) validateComponents() { + for msgName, msg := range b.componentMessages { + if msgName == "" { + exception.Throw(fmt.Errorf("invalid message name")) + } + if len(msg.Payload) == 0 { + exception.Throw(fmt.Errorf("invalid message schema")) + } + } +} + +func (b *asyncAPIBuilder) validate(id, title, description, version string) (err error) { + exception.Block{ + Try: func() { + b.validateInfo(id, title, description, version) + b.validateServers() + b.validateChannels() + b.validateComponents() + }, + Catch: func(e error) { + err = e + }, + }.Do() + + return +} + +func (b *asyncAPIBuilder) buildServers(api *spec.AsyncAPI) { + for name, server := range b.servers { + api.AddServer(name, server) + } +} + +func (b *asyncAPIBuilder) buildChannels(api *spec.AsyncAPI) { + for name, publishMsgRef := range b.channelPublishMessageRef { + ch := b.channels[name] + ch.Publish.Message = &spec.Message{ + Reference: &spec.Reference{ + Ref: fmt.Sprintf(componentMessageRefTemplate, publishMsgRef), + }, + } + } + for name, subscribeMsgRef := range b.channelSubscribeMessageRef { + ch := b.channels[name] + ch.Subscribe.Message = &spec.Message{ + Reference: &spec.Reference{ + Ref: fmt.Sprintf(componentMessageRefTemplate, subscribeMsgRef), + }, + } + } + api.WithChannels(b.channels) +} + +func (b *asyncAPIBuilder) buildComponents(api *spec.AsyncAPI) { + components := spec.Components{} + components.Messages = make(map[string]spec.Message) + for msgName, msg := range b.componentMessages { + components.Messages[msgName] = spec.Message{ + OneOf1: &spec.MessageOneOf1{ + MessageEntity: &msg, + }, + } + } + if len(b.securitySchemas) > 0 { + components.SecuritySchemes = &spec.ComponentsSecuritySchemes{ + MapOfComponentsSecuritySchemesWDValues: make(map[string]spec.ComponentsSecuritySchemesWD), + } + for name, securitySchema := range b.securitySchemas { + components.SecuritySchemes.MapOfComponentsSecuritySchemesWDValues[name] = spec.ComponentsSecuritySchemesWD{ + SecurityScheme: securitySchema, + } + } + } + + api.WithComponents(components) +} + +func (b *asyncAPIBuilder) Build(id, title, description, version string) (AsyncSpecProcessor, error) { + err := b.validate(id, title, description, version) + if err != nil { + return nil, err + } + + api := &spec.AsyncAPI{ + ID: id, + Info: spec.Info{ + Title: title, + Description: description, + Version: version, + }, + } + b.buildServers(api) + b.buildChannels(api) + b.buildComponents(api) + + raw, err := api.MarshalYAML() + if err != nil { + return nil, err + } + + return &asyncApi{spec: api, raw: raw}, nil +} diff --git a/pkg/apic/specparser.go b/pkg/apic/specparser.go index 4d522c7b1..82cdf90fd 100644 --- a/pkg/apic/specparser.go +++ b/pkg/apic/specparser.go @@ -6,6 +6,7 @@ import ( "errors" "strings" + management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1alpha1" "github.com/Axway/agent-sdk/pkg/util" "github.com/Axway/agent-sdk/pkg/util/oas" @@ -28,6 +29,15 @@ type SpecProcessor interface { GetResourceType() string } +type AsyncSpecProcessor interface { + GetID() string + GetTitle() string + GetVersion() string + GetEndpoints() ([]management.ApiServiceInstanceSpecEndpoint, error) + GetResourceType() string + GetSpecBytes() []byte +} + // OasSpecProcessor - type OasSpecProcessor interface { ParseAuthInfo()