package backupformat import ( "errors" "fmt" "reflect" "github.com/hamba/avro/v2" ) func init() { avro.DefaultConfig.Register(spiceDBBackupNamespace+"."+schemaV1SchemaName, SchemaV1{}) avro.DefaultConfig.Register(spiceDBBackupNamespace+"."+relationshipV1SchemaName, RelationshipV1{}) } type RelationshipV1 struct { ObjectType string `avro:"object_type"` ObjectID string `avro:"object_id"` Relation string `avro:"relation"` SubjectObjectType string `avro:"subject_object_type"` SubjectObjectID string `avro:"subject_object_id"` SubjectRelation string `avro:"subject_relation"` CaveatName string `avro:"caveat_name"` CaveatContext []byte `avro:"caveat_context"` } type SchemaV1 struct { SchemaText string `avro:"schema_text"` } const ( spiceDBBackupNamespace = "com.authzed.spicedb.backup" relationshipV1SchemaName = "relationship_v1" schemaV1SchemaName = "schema_v1" metadataKeyZT = "com.authzed.spicedb.zedtoken.v1" ) func avroSchemaV1() (string, error) { relationshipSchema, err := recordSchemaFromAvroStruct( relationshipV1SchemaName, spiceDBBackupNamespace, RelationshipV1{}, ) if err != nil { return "", fmt.Errorf("unable to create schema: %w", err) } schemaSchema, err := recordSchemaFromAvroStruct( schemaV1SchemaName, spiceDBBackupNamespace, SchemaV1{}, ) if err != nil { return "", fmt.Errorf("unable to create avro SpiceDB schema schema: %w", err) } unionSchema, err := avro.NewUnionSchema([]avro.Schema{relationshipSchema, schemaSchema}) if err != nil { return "", fmt.Errorf("unable to create avro union schema: %w", err) } serialized, err := unionSchema.MarshalJSON() return string(serialized), err } func recordSchemaFromAvroStruct(name, namespace string, avroStruct any) (*avro.RecordSchema, error) { v := reflect.TypeOf(avroStruct) schemaFields := make([]*avro.Field, 0, v.NumField()) for i := 0; i < v.NumField(); i++ { f := v.Field(i) fieldName := f.Tag.Get("avro") if fieldName == "" { return nil, fmt.Errorf("field `%s` missing avro struct tag", f.Name) } fieldGoType := f.Type var fieldType avro.Type switch fieldGoType.Kind() { case reflect.String: fieldType = avro.String case reflect.Slice: if fieldGoType.Elem().Kind() != reflect.Uint8 { return nil, errors.New("unable to build schema for slice, only byte slices are supported") } fieldType = avro.Bytes default: return nil, fmt.Errorf("unsupported struct kind: %s", fieldGoType) } schemaField, err := avro.NewField(fieldName, avro.NewPrimitiveSchema(fieldType, nil)) if err != nil { return nil, fmt.Errorf("unable to create avro schema field: %w", err) } schemaFields = append(schemaFields, schemaField) } return avro.NewRecordSchema(name, namespace, schemaFields) }