Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/realtime/kv.go

158 lines
3.6 KiB
Go

package realtime
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
)
type RealtimeKVField struct {
Protocol envelope.Protocol
VIN string
Domain string
Field string
Value string
ValueType string
EventTimeMS int64
ReceivedAtMS int64
EventID string
}
func realtimeKVFields(env envelope.FrameEnvelope, parsed map[string]any) []RealtimeKVField {
vin := strings.TrimSpace(env.VIN)
if vin == "" {
return nil
}
eventID := env.StableEventID()
add := func(rows []RealtimeKVField, domain string, fields any) []RealtimeKVField {
flat := map[string]any{}
flattenKV("", fields, flat)
names := make([]string, 0, len(flat))
for name := range flat {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
value, valueType, ok := stringifyKVValue(flat[name])
if !ok {
continue
}
rows = append(rows, RealtimeKVField{
Protocol: env.Protocol,
VIN: vin,
Domain: domain,
Field: name,
Value: value,
ValueType: valueType,
EventTimeMS: env.EventTimeMS,
ReceivedAtMS: env.ReceivedAtMS,
EventID: eventID,
})
}
return rows
}
var rows []RealtimeKVField
keys := make([]string, 0, len(parsed))
for key := range parsed {
keys = append(keys, key)
}
sort.Strings(keys)
metadata := map[string]any{}
for _, key := range keys {
domain := strings.TrimSpace(key)
if domain == "" {
continue
}
value := parsed[key]
switch typed := value.(type) {
case map[string]any:
rows = add(rows, domain, typed)
case []any:
rows = add(rows, domain, typed)
case []map[string]any:
rows = add(rows, domain, typed)
default:
metadata[domain] = value
}
}
if len(metadata) > 0 {
rows = add(rows, "metadata", metadata)
}
return rows
}
func flattenKV(prefix string, value any, out map[string]any) {
switch typed := value.(type) {
case map[string]any:
keys := make([]string, 0, len(typed))
for key := range typed {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
next := key
if prefix != "" {
next = prefix + "." + key
}
flattenKV(next, typed[key], out)
}
case []any:
for index, item := range typed {
itemMap, ok := item.(map[string]any)
if !ok {
out[prefix] = typed
return
}
itemKey := strconv.Itoa(index)
next := itemKey
if prefix != "" {
next = prefix + "." + itemKey
}
flattenKV(next, itemMap, out)
}
case []map[string]any:
items := make([]any, len(typed))
for index, item := range typed {
items[index] = item
}
flattenKV(prefix, items, out)
default:
if prefix != "" && value != nil {
out[prefix] = value
}
}
}
func stringifyKVValue(value any) (string, string, bool) {
switch typed := value.(type) {
case string:
text := strings.TrimSpace(typed)
return text, "string", text != ""
case bool:
return strconv.FormatBool(typed), "bool", true
case float64:
return strconv.FormatFloat(typed, 'f', -1, 64), "number", true
case float32:
return strconv.FormatFloat(float64(typed), 'f', -1, 64), "number", true
case json.Number:
return typed.String(), "number", strings.TrimSpace(typed.String()) != ""
case int:
return strconv.Itoa(typed), "number", true
case int8, int16, int32, int64:
return fmt.Sprintf("%d", typed), "number", true
case uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", typed), "number", true
default:
data, err := json.Marshal(typed)
if err != nil || string(data) == "null" {
return "", "", false
}
return string(data), "json", true
}
}