github.com/Axway/agent-sdk@v1.1.101/pkg/apic/asyncapibuilder.go (about)

     1  package apic
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  
     7  	"github.com/Axway/agent-sdk/pkg/util/exception"
     8  	"github.com/swaggest/go-asyncapi/spec-2.4.0"
     9  )
    10  
    11  const (
    12  	componentMessageRefTemplate = "#/components/messages/%s"
    13  )
    14  
    15  type asyncAPIChannelOpts func(channel *spec.ChannelItem)
    16  
    17  func WithKafkaPublishOperationBinding(useGroupID, useClientID bool) asyncAPIChannelOpts {
    18  	return func(c *spec.ChannelItem) {
    19  		kafkaOp := &spec.KafkaOperation{}
    20  		if useGroupID {
    21  			kafkaOp.GroupID = &spec.KafkaOperationGroupID{
    22  				Schema: map[string]interface{}{
    23  					"type": "string",
    24  				},
    25  			}
    26  		}
    27  		if useClientID {
    28  			kafkaOp.ClientID = &spec.KafkaOperationClientID{
    29  				Schema: map[string]interface{}{
    30  					"type": "string",
    31  				},
    32  			}
    33  		}
    34  		c.Publish = &spec.Operation{
    35  			Bindings: &spec.OperationBindingsObject{Kafka: kafkaOp},
    36  		}
    37  	}
    38  }
    39  
    40  func WithKafkaSubscribeOperationBinding(useGroupID, useClientID bool) asyncAPIChannelOpts {
    41  	return func(c *spec.ChannelItem) {
    42  		kafkaOp := &spec.KafkaOperation{}
    43  		if useGroupID {
    44  			kafkaOp.GroupID = &spec.KafkaOperationGroupID{
    45  				Schema: map[string]interface{}{
    46  					"type": "string",
    47  				},
    48  			}
    49  		}
    50  		if useClientID {
    51  			kafkaOp.ClientID = &spec.KafkaOperationClientID{
    52  				Schema: map[string]interface{}{
    53  					"type": "string",
    54  				},
    55  			}
    56  		}
    57  		c.Subscribe = &spec.Operation{
    58  			Bindings: &spec.OperationBindingsObject{Kafka: kafkaOp},
    59  		}
    60  	}
    61  }
    62  
    63  // TBD ?
    64  // func WithKafkaPublishBinding() asyncAPIChannelOpts {}
    65  
    66  type AsyncAPIServerOpts func(*asyncAPIBuilder, *spec.Server)
    67  
    68  func WithSaslPlainSecurity(description string) AsyncAPIServerOpts {
    69  	return func(b *asyncAPIBuilder, s *spec.Server) {
    70  		if s.Security == nil {
    71  			s.Security = make([]map[string][]string, 0)
    72  		}
    73  		s.Security = append(s.Security, map[string][]string{
    74  			"saslPlainCreds": {},
    75  		})
    76  		b.securitySchemas["saslPlainCreds"] = &spec.SecurityScheme{
    77  			SaslSecurityScheme: &spec.SaslSecurityScheme{
    78  				SaslPlainSecurityScheme: &spec.SaslPlainSecurityScheme{
    79  					Description: description,
    80  				},
    81  			},
    82  		}
    83  	}
    84  }
    85  
    86  func WithSaslScramSecurity(scramMechanism, description string) AsyncAPIServerOpts {
    87  	return func(b *asyncAPIBuilder, s *spec.Server) {
    88  		if s.Security == nil {
    89  			s.Security = make([]map[string][]string, 0)
    90  		}
    91  		s.Security = append(s.Security, map[string][]string{
    92  			"saslScramCreds": {},
    93  		})
    94  		scramType := spec.SaslScramSecuritySchemeTypeScramSha256
    95  		if scramMechanism == "SCRAM-SHA-512" {
    96  			scramType = spec.SaslScramSecuritySchemeTypeScramSha512
    97  		}
    98  		b.securitySchemas["saslScramCreds"] = &spec.SecurityScheme{
    99  			SaslSecurityScheme: &spec.SaslSecurityScheme{
   100  				SaslScramSecurityScheme: &spec.SaslScramSecurityScheme{
   101  					Type:        scramType,
   102  					Description: description,
   103  				},
   104  			},
   105  		}
   106  	}
   107  }
   108  
   109  func WithProtocol(protocol, protocolVersion string) AsyncAPIServerOpts {
   110  	return func(b *asyncAPIBuilder, s *spec.Server) {
   111  		s.Protocol = protocol
   112  		s.ProtocolVersion = protocolVersion
   113  	}
   114  }
   115  
   116  type AsyncAPIBuilder interface {
   117  	AddServer(name, description, url string, opts ...AsyncAPIServerOpts) AsyncAPIBuilder
   118  	AddChannel(name, description string, opts ...asyncAPIChannelOpts) AsyncAPIBuilder
   119  	SetPublishMessageRef(channelName, componentMessage string) AsyncAPIBuilder
   120  	SetSubscribeMessageRef(channelName, componentMessage string) AsyncAPIBuilder
   121  	AddComponentMessage(msgName, schemaFormat, contentType string, payload map[string]interface{}) AsyncAPIBuilder
   122  	Build(id, title, description, version string) (AsyncSpecProcessor, error)
   123  }
   124  
   125  type asyncAPIBuilder struct {
   126  	servers                    map[string]spec.Server
   127  	channels                   map[string]spec.ChannelItem
   128  	channelPublishMessageRef   map[string]string
   129  	channelSubscribeMessageRef map[string]string
   130  	componentMessages          map[string]spec.MessageEntity
   131  	securitySchemas            map[string]*spec.SecurityScheme
   132  }
   133  
   134  func CreateAsyncAPIBuilder() AsyncAPIBuilder {
   135  	return &asyncAPIBuilder{
   136  		servers:                    make(map[string]spec.Server),
   137  		channels:                   make(map[string]spec.ChannelItem),
   138  		channelPublishMessageRef:   make(map[string]string),
   139  		channelSubscribeMessageRef: make(map[string]string),
   140  		componentMessages:          make(map[string]spec.MessageEntity),
   141  		securitySchemas:            make(map[string]*spec.SecurityScheme),
   142  	}
   143  }
   144  
   145  func (b *asyncAPIBuilder) AddServer(name, description, url string, opts ...AsyncAPIServerOpts) AsyncAPIBuilder {
   146  	server := spec.Server{
   147  		URL:         url,
   148  		Description: description,
   149  	}
   150  	for _, o := range opts {
   151  		o(b, &server)
   152  	}
   153  	b.servers[name] = server
   154  	return b
   155  }
   156  
   157  func (b *asyncAPIBuilder) AddChannel(name, description string, opts ...asyncAPIChannelOpts) AsyncAPIBuilder {
   158  	channel := spec.ChannelItem{
   159  		Description: description,
   160  	}
   161  
   162  	for _, o := range opts {
   163  		o(&channel)
   164  	}
   165  
   166  	b.channels[name] = channel
   167  	return b
   168  }
   169  
   170  func setChannelMessageRef(m map[string]string, channelName, messageSubject string) {
   171  	m[channelName] = messageSubject
   172  }
   173  
   174  func (b *asyncAPIBuilder) SetPublishMessageRef(channelName, componentMessage string) AsyncAPIBuilder {
   175  	setChannelMessageRef(b.channelPublishMessageRef, channelName, componentMessage)
   176  	return b
   177  }
   178  
   179  func (b *asyncAPIBuilder) SetSubscribeMessageRef(channelName, componentMessage string) AsyncAPIBuilder {
   180  	setChannelMessageRef(b.channelSubscribeMessageRef, channelName, componentMessage)
   181  	return b
   182  }
   183  
   184  func (b *asyncAPIBuilder) AddComponentMessage(msgName, schemaFormat, contentType string, payload map[string]interface{}) AsyncAPIBuilder {
   185  	msg := spec.MessageEntity{
   186  		Payload:      payload,
   187  		ContentType:  contentType,
   188  		SchemaFormat: schemaFormat,
   189  	}
   190  
   191  	b.componentMessages[msgName] = msg
   192  	return b
   193  }
   194  
   195  func (b *asyncAPIBuilder) validateInfo(id, title, description, version string) {
   196  	if id == "" {
   197  		exception.Throw(fmt.Errorf("no identifier defined"))
   198  	}
   199  	u, _ := url.Parse(id)
   200  	if u.Scheme == "" {
   201  		exception.Throw(fmt.Errorf("invalid api id, should be uri format"))
   202  	}
   203  
   204  	if title == "" {
   205  		exception.Throw(fmt.Errorf("no title defined"))
   206  	}
   207  	if version == "" {
   208  		exception.Throw(fmt.Errorf("no version defined"))
   209  	}
   210  }
   211  
   212  func (b *asyncAPIBuilder) validateServers() {
   213  	if len(b.servers) == 0 {
   214  		exception.Throw(fmt.Errorf("no server defined"))
   215  	}
   216  	for _, s := range b.servers {
   217  		if s.URL == "" {
   218  			exception.Throw(fmt.Errorf("invalid server URL"))
   219  		}
   220  		if s.Protocol == "" {
   221  			exception.Throw(fmt.Errorf("invalid server protocol"))
   222  		}
   223  		if s.ProtocolVersion == "" {
   224  			exception.Throw(fmt.Errorf("invalid server protocol version"))
   225  		}
   226  	}
   227  
   228  }
   229  func validateMessageRef(channelOp string, channelMessageRef map[string]string, componentMessages map[string]spec.MessageEntity) {
   230  	for channel, message := range channelMessageRef {
   231  		if message == "" {
   232  			exception.Throw(fmt.Errorf("invalid message reference for %s operation in channel %s", channelOp, channel))
   233  		}
   234  		if _, ok := componentMessages[message]; !ok {
   235  			exception.Throw(fmt.Errorf("invalid message reference %s for %s operation in channel %s", message, channelOp, channel))
   236  		}
   237  	}
   238  }
   239  
   240  func (b *asyncAPIBuilder) validateChannels() {
   241  	if len(b.channels) == 0 {
   242  		exception.Throw(fmt.Errorf("no channels defined"))
   243  	}
   244  	validateMessageRef("publish", b.channelPublishMessageRef, b.componentMessages)
   245  	validateMessageRef("subscribe", b.channelSubscribeMessageRef, b.componentMessages)
   246  }
   247  
   248  func (b *asyncAPIBuilder) validateComponents() {
   249  	for msgName, msg := range b.componentMessages {
   250  		if msgName == "" {
   251  			exception.Throw(fmt.Errorf("invalid message name"))
   252  		}
   253  		if len(msg.Payload) == 0 {
   254  			exception.Throw(fmt.Errorf("invalid message schema"))
   255  		}
   256  	}
   257  }
   258  
   259  func (b *asyncAPIBuilder) validate(id, title, description, version string) (err error) {
   260  	exception.Block{
   261  		Try: func() {
   262  			b.validateInfo(id, title, description, version)
   263  			b.validateServers()
   264  			b.validateChannels()
   265  			b.validateComponents()
   266  		},
   267  		Catch: func(e error) {
   268  			err = e
   269  		},
   270  	}.Do()
   271  
   272  	return
   273  }
   274  
   275  func (b *asyncAPIBuilder) buildServers(api *spec.AsyncAPI) {
   276  	for name, server := range b.servers {
   277  		api.AddServer(name, server)
   278  	}
   279  }
   280  
   281  func (b *asyncAPIBuilder) buildChannels(api *spec.AsyncAPI) {
   282  	for name, publishMsgRef := range b.channelPublishMessageRef {
   283  		ch := b.channels[name]
   284  		ch.Publish.Message = &spec.Message{
   285  			Reference: &spec.Reference{
   286  				Ref: fmt.Sprintf(componentMessageRefTemplate, publishMsgRef),
   287  			},
   288  		}
   289  	}
   290  	for name, subscribeMsgRef := range b.channelSubscribeMessageRef {
   291  		ch := b.channels[name]
   292  		ch.Subscribe.Message = &spec.Message{
   293  			Reference: &spec.Reference{
   294  				Ref: fmt.Sprintf(componentMessageRefTemplate, subscribeMsgRef),
   295  			},
   296  		}
   297  	}
   298  	api.WithChannels(b.channels)
   299  }
   300  
   301  func (b *asyncAPIBuilder) buildComponents(api *spec.AsyncAPI) {
   302  	components := spec.Components{}
   303  	components.Messages = make(map[string]spec.Message)
   304  	for msgName, msg := range b.componentMessages {
   305  		components.Messages[msgName] = spec.Message{
   306  			OneOf1: &spec.MessageOneOf1{
   307  				MessageEntity: &msg,
   308  			},
   309  		}
   310  	}
   311  	if len(b.securitySchemas) > 0 {
   312  		components.SecuritySchemes = &spec.ComponentsSecuritySchemes{
   313  			MapOfComponentsSecuritySchemesWDValues: make(map[string]spec.ComponentsSecuritySchemesWD),
   314  		}
   315  		for name, securitySchema := range b.securitySchemas {
   316  			components.SecuritySchemes.MapOfComponentsSecuritySchemesWDValues[name] = spec.ComponentsSecuritySchemesWD{
   317  				SecurityScheme: securitySchema,
   318  			}
   319  		}
   320  	}
   321  
   322  	api.WithComponents(components)
   323  }
   324  
   325  func (b *asyncAPIBuilder) Build(id, title, description, version string) (AsyncSpecProcessor, error) {
   326  	err := b.validate(id, title, description, version)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	api := &spec.AsyncAPI{
   332  		ID: id,
   333  		Info: spec.Info{
   334  			Title:       title,
   335  			Description: description,
   336  			Version:     version,
   337  		},
   338  	}
   339  	b.buildServers(api)
   340  	b.buildChannels(api)
   341  	b.buildComponents(api)
   342  
   343  	raw, err := api.MarshalYAML()
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  
   348  	return &asyncApi{spec: api, raw: raw}, nil
   349  }