package protocol import ( "encoding/json" "fmt" "time" ) // Message contains validated routing, metadata, and a typed payload. Retained // is the delivery flag. Live delivery of retained-topic data can be false. type Message[T any] struct { Address Address Metadata Metadata Payload T Retained bool } // Publication is a broker-independent outgoing message. The transport sets // UTF-8 payload format and application/json content type. Commands and hub // status also need MQTT expiry computed from their envelope deadlines. type Publication struct { Topic string Data []byte QoS byte Retain bool // ExpiresAt is the command, earliest batch member, or hub deadline. ExpiresAt *time.Time // ResponseTopic and CorrelationData apply to individual commands only. ResponseTopic string CorrelationData string } // Parse routes an incoming publication to a typed payload without caller topic // operations. Payload is DescriptorPayload, AvailabilityPayload, ValuePayload, // EventPayload, BatchPayload, ResultPayload, or HubStatusPayload. Address.Kind // distinguishes command, reported, and telemetry value payloads. func Parse(topic string, data []byte, retained bool) (Message[any], error) { address, err := parseTopic(topic) if err != nil { return Message[any]{}, fmt.Errorf("parse address: %w", err) } switch address.Kind { case Descriptor: return eraseMessage(parsePayload[DescriptorPayload](address, data, retained)) case Availability: return eraseMessage(parsePayload[AvailabilityPayload](address, data, retained)) case Reported: return eraseMessage(parsePayload[ValuePayload](address, data, retained)) case Telemetry: return eraseMessage(parsePayload[ValuePayload](address, data, retained)) case Event: return eraseMessage(parsePayload[EventPayload](address, data, retained)) case Command: return eraseMessage(parsePayload[ValuePayload](address, data, retained)) case CommandBatch: return eraseMessage(parsePayload[BatchPayload](address, data, retained)) case Result: return eraseMessage(parsePayload[ResultPayload](address, data, retained)) case HubStatus: return eraseMessage(parsePayload[HubStatusPayload](address, data, retained)) default: return Message[any]{}, fmt.Errorf("unsupported message kind") } } func buildMessage[T any](address Address, metadata Metadata, payload T) (Publication, error) { topic, err := address.topic() if err != nil { return Publication{}, fmt.Errorf("build address: %w", err) } normalized := normalizePayload(payload) err = validatePayload(address, metadata, normalized) if err != nil { return Publication{}, fmt.Errorf("validate publication: %w", err) } data, err := encodeEnvelope(metadata, normalized) if err != nil { return Publication{}, fmt.Errorf("build envelope: %w", err) } retain := address.Kind == Descriptor || address.Kind == Availability || address.Kind == Reported || address.Kind == HubStatus qos := byte(1) if address.Kind == Telemetry { qos = 0 } publication := Publication{ Topic: topic, Data: data, QoS: qos, Retain: retain, } switch address.Kind { case Command: publication.ExpiresAt = metadata.ExpiresAt publication.CorrelationData = metadata.MessageID publication.ResponseTopic, err = (Address{ Kind: Result, ID: address.ID, Target: metadata.MessageID, }).topic() if err != nil { return Publication{}, fmt.Errorf("build command response topic: %w", err) } case CommandBatch: batch, ok := any(payload).(BatchPayload) if !ok { return Publication{}, fmt.Errorf("batch requires command members") } for _, command := range batch.Commands { if publication.ExpiresAt == nil || command.ExpiresAt.Before(*publication.ExpiresAt) { publication.ExpiresAt = command.ExpiresAt } } case HubStatus: publication.ExpiresAt = metadata.ExpiresAt } return publication, nil } func parseMessage[T any](kind Kind, topic string, data []byte, retained bool) (Message[T], error) { address, err := parseTopic(topic) if err != nil { return Message[T]{}, fmt.Errorf("parse address: %w", err) } if address.Kind != kind { return Message[T]{}, fmt.Errorf("message topic does not match parser") } message, err := parsePayload[T](address, data, retained) if err != nil { return Message[T]{}, fmt.Errorf("parse payload: %w", err) } return message, nil } func eraseMessage[T any](message Message[T], err error) (Message[any], error) { if err != nil { return Message[any]{}, fmt.Errorf("parse message: %w", err) } return Message[any]{ Address: message.Address, Metadata: message.Metadata, Payload: message.Payload, Retained: message.Retained, }, nil } func parsePayload[T any](address Address, data []byte, retained bool) (Message[T], error) { kind := address.Kind if retained && (kind == Telemetry || kind == Event || kind == Command || kind == CommandBatch || kind == Result) { return Message[T]{}, fmt.Errorf("message must not be retained") } envelope, err := parseEnvelope(data) if err != nil { return Message[T]{}, fmt.Errorf("parse envelope: %w", err) } var payload T err = decodeObject(envelope.Payload, &payload) if err != nil { return Message[T]{}, fmt.Errorf("decode payload: %w", err) } err = validatePayload(address, envelope.Metadata, payload) if err != nil { return Message[T]{}, fmt.Errorf("validate payload: %w", err) } if address.Kind == Descriptor { err = validateDescriptorFlags(envelope.Payload) if err != nil { return Message[T]{}, fmt.Errorf("validate descriptor access flags: %w", err) } } return Message[T]{ Address: address, Metadata: envelope.Metadata, Payload: payload, Retained: retained, }, nil } // normalizePayload keeps nil raw values as JSON null without changing caller // data. func normalizePayload(payload any) any { switch value := payload.(type) { case ValuePayload: if value.Value == nil { value.Value = json.RawMessage(`null`) } return value case BatchPayload: commands := make([]BatchCommand, len(value.Commands)) copy(commands, value.Commands) for index := range commands { if commands[index].Payload.Value == nil { commands[index].Payload.Value = json.RawMessage(`null`) } } value.Commands = commands return value default: return payload } } func validatePayload(address Address, metadata Metadata, payload any) error { var err error switch value := payload.(type) { case DescriptorPayload: err = validateDescriptor(value) case AvailabilityPayload: err = validateAvailability(value) case ValuePayload: err = validateValue(value, address.Kind, metadata) case EventPayload: err = validateEvent(value) case ResultPayload: err = validateResult(value, address, metadata) case BatchPayload: err = validateBatch(value, metadata) case HubStatusPayload: err = validateHub(value, address, metadata) default: return fmt.Errorf("unsupported payload type") } if err != nil { return fmt.Errorf("validate %s payload: %w", address.Kind, err) } return nil } func validateDescriptorFlags(raw json.RawMessage) error { var fields struct { Capabilities []map[string]json.RawMessage `json:"capabilities"` } err := json.Unmarshal(raw, &fields) if err != nil { return fmt.Errorf("read descriptor access flags: %w", err) } for _, capability := range fields.Capabilities { for _, name := range []string{"reports_state", "accepts_commands"} { field := capability[name] if string(field) != "true" && string(field) != "false" { return fmt.Errorf("capability access flags must be present booleans") } } } return nil } func validateDescriptor(payload DescriptorPayload) error { if payload.Manufacturer == "" || payload.Model == "" || payload.Firmware == "" || len(string([]rune(payload.Attribution))) > 2000 || len(payload.Capabilities) == 0 { return fmt.Errorf("descriptor metadata or capabilities are invalid") } seen := make(map[string]bool, len(payload.Capabilities)) for _, capability := range payload.Capabilities { if !ValidIdentifier(capability.ID) || !ValidIdentifier(capability.Type) || seen[capability.ID] { return fmt.Errorf("capability IDs and types must be valid and unique") } if !capability.ReportsState && !capability.AcceptsCommands { return fmt.Errorf("capability must have at least one access mode") } seen[capability.ID] = true } return nil } func validateAvailability(payload AvailabilityPayload) error { if payload.Status != "online" && payload.Status != "offline" { return fmt.Errorf("availability must be online or offline") } return nil } func validateValue(payload ValuePayload, kind Kind, metadata Metadata) error { if !json.Valid(payload.Value) { return fmt.Errorf("value is required and must be valid JSON") } if kind == Command && metadata.ExpiresAt == nil { return fmt.Errorf("command must have an expiry") } return nil } func validateEvent(payload EventPayload) error { if payload == nil { return fmt.Errorf("event must be a JSON object") } for _, value := range payload { if value != nil && !json.Valid(value) { return fmt.Errorf("event values must be valid JSON") } } return nil } func validateResult(payload ResultPayload, address Address, metadata Metadata) error { if metadata.CorrelationID != address.Target { return fmt.Errorf("result correlation must match its command ID") } switch payload.Status { case "accepted", "completed": if payload.Reason != "" { return fmt.Errorf("successful result must not have a reason") } case "rejected", "failed": if payload.Reason == "" { return fmt.Errorf("unsuccessful result must have a reason") } default: return fmt.Errorf("result status is invalid") } return nil } func validateBatch(payload BatchPayload, metadata Metadata) error { if len(payload.Commands) < 2 || len(payload.Commands) > MaximumCommandBatchSize { return fmt.Errorf("batch size must be 2 through %d", MaximumCommandBatchSize) } if metadata.MessageID != payload.Commands[0].MessageID { return fmt.Errorf("batch ID must equal its first command ID") } ids := make(map[string]bool, len(payload.Commands)) targets := make(map[string]bool, len(payload.Commands)) for _, command := range payload.Commands { if !ValidIdentifier(command.Capability) || ids[command.MessageID] || targets[command.Capability] { return fmt.Errorf("batch targets and IDs must be valid and distinct") } err := validateMetadata(command.Metadata) if err != nil { return fmt.Errorf("validate batch command metadata: %w", err) } err = validateValue(command.Payload, Command, command.Metadata) if err != nil { return fmt.Errorf("validate batch command value: %w", err) } ids[command.MessageID] = true targets[command.Capability] = true } return nil } func validateHub(payload HubStatusPayload, address Address, metadata Metadata) error { if payload.ID != address.ID || metadata.ExpiresAt == nil { return fmt.Errorf("hub status requires a matching ID and an expiry") } if payload.Status != "ready" && payload.Status != "unavailable" { return fmt.Errorf("hub status must be ready or unavailable") } if payload.Address == "" || payload.ConfiguredDevices < 0 || payload.PairedDevices != nil && *payload.PairedDevices < 0 { return fmt.Errorf("hub address or device counts are invalid") } timestamps := []*time.Time{payload.UpdatedAt} if payload.Diagnostics != nil { timestamps = append(timestamps, &payload.Diagnostics.ObservedAt) } if payload.Pairing != nil { timestamps = append(timestamps, &payload.Pairing.RequestedAt, &payload.Pairing.Deadline) } for _, timestamp := range timestamps { if timestamp != nil { _, offset := timestamp.Zone() if offset != 0 || timestamp.IsZero() { return fmt.Errorf("hub timestamps must be nonzero UTC times") } } } return nil }