213 lines
6.2 KiB
Go
213 lines
6.2 KiB
Go
package topics
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
|
|
)
|
|
|
|
const (
|
|
RawGB32960 = "vehicle.raw.go.gb32960.v1"
|
|
RawJT808 = "vehicle.raw.go.jt808.v1"
|
|
RawYutongMQTT = "vehicle.raw.go.yutong-mqtt.v1"
|
|
FieldsGB32960 = "vehicle.fields.go.gb32960.v1"
|
|
FieldsJT808 = "vehicle.fields.go.jt808.v1"
|
|
FieldsYutongMQTT = "vehicle.fields.go.yutong-mqtt.v1"
|
|
Unified = "vehicle.event.go.unified.v1"
|
|
)
|
|
|
|
const (
|
|
RawPrefix = "vehicle.raw."
|
|
FieldsPrefix = "vehicle.fields."
|
|
)
|
|
|
|
func ProtocolForKnownRawTopic(topic string) (string, bool) {
|
|
switch strings.TrimSpace(topic) {
|
|
case RawGB32960:
|
|
return "GB32960", true
|
|
case RawJT808:
|
|
return "JT808", true
|
|
case RawYutongMQTT:
|
|
return "YUTONG_MQTT", true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func ProtocolForKnownFieldsTopic(topic string) (string, bool) {
|
|
switch strings.TrimSpace(topic) {
|
|
case FieldsGB32960:
|
|
return "GB32960", true
|
|
case FieldsJT808:
|
|
return "JT808", true
|
|
case FieldsYutongMQTT:
|
|
return "YUTONG_MQTT", true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func ValidateKnownRawTopicProtocol(topic string, protocol string) error {
|
|
expectedProtocol, ok := ProtocolForKnownRawTopic(topic)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
protocol = strings.TrimSpace(protocol)
|
|
if protocol == expectedProtocol {
|
|
return nil
|
|
}
|
|
if protocol == "" {
|
|
protocol = "UNKNOWN"
|
|
}
|
|
return fmt.Errorf("raw topic %q expects protocol %s, got %s", strings.TrimSpace(topic), expectedProtocol, protocol)
|
|
}
|
|
|
|
func ValidateKnownFieldsTopicProtocol(topic string, protocol string) error {
|
|
expectedProtocol, ok := ProtocolForKnownFieldsTopic(topic)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
protocol = strings.TrimSpace(protocol)
|
|
if protocol == expectedProtocol {
|
|
return nil
|
|
}
|
|
if protocol == "" {
|
|
protocol = "UNKNOWN"
|
|
}
|
|
return fmt.Errorf("fields topic %q expects protocol %s, got %s", strings.TrimSpace(topic), expectedProtocol, protocol)
|
|
}
|
|
|
|
func ValidateRawEnvelope(topic string, env envelope.FrameEnvelope) (string, error) {
|
|
if err := ValidateKnownRawTopicProtocol(topic, string(env.Protocol)); err != nil {
|
|
return "protocol_topic_mismatch", err
|
|
}
|
|
if strings.HasPrefix(strings.TrimSpace(topic), RawPrefix) {
|
|
switch env.EventKind {
|
|
case "", envelope.EventKindRaw:
|
|
return "", nil
|
|
default:
|
|
return "event_kind_mismatch", fmt.Errorf("raw topic %q expects event kind %s, got %s", strings.TrimSpace(topic), envelope.EventKindRaw, env.EventKind)
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func ValidateFieldsEnvelope(topic string, env envelope.FrameEnvelope) (string, error) {
|
|
if err := ValidateKnownFieldsTopicProtocol(topic, string(env.Protocol)); err != nil {
|
|
return "protocol_topic_mismatch", err
|
|
}
|
|
if strings.HasPrefix(strings.TrimSpace(topic), FieldsPrefix) {
|
|
if env.EventKind != envelope.EventKindFields {
|
|
return "event_kind_mismatch", fmt.Errorf("fields topic %q expects event kind %s, got %s", strings.TrimSpace(topic), envelope.EventKindFields, env.EventKind)
|
|
}
|
|
if strings.TrimSpace(env.FieldMapping) == "" {
|
|
return "missing_field_mapping", fmt.Errorf("fields topic %q expects non-empty field mapping", strings.TrimSpace(topic))
|
|
}
|
|
if len(env.Fields) == 0 {
|
|
return "missing_fields", fmt.Errorf("fields topic %q expects non-empty fields payload", strings.TrimSpace(topic))
|
|
}
|
|
if err := ValidateProtocolFieldNames(env.Protocol, env.Fields); err != nil {
|
|
return "invalid_field_name", err
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func ValidateProtocolFieldNames(protocol envelope.Protocol, fields map[string]any) error {
|
|
prefix, ok := protocolFieldPrefix(protocol)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
invalid := make([]string, 0)
|
|
for key := range fields {
|
|
trimmed := strings.TrimSpace(key)
|
|
if key == trimmed && len(trimmed) > len(prefix) && strings.HasPrefix(trimmed, prefix) {
|
|
continue
|
|
}
|
|
invalid = append(invalid, key)
|
|
}
|
|
if len(invalid) == 0 {
|
|
return nil
|
|
}
|
|
sort.Strings(invalid)
|
|
if len(invalid) > 3 {
|
|
invalid = invalid[:3]
|
|
}
|
|
return fmt.Errorf("fields protocol %s expects every field name under %q; invalid fields: %q", protocol, prefix, invalid)
|
|
}
|
|
|
|
func protocolFieldPrefix(protocol envelope.Protocol) (string, bool) {
|
|
switch protocol {
|
|
case envelope.ProtocolGB32960:
|
|
return "gb32960.", true
|
|
case envelope.ProtocolJT808:
|
|
return "jt808.", true
|
|
case envelope.ProtocolYutongMQTT:
|
|
return "yutong_mqtt.", true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func ValidateKnownRawFieldsProtocols(raw map[string]string, fields map[string]string, valueName string) error {
|
|
for protocol, value := range raw {
|
|
if err := ValidateKnownRawTopicProtocol(value, protocol); err != nil {
|
|
return fmt.Errorf("raw %s for %s must match protocol: %w", valueName, protocol, err)
|
|
}
|
|
}
|
|
for protocol, value := range fields {
|
|
if err := ValidateKnownFieldsTopicProtocol(value, protocol); err != nil {
|
|
return fmt.Errorf("fields %s for %s must match protocol: %w", valueName, protocol, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateKafkaRawFields(raw map[string]string, fields map[string]string) error {
|
|
for name, topic := range raw {
|
|
topic = strings.TrimSpace(topic)
|
|
if topic == "" {
|
|
return fmt.Errorf("raw kafka topic for %s is empty", name)
|
|
}
|
|
if !strings.HasPrefix(topic, RawPrefix) {
|
|
return fmt.Errorf("raw kafka topic for %s must start with %q, got %q", name, RawPrefix, topic)
|
|
}
|
|
}
|
|
for name, topic := range fields {
|
|
topic = strings.TrimSpace(topic)
|
|
if topic == "" {
|
|
return fmt.Errorf("fields kafka topic for %s is empty", name)
|
|
}
|
|
if !strings.HasPrefix(topic, FieldsPrefix) {
|
|
return fmt.Errorf("fields kafka topic for %s must start with %q, got %q", name, FieldsPrefix, topic)
|
|
}
|
|
}
|
|
if err := ValidateKnownRawFieldsProtocols(raw, fields, "kafka topic"); err != nil {
|
|
return err
|
|
}
|
|
return ValidateRawFieldsDisjoint(raw, fields, "kafka topic")
|
|
}
|
|
|
|
func ValidateRawFieldsDisjoint(raw map[string]string, fields map[string]string, valueName string) error {
|
|
rawByValue := map[string]string{}
|
|
for name, value := range raw {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
rawByValue[value] = name
|
|
}
|
|
for fieldsName, value := range fields {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if rawName, ok := rawByValue[value]; ok {
|
|
return fmt.Errorf("raw and fields %s must be different: raw %s and fields %s both use %q", valueName, rawName, fieldsName, value)
|
|
}
|
|
}
|
|
return nil
|
|
}
|