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 }