Go output
Every snippet below comes from examples/chat: the spec is chat.yaml, the output is chat.gen.go, and chat_test.go runs all of it.
Structs and strict decoding
JoinRequest:
type: object
additionalProperties: false
required: [type, id, room]
properties:
type: { type: string, const: join }
id: { type: string, format: uuid }
room: { type: string, minLength: 1, maxLength: 64, pattern: '^[a-z0-9-]+$' }
nickname: { type: string }
type JoinRequest struct {
Type string `json:"type"`
ID string `json:"id"`
Room string `json:"room"`
Nickname *string `json:"nickname,omitempty"`
}
Required fields are values, optional ones are pointers. The const fills itself in when you marshal:
data, _ := json.Marshal(chat.JoinRequest{ID: "1", Room: "lobby"})
// {"type":"join","id":"1","room":"lobby"}
Decoding checks everything the schema says, and the errors wrap ctxerrors/commerr sentinels:
var req chat.JoinRequest
err := json.Unmarshal([]byte(`{"type":"join","id":"1"}`), &req)
errors.Is(err, commerr.ErrRequiredFieldNotSet) // true, room is missing
err = json.Unmarshal([]byte(`{"type":"join","id":"1","room":"Lobby!"}`), &req)
errors.Is(err, commerr.ErrValidationFailed) // true, pattern
err = json.Unmarshal([]byte(`{"type":"join","id":"1","room":"x","extra":1}`), &req)
errors.Is(err, commerr.ErrValidationFailed) // true, additionalProperties: false
Every type also has Validate(), so you can check a value you built yourself before sending it.
Enums
role:
type: string
enum: [owner, member, guest]
type MemberRole string
const (
MemberRoleOwner MemberRole = "owner"
MemberRoleMember MemberRole = "member"
MemberRoleGuest MemberRole = "guest"
)
A value outside the list fails decoding with commerr.ErrValidationFailed.
Unions
ServerEvent:
oneOf:
- $ref: '#/components/schemas/MessagePosted' # type: { const: message.posted }
- $ref: '#/components/schemas/MemberJoined' # type: { const: member.joined }
- $ref: '#/components/schemas/Ping' # type: { const: ping }
discriminator: type
The union gets FromX, AsX, Discriminator and Value. Value decodes whichever variant the type field picks:
var event chat.ServerEvent
if err := json.Unmarshal(frame, &event); err != nil {
return err // unknown type, or the picked variant is invalid
}
value, err := event.Value()
if err != nil {
return err
}
switch e := value.(type) {
case chat.MessagePosted:
fmt.Println(e.Author.ID, "said", e.Text)
case chat.MemberJoined:
fmt.Println(e.Member.ID, "joined")
case chat.Ping:
// keepalive
}
Building one goes the other way:
var event chat.ServerEvent
err := event.FromPing(chat.Ping{}) // marshals to {"type":"ping"}
Without a discriminator, a property every variant pins with const does the same job. With neither, exactly one variant has to decode.
JSON inside a string
Some APIs send a JSON document as a string field. contentMediaType plus contentSchema says what is inside:
meta:
type: string
contentMediaType: application/json
contentSchema:
$ref: '#/components/schemas/AttachmentMeta'
meta, err := attachment.MetaValue() // decodes and validates the string as AttachmentMeta
err = attachment.SetMetaValue(chat.AttachmentMeta{Size: 42}) // encodes it back into the string
Sending
Every message of a send operation becomes a client method. It validates the payload, encodes it and writes it to the connection:
conn, err := gorillaws.Dial(
ctx,
chat.ServerProductionURL,
gorillaws.WithBearerToken(token),
)
if err != nil {
return err
}
client := chat.NewClient(conn)
defer client.Close()
err = client.JoinRoom(ctx, chat.JoinRequest{ID: id, Room: "lobby"})
gorillaws.Dial also takes WithHeader, WithQueryParam, WithSubprotocols, WithBinaryFrames, WithReadLimit and WithDialer. Any type with WriteMessage, ReadMessage and Close works as a transport: that is the asyncapirt.Transport interface in asyncapirt/transport.go. A transport must allow one goroutine reading while others write.
Receiving
Messages from receive operations and from replies land in a Handler. Embed UnimplementedHandler and write only the methods you care about:
type handler struct {
chat.UnimplementedHandler
}
func (handler) HandleEvent(ctx context.Context, event chat.ServerEvent) error {
value, err := event.Value()
if err != nil {
return err
}
log.Printf("event: %+v", value)
return nil
}
func (handler) HandleAck(ctx context.Context, ack chat.Ack) error {
if !ack.Ok {
log.Printf("request %s failed", ack.ID)
}
return nil
}
err := client.Run(ctx, handler{}) // reads until ctx ends or the connection drops
Frames that match nothing go to HandleUnknownMessage with the reason. The default logs a warning and keeps reading. Without the client, chat.DecodeReceivedMessage(frame) and chat.Dispatch(ctx, h, msg) do the same two steps by hand. Message matching explains how a frame is matched to a message.
Servers and channels
From examples/kitchensink:
servers:
production:
host: '{region}.example.com:{port}'
pathname: /v{version}/ws
protocol: wss
variables:
region: { enum: [eu, us], default: eu }
port: { enum: ['443', '8443'], default: '443' }
version: { default: '2' }
channels:
commands:
address: tenants/{tenantId}/commands
kitchensink.ServerProductionURL // "wss://eu.example.com:443/v2/ws"
kitchensink.ServerProductionURLWith("us", "8443", "1") // "wss://us.example.com:8443/v1/ws"
kitchensink.CommandsAddress("acme") // "tenants/acme/commands"
kitchensink.CommandsAddressTemplate // "tenants/{tenantId}/commands"