Skip to content

Commit

Permalink
feat: add array support to http ingress
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman committed Feb 13, 2024
1 parent 5926a17 commit c380069
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 68 deletions.
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)
}
}
75 changes: 47 additions & 28 deletions backend/controller/ingress/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,58 +80,77 @@ func extractHTTPRequestBody(route *dal.IngressRoute, r *http.Request, dataRef *s
return nil, err
}

switch bodyType := bodyField.Type.(type) {
if dataRef, ok := bodyField.Type.(*schema.DataRef); ok {
return buildRequestMap(route, r, dataRef, sch)
}

bodyData, err := readRequestBody(r)
if err != nil {
return nil, err
}

return valueForData(bodyField.Type, bodyData)
}

func valueForData(typ schema.Type, data []byte) (any, error) {
switch typ.(type) {
case *schema.DataRef:
bodyMap, err := buildRequestMap(route, r, bodyType, sch)
var bodyMap map[string]any
err := json.Unmarshal(data, &bodyMap)
if err != nil {
return nil, err
return nil, fmt.Errorf("HTTP request body is not valid JSON: %w", err)
}
return bodyMap, nil

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

case *schema.String:
bodyData, err := readRequestBody(r)
if err != nil {
return nil, err
arrayData := make([]any, len(rawData))
for i, rawElement := range rawData {
var parsedElement any
err := json.Unmarshal(rawElement, &parsedElement)
if err != nil {
return nil, fmt.Errorf("failed to parse array element: %w", err)
}
arrayData[i] = parsedElement
}
return string(bodyData), nil

case *schema.Int:
bodyData, err := readRequestBody(r)
return arrayData, nil

case *schema.Map:
var bodyMap map[string]any
err := json.Unmarshal(data, &bodyMap)
if err != nil {
return nil, err
return nil, fmt.Errorf("HTTP request body is not valid JSON: %w", err)
}
return bodyMap, nil

case *schema.Bytes:
return data, nil

case *schema.String:
return string(data), nil

intVal, err := strconv.ParseInt(string(bodyData), 10, 64)
case *schema.Int:
intVal, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse integer from request body: %w", err)
}
return intVal, nil

case *schema.Float:
bodyData, err := readRequestBody(r)
if err != nil {
return nil, err
}

floatVal, err := strconv.ParseFloat(string(bodyData), 64)
floatVal, err := strconv.ParseFloat(string(data), 64)
if err != nil {
return nil, fmt.Errorf("failed to parse float from request body: %w", err)
}
return floatVal, nil

case *schema.Bool:
bodyData, err := readRequestBody(r)
if err != nil {
return nil, err
}
boolVal, err := strconv.ParseBool(string(bodyData))
boolVal, err := strconv.ParseBool(string(data))
if err != nil {
return nil, fmt.Errorf("failed to parse boolean from request body: %w", err)
}
Expand All @@ -141,7 +160,7 @@ func extractHTTPRequestBody(route *dal.IngressRoute, r *http.Request, dataRef *s
return map[string]any{}, nil

default:
return nil, fmt.Errorf("unsupported HttpRequest.Body type %T", bodyField.Type)
return nil, fmt.Errorf("unsupported data type %T", typ)
}
}

Expand Down
90 changes: 66 additions & 24 deletions backend/controller/ingress/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,65 +63,107 @@ func ResponseForVerb(sch *schema.Schema, verb *schema.Verb, response HTTPRespons
}
}

switch bodyType := fieldType.(type) {
outBody, err := bodyForType(fieldType, sch, body)
return outBody, headers, err
}

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(body, &responseMap)
err := json.Unmarshal(data, &responseMap)
if err != nil {
return nil, nil, fmt.Errorf("HTTP response body is not valid JSON: %w", err)
return nil, fmt.Errorf("HTTP response body is not valid JSON: %w", err)
}

aliasedResponseMap, err := transformToAliasedFields(bodyType, sch, responseMap)
aliasedResponseMap, err := transformToAliasedFields(t, sch, responseMap)
if err != nil {
return nil, nil, err
return nil, err
}
outBody, err := json.Marshal(aliasedResponseMap)
return outBody, headers, err
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(body, &base64String); err != nil {
return nil, nil, fmt.Errorf("HTTP response body is not valid base64: %w", err)
if err := json.Unmarshal(data, &base64String); err != nil {
return nil, fmt.Errorf("HTTP response body is not valid base64: %w", err)
}
decodedBody, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode base64 response body: %w", err)
return nil, fmt.Errorf("failed to decode base64 response body: %w", err)
}
return decodedBody, headers, nil
return decodedBody, nil

case *schema.String:
var responseString string
if err := json.Unmarshal(body, &responseString); err != nil {
return nil, nil, fmt.Errorf("HTTP response body is not a valid string: %w", err)
if err := json.Unmarshal(data, &responseString); err != nil {
return nil, fmt.Errorf("HTTP response body is not a valid string: %w", err)
}
return []byte(responseString), headers, nil
return []byte(responseString), nil

case *schema.Int:
var responseInt int
if err := json.Unmarshal(body, &responseInt); err != nil {
return nil, nil, fmt.Errorf("HTTP response body is not a valid int: %w", err)
if err := json.Unmarshal(data, &responseInt); err != nil {
return nil, fmt.Errorf("HTTP response body is not a valid int: %w", err)
}
return []byte(strconv.Itoa(responseInt)), headers, nil
return []byte(strconv.Itoa(responseInt)), nil

case *schema.Float:
var responseFloat float64
if err := json.Unmarshal(body, &responseFloat); err != nil {
return nil, nil, fmt.Errorf("HTTP response body is not a valid float: %w", err)
if err := json.Unmarshal(data, &responseFloat); err != nil {
return nil, fmt.Errorf("HTTP response body is not a valid float: %w", err)
}
return []byte(strconv.FormatFloat(responseFloat, 'f', -1, 64)), headers, nil
return []byte(strconv.FormatFloat(responseFloat, 'f', -1, 64)), nil

case *schema.Bool:
var responseBool bool
if err := json.Unmarshal(body, &responseBool); err != nil {
return nil, nil, fmt.Errorf("HTTP response body is not a valid bool: %w", err)
if err := json.Unmarshal(data, &responseBool); err != nil {
return nil, fmt.Errorf("HTTP response body is not a valid bool: %w", err)
}
return []byte(strconv.FormatBool(responseBool)), headers, nil
return []byte(strconv.FormatBool(responseBool)), nil

case *schema.Unit:
return []byte{}, headers, nil
return []byte{}, nil

default:
return body, headers, nil
return data, nil
}
}

Expand Down
4 changes: 2 additions & 2 deletions backend/schema/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ func DataToSchema(s *schemapb.Data) *Data {

// MonoType returns the monomorphised type of this data type if applicable, or returns the original type.
func maybeMonomorphiseType(t Type, typeParameters map[string]Type) (Type, error) {
if t, ok := t.(*DataRef); ok {
if tp, ok := typeParameters[t.Name]; ok && t.Module == "" {
if t, ok := t.(*DataRef); ok && t.Module == "" {
if tp, ok := typeParameters[t.Name]; ok {
return tp, nil
}
return nil, fmt.Errorf("%s: unknown type parameter %q", t.Position(), t.Name)
Expand Down
37 changes: 29 additions & 8 deletions backend/schema/data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,34 @@ func TestMonomorphisation(t *testing.T) {
{Name: "a", Type: &DataRef{Name: "T"}},
},
}
actual, err := data.Monomorphise(&DataRef{TypeParameters: []Type{&String{}}})
assert.NoError(t, err)
expected := &Data{
Comments: []string{},
Name: "Data",
Fields: []*Field{{Comments: []string{}, Name: "a", Type: &String{}}},
Metadata: []Metadata{},

tests := []struct {
typ Type
}{
{typ: &String{}},
{typ: &Int{}},
{typ: &Float{}},
{typ: &Bool{}},
{typ: &Array{Element: &String{}}},
{typ: &Array{Element: &DataRef{Module: "builtin", Name: "Test"}}},
{typ: &DataRef{Module: "builtin", Name: "Test"}},
{typ: &DataRef{Module: "builtin", Name: "Test", TypeParameters: []Type{&String{}}}},
{typ: &Map{Key: &String{}, Value: &Int{}}},
{typ: &Map{Key: &DataRef{Module: "builtin", Name: "Test"}, Value: &DataRef{Module: "builtin", Name: "Test"}}},
{typ: &Optional{Type: &String{}}},
{typ: &Optional{Type: &DataRef{Module: "builtin", Name: "Test"}}},
{typ: &Any{}},
}

for _, test := range tests {
actual, err := data.Monomorphise(&DataRef{TypeParameters: []Type{test.typ}})
assert.NoError(t, err)
expected := &Data{
Comments: []string{},
Name: "Data",
Fields: []*Field{{Comments: []string{}, Name: "a", Type: test.typ}},
Metadata: []Metadata{},
}
assert.Equal(t, expected, actual, assert.OmitEmpty())
}
assert.Equal(t, expected, actual, assert.OmitEmpty())
}
2 changes: 1 addition & 1 deletion go-runtime/encoding/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func encodeValue(v reflect.Value, w *bytes.Buffer) error {
return encodeBool(v, w)

default:
panic(fmt.Sprintf("unsupported typefoo: %s", v.Type()))
panic(fmt.Sprintf("unsupported type: %s", v.Type()))
}
}

Expand Down
1 change: 1 addition & 0 deletions go-runtime/encoding/encoding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func TestMarshal(t *testing.T) {
{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: "SliceOfStrings", input: struct{ Slice []string }{[]string{"hello", "world"}}, expected: `{"slice":["hello","world"]}`},
{name: "Map", input: struct{ Map map[string]int }{map[string]int{"foo": 42}}, expected: `{"map":{"foo":42}}`},
{name: "Option", input: struct{ Option ftl.Option[int] }{ftl.Some(42)}, expected: `{"option":42}`},
{name: "OptionPtr", input: struct{ Option *ftl.Option[int] }{&somePtr}, expected: `{"option":42}`},
Expand Down
1 change: 1 addition & 0 deletions go-runtime/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func Handle[Req, Resp any](verb func(ctx context.Context, req Req) (Resp, error)
if err != nil {
return nil, err
}

return respdata, nil
},
}
Expand Down
14 changes: 12 additions & 2 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,19 @@ func TestHttpIngress(t *testing.T) {
assert.Equal(t, []byte("true"), resp.bodyBytes)
}),
httpCall(rd, http.MethodGet, "/error", nil, func(t testing.TB, resp *httpResponse) {
assert.Equal(t, 500, resp.status)
assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.headers["Content-Type"])
assert.Equal(t, []byte("Error from FTL"), resp.bodyBytes)
assert.Equal(t, 500, resp.status)
}),
httpCall(rd, http.MethodGet, "/array/string", jsonData(t, []string{"hello", "world"}), func(t testing.TB, resp *httpResponse) {
assert.Equal(t, 200, resp.status)
assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.headers["Content-Type"])
assert.Equal(t, jsonData(t, []string{"hello", "world"}), resp.bodyBytes)
}),
httpCall(rd, http.MethodPost, "/array/data", jsonData(t, []obj{{"item": "a"}, {"item": "b"}}), func(t testing.TB, resp *httpResponse) {
assert.Equal(t, 200, resp.status)
assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.headers["Content-Type"])
assert.Equal(t, jsonData(t, []obj{{"item": "a"}, {"item": "b"}}), resp.bodyBytes)
}),
}},
}
Expand Down Expand Up @@ -407,7 +417,7 @@ type httpResponse struct {
bodyBytes []byte
}

func jsonData(t testing.TB, body obj) []byte {
func jsonData(t testing.TB, body interface{}) []byte {
b, err := json.Marshal(body)
assert.NoError(t, err)
return b
Expand Down
Loading

0 comments on commit c380069

Please sign in to comment.