generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: ensure Go runtime uses the correct field names without "json" ta…
…gs (#706) FTL's schema requires all fields be in "lowerCamelCase", but the go-runtime has been encoding fields with the literal field name, usually resulting in "UpperCamelCase". This change adds a custom encoder (because there is seemingly no existing library that will do this). Decoding should be fine because Go's JSON decoder is case-insensitive.
- Loading branch information
1 parent
d602c21
commit 3a15b55
Showing
3 changed files
with
163 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
// Package encoding defines the internal encoding that FTL uses to encode and | ||
// decode messages. It is currently JSON. | ||
package encoding | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"reflect" | ||
|
||
"github.com/iancoleman/strcase" | ||
) | ||
|
||
func Marshal(v any) ([]byte, error) { | ||
w := &bytes.Buffer{} | ||
err := encodeValue(reflect.ValueOf(v), w) | ||
return w.Bytes(), err | ||
} | ||
|
||
func encodeValue(v reflect.Value, w *bytes.Buffer) error { | ||
switch v.Kind() { | ||
case reflect.Struct: | ||
return encodeStruct(v, w) | ||
|
||
case reflect.Ptr: | ||
if v.IsNil() { | ||
w.WriteString("null") | ||
return nil | ||
} | ||
return encodeValue(v.Elem(), w) | ||
|
||
case reflect.Slice: | ||
return encodeSlice(v, w) | ||
|
||
case reflect.Map: | ||
return encodeMap(v, w) | ||
|
||
case reflect.String: | ||
return encodeString(v, w) | ||
|
||
case reflect.Int: | ||
return encodeInt(v, w) | ||
|
||
case reflect.Float64: | ||
return encodeFloat(v, w) | ||
|
||
case reflect.Bool: | ||
return encodeBool(v, w) | ||
|
||
default: | ||
return fmt.Errorf("unsupported type: %s", v.Type()) | ||
} | ||
} | ||
|
||
func encodeStruct(v reflect.Value, w *bytes.Buffer) error { | ||
w.WriteRune('{') | ||
for i := 0; i < v.NumField(); i++ { | ||
if i > 0 { | ||
w.WriteRune(',') | ||
} | ||
field := v.Type().Field(i) | ||
w.WriteString(`"` + strcase.ToLowerCamel(field.Name) + `":`) | ||
if err := encodeValue(v.Field(i), w); err != nil { | ||
return err | ||
} | ||
} | ||
w.WriteRune('}') | ||
return nil | ||
} | ||
|
||
func encodeSlice(v reflect.Value, w *bytes.Buffer) error { | ||
w.WriteRune('[') | ||
for i := 0; i < v.Len(); i++ { | ||
if i > 0 { | ||
w.WriteRune(',') | ||
} | ||
if err := encodeValue(v.Index(i), w); err != nil { | ||
return err | ||
} | ||
} | ||
w.WriteRune(']') | ||
return nil | ||
} | ||
|
||
func encodeMap(v reflect.Value, w *bytes.Buffer) error { | ||
w.WriteRune('{') | ||
for i, key := range v.MapKeys() { | ||
if i > 0 { | ||
w.WriteRune(',') | ||
} | ||
w.WriteRune('"') | ||
w.WriteString(key.String()) | ||
w.WriteString(`":`) | ||
if err := encodeValue(v.MapIndex(key), w); err != nil { | ||
return err | ||
} | ||
} | ||
w.WriteRune('}') | ||
return nil | ||
} | ||
|
||
func encodeBool(v reflect.Value, w *bytes.Buffer) error { | ||
if v.Bool() { | ||
w.WriteString("true") | ||
} else { | ||
w.WriteString("false") | ||
} | ||
return nil | ||
} | ||
|
||
func encodeInt(v reflect.Value, w *bytes.Buffer) error { | ||
fmt.Fprintf(w, "%d", v.Int()) | ||
return nil | ||
} | ||
|
||
func encodeFloat(v reflect.Value, w *bytes.Buffer) error { | ||
fmt.Fprintf(w, "%g", v.Float()) | ||
return nil | ||
} | ||
|
||
func encodeString(v reflect.Value, w *bytes.Buffer) error { | ||
w.WriteRune('"') | ||
fmt.Fprintf(w, "%s", v.String()) | ||
w.WriteRune('"') | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package encoding | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/alecthomas/assert/v2" | ||
) | ||
|
||
func TestMarshal(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
input any | ||
expected string | ||
err string | ||
}{ | ||
{name: "FieldRenaming", input: struct{ FooBar string }{""}, expected: `{"fooBar":""}`}, | ||
{name: "String", input: struct{ String string }{"foo"}, expected: `{"string":"foo"}`}, | ||
{name: "Int", input: struct{ Int int }{42}, expected: `{"int":42}`}, | ||
{name: "Float", input: struct{ Float float64 }{42.42}, expected: `{"float":42.42}`}, | ||
{name: "Bool", input: struct{ Bool bool }{true}, expected: `{"bool":true}`}, | ||
{name: "Nil", input: struct{ Nil *int }{nil}, expected: `{"nil":null}`}, | ||
{name: "Slice", input: struct{ Slice []int }{[]int{1, 2, 3}}, expected: `{"slice":[1,2,3]}`}, | ||
{name: "Map", input: struct{ Map map[string]int }{map[string]int{"foo": 42}}, expected: `{"map":{"foo":42}}`}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
actual, err := Marshal(tt.input) | ||
if tt.err != "" { | ||
assert.EqualError(t, err, tt.err) | ||
} else { | ||
assert.NoError(t, err) | ||
} | ||
assert.Equal(t, tt.expected, string(actual)) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters