package microcosm import ( "encoding/json" "errors" "math" "strings" "provenance/internal/atproto" "provenance/internal/domain" ) type recordField int const ( fieldType recordField = iota fieldSubject fieldScore fieldTag fieldCreatedAt fieldComment fieldList fieldPurpose fieldCount ) var recordKeys = [fieldCount]string{"$type", "subject", "score", "tag", "createdAt", "comment", "list", "purpose"} type recordFields struct { values [fieldCount]any err error } func (c *commitEvent) hasRecord() bool { return c.values != nil || len(c.Record) > 0 && string(c.Record) != "null" } // Legacy streams parse JSON. SDK records already contain decoded values. func (c *commitEvent) readRecord(out any) error { if c.values == nil { return json.Unmarshal(c.Record, out) } // Preserve JSON alias and null rules for noncanonical field names. for name := range c.values { field := mappedField(name) if field != fieldCount && name != recordKeys[field] { data, err := json.Marshal(c.values) if err != nil { return err } return json.Unmarshal(data, out) } } fields := mappedFields(c.values) switch record := out.(type) { case *atproto.Record: *record = atproto.Record{Type: fields.text(fieldType), Subject: fields.text(fieldSubject), Score: fields.score(), Tag: fields.text(fieldTag), CreatedAt: fields.text(fieldCreatedAt), Comment: fields.text(fieldComment)} case *atproto.SoftRecord: *record = atproto.SoftRecord{Type: fields.text(fieldType), Subject: fields.text(fieldSubject), CreatedAt: fields.text(fieldCreatedAt)} case *atproto.ListRecord: *record = atproto.ListRecord{Type: fields.text(fieldType), Subject: fields.text(fieldSubject), List: fields.text(fieldList), Purpose: fields.text(fieldPurpose), CreatedAt: fields.text(fieldCreatedAt)} default: return errors.New("unsupported replay record type") } return fields.err } func mappedFields(values map[string]any) recordFields { var fields recordFields for field, key := range recordKeys { fields.values[field] = values[key] } return fields } func mappedField(name string) recordField { switch name { case "$type": return fieldType case "subject": return fieldSubject case "score": return fieldScore case "tag": return fieldTag case "createdAt": return fieldCreatedAt case "comment": return fieldComment case "list": return fieldList case "purpose": return fieldPurpose } for field, key := range recordKeys { if strings.EqualFold(name, key) { return recordField(field) } } return fieldCount } func (f *recordFields) text(field recordField) string { if f.err != nil || f.values[field] == nil { return "" } value, ok := f.values[field].(string) if !ok { f.err = errors.New("replay record field must be a string") } return value } func (f *recordFields) score() *int { if f.err != nil || f.values[fieldScore] == nil { return nil } // The SDK represents ATProto numbers as float64. Scores require bounded integers. value, ok := f.values[fieldScore].(float64) if !ok || math.IsNaN(value) || value < domain.Blocked || value > domain.MaxScore || math.Trunc(value) != value { f.err = errors.New("replay record score must be an integer within the explicit score range") return nil } score := int(value) return &score }