diff --git a/backend/controller/ingress/ingress.go b/backend/controller/ingress/ingress.go index 8a66ee84b0..5918b96e65 100644 --- a/backend/controller/ingress/ingress.go +++ b/backend/controller/ingress/ingress.go @@ -164,10 +164,10 @@ func validateValue(fieldType schema.Type, path path, value any, sch *schema.Sche case *schema.Int: switch value := value.(type) { - case float64: + case int64, float64: typeMatches = true case string: - if _, err := strconv.ParseFloat(value, 64); err == nil { + if _, err := strconv.ParseInt(value, 10, 64); err == nil { typeMatches = true } } diff --git a/backend/controller/ingress/request.go b/backend/controller/ingress/request.go index 090fc5f776..f0833c680b 100644 --- a/backend/controller/ingress/request.go +++ b/backend/controller/ingress/request.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/url" + "strconv" "strings" "github.com/TBD54566975/ftl/backend/controller/dal" @@ -101,6 +102,41 @@ func extractHTTPRequestBody(route *dal.IngressRoute, r *http.Request, dataRef *s } return string(bodyData), nil + case *schema.Int: + bodyData, err := readRequestBody(r) + if err != nil { + return nil, err + } + + intVal, err := strconv.ParseInt(string(bodyData), 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) + 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)) + if err != nil { + return nil, fmt.Errorf("failed to parse boolean from request body: %w", err) + } + return boolVal, nil + case *schema.Unit: return map[string]any{}, nil diff --git a/backend/controller/ingress/response.go b/backend/controller/ingress/response.go index dcf0f2b5f6..cf355e9ac7 100644 --- a/backend/controller/ingress/response.go +++ b/backend/controller/ingress/response.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "strconv" "github.com/TBD54566975/ftl/backend/schema" ) @@ -47,10 +48,31 @@ func ResponseBodyForVerb(sch *schema.Schema, verb *schema.Verb, body []byte, hea case *schema.String: var responseString string if err := json.Unmarshal(body, &responseString); err != nil { - return nil, fmt.Errorf("HTTP response body is not valid string: %w", err) + return nil, fmt.Errorf("HTTP response body is not a valid string: %w", err) } return []byte(responseString), nil + case *schema.Int: + var responseInt int + if err := json.Unmarshal(body, &responseInt); err != nil { + return nil, fmt.Errorf("HTTP response body is not a valid int: %w", err) + } + return []byte(strconv.Itoa(responseInt)), nil + + case *schema.Float: + var responseFloat float64 + if err := json.Unmarshal(body, &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)), nil + + case *schema.Bool: + var responseBool bool + if err := json.Unmarshal(body, &responseBool); err != nil { + return nil, fmt.Errorf("HTTP response body is not a valid bool: %w", err) + } + return []byte(strconv.FormatBool(responseBool)), nil + case *schema.Unit: return []byte{}, nil diff --git a/integration/integration_test.go b/integration/integration_test.go index f6b6cb949d..f5eb70e4b5 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -127,6 +127,22 @@ func TestHttpIngress(t *testing.T) { assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.headers["Content-Type"]) assert.Equal(t, []byte("Hello, World!"), resp.bodyBytes) }), + + httpCall(rd, http.MethodGet, "/int", []byte("1234"), func(t testing.TB, resp *httpResponse) { + assert.Equal(t, 200, resp.status) + assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.headers["Content-Type"]) + assert.Equal(t, []byte("1234"), resp.bodyBytes) + }), + httpCall(rd, http.MethodGet, "/float", []byte("1234.56789"), func(t testing.TB, resp *httpResponse) { + assert.Equal(t, 200, resp.status) + assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.headers["Content-Type"]) + assert.Equal(t, []byte("1234.56789"), resp.bodyBytes) + }), + httpCall(rd, http.MethodGet, "/bool", []byte("true"), func(t testing.TB, resp *httpResponse) { + assert.Equal(t, 200, resp.status) + assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.headers["Content-Type"]) + assert.Equal(t, []byte("true"), resp.bodyBytes) + }), }}, } }) diff --git a/integration/testdata/go/httpingress/echo.go b/integration/testdata/go/httpingress/echo.go index ec20cd8997..c3e1f71000 100644 --- a/integration/testdata/go/httpingress/echo.go +++ b/integration/testdata/go/httpingress/echo.go @@ -103,23 +103,35 @@ func Html(ctx context.Context, req builtin.HttpRequest[HtmlRequest]) (builtin.Ht //ftl:verb //ftl:ingress http POST /bytes func Bytes(ctx context.Context, req builtin.HttpRequest[[]byte]) (builtin.HttpResponse[[]byte], error) { - return builtin.HttpResponse[[]byte]{ - Body: req.Body, - }, nil + return builtin.HttpResponse[[]byte]{Body: req.Body}, nil } //ftl:verb //ftl:ingress http GET /empty func Empty(ctx context.Context, req builtin.HttpRequest[ftl.Unit]) (builtin.HttpResponse[ftl.Unit], error) { - return builtin.HttpResponse[ftl.Unit]{ - Body: ftl.Unit{}, - }, nil + return builtin.HttpResponse[ftl.Unit]{Body: ftl.Unit{}}, nil } //ftl:verb //ftl:ingress http GET /string func String(ctx context.Context, req builtin.HttpRequest[string]) (builtin.HttpResponse[string], error) { - return builtin.HttpResponse[string]{ - Body: req.Body, - }, nil + return builtin.HttpResponse[string]{Body: req.Body}, nil +} + +//ftl:verb +//ftl:ingress http GET /int +func Int(ctx context.Context, req builtin.HttpRequest[int]) (builtin.HttpResponse[int], error) { + return builtin.HttpResponse[int]{Body: req.Body}, nil +} + +//ftl:verb +//ftl:ingress http GET /float +func Float(ctx context.Context, req builtin.HttpRequest[float64]) (builtin.HttpResponse[float64], error) { + return builtin.HttpResponse[float64]{Body: req.Body}, nil +} + +//ftl:verb +//ftl:ingress http GET /bool +func Bool(ctx context.Context, req builtin.HttpRequest[bool]) (builtin.HttpResponse[bool], error) { + return builtin.HttpResponse[bool]{Body: req.Body}, nil } diff --git a/integration/testdata/kotlin/httpingress/Echo.kt b/integration/testdata/kotlin/httpingress/Echo.kt index 379df6870f..0eb3c65cbd 100644 --- a/integration/testdata/kotlin/httpingress/Echo.kt +++ b/integration/testdata/kotlin/httpingress/Echo.kt @@ -127,4 +127,34 @@ class Echo { body = req.body ) } + + @Verb + @HttpIngress(Method.GET, "/int") + fun int(context: Context, req: HttpRequest): HttpResponse { + return HttpResponse( + status = 200, + headers = mapOf("Int" to arrayListOf("Header from FTL")), + body = req.body + ) + } + + @Verb + @HttpIngress(Method.GET, "/float") + fun float(context: Context, req: HttpRequest): HttpResponse { + return HttpResponse( + status = 200, + headers = mapOf("Float" to arrayListOf("Header from FTL")), + body = req.body + ) + } + + @Verb + @HttpIngress(Method.GET, "/bool") + fun bool(context: Context, req: HttpRequest): HttpResponse { + return HttpResponse( + status = 200, + headers = mapOf("Bool" to arrayListOf("Header from FTL")), + body = req.body + ) + } }