1package vnext23import (4 "crypto/sha256"5 "encoding/json"6 "fmt"7 "slices"8)910// Kind is a stable primitive wire kind. New encodings may be added for a kind11// without changing its semantic identity.12type Kind uint81314const (15 KindInvalid Kind = iota16 KindBool17 KindInt6418 KindFloat6419 KindString20 KindBytes21 KindID22)2324func (kind Kind) valid() bool { return kind >= KindBool && kind <= KindID }2526// Field describes one independently evolvable claim on a type.27type Field struct {28 ID ID `json:"id"`29 Name string `json:"name"`30 Kind Kind `json:"kind"`31 Optional bool `json:"optional,omitempty"`32}3334// Type is a globally identified record whose fields are also globally35// identified. Declaration order carries no meaning.36type Type struct {37 ID ID `json:"id"`38 Name string `json:"name"`39 Fields []Field `json:"fields"`40}4142// Field finds a field by identity.43func (typ Type) Field(id ID) (Field, bool) {44 for _, field := range typ.Fields {45 if field.ID == id {46 return field, true47 }48 }49 return Field{}, false50}5152// Schema is the embedded vocabulary required to interpret a bundle.53type Schema struct {54 Types []Type `json:"types"`55}5657// Type finds a record type by identity.58func (schema Schema) Type(id ID) (Type, bool) {59 for _, typ := range schema.Types {60 if typ.ID == id {61 return typ, true62 }63 }64 return Type{}, false65}6667// Canonical returns the one order-independent JSON encoding of a schema.68func (schema Schema) Canonical() ([]byte, error) {69 normalized := Schema{Types: append([]Type(nil), schema.Types...)}70 for index := range normalized.Types {71 normalized.Types[index].Fields = append([]Field(nil), normalized.Types[index].Fields...)72 slices.SortFunc(normalized.Types[index].Fields, func(left, right Field) int {73 return slices.Compare(left.ID[:], right.ID[:])74 })75 }76 slices.SortFunc(normalized.Types, func(left, right Type) int {77 return slices.Compare(left.ID[:], right.ID[:])78 })79 if err := normalized.validate(); err != nil {80 return nil, err81 }82 data, err := json.Marshal(normalized)83 if err != nil {84 return nil, fmt.Errorf("marshal canonical schema: %w", err)85 }86 return data, nil87}8889// Hash is the cache identity of the canonical schema, never a compatibility90// gate. Readers merge the embedded schema with the schema they know.91func (schema Schema) Hash() ([32]byte, error) {92 data, err := schema.Canonical()93 if err != nil {94 return [32]byte{}, err95 }96 return sha256.Sum256(data), nil97}9899// UnionSchemas merges schemas produced independently. A shared ID must retain100// its wire kind; names are descriptive and converge deterministically.101func UnionSchemas(schemas ...Schema) (Schema, error) {102 types := make(map[ID]Type)103 for _, schema := range schemas {104 if err := mergeSchema(types, schema); err != nil {105 return Schema{}, err106 }107 }108 out := Schema{Types: make([]Type, 0, len(types))}109 for _, typ := range types {110 out.Types = append(out.Types, typ)111 }112 if _, err := out.Canonical(); err != nil {113 return Schema{}, err114 }115 return canonicalSchema(out), nil116}117118func mergeSchema(types map[ID]Type, schema Schema) error {119 for _, incoming := range schema.Types {120 if incoming.ID.isZero() || incoming.Name == "" {121 return fmt.Errorf("schema carries an unnamed or zero-ID type")122 }123 current, held := types[incoming.ID]124 if !held {125 current = Type{ID: incoming.ID, Name: incoming.Name}126 } else if incoming.Name < current.Name {127 current.Name = incoming.Name128 }129 fields := make(map[ID]Field, len(current.Fields)+len(incoming.Fields))130 for _, field := range current.Fields {131 fields[field.ID] = field132 }133 for _, field := range incoming.Fields {134 if err := mergeField(fields, field, incoming.ID); err != nil {135 return err136 }137 }138 current.Fields = current.Fields[:0]139 for _, field := range fields {140 current.Fields = append(current.Fields, field)141 }142 types[incoming.ID] = current143 }144 return nil145}146147func mergeField(fields map[ID]Field, incoming Field, typeID ID) error {148 if incoming.ID.isZero() || incoming.Name == "" || !incoming.Kind.valid() {149 return fmt.Errorf("type %s carries an invalid field", typeID)150 }151 current, held := fields[incoming.ID]152 if !held {153 fields[incoming.ID] = incoming154 return nil155 }156 if current.Kind != incoming.Kind {157 return fmt.Errorf("field %s is both wire kind %d and %d", incoming.ID, current.Kind, incoming.Kind)158 }159 if incoming.Name < current.Name {160 current.Name = incoming.Name161 }162 current.Optional = current.Optional || incoming.Optional163 fields[incoming.ID] = current164 return nil165}166167func (schema Schema) validate() error {168 types := make(map[ID]bool, len(schema.Types))169 for _, typ := range schema.Types {170 if typ.ID.isZero() || typ.Name == "" || types[typ.ID] {171 return fmt.Errorf("schema carries a duplicate, unnamed, or zero-ID type")172 }173 types[typ.ID] = true174 fields := make(map[ID]bool, len(typ.Fields))175 for _, field := range typ.Fields {176 if field.ID.isZero() || field.Name == "" || !field.Kind.valid() || fields[field.ID] {177 return fmt.Errorf("type %s carries a duplicate or invalid field", typ.ID)178 }179 fields[field.ID] = true180 }181 }182 return nil183}184185func canonicalSchema(schema Schema) Schema {186 for index := range schema.Types {187 slices.SortFunc(schema.Types[index].Fields, func(left, right Field) int {188 return slices.Compare(left.ID[:], right.ID[:])189 })190 }191 slices.SortFunc(schema.Types, func(left, right Type) int {192 return slices.Compare(left.ID[:], right.ID[:])193 })194 return schema195}