Files
warmbly/internal/models/event_schema_document.go

179 lines
5.0 KiB
Go

package models
import (
"encoding/json"
"fmt"
"strings"
"github.com/hamba/avro/v2"
)
// SchemaDocument is the JSON to hand a schema registry for one of the derived
// schemas. It is not json.Marshal(schema), because the library's MarshalJSON
// writes two defaults in a form the spec forbids and no parser accepts, while
// Schema.String() drops every default (#583, #586):
//
// - a fixed field's default is written as the byte array it was validated
// into, where the spec wants a string of code points 0-255. Every uint64
// on the bus is a fixed
// - inside a record's default, a nested optional field's null is stored as
// the library's internal sentinel and comes out as {}
//
// So the marshalled document is re-read and every default is normalised
// against the type it belongs to, then parsed back before it is used: the
// failure this guards against is exactly a document that marshals fine and
// parses never.
func SchemaDocument(s avro.Schema) ([]byte, error) {
raw, err := json.Marshal(s)
if err != nil {
return nil, err
}
var doc any
if err := json.Unmarshal(raw, &doc); err != nil {
return nil, err
}
(&schemaDefaults{named: map[string]any{}}).walk(doc, "")
out, err := json.Marshal(doc)
if err != nil {
return nil, err
}
if _, err := avro.Parse(string(out)); err != nil {
return nil, fmt.Errorf("schema document does not round-trip: %w", err)
}
return out, nil
}
// schemaDefaults normalises defaults in a parsed schema document. It walks in
// definition order and remembers every named type, because Avro defines a
// named type at its first appearance and refers to it by name after that, so
// a field typed by name can only be normalised once its definition was seen.
type schemaDefaults struct {
named map[string]any
}
func (d *schemaDefaults) walk(node any, namespace string) {
switch v := node.(type) {
case []any:
for _, b := range v {
d.walk(b, namespace)
}
case map[string]any:
if ns, ok := v["namespace"].(string); ok && ns != "" {
namespace = ns
}
switch v["type"] {
case "fixed", "enum":
d.named[fullName(v["name"], namespace)] = v
case "record":
d.named[fullName(v["name"], namespace)] = v
fields, _ := v["fields"].([]any)
for _, f := range fields {
field, ok := f.(map[string]any)
if !ok {
continue
}
d.walk(field["type"], namespace)
if def, ok := field["default"]; ok {
field["default"] = d.normalize(field["type"], def, namespace)
}
}
case "array":
// A record can be defined for the first time inside an array's
// items (a []Mailbox), and everything referring to it by name
// afterwards depends on it having been seen here.
d.walk(v["items"], namespace)
case "map":
d.walk(v["values"], namespace)
default:
d.walk(v["type"], namespace)
}
}
}
// normalize returns the spec's form of a default for a value of type typ.
func (d *schemaDefaults) normalize(typ, def any, namespace string) any {
switch t := typ.(type) {
case string:
// A primitive name, or a reference to a named type seen earlier.
if named, ok := d.named[fullName(t, namespace)]; ok {
return d.normalize(named, def, namespace)
}
return def
case []any:
// A union default is written against its first branch. When that is
// null, the library's sentinel arrives here as an empty object.
if len(t) == 0 {
return def
}
if t[0] == "null" {
if m, ok := def.(map[string]any); ok && len(m) == 0 {
return nil
}
return def
}
return d.normalize(t[0], def, namespace)
case map[string]any:
if ns, ok := t["namespace"].(string); ok && ns != "" {
namespace = ns
}
switch t["type"] {
case "fixed":
if bytes, ok := def.([]any); ok {
return bytesToDefaultString(bytes)
}
case "record":
m, ok := def.(map[string]any)
if !ok {
return def
}
fields, _ := t["fields"].([]any)
for _, f := range fields {
field, ok := f.(map[string]any)
if !ok {
continue
}
name, _ := field["name"].(string)
if v, ok := m[name]; ok {
m[name] = d.normalize(field["type"], v, namespace)
}
}
return m
case "array":
if items, ok := def.([]any); ok {
for i, v := range items {
items[i] = d.normalize(t["items"], v, namespace)
}
}
case "map":
if m, ok := def.(map[string]any); ok {
for k, v := range m {
m[k] = d.normalize(t["values"], v, namespace)
}
}
default:
// A wrapped primitive, e.g. a long with a logical type.
return d.normalize(t["type"], def, namespace)
}
}
return def
}
func fullName(name any, namespace string) string {
n, _ := name.(string)
if namespace == "" || strings.Contains(n, ".") {
return n
}
return namespace + "." + n
}
// bytesToDefaultString is the spec's encoding for a bytes or fixed default:
// each byte becomes the Unicode code point of the same value.
func bytesToDefaultString(bytes []any) string {
runes := make([]rune, 0, len(bytes))
for _, b := range bytes {
n, _ := b.(float64)
runes = append(runes, rune(int(n)&0xff))
}
return string(runes)
}