diff --git a/internal/apijson/port.go b/internal/apijson/port.go index 80b323b..84fbb89 100644 --- a/internal/apijson/port.go +++ b/internal/apijson/port.go @@ -34,22 +34,33 @@ func Port(from any, to any) error { fromJSON := fromVal.FieldByName("JSON") toJSON := toVal.FieldByName("JSON") - // First, iterate through the from fields and load all the "normal" fields in the struct to the map of - // string to reflect.Value, as well as their raw .JSON.Foo counterpart. - for i := 0; i < fromType.NumField(); i++ { - field := fromType.Field(i) - ptag, ok := parseJSONStructTag(field) - if !ok { - continue - } - if ptag.name == "-" { - continue + // Iterate through the fields of v and load all the "normal" fields in the struct to the map of + // string to reflect.Value, as well as their raw .JSON.Foo counterpart indicated by j. + var getFields func(t reflect.Type, v, j reflect.Value) + getFields = func(t reflect.Type, v, j reflect.Value) { + // Recurse into anonymous fields first, since the fields on the object should win over the fields in the + // embedded object. + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.Anonymous { + getFields(field.Type, v.Field(i), v.FieldByName("JSON")) + continue + } } - values[ptag.name] = fromVal.Field(i) - if fromJSON.IsValid() { - fields[ptag.name] = fromJSON.FieldByName(field.Name) + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + ptag, ok := parseJSONStructTag(field) + if !ok || ptag.name == "-" { + continue + } + values[ptag.name] = v.Field(i) + if j.IsValid() { + fields[ptag.name] = j.FieldByName(field.Name) + } } } + getFields(fromType, fromVal, fromJSON) // Use the values from the previous step to populate the 'to' struct. for i := 0; i < toType.NumField(); i++ { diff --git a/internal/apijson/port_test.go b/internal/apijson/port_test.go index f9b6e3f..a26a15b 100644 --- a/internal/apijson/port_test.go +++ b/internal/apijson/port_test.go @@ -94,6 +94,37 @@ type CardMastercardData struct { Bar int64 `json:"bar"` } +type CommonFields struct { + Metadata Metadata `json:"metadata"` + Value string `json:"value"` +} + +type commonFieldsJSON struct { + Metadata Field + Value Field + ExtraFields map[string]Field +} + +type CardEmbedded struct { + CommonFields + Processor CardVisaProcessor `json:"processor"` + Data CardVisaData `json:"data"` + IsFoo bool `json:"is_foo"` + + JSON cardEmbeddedJSON +} + +type cardEmbeddedJSON struct { + commonFieldsJSON + Processor Field + Data Field + IsFoo Field + ExtraFields map[string]Field + raw string +} + +func (r cardEmbeddedJSON) RawJSON() string { return r.raw } + var portTests = map[string]struct { from any to any @@ -158,6 +189,51 @@ var portTests = map[string]struct { Value: false, }, }, + "embedded to card": { + CardEmbedded{ + CommonFields: CommonFields{ + Metadata: Metadata{ + CreatedAt: "Mar 29 2024", + }, + Value: "embedded_value", + }, + Processor: "visa", + IsFoo: true, + Data: CardVisaData{ + Foo: "embedded_foo", + }, + JSON: cardEmbeddedJSON{ + commonFieldsJSON: commonFieldsJSON{ + Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: valid}, + Value: Field{raw: `"embedded_value"`, status: valid}, + }, + raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"}}`, + Processor: Field{raw: `"visa"`, status: valid}, + IsFoo: Field{raw: `true`, status: valid}, + Data: Field{raw: `{"foo":"embedded_foo"}`, status: valid}, + }, + }, + Card{ + Processor: "visa", + IsFoo: true, + IsBar: false, + Data: CardVisaData{ + Foo: "embedded_foo", + }, + Metadata: Metadata{ + CreatedAt: "Mar 29 2024", + }, + Value: "embedded_value", + JSON: cardJSON{ + raw: "{\"processor\":\"visa\",\"is_foo\":true,\"data\":{\"foo\":\"embedded_foo\"}}", + Processor: Field{raw: `"visa"`, status: 0x3}, + IsFoo: Field{raw: "true", status: 0x3}, + Data: Field{raw: `{"foo":"embedded_foo"}`, status: 0x3}, + Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: 0x3}, + Value: Field{raw: `"embedded_value"`, status: 0x3}, + }, + }, + }, } func TestPort(t *testing.T) {