9 // Message contains validated routing, metadata, and a typed payload. Retained
10 // is the delivery flag. Live delivery of retained-topic data can be false.
11 type Message[T any] struct {
18 // Publication is a broker-independent outgoing message. The transport sets
19 // UTF-8 payload format and application/json content type. Commands and hub
20 // status also need MQTT expiry computed from their envelope deadlines.
21 type Publication struct {
26 // ExpiresAt is the command, earliest batch member, or hub deadline.
28 // ResponseTopic and CorrelationData apply to individual commands only.
30 CorrelationData string
33 // Parse routes an incoming publication to a typed payload without caller topic
34 // operations. Payload is DescriptorPayload, AvailabilityPayload, ValuePayload,
35 // EventPayload, BatchPayload, ResultPayload, or HubStatusPayload. Address.Kind
36 // distinguishes command, reported, and telemetry value payloads.
37 func Parse(topic string, data []byte, retained bool) (Message[any], error) {
38 address, err := parseTopic(topic)
40 return Message[any]{}, fmt.Errorf("parse address: %w", err)
45 return eraseMessage(ParseDescriptor(topic, data, retained))
47 return eraseMessage(ParseAvailability(topic, data, retained))
49 return eraseMessage(ParseReported(topic, data, retained))
51 return eraseMessage(ParseTelemetry(topic, data, retained))
53 return eraseMessage(ParseEvent(topic, data, retained))
55 return eraseMessage(ParseCommand(topic, data, retained))
57 return eraseMessage(ParseCommandBatch(topic, data, retained))
59 return eraseMessage(ParseResult(topic, data, retained))
61 return eraseMessage(ParseHubStatus(topic, data, retained))
63 return Message[any]{}, fmt.Errorf("unsupported message kind")
67 func eraseMessage[T any](message Message[T], err error) (Message[any], error) {
69 return Message[any]{}, fmt.Errorf("parse message: %w", err)
73 Address: message.Address,
74 Metadata: message.Metadata,
75 Payload: message.Payload,
76 Retained: message.Retained,
80 func buildMessage[T any](address Address, metadata Metadata, payload T) (Publication, error) {
81 topic, err := address.topic()
83 return Publication{}, fmt.Errorf("build address: %w", err)
86 normalized := normalizePayload(payload)
87 err = validatePayload(address, metadata, normalized)
89 return Publication{}, fmt.Errorf("validate publication: %w", err)
92 data, err := encodeEnvelope(metadata, normalized)
94 return Publication{}, fmt.Errorf("build envelope: %w", err)
97 retain := address.Kind == Descriptor || address.Kind == Availability || address.Kind == Reported || address.Kind == HubStatus
100 if address.Kind == Telemetry {
104 publication := Publication{
111 switch address.Kind {
113 publication.ExpiresAt = metadata.ExpiresAt
114 publication.CorrelationData = metadata.MessageID
115 publication.ResponseTopic, err = (Address{
118 Target: metadata.MessageID,
121 return Publication{}, fmt.Errorf("build command response topic: %w", err)
124 batch, ok := any(payload).(BatchPayload)
126 return Publication{}, fmt.Errorf("batch requires command members")
129 for _, command := range batch.Commands {
130 if publication.ExpiresAt == nil || command.ExpiresAt.Before(*publication.ExpiresAt) {
131 publication.ExpiresAt = command.ExpiresAt
135 publication.ExpiresAt = metadata.ExpiresAt
138 return publication, nil
141 func parseMessage[T any](kind Kind, topic string, data []byte, retained bool) (Message[T], error) {
142 address, err := parseTopic(topic)
144 return Message[T]{}, fmt.Errorf("parse address: %w", err)
147 if address.Kind != kind {
148 return Message[T]{}, fmt.Errorf("message topic does not match parser")
151 message, err := parsePayload[T](address, data, retained)
153 return Message[T]{}, fmt.Errorf("parse payload: %w", err)
159 func parsePayload[T any](address Address, data []byte, retained bool) (Message[T], error) {
162 if retained && (kind == Telemetry || kind == Event || kind == Command || kind == CommandBatch || kind == Result) {
163 return Message[T]{}, fmt.Errorf("message must not be retained")
166 envelope, err := parseEnvelope(data)
168 return Message[T]{}, fmt.Errorf("parse envelope: %w", err)
173 err = decodeObject(envelope.Payload, &payload)
175 return Message[T]{}, fmt.Errorf("decode payload: %w", err)
178 err = validatePayload(address, envelope.Metadata, payload)
180 return Message[T]{}, fmt.Errorf("validate payload: %w", err)
183 if address.Kind == Descriptor {
184 err = validateDescriptorFlags(envelope.Payload)
186 return Message[T]{}, fmt.Errorf("validate descriptor access flags: %w", err)
192 Metadata: envelope.Metadata,
198 // normalizePayload keeps nil raw values as JSON null without changing caller
200 func normalizePayload(payload any) any {
201 switch value := payload.(type) {
203 if value.Value == nil {
204 value.Value = json.RawMessage(`null`)
209 commands := make([]BatchCommand, len(value.Commands))
210 copy(commands, value.Commands)
212 for index := range commands {
213 if commands[index].Payload.Value == nil {
214 commands[index].Payload.Value = json.RawMessage(`null`)
218 value.Commands = commands
226 func validatePayload(address Address, metadata Metadata, payload any) error {
229 switch value := payload.(type) {
230 case DescriptorPayload:
231 err = validateDescriptor(value)
232 case AvailabilityPayload:
233 err = validateAvailability(value)
235 err = validateValue(value, address.Kind, metadata)
237 err = validateEvent(value)
239 err = validateResult(value, address, metadata)
241 err = validateBatch(value, metadata)
242 case HubStatusPayload:
243 err = validateHub(value, address, metadata)
245 return fmt.Errorf("unsupported payload type")
249 return fmt.Errorf("validate %s payload: %w", address.Kind, err)
255 func validateDescriptor(payload DescriptorPayload) error {
256 if payload.Manufacturer == "" || payload.Model == "" || payload.Firmware == "" || len(string([]rune(payload.Attribution))) > 2000 || len(payload.Capabilities) == 0 {
257 return fmt.Errorf("descriptor metadata or capabilities are invalid")
260 seen := make(map[string]bool, len(payload.Capabilities))
262 for _, capability := range payload.Capabilities {
263 if !ValidIdentifier(capability.ID) || !ValidIdentifier(capability.Type) || seen[capability.ID] {
264 return fmt.Errorf("capability IDs and types must be valid and unique")
267 if !capability.ReportsState && !capability.AcceptsCommands {
268 return fmt.Errorf("capability must have at least one access mode")
271 seen[capability.ID] = true
277 func validateDescriptorFlags(raw json.RawMessage) error {
279 Capabilities []map[string]json.RawMessage `json:"capabilities"`
282 err := json.Unmarshal(raw, &fields)
284 return fmt.Errorf("read descriptor access flags: %w", err)
287 for _, capability := range fields.Capabilities {
288 for _, name := range []string{"reports_state", "accepts_commands"} {
289 field := capability[name]
290 if string(field) != "true" && string(field) != "false" {
291 return fmt.Errorf("capability access flags must be present booleans")
299 func validateAvailability(payload AvailabilityPayload) error {
300 if payload.Status != "online" && payload.Status != "offline" {
301 return fmt.Errorf("availability must be online or offline")
307 func validateValue(payload ValuePayload, kind Kind, metadata Metadata) error {
308 if !json.Valid(payload.Value) {
309 return fmt.Errorf("value is required and must be valid JSON")
312 if kind == Command && metadata.ExpiresAt == nil {
313 return fmt.Errorf("command must have an expiry")
319 func validateEvent(payload EventPayload) error {
321 return fmt.Errorf("event must be a JSON object")
324 for _, value := range payload {
325 if value != nil && !json.Valid(value) {
326 return fmt.Errorf("event values must be valid JSON")
333 func validateResult(payload ResultPayload, address Address, metadata Metadata) error {
334 if metadata.CorrelationID != address.Target {
335 return fmt.Errorf("result correlation must match its command ID")
338 switch payload.Status {
339 case "accepted", "completed":
340 if payload.Reason != "" {
341 return fmt.Errorf("successful result must not have a reason")
343 case "rejected", "failed":
344 if payload.Reason == "" {
345 return fmt.Errorf("unsuccessful result must have a reason")
348 return fmt.Errorf("result status is invalid")
354 func validateBatch(payload BatchPayload, metadata Metadata) error {
355 if len(payload.Commands) < 2 || len(payload.Commands) > MaximumCommandBatchSize {
356 return fmt.Errorf("batch size must be 2 through %d", MaximumCommandBatchSize)
359 if metadata.MessageID != payload.Commands[0].MessageID {
360 return fmt.Errorf("batch ID must equal its first command ID")
363 ids := make(map[string]bool, len(payload.Commands))
364 targets := make(map[string]bool, len(payload.Commands))
366 for _, command := range payload.Commands {
367 if !ValidIdentifier(command.Capability) || ids[command.MessageID] || targets[command.Capability] {
368 return fmt.Errorf("batch targets and IDs must be valid and distinct")
371 err := validateMetadata(command.Metadata)
373 return fmt.Errorf("validate batch command metadata: %w", err)
376 err = validateValue(command.Payload, Command, command.Metadata)
378 return fmt.Errorf("validate batch command value: %w", err)
381 ids[command.MessageID] = true
382 targets[command.Capability] = true
388 func validateHub(payload HubStatusPayload, address Address, metadata Metadata) error {
389 if payload.ID != address.ID || metadata.ExpiresAt == nil {
390 return fmt.Errorf("hub status requires a matching ID and an expiry")
393 if payload.Status != "ready" && payload.Status != "unavailable" {
394 return fmt.Errorf("hub status must be ready or unavailable")
397 if payload.Address == "" || payload.ConfiguredDevices < 0 || payload.PairedDevices != nil && *payload.PairedDevices < 0 {
398 return fmt.Errorf("hub address or device counts are invalid")
401 timestamps := []*time.Time{payload.UpdatedAt}
402 if payload.Diagnostics != nil {
403 timestamps = append(timestamps, &payload.Diagnostics.ObservedAt)
406 if payload.Pairing != nil {
407 timestamps = append(timestamps, &payload.Pairing.RequestedAt, &payload.Pairing.Deadline)
410 for _, timestamp := range timestamps {
411 if timestamp != nil {
412 _, offset := timestamp.Zone()
413 if offset != 0 || timestamp.IsZero() {
414 return fmt.Errorf("hub timestamps must be nonzero UTC times")