Skip to content

Commit

Permalink
refactor: add generalised aliasing function that takes a callback
Browse files Browse the repository at this point in the history
  • Loading branch information
alecthomas authored and wesbillman committed Feb 13, 2024
1 parent c380069 commit fbfd47f
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 80 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
54 changes: 12 additions & 42 deletions backend/controller/ingress/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,57 +69,27 @@ func ResponseForVerb(sch *schema.Schema, verb *schema.Verb, response HTTPRespons

func bodyForType(typ schema.Type, sch *schema.Schema, data []byte) ([]byte, error) {
switch t := typ.(type) {
case *schema.DataRef:
var responseMap map[string]any
err := json.Unmarshal(data, &responseMap)
case *schema.DataRef, *schema.Array, *schema.Map:
var response any
err := json.Unmarshal(data, &response)
if err != nil {
return nil, fmt.Errorf("HTTP response body is not valid JSON: %w", err)
}

aliasedResponseMap, err := transformToAliasedFields(t, sch, responseMap)
err = transformAliasedFields(sch, t, response, 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 field.Name
})
if err != nil {
return nil, err
}
outBody, err := json.Marshal(aliasedResponseMap)
outBody, err := json.Marshal(response)
return outBody, err

case *schema.Array:
var responseArray []json.RawMessage
err := json.Unmarshal(data, &responseArray)
if err != nil {
return nil, fmt.Errorf("HTTP response body is not valid JSON array: %w", err)
}

transformedArrayData := make([]any, len(responseArray))
for i, rawElement := range responseArray {
var transformedElement any
err := json.Unmarshal(rawElement, &transformedElement)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal array element: %w", err)
}

// Transform the array element to its aliased form.
if dataRef, ok := t.Element.(*schema.DataRef); ok {
if elementMap, ok := transformedElement.(map[string]any); ok {
aliasedElement, err := transformToAliasedFields(dataRef, sch, elementMap)
if err != nil {
return nil, err
}
transformedElement = aliasedElement
}
}

transformedArrayData[i] = transformedElement
}

// Marshal the transformed array back to JSON.
outBody, err := json.Marshal(transformedArrayData)
if err != nil {
return nil, fmt.Errorf("failed to marshal transformed array data: %w", err)
}

return outBody, nil

case *schema.Bytes:
var base64String string
if err := json.Unmarshal(data, &base64String); err != nil {
Expand Down
1 change: 1 addition & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ func httpCall(rd runtimeData, method string, path string, body []byte, onRespons

bodyBytes, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
fmt.Printf("%s\n", bodyBytes)

var resBody map[string]any
// ignore the error here since some responses are just `[]byte`.
Expand Down

0 comments on commit fbfd47f

Please sign in to comment.