Repository: hamba/avro
Pull request #10 (2019-04-05 10:26:33)

Added schema property support


Patch:
diff --git a/schema.go b/schema.go
index 173ddf2..7318325 100644
--- a/schema.go
+++ b/schema.go
@@ -83,9 +83,21 @@ type Schema interface {
 	Fingerprint() [32]byte
 }
 
+// PropertySchema represents a schema with properties.
+type PropertySchema interface {
+	// AddProp adds a property to the schema.
+	//
+	// AddProp will not overwrite existing properties.
+	AddProp(name string, value interface{})
+
+	// Prop gets a property from the schema.
+	Prop(string) interface{}
+}
+
 // NamedSchema represents a schema with a name.
 type NamedSchema interface {
 	Schema
+	PropertySchema
 
 	// Name returns the name of the schema.
 	Name() string
@@ -156,6 +168,44 @@ func (f *fingerprinter) Fingerprint(stringer fmt.Stringer) [32]byte {
 	return f.fingerprint
 }
 
+type properties struct {
+	reserved []string
+	props    map[string]interface{}
+}
+
+// AddProp adds a property to the schema.
+//
+// AddProp will not overwrite existing properties.
+func (p *properties) AddProp(name string, value interface{}) {
+	// Create the props is needed.
+	if p.props == nil {
+		p.props = map[string]interface{}{}
+	}
+
+	// Dont allow reserved properties
+	for _, res := range p.reserved {
+		if name == res {
+			return
+		}
+	}
+
+	// Dont overwrite a property
+	if _, ok := p.props[name]; ok {
+		return
+	}
+
+	p.props[name] = value
+}
+
+// Prop gets a property from the schema.
+func (p *properties) Prop(name string) interface{} {
+	if p.props == nil {
+		return nil
+	}
+
+	return p.props[name]
+}
+
 // PrimitiveSchema is an Avro primitive type schema.
 type PrimitiveSchema struct {
 	fingerprinter
@@ -188,6 +238,7 @@ func (s *PrimitiveSchema) Fingerprint() [32]byte {
 // RecordSchema is an Avro record type schema.
 type RecordSchema struct {
 	name
+	properties
 	fingerprinter
 
 	fields []*Field
@@ -201,7 +252,8 @@ func NewRecordSchema(name, space string) (*RecordSchema, error) {
 	}
 
 	return &RecordSchema{
-		name: n,
+		name:       n,
+		properties: properties{reserved: schemaReserved},
 	}, nil
 }
 
@@ -243,6 +295,8 @@ func (s *RecordSchema) Fingerprint() [32]byte {
 
 // Field is an Avro record type field.
 type Field struct {
+	properties
+
 	name string
 	typ  Schema
 	def  interface{}
@@ -260,9 +314,10 @@ func NewField(name string, typ Schema, def interface{}) (*Field, error) {
 	}
 
 	return &Field{
-		name: name,
-		typ:  typ,
-		def:  def,
+		properties: properties{reserved: fieldReserved},
+		name:       name,
+		typ:        typ,
+		def:        def,
 	}, nil
 }
 
@@ -291,6 +346,7 @@ func (s *Field) String() string {
 // EnumSchema is an Avro enum type schema.
 type EnumSchema struct {
 	name
+	properties
 	fingerprinter
 
 	symbols []string
@@ -313,8 +369,9 @@ func NewEnumSchema(name, namespace string, symbols []string) (*EnumSchema, error
 	}
 
 	return &EnumSchema{
-		name:    n,
-		symbols: symbols,
+		name:       n,
+		properties: properties{reserved: schemaReserved},
+		symbols:    symbols,
 	}, nil
 }
 
@@ -348,6 +405,7 @@ func (s *EnumSchema) Fingerprint() [32]byte {
 
 // ArraySchema is an Avro array type schema.
 type ArraySchema struct {
+	properties
 	fingerprinter
 
 	items Schema
@@ -356,7 +414,8 @@ type ArraySchema struct {
 // NewArraySchema creates an array schema instance.
 func NewArraySchema(items Schema) *ArraySchema {
 	return &ArraySchema{
-		items: items,
+		properties: properties{reserved: schemaReserved},
+		items:      items,
 	}
 }
 
@@ -382,6 +441,7 @@ func (s *ArraySchema) Fingerprint() [32]byte {
 
 // MapSchema is an Avro map type schema.
 type MapSchema struct {
+	properties
 	fingerprinter
 
 	values Schema
@@ -390,7 +450,8 @@ type MapSchema struct {
 // NewMapSchema creates a map schema instance.
 func NewMapSchema(values Schema) *MapSchema {
 	return &MapSchema{
-		values: values,
+		properties: properties{reserved: schemaReserved},
+		values:     values,
 	}
 }
 
@@ -485,6 +546,7 @@ func (s *UnionSchema) Fingerprint() [32]byte {
 // FixedSchema is an Avro fixed type schema.
 type FixedSchema struct {
 	name
+	properties
 	fingerprinter
 
 	size int
@@ -498,8 +560,9 @@ func NewFixedSchema(name, namespace string, size int) (*FixedSchema, error) {
 	}
 
 	return &FixedSchema{
-		name: n,
-		size: size,
+		name:       n,
+		properties: properties{reserved: schemaReserved},
+		size:       size,
 	}, nil
 }
 
diff --git a/schema_parse.go b/schema_parse.go
index 256ee78..634b42b 100644
--- a/schema_parse.go
+++ b/schema_parse.go
@@ -9,6 +9,11 @@ import (
 	"github.com/json-iterator/go"
 )
 
+var (
+	schemaReserved = []string{"doc", "fields", "items", "name", "namespace", "size", "symbols", "values", "type", "aliases"}
+	fieldReserved = []string{"default", "doc", "name", "order", "type", "aliases"}
+)
+
 // Parse parses a schema string.
 func Parse(schema string) (Schema, error) {
 	var json interface{}
@@ -145,6 +150,10 @@ func parseRecord(namespace string, m map[string]interface{}) (Schema, error) {
 
 	schemaConfig.addSchemaToCache(rec.FullName(), NewRefSchema(rec))
 
+	for k, v := range m {
+		rec.AddProp(k, v)
+	}
+
 	for _, f := range fields {
 		field, err := parseField(namespace, f)
 		if err != nil {
@@ -182,6 +191,10 @@ func parseField(namespace string, v interface{}) (*Field, error) {
 		return nil, err
 	}
 
+	for k, v := range m {
+		field.AddProp(k, v)
+	}
+
 	return field, nil
 }
 
@@ -216,6 +229,10 @@ func parseEnum(namespace string, m map[string]interface{}) (Schema, error) {
 
 	schemaConfig.addSchemaToCache(enum.FullName(), enum)
 
+	for k, v := range m {
+		enum.AddProp(k, v)
+	}
+
 	return enum, nil
 }
 
@@ -230,7 +247,13 @@ func parseArray(namespace string, m map[string]interface{}) (Schema, error) {
 		return nil, err
 	}
 
-	return NewArraySchema(schema), nil
+	arr := NewArraySchema(schema)
+
+	for k, v := range m {
+		arr.AddProp(k, v)
+	}
+
+	return arr, nil
 }
 
 func parseMap(namespace string, m map[string]interface{}) (Schema, error) {
@@ -244,7 +267,13 @@ func parseMap(namespace string, m map[string]interface{}) (Schema, error) {
 		return nil, err
 	}
 
-	return NewMapSchema(schema), nil
+	ms := NewMapSchema(schema)
+
+	for k, v := range m {
+		ms.AddProp(k, v)
+	}
+
+	return ms, nil
 }
 
 func parseUnion(namespace string, v []interface{}) (Schema, error) {
@@ -281,6 +310,10 @@ func parseFixed(namespace string, m map[string]interface{}) (Schema, error) {
 
 	schemaConfig.addSchemaToCache(fixed.FullName(), fixed)
 
+	for k, v := range m {
+		fixed.AddProp(k, v)
+	}
+
 	return fixed, nil
 }
 


Test patch:
diff --git a/schema_internal_test.go b/schema_internal_test.go
index eaeb112..bf95bef 100644
--- a/schema_internal_test.go
+++ b/schema_internal_test.go
@@ -46,6 +46,29 @@ func TestName_InvalidNamespaceOtherChar(t *testing.T) {
 	assert.Error(t, err)
 }
 
+func TestProperties_AddPropDoesNotAddReservedProperties(t *testing.T) {
+	p := properties{reserved: []string{"test"}}
+
+	p.AddProp("test", "foo")
+
+	assert.Nil(t, p.Prop("test"))
+}
+
+func TestProperties_AddPropDoesNotOverwriteProperties(t *testing.T) {
+	p := properties{}
+
+	p.AddProp("test", "foo")
+	p.AddProp("test", "bar")
+
+	assert.Equal(t, "foo", p.Prop("test"))
+}
+
+func TestProperties_PropGetsFromEmptySet(t *testing.T) {
+	p := properties{}
+
+	assert.Nil(t, p.Prop("test"))
+}
+
 func TestIsValidDefault(t *testing.T) {
 	tests := []struct {
 		name     string
diff --git a/schema_test.go b/schema_test.go
index 670b6c1..7abc960 100644
--- a/schema_test.go
+++ b/schema_test.go
@@ -167,11 +167,6 @@ func TestRecordSchema(t *testing.T) {
 			schema:  `{"type":"record", "name":"test", "namespace": "org.apache.avro", "doc": "docs", "fields":[{"name": "field", "type": "int"}]}`,
 			wantErr: false,
 		},
-		{
-			name:    "Full Name",
-			schema:  `{"type":"record", "name":"org.apache.avro.test", "doc": "docs", "fields":[{"name": "field", "type": "int"}]}`,
-			wantErr: false,
-		},
 		{
 			name:    "Invalid Name First Char",
 			schema:  `{"type":"record", "name":"0test", "namespace": "org.apache.avro", "fields":[{"name": "field", "type": "int"}]}`,
@@ -356,6 +351,27 @@ func TestRecordSchema_ValidatesDefault(t *testing.T) {
 	}
 }
 
+func TestRecordSchema_HandlesProps(t *testing.T) {
+	schm := `
+{
+   "type": "record",
+   "name": "valid_name",
+   "namespace": "org.apache.avro",
+   "foo": "bar1",
+   "fields": [
+       {"name": "intField", "type": "int", "foo": "bar2"}
+   ]
+}
+`
+
+	s, err := avro.Parse(schm)
+
+	assert.NoError(t, err)
+	assert.Equal(t, avro.Record, s.Type())
+	assert.Equal(t, "bar1", s.(*avro.RecordSchema).Prop("foo"))
+	assert.Equal(t, "bar2", s.(*avro.RecordSchema).Fields()[0].Prop("foo"))
+}
+
 func TestRecordSchema_WithReference(t *testing.T) {
 	schm := `
 {
@@ -363,14 +379,8 @@ func TestRecordSchema_WithReference(t *testing.T) {
    "name": "valid_name",
    "namespace": "org.apache.avro",
    "fields": [
-       {
-           "name": "intField",
-           "type": "int"
-       },
-       {
-           "name": "Ref",
-           "type": "valid_name"
-       }
+       {"name": "intField", "type": "int"},
+       {"name": "Ref", "type": "valid_name"}
    ]
 }
 `
@@ -460,6 +470,16 @@ func TestEnumSchema(t *testing.T) {
 	}
 }
 
+func TestEnumSchema_HandlesProps(t *testing.T) {
+	schm := `{"type":"enum", "name":"test", "namespace": "org.apache.avro", "symbols":["TEST"], "foo":"bar"}`
+
+	s, err := avro.Parse(schm)
+
+	assert.NoError(t, err)
+	assert.Equal(t, avro.Enum, s.Type())
+	assert.Equal(t, "bar", s.(*avro.EnumSchema).Prop("foo"))
+}
+
 func TestArraySchema(t *testing.T) {
 	tests := []struct {
 		name    string
@@ -498,6 +518,16 @@ func TestArraySchema(t *testing.T) {
 	}
 }
 
+func TestArraySchema_HandlesProps(t *testing.T) {
+	schm := `{"type":"array", "items": "int", "foo":"bar"}`
+
+	s, err := avro.Parse(schm)
+
+	assert.NoError(t, err)
+	assert.Equal(t, avro.Array, s.Type())
+	assert.Equal(t, "bar", s.(*avro.ArraySchema).Prop("foo"))
+}
+
 func TestMapSchema(t *testing.T) {
 	tests := []struct {
 		name    string
@@ -536,6 +566,16 @@ func TestMapSchema(t *testing.T) {
 	}
 }
 
+func TestMapSchema_HandlesProps(t *testing.T) {
+	schm := `{"type":"map", "values": "int", "foo":"bar"}`
+
+	s, err := avro.Parse(schm)
+
+	assert.NoError(t, err)
+	assert.Equal(t, avro.Map, s.Type())
+	assert.Equal(t, "bar", s.(*avro.MapSchema).Prop("foo"))
+}
+
 func TestUnionSchema(t *testing.T) {
 	tests := []struct {
 		name            string
@@ -663,6 +703,16 @@ func TestFixedSchema(t *testing.T) {
 	}
 }
 
+func TestFixedSchema_HandlesProps(t *testing.T) {
+	schm := `{"type":"fixed", "name":"test", "namespace": "org.apache.avro", "size": 12, "foo":"bar"}`
+
+	s, err := avro.Parse(schm)
+
+	assert.NoError(t, err)
+	assert.Equal(t, avro.Fixed, s.Type())
+	assert.Equal(t, "bar", s.(*avro.FixedSchema).Prop("foo"))
+}
+
 func TestSchema_Interop(t *testing.T) {
 	schm := `
 {