Skip to content

Commit

Permalink
feat: add array support to http ingress (#915)
Browse files Browse the repository at this point in the history
Co-authored-by: Alec Thomas <[email protected]>
  • Loading branch information
wesbillman and alecthomas authored Feb 13, 2024
1 parent 5926a17 commit 2c5f3fe
Show file tree
Hide file tree
Showing 14 changed files with 415 additions and 112 deletions.
102 changes: 64 additions & 38 deletions backend/controller/ingress/alias.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,83 @@
package ingress

import (
"fmt"

"github.com/TBD54566975/ftl/backend/schema"
)

func transformFromAliasedFields(dataRef *schema.DataRef, sch *schema.Schema, request map[string]any) (map[string]any, error) {
data, err := sch.ResolveDataRefMonomorphised(dataRef)
if err != nil {
return nil, err
}
func transformAliasedFields(sch *schema.Schema, t schema.Type, obj any, aliaser func(obj map[string]any, field *schema.Field) string) error {
switch t := t.(type) {
case *schema.DataRef:
data, err := sch.ResolveDataRefMonomorphised(t)
if err != nil {
return fmt.Errorf("failed to resolve data type: %w", err)
}
m, ok := obj.(map[string]any)
if !ok {
return fmt.Errorf("expected map, got %T", obj)
}
for _, field := range data.Fields {
name := aliaser(m, field)
if err := transformAliasedFields(sch, field.Type, m[name], aliaser); err != nil {
return err
}
}

for _, field := range data.Fields {
if _, ok := request[field.Name]; !ok && field.Alias != "" && request[field.Alias] != nil {
request[field.Name] = request[field.Alias]
delete(request, field.Alias)
case *schema.Array:
a, ok := obj.([]any)
if !ok {
return fmt.Errorf("expected array, got %T", obj)
}
for _, elem := range a {
if err := transformAliasedFields(sch, t.Element, elem, aliaser); err != nil {
return err
}
}

if d, ok := field.Type.(*schema.DataRef); ok {
if _, found := request[field.Name]; found {
rMap, err := transformFromAliasedFields(d, sch, request[field.Name].(map[string]any))
if err != nil {
return nil, err
}
request[field.Name] = rMap
case *schema.Map:
m, ok := obj.(map[string]any)
if !ok {
return fmt.Errorf("expected map, got %T", obj)
}
for key, value := range m {
if err := transformAliasedFields(sch, t.Key, key, aliaser); err != nil {
return err
}
if err := transformAliasedFields(sch, t.Value, value, aliaser); err != nil {
return err
}
}
}

return request, nil
}
case *schema.Optional:
if obj == nil {
return nil
}
return transformAliasedFields(sch, t.Type, obj, aliaser)

func transformToAliasedFields(dataRef *schema.DataRef, sch *schema.Schema, request map[string]any) (map[string]any, error) {
data, err := sch.ResolveDataRefMonomorphised(dataRef)
if err != nil {
return nil, err
case *schema.Any, *schema.Bool, *schema.Bytes, *schema.Float, *schema.Int,
*schema.String, *schema.Time, *schema.Unit:
}
return nil
}

for _, field := range data.Fields {
if field.Alias != "" && field.Name != field.Alias {
request[field.Alias] = request[field.Name]
delete(request, field.Name)
func transformFromAliasedFields(dataRef *schema.DataRef, sch *schema.Schema, request map[string]any) (map[string]any, error) {
return request, transformAliasedFields(sch, dataRef, request, func(obj map[string]any, field *schema.Field) string {
if _, ok := obj[field.Name]; !ok && field.Alias != "" && obj[field.Alias] != nil {
obj[field.Name] = obj[field.Alias]
delete(obj, field.Alias)
}
return field.Name
})
}

if d, ok := field.Type.(*schema.DataRef); ok {
if _, found := request[field.Name]; found {
rMap, err := transformToAliasedFields(d, sch, request[field.Name].(map[string]any))
if err != nil {
return nil, err
}
request[field.Name] = rMap
}
func transformToAliasedFields(dataRef *schema.DataRef, sch *schema.Schema, request map[string]any) (map[string]any, error) {
return request, transformAliasedFields(sch, dataRef, request, func(obj map[string]any, field *schema.Field) string {
if field.Alias != "" && field.Name != field.Alias {
obj[field.Alias] = obj[field.Name]
delete(obj, field.Name)
return field.Alias
}
}

return request, nil
return field.Name
})
}
129 changes: 129 additions & 0 deletions backend/controller/ingress/alias_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package ingress

import (
"testing"

"github.com/alecthomas/assert/v2"

"github.com/TBD54566975/ftl/backend/schema"
)

func TestTransformFromAliasedFields(t *testing.T) {
schemaText := `
module test {
data Inner {
waz String alias foo
}
data Test {
scalar String alias bar
inner Inner
array [Inner]
map {String: Inner}
optional Inner
}
}
`
sch, err := schema.ParseString("test", schemaText)
assert.NoError(t, err)
actual, err := transformFromAliasedFields(&schema.DataRef{Module: "test", Name: "Test"}, sch, map[string]any{
"bar": "value",
"inner": map[string]any{
"foo": "value",
},
"array": []any{
map[string]any{
"foo": "value",
},
},
"map": map[string]any{
"key": map[string]any{
"foo": "value",
},
},
"optional": map[string]any{
"foo": "value",
},
})
expected := map[string]any{
"scalar": "value",
"inner": map[string]any{
"waz": "value",
},
"array": []any{
map[string]any{
"waz": "value",
},
},
"map": map[string]any{
"key": map[string]any{
"waz": "value",
},
},
"optional": map[string]any{
"waz": "value",
},
}
assert.NoError(t, err)
assert.Equal(t, expected, actual)
}

func TestTransformToAliasedFields(t *testing.T) {
schemaText := `
module test {
data Inner {
waz String alias foo
}
data Test {
scalar String alias bar
inner Inner
array [Inner]
map {String: Inner}
optional Inner
}
}
`
sch, err := schema.ParseString("test", schemaText)
assert.NoError(t, err)
actual, err := transformToAliasedFields(&schema.DataRef{Module: "test", Name: "Test"}, sch, map[string]any{
"scalar": "value",
"inner": map[string]any{
"waz": "value",
},
"array": []any{
map[string]any{
"waz": "value",
},
},
"map": map[string]any{
"key": map[string]any{
"waz": "value",
},
},
"optional": map[string]any{
"waz": "value",
},
})
expected := map[string]any{
"bar": "value",
"inner": map[string]any{
"foo": "value",
},
"array": []any{
map[string]any{
"foo": "value",
},
},
"map": map[string]any{
"key": map[string]any{
"foo": "value",
},
},
"optional": map[string]any{
"foo": "value",
},
}
assert.NoError(t, err)
assert.Equal(t, expected, actual)
}
1 change: 1 addition & 0 deletions backend/controller/ingress/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func validateValue(fieldType schema.Type, path path, value any, sch *schema.Sche
typeMatches = true

case *schema.Unit:
// TODO: Use type assertions consistently in this function rather than reflection.
rv := reflect.ValueOf(value)
if rv.Kind() != reflect.Map || rv.Len() != 0 {
return fmt.Errorf("%s must be an empty map", path)
Expand Down
22 changes: 22 additions & 0 deletions backend/controller/ingress/ingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,25 @@ func TestResponseBodyForVerb(t *testing.T) {
})
}
}

func TestValueForData(t *testing.T) {
tests := []struct {
typ schema.Type
data []byte
result any
}{
{&schema.String{}, []byte("test"), "test"},
{&schema.Int{}, []byte("1234"), 1234},
{&schema.Float{}, []byte("12.34"), 12.34},
{&schema.Bool{}, []byte("true"), true},
{&schema.Array{Element: &schema.String{}}, []byte(`["test1", "test2"]`), []any{"test1", "test2"}},
{&schema.Map{Key: &schema.String{}, Value: &schema.String{}}, []byte(`{"key1": "value1", "key2": "value2"}`), obj{"key1": "value1", "key2": "value2"}},
{&schema.DataRef{Module: "test", Name: "Test"}, []byte(`{"intValue": 10.0}`), obj{"intValue": 10.0}},
}

for _, test := range tests {
result, err := valueForData(test.typ, test.data)
assert.NoError(t, err)
assert.Equal(t, test.result, result)
}
}
Loading

0 comments on commit 2c5f3fe

Please sign in to comment.