diff --git a/backend/controller/ingress/alias.go b/backend/controller/ingress/alias.go index b10b3aafd0..8f678d9aa1 100644 --- a/backend/controller/ingress/alias.go +++ b/backend/controller/ingress/alias.go @@ -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 + }) } diff --git a/backend/controller/ingress/alias_test.go b/backend/controller/ingress/alias_test.go new file mode 100644 index 0000000000..78186f7c4b --- /dev/null +++ b/backend/controller/ingress/alias_test.go @@ -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) +} diff --git a/backend/controller/ingress/ingress.go b/backend/controller/ingress/ingress.go index 3267e5c433..6af57fe3bb 100644 --- a/backend/controller/ingress/ingress.go +++ b/backend/controller/ingress/ingress.go @@ -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) diff --git a/backend/controller/ingress/response.go b/backend/controller/ingress/response.go index 4dcf29f23b..456bd69e1b 100644 --- a/backend/controller/ingress/response.go +++ b/backend/controller/ingress/response.go @@ -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 { diff --git a/integration/integration_test.go b/integration/integration_test.go index 24a59bb36f..a0dcf8749f 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -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`.