Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

Commit

Permalink
feat(model): add message model and dependants (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
smoya committed Jul 19, 2021
1 parent a18a2e0 commit cd5268b
Show file tree
Hide file tree
Showing 4 changed files with 926 additions and 20 deletions.
139 changes: 137 additions & 2 deletions asyncapi/document.go
Expand Up @@ -2,21 +2,149 @@ package asyncapi

// Document is an object representing an AsyncAPI document.
// It's API implements https://github.com/asyncapi/parser-api/blob/master/docs/v1.md.
// NOTE: this interface is not completed yet.
type Document interface {
Extendable
Channels() []Channel
HasChannels() bool
ApplicationPublishableChannels() []Channel
ApplicationPublishableMessages() []Message
ApplicationPublishOperations() []Operation
ApplicationSubscribableChannels() []Channel
ApplicationSubscribableMessages() []Message
ApplicationSubscribeOperations() []Operation
ClientPublishableChannels() []Channel
ClientPublishableMessages() []Message
ClientPublishOperations() []Operation
ClientSubscribableChannels() []Channel
ClientSubscribableMessages() []Message
ClientSubscribeOperations() []Operation
Messages() []Message
Server(name string) (Server, bool)
Servers() []Server
HasServers() bool
}

// Channel is an addressable component, made available by the server, for the organization of messages.
// Producer applications send messages to channels and consumer applications consume messages from channels.
type Channel interface {
Extendable
Identifiable
Describable
Path() string // Path is the identifier
Parameters() []ChannelParameter
HasParameters() bool
Operations() []Operation
Messages() []Message
}

// ChannelParameter describes a parameter included in a channel name.
type ChannelParameter interface {
Extendable
Identifiable
Describable
Name() string
Schema() Schema
}

// OperationType is the type of an operation.
type OperationType string

// Operation describes a publish or a subscribe operation.
// This provides a place to document how and why messages are sent and received.
type Operation interface {
Extendable
Describable
ID() string
IsApplicationPublishing() bool
IsApplicationSubscribing() bool
IsClientPublishing() bool
IsClientSubscribing() bool
Messages() []Message
Summary() string
HasSummary() bool
Type() OperationType
}

// Message describes a message received on a given channel and operation.
type Message interface {
Extendable
Describable
UID() string
Name() string
Title() string
HasTitle() bool
Summary() string
HasSummary() bool
ContentType() string
Payload() Schema
}

// Schema is an object that allows the definition of input and output data types.
// These types can be objects, but also primitives and arrays.
// This object is a superset of the JSON Schema Specification Draft 07.
type Schema interface {
Extendable
ID() string
AdditionalItems() Schema
AdditionalProperties() Schema // TODO (boolean | Schema)
AllOf() []Schema
AnyOf() []Schema
CircularProps() []string
Const() interface{}
Contains() Schema
ContentEncoding() string
ContentMediaType() string
Default() interface{}
Definitions() map[string]Schema
Dependencies() map[string]Schema // TODO Map[string, Schema|string[]]
Deprecated() bool
Description() string
Discriminator() string
Else() Schema
Enum() []interface{}
Examples() []interface{}
ExclusiveMaximum() *float64
ExclusiveMinimum() *float64
Format() string
HasCircularProps() bool
If() Schema
IsCircular() bool
Items() []Schema // TODO Schema | Schema[]
Maximum() *float64
MaxItems() *float64
MaxLength() *float64
MaxProperties() *float64
Minimum() *float64
MinItems() *float64
MinLength() *float64
MinProperties() *float64
MultipleOf() *float64
Not() Schema
OneOf() Schema
Pattern() string
PatternProperties() map[string]Schema
Properties() map[string]Schema
Property(name string) Schema
PropertyNames() Schema
ReadOnly() bool
Required() string // TODO string[]
Then() Schema
Title() string
Type() []string // TODO // string | string[]
UID() string
UniqueItems() bool
WriteOnly() bool
}

// Server is an object representing a message broker, a server or any other kind of computer program capable of
// sending and/or receiving data.
type Server interface {
Extendable
Identifiable
Describable
Name() string
HasName() bool
Description() string
HasDescription() bool
URL() string
HasURL() bool
Protocol() string
Expand All @@ -27,6 +155,7 @@ type Server interface {
// ServerVariable is an object representing a Server Variable for server URL template substitution.
type ServerVariable interface {
Extendable
Identifiable
Name() string
HasName() bool
DefaultValue() string
Expand All @@ -41,6 +170,12 @@ type Extendable interface {
Extension(name string) interface{}
}

// Describable means the object can have a description.
type Describable interface {
Description() string
HasDescription() bool
}

// Identifiable identifies objects. Some objects can have fields that identify themselves as unique resources.
// For example: `id` and `name` fields.
type Identifiable interface {
Expand Down
35 changes: 33 additions & 2 deletions asyncapi/v2/decode.go
Expand Up @@ -34,7 +34,7 @@ func Decode(b []byte, dst interface{}) error {
}

dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: setModelIdentifierHook,
DecodeHook: mapstructure.ComposeDecodeHookFunc(setModelIdentifierHook, setDefaultsHook),
Squash: true,
Result: dst,
})
Expand All @@ -46,7 +46,7 @@ func Decode(b []byte, dst interface{}) error {
}

// setModelIdentifierHook is a hook for the mapstructure decoder.
// It checks if the destination field is a map of Identifiable elements and sets the proper identifier (name, id, etc) to it.
// It checks if the destination type is a map of Identifiable elements and sets the proper identifier (name, id, etc) to it.
// Example: Useful for storing the name of the server in the Server struct (AsyncAPI doc does not have such field because it assumes the name is the key of the map).
func setModelIdentifierHook(from reflect.Type, to reflect.Type, data interface{}) (interface{}, error) {
if from.Kind() != reflect.Map || to.Kind() != reflect.Map {
Expand All @@ -66,3 +66,34 @@ func setModelIdentifierHook(from reflect.Type, to reflect.Type, data interface{}

return data, nil
}

// MapStructureDefaultsProvider tells to mapstructure setDefaultsHook the defaults value for that type.
type MapStructureDefaultsProvider interface {
MapStructureDefaults() map[string]interface{}
}

// setDefaultsHook is a hook for the mapstructure decoder.
// It checks if the destination type implements MapStructureDefaultsProvider.
// If so, it gets the defaults values from it and sets them if empty.
func setDefaultsHook(_ reflect.Type, to reflect.Type, data interface{}) (interface{}, error) {
if !to.Implements(reflect.TypeOf((*MapStructureDefaultsProvider)(nil)).Elem()) {
return data, nil
}

var toType reflect.Type
switch to.Kind() { //nolint:exhaustive
case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice:
toType = to.Elem()
default:
toType = to
}

defaults := reflect.New(toType).Interface().(MapStructureDefaultsProvider).MapStructureDefaults()
for k, v := range defaults {
if _, ok := data.(map[string]interface{})[k]; !ok {
data.(map[string]interface{})[k] = v
}
}

return data, nil
}
99 changes: 98 additions & 1 deletion asyncapi/v2/decode_test.go
@@ -1,6 +1,7 @@
package v2

import (
"strings"
"testing"

"github.com/asyncapi/event-gateway/asyncapi"
Expand Down Expand Up @@ -30,9 +31,31 @@ func TestDecodeFromFile(t *testing.T) {
assert.True(t, s.HasExtension(asyncapi.ExtensionEventGatewayDialMapping))
assert.Equal(t, "broker:9092", s.Extension(asyncapi.ExtensionEventGatewayDialMapping))
assert.Empty(t, s.Variables())

channelPaths := []string{
"smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured",
"smartylighting.streetlights.1.0.action.{streetlightId}.turn.on",
"smartylighting.streetlights.1.0.action.{streetlightId}.turn.off",
"smartylighting.streetlights.1.0.action.{streetlightId}.dim",
}

require.Len(t, doc.Channels(), 4)
for _, c := range doc.Channels() {
assert.Containsf(t, channelPaths, c.Path(), "Channel path %q is not one of: %s", c.Path(), strings.Join(channelPaths, ", "))
assert.Len(t, c.Parameters(), 1)
assert.Len(t, c.Operations(), 1)
assert.Len(t, c.Messages(), 1)
for _, o := range c.Operations() {
assert.Containsf(t, []asyncapi.OperationType{OperationTypePublish, OperationTypeSubscribe}, o.Type(), "Operation type %q is not one of %s, %s", o.Type(), OperationTypePublish, OperationTypeSubscribe)
}

for _, m := range c.Messages() {
assert.NotNil(t, m.Payload())
}
}
}

//nolint:misspell
//nolint:misspell,funlen
func TestDecodeFromPlainText(t *testing.T) {
raw := []byte(`
asyncapi: '2.0.0'
Expand Down Expand Up @@ -90,4 +113,78 @@ channels:
assert.False(t, s.HasExtension(asyncapi.ExtensionEventGatewayListener))
assert.False(t, s.HasExtension(asyncapi.ExtensionEventGatewayDialMapping))
assert.Empty(t, s.Variables())

assert.Len(t, doc.ApplicationPublishableChannels(), 1)
assert.Len(t, doc.ApplicationPublishOperations(), 1)
assert.Len(t, doc.ApplicationPublishableMessages(), 1)

assert.Empty(t, doc.ApplicationSubscribableChannels())
assert.Empty(t, doc.ApplicationSubscribeOperations())
assert.Empty(t, doc.ApplicationSubscribableMessages())

assert.Len(t, doc.ClientSubscribableChannels(), 1)
assert.Len(t, doc.ClientSubscribeOperations(), 1)
assert.Len(t, doc.ClientSubscribableMessages(), 1)

assert.Empty(t, doc.ClientPublishableChannels())
assert.Empty(t, doc.ClientPublishOperations())
assert.Empty(t, doc.ClientPublishableMessages())

channels := doc.Channels()
require.Len(t, channels, 1)
assert.Equal(t, "light/measured", channels[0].Path())
assert.Empty(t, channels[0].Parameters())
assert.False(t, channels[0].HasDescription())

operations := channels[0].Operations()
require.Len(t, operations, 1)
assert.Equal(t, OperationTypePublish, operations[0].Type())
assert.True(t, operations[0].IsApplicationPublishing())
assert.False(t, operations[0].IsApplicationSubscribing())
assert.True(t, operations[0].IsClientSubscribing())
assert.False(t, operations[0].IsClientPublishing())
assert.False(t, operations[0].HasDescription())
assert.True(t, operations[0].HasSummary())
assert.Equal(t, "Inform about environmental lighting conditions for a particular streetlight.", operations[0].Summary())
assert.Equal(t, "onLightMeasured", operations[0].ID())

messages := operations[0].Messages()
require.Len(t, messages, 1)

assert.Equal(t, "LightMeasured", messages[0].Name())
assert.False(t, messages[0].HasSummary())
assert.False(t, messages[0].HasDescription())
assert.False(t, messages[0].HasTitle())
assert.Empty(t, messages[0].ContentType())

payload := messages[0].Payload()
require.NotNil(t, payload)

assert.Equal(t, []string{"object"}, payload.Type())
properties := payload.Properties()
require.Len(t, properties, 3)

expectedProperties := map[string]asyncapi.Schema{
"id": &Schema{
DescriptionField: "Id of the streetlight.",
MinimumField: refFloat64(0),
TypeField: "integer",
},
"lumens": &Schema{
DescriptionField: "Light intensity measured in lumens.",
MinimumField: refFloat64(0),
TypeField: "integer",
},
"sentAt": &Schema{
DescriptionField: "Date and time when the message was sent.",
FormatField: "date-time",
TypeField: "string",
},
}

assert.Equal(t, expectedProperties, properties)
}

func refFloat64(v float64) *float64 {
return &v
}

0 comments on commit cd5268b

Please sign in to comment.