mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-03 08:02:04 +00:00
179 lines
5.0 KiB
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)
|
|
}
|