diff --git a/README.md b/README.md index 45836c4..c1e8936 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ protoc --avro_out=. --avro_opt=namespace_map=foo:bar,baz:spam *.proto To Do List: * Add tests -* Map is currently outputting as Array due to how Protobuf handles [maps](https://protobuf.com/docs/descriptors#map-fields) (as repeated entries). Need to fix. * Need to decide on how to truly differentiate between optional and required fields (technically all fields are optional on Protobuf, but maybe we should use the actual `optional` keyword and only have those be optional in Avro?) * Split up `main.go` - it's trying to do too much diff --git a/avro/array.go b/avro/array.go index fcdf577..4ec0987 100644 --- a/avro/array.go +++ b/avro/array.go @@ -14,6 +14,16 @@ func (t Array) ToJSON(types *TypeRepo) (any, error) { if err != nil { return nil, fmt.Errorf("error parsing item type: %w", err) } + // you can't have a repeated map in protobuf, so if we are an array enclosing a map, + // we are *really* just a plain map. We'd see this code path because a map is encoded + // as a repeated "fake message type" that has key and value fields. + mapType, ok := itemJson.(*orderedmap.OrderedMap) + if ok { + returnedType, _ := mapType.Get("type") + if returnedType == "map" { + return itemJson, nil + } + } jsonMap := orderedmap.New() jsonMap.Set("type", "array") jsonMap.Set("items", itemJson) diff --git a/avro/field.go b/avro/field.go index 27dde12..e99bbd3 100644 --- a/avro/field.go +++ b/avro/field.go @@ -23,7 +23,7 @@ func (t Field) ToJSON(types *TypeRepo) (any, error) { if t.Default != "" { jsonMap.Set("default", t.Default) } else { - jsonMap.Set("default", DefaultValue(t.Type)) + jsonMap.Set("default", DefaultValue(typeJson)) } return jsonMap, nil } @@ -86,3 +86,36 @@ func BasicFieldTypeFromProto(proto *descriptorpb.FieldDescriptorProto) Type { } return Bare(proto.GetName()) } + +func DefaultValue(t any) any { + switch t { + case "null": + return nil + case "boolean": + return false + case "int": + return 0 + case "long": + return 0 + case "float": + return 0.0 + case "double": + return 0.0 + case "map": + return map[string]any{} + case "record": + return map[string]any{} + case "array": + return []any{} + } + + switch typedT := t.(type) { + case []any: + return DefaultValue(typedT[0]) + case *orderedmap.OrderedMap: + val, _ := typedT.Get("type") + return DefaultValue(val) + } + + return "" +} diff --git a/avro/map.go b/avro/map.go index ed4dda7..e49c04a 100644 --- a/avro/map.go +++ b/avro/map.go @@ -24,6 +24,12 @@ func (t Map) ToJSON(types *TypeRepo) (any, error) { if err != nil { return nil, fmt.Errorf("error parsing map value type: %w", err) } + // values are pretty much never a union of null and something else - but since Protobuf is + // optional by default, it seems like it is + mapType, ok := valueJson.([]any) + if ok && mapType[0] == "null"{ + valueJson = mapType[1] + } jsonMap := orderedmap.New() jsonMap.Set("type", "map") jsonMap.Set("values", valueJson) diff --git a/avro/type.go b/avro/type.go index b74c743..af3a921 100644 --- a/avro/type.go +++ b/avro/type.go @@ -29,35 +29,3 @@ func FullName(t NamedType) string { return fmt.Sprintf(".%s.%s", t.GetNamespace(), t.GetName()) } -func DefaultValue(t Type) any { - switch t { - case Bare("null"): - return nil - case Bare("boolean"): - return false - case Bare("int"): - return 0 - case Bare("long"): - return 0 - case Bare("float"): - return 0.0 - case Bare("double"): - return 0.0 - } - - switch typedT := t.(type) { - case Record: - return map[string]any{} - case Map: - return map[string]any{} - case Array: - return []string{} - case Union: - if typedT.Types[0] == Bare("null") { - return nil - } - return DefaultValue(typedT.Types[0]) - } - - return "" -}