Skip to content

Commit

Permalink
better support for struct-field-typed record field defaults
Browse files Browse the repository at this point in the history
This helps #37 but doesn't fix it entirely.

We also fail generation if there's a default value that doesn't
contain all the required fields, rather than generating invalid code.
  • Loading branch information
rogpeppe committed Apr 6, 2020
1 parent 4c8b655 commit f6940c4
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 28 deletions.
11 changes: 4 additions & 7 deletions cmd/avrogo/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func fprintf(w io.Writer, f string, a ...interface{}) {
fmt.Fprintf(w, f, a...)
}

func (gc *generateContext) RecordInfoLiteral(t *schema.RecordDefinition) string {
func (gc *generateContext) RecordInfoLiteral(t *schema.RecordDefinition) (string, error) {
w := new(strings.Builder)
fprintf(w, "avrotypegen.RecordInfo{\n")
schemaStr, err := t.Schema()
Expand Down Expand Up @@ -128,7 +128,7 @@ func (gc *generateContext) RecordInfoLiteral(t *schema.RecordDefinition) string
fprintf(w, "%d: ", i)
lit, err := gc.defaultFuncLiteral(f.Default(), f.Type())
if err != nil {
fprintf(w, "func() interface{} {}, // ERROR: %v\n", err)
return "", fmt.Errorf("cannot generate code for field %s of record %v: %v", f.Name(), t.AvroName(), err)
} else {
fprintf(w, "func() interface{} {\nreturn %s\n},\n", lit)
}
Expand All @@ -155,7 +155,7 @@ func (gc *generateContext) RecordInfoLiteral(t *schema.RecordDefinition) string
fprintf(w, "},\n")
}
fprintf(w, "}")
return w.String()
return w.String(), nil
}

// canOmitUnionInfo reports whether the info for the
Expand Down Expand Up @@ -364,10 +364,7 @@ func (gc *generateContext) defaultFuncLiteral(v interface{}, t schema.AvroType)
fieldVal, ok := m[field.Name()]
var lit string
if !ok {
if !field.HasDefault() {
return "", fmt.Errorf("field %q not present", field.Name())
}
fieldVal = field.Default()
return "", fmt.Errorf("field %q of record %s must be present in default value but is missing", field.Name(), t.TypeName)
}
lit, err := gc.defaultFuncLiteral(fieldVal, field.Type())
if err != nil {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
},
"default": {
"F1": 44,
"F2": "whee"
"F2": "whee",
"F3": "ok"
}
}
]
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 30 additions & 1 deletion cmd/avrogo/testdata/default.cue
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,46 @@ tests: recordDefault: {
default: {
F1: 44
F2: "whee"
F3: "ok"
}
}]
}
inData: emptyRecordData
outData: recordField: {
F1: 44
F2: "whee"
F3: "hello"
F3: "ok"
}
}

tests: recordDefaultFieldNotProvided: {
inSchema: emptyRecord
inSchema: name: "R"
outSchema: {
type: "record"
name: "R"
fields: [{
name: "recordField"
type: {
type: "record"
name: "Foo"
fields: [{
name: "F1"
type: "string"
}, {
name: "F2"
type: "string"
default: "hello"
}]
}
default: {
F1: ""
}
}]
}
generateError: #"avrogo: cannot generate code for schema.avsc: template: .*: executing "" at <\$.Ctx.RecordInfoLiteral>: error calling RecordInfoLiteral: cannot generate code for field recordField of record R: field "F2" of record Foo must be present in default value but is missing"#
}

tests: enumDefault: {
inSchema: emptyRecord
inSchema: name: "R"
Expand Down
55 changes: 41 additions & 14 deletions gotype.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func (gts *goTypeSchema) schemaForGoType(t reflect.Type) (interface{}, error) {
if t == nil {
return "null", nil
}
if r, ok := reflect.Zero(t).Interface().(avrotypegen.AvroRecord); ok {
if r := avroRecordOf(t); r != nil {
// It's a generated type which comes with its own schema.
return gts.define(t, json.RawMessage(r.AvroRecord().Schema), "")
}
Expand Down Expand Up @@ -226,9 +226,13 @@ func (gts *goTypeSchema) schemaForGoType(t reflect.Type) (interface{}, error) {
if err != nil {
return nil, err
}
d, err := gts.defaultForType(f.Type)
if err != nil {
return nil, err
}
fields = append(fields, map[string]interface{}{
"name": name,
"default": gts.defaultForType(f.Type),
"default": d,
"type": ftype,
})
}
Expand Down Expand Up @@ -417,31 +421,54 @@ func isDigit(c byte) bool {
return '0' <= c && c <= '9'
}

func (gts *goTypeSchema) defaultForType(t reflect.Type) interface{} {
func (gts *goTypeSchema) defaultForType(t reflect.Type) (interface{}, error) {
// TODO perhaps a Go slice/map should accept a union
// of null and array/map? See https://github.com/heetch/avro/issues/19
switch t.Kind() {
case reflect.Slice:
return reflect.MakeSlice(t, 0, 0).Interface()
return reflect.MakeSlice(t, 0, 0).Interface(), nil
case reflect.Map:
return reflect.MakeMap(t).Interface()
return reflect.MakeMap(t).Interface(), nil
case reflect.Array:
return strings.Repeat("\u0000", t.Len())
return strings.Repeat("\u0000", t.Len()), nil
case reflect.Struct:
if t == timeType {
return 0
return 0, nil
}
// TODO support other struct types better - we're using
// default JSON marshaling here, and that won't work
// when the struct contains enum types or time.Time.
// See https://github.com/heetch/avro/issues/37
fallthrough
if avroRecordOf(t) != nil {
// It's a generated type - producing a correctly formed default value
// for it needs a bit more work so we punt on doing it for now.
// TODO make default values for struct-typed fields work in all cases.
return nil, fmt.Errorf("value fields of struct types generated by avrogo are not yet supported (type %s)", t)
}
fields := make(map[string]interface{})
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if f.Anonymous {
return nil, fmt.Errorf("anonymous fields not yet supported (in %s)", t)
}
name, _ := typeinfo.JSONFieldName(f)
if name == "" {
continue
}
v, err := gts.defaultForType(f.Type)
if err != nil {
return nil, err
}
fields[name] = v
}
return fields, nil
default:
if def, ok := gts.defs[t]; ok {
if o, ok := def.schema.(map[string]interface{}); ok && o["type"] == "enum" {
return reflect.Zero(t).Interface().(fmt.Stringer).String()
return reflect.Zero(t).Interface().(fmt.Stringer).String(), nil
}
}
return reflect.Zero(t).Interface()
return reflect.Zero(t).Interface(), nil
}
}

func avroRecordOf(t reflect.Type) avrotypegen.AvroRecord {
r, _ := reflect.Zero(t).Interface().(avrotypegen.AvroRecord)
return r
}
75 changes: 75 additions & 0 deletions gotype_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ func TestGoTypeWithTime(t *testing.T) {
c.Assert(x, qt.DeepEquals, R{
T: time.Date(2020, 1, 15, 18, 47, 8, 888888000, time.UTC),
})

c.Assert(mustTypeOf(R{}).String(), qt.JSONEquals, json.RawMessage(`{
"type": "record",
"name": "R",
"fields": [{
"name": "T",
"default": 0,
"type": {
"logicalType": "timestamp-micros",
"type": "long"
}
}]
}`))
}

func TestGoTypeWithZeroTime(t *testing.T) {
Expand All @@ -160,6 +173,68 @@ func TestGoTypeWithZeroTime(t *testing.T) {
}
}

func TestGoTypeWithStructField(t *testing.T) {
c := qt.New(t)
type F2 struct {
F3 int
}
type F1 struct {
// Make sure we're respecting JSON tags and unexported fields.
ignore int
F2 F2 `json:"f2"`
}
type R struct {
F1 F1
}

c.Assert(mustTypeOf(R{}).String(), qt.JSONEquals, json.RawMessage(`{
"name": "R",
"type": "record",
"fields": [
{
"name": "F1",
"type": {
"name": "F1",
"type": "record",
"fields": [
{
"name": "f2",
"type": {
"name": "F2",
"type": "record",
"fields": [
{
"name": "F3",
"type": "long",
"default": 0
}
]
},
"default": {
"F3": 0
}
}
]
},
"default": {
"f2": {
"F3": 0
}
}
}
]
}`))
}

func TestGoTypeWithGeneratedGoStruct(t *testing.T) {
c := qt.New(t)
type R struct {
F TestRecord
}
_, err := avro.TypeOf(R{})
c.Assert(err, qt.ErrorMatches, `value fields of struct types generated by avrogo are not yet supported \(type avro_test.TestRecord\)`)
}

func TestGoTypeStringerEnum(t *testing.T) {
c := qt.New(t)
type R struct {
Expand Down

0 comments on commit f6940c4

Please sign in to comment.