Skip to content

Commit

Permalink
feat: HttpRequest path and query mapping
Browse files Browse the repository at this point in the history
fixes: #2230
  • Loading branch information
stuartwdouglas committed Aug 19, 2024
1 parent 1339293 commit b97affd
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 126 deletions.
15 changes: 8 additions & 7 deletions backend/controller/console/console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package console
import (
"testing"

"github.com/TBD54566975/ftl/backend/schema"
"github.com/alecthomas/assert/v2"

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

func TestVerbSchemaString(t *testing.T) {
Expand All @@ -15,7 +16,7 @@ func TestVerbSchemaString(t *testing.T) {
}
ingressVerb := &schema.Verb{
Name: "Ingress",
Request: &schema.Ref{Module: "builtin", Name: "HttpRequest", TypeParameters: []schema.Type{&schema.String{}}},
Request: &schema.Ref{Module: "builtin", Name: "HttpRequest", TypeParameters: []schema.Type{&schema.String{}, &schema.Unit{}, &schema.Unit{}}},
Response: &schema.Ref{Module: "builtin", Name: "HttpResponse", TypeParameters: []schema.Type{&schema.String{}, &schema.String{}}},
Metadata: []schema.Metadata{
&schema.MetadataIngress{Type: "http", Method: "GET", Path: []schema.IngressPathComponent{&schema.IngressPathLiteral{Text: "test"}}},
Expand Down Expand Up @@ -107,7 +108,7 @@ verb Echo(foo.EchoRequest) foo.EchoResponse`
func TestVerbSchemaStringIngress(t *testing.T) {
verb := &schema.Verb{
Name: "Ingress",
Request: &schema.Ref{Module: "builtin", Name: "HttpRequest", TypeParameters: []schema.Type{&schema.Ref{Module: "foo", Name: "FooRequest"}}},
Request: &schema.Ref{Module: "builtin", Name: "HttpRequest", TypeParameters: []schema.Type{&schema.Ref{Module: "foo", Name: "FooRequest"}, &schema.Unit{}, &schema.Unit{}}},
Response: &schema.Ref{Module: "builtin", Name: "HttpResponse", TypeParameters: []schema.Type{&schema.Ref{Module: "foo", Name: "FooResponse"}, &schema.String{}}},
Metadata: []schema.Metadata{
&schema.MetadataIngress{Type: "http", Method: "GET", Path: []schema.IngressPathComponent{&schema.IngressPathLiteral{Text: "foo"}}},
Expand Down Expand Up @@ -135,11 +136,11 @@ func TestVerbSchemaStringIngress(t *testing.T) {
}

expected := `// HTTP request structure used for HTTP ingress verbs.
export data HttpRequest<Body> {
export data HttpRequest<Body, Path, Query> {
method String
path String
pathParameters {String: String}
query {String: [String]}
pathParameters Path
query Query
headers {String: [String]}
body Body
}
Expand All @@ -161,7 +162,7 @@ data FooResponse {
Message String
}
verb Ingress(builtin.HttpRequest<foo.FooRequest>) builtin.HttpResponse<foo.FooResponse, String>
verb Ingress(builtin.HttpRequest<foo.FooRequest, Unit, Unit>) builtin.HttpResponse<foo.FooResponse, String>
+ingress http GET /foo`

schemaString, err := verbSchemaString(sch, verb)
Expand Down
8 changes: 4 additions & 4 deletions backend/controller/ingress/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ func TestIngress(t *testing.T) {
foo String
}
export verb getAlias(HttpRequest<test.AliasRequest>) HttpResponse<Empty, Empty>
export verb getAlias(HttpRequest<test.AliasRequest, Unit, Unit>) HttpResponse<Empty, Empty>
+ingress http GET /getAlias
export verb getPath(HttpRequest<test.PathParameterRequest>) HttpResponse<Empty, Empty>
export verb getPath(HttpRequest<Unit, test.PathParameterRequest, Unit>) HttpResponse<Empty, Empty>
+ingress http GET /getPath/{username}
export verb postMissingTypes(HttpRequest<test.MissingTypes>) HttpResponse<Empty, Empty>
export verb postMissingTypes(HttpRequest<test.MissingTypes, Unit, Unit>) HttpResponse<Empty, Empty>
+ingress http POST /postMissingTypes
export verb postJsonPayload(HttpRequest<test.JsonPayload>) HttpResponse<Empty, Empty>
export verb postJsonPayload(HttpRequest<test.JsonPayload, Unit, Unit>) HttpResponse<Empty, Empty>
+ingress http POST /postJsonPayload
}
`)
Expand Down
6 changes: 3 additions & 3 deletions backend/controller/ingress/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,21 @@ func ValidateCallBody(body []byte, verb *schema.Verb, sch *schema.Schema) error
return nil
}

func getBodyField(ref *schema.Ref, sch *schema.Schema) (*schema.Field, error) {
func getField(name string, ref *schema.Ref, sch *schema.Schema) (*schema.Field, error) {
data, err := sch.ResolveMonomorphised(ref)
if err != nil {
return nil, err
}
var bodyField *schema.Field
for _, field := range data.Fields {
if field.Name == "body" {
if field.Name == name {
bodyField = field
break
}
}

if bodyField == nil {
return nil, fmt.Errorf("verb %s must have a 'body' field", ref.Name)
return nil, fmt.Errorf("verb %s must have a '%s' field", ref.Name, name)
}

return bodyField, nil
Expand Down
135 changes: 96 additions & 39 deletions backend/controller/ingress/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,16 @@ func BuildRequestBody(route *dal.IngressRoute, r *http.Request, sch *schema.Sche
var requestMap map[string]any

if metadata, ok := verb.GetMetadataIngress().Get(); ok && metadata.Type == "http" {
pathParameters := map[string]any{}
pathParametersMap := map[string]string{}
matchSegments(route.Path, r.URL.Path, func(segment, value string) {
pathParameters[segment] = value
pathParametersMap[segment] = value
})
pathParameters, err := manglePathParameters(pathParametersMap, request, sch)
if err != nil {
return nil, err
}

httpRequestBody, err := extractHTTPRequestBody(route, r, request, sch)
httpRequestBody, err := extractHTTPRequestBody(r, request, sch)
if err != nil {
return nil, err
}
Expand All @@ -56,6 +60,7 @@ func BuildRequestBody(route *dal.IngressRoute, r *http.Request, sch *schema.Sche
queryMap[key] = valuesAny
}

finalQueryParams, err := mangleQueryParameters(queryMap, r.URL.Query(), request, sch)
headerMap := make(map[string]any)
for key, values := range r.Header {
valuesAny := make([]any, len(values))
Expand All @@ -69,15 +74,16 @@ func BuildRequestBody(route *dal.IngressRoute, r *http.Request, sch *schema.Sche
requestMap["method"] = r.Method
requestMap["path"] = r.URL.Path
requestMap["pathParameters"] = pathParameters
requestMap["query"] = queryMap
requestMap["query"] = finalQueryParams
requestMap["headers"] = headerMap
requestMap["body"] = httpRequestBody
} else {
var err error
requestMap, err = buildRequestMap(route, r, request, sch)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("no HTTP ingress metadata for verb %s", verb.Name)
//var err error
//requestMap, err = buildRequestMap(route, r, request, sch)
//if err != nil {
// return nil, err
//}
}

requestMap, err = schema.TransformFromAliasedFields(request, sch, requestMap)
Expand All @@ -102,15 +108,15 @@ func BuildRequestBody(route *dal.IngressRoute, r *http.Request, sch *schema.Sche
return body, nil
}

func extractHTTPRequestBody(route *dal.IngressRoute, r *http.Request, ref *schema.Ref, sch *schema.Schema) (any, error) {
bodyField, err := getBodyField(ref, sch)
func extractHTTPRequestBody(r *http.Request, ref *schema.Ref, sch *schema.Schema) (any, error) {
bodyField, err := getField("body", ref, sch)
if err != nil {
return nil, err
}

if ref, ok := bodyField.Type.(*schema.Ref); ok {
if err := sch.ResolveToType(ref, &schema.Data{}); err == nil {
return buildRequestMap(route, r, ref, sch)
return buildRequestMap(r)
}
}

Expand All @@ -122,6 +128,81 @@ func extractHTTPRequestBody(route *dal.IngressRoute, r *http.Request, ref *schem
return valueForData(bodyField.Type, bodyData)
}

// Takes the map of path parameters and transforms them into the appropriate type
func manglePathParameters(params map[string]string, ref *schema.Ref, sch *schema.Schema) (any, error) {

paramsField, err := getField("pathParameters", ref, sch)
if err != nil {
return nil, err
}

switch paramsField.Type.(type) {
case *schema.Ref, *schema.Map:
ret := map[string]any{}
for k, v := range params {
ret[k] = v
}
return ret, nil

}
// This is a scalar, there should only be a single param
// This is validated by the schema, we don't need extra validation here
for _, val := range params {

switch paramsField.Type.(type) {
case *schema.String:
return val, nil
case *schema.Int:
parsed, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse int from path parameter: %w", err)
}
return parsed, err
case *schema.Float:
float, err := strconv.ParseFloat(val, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse float from path parameter: %w", err)
}
return float, err
case *schema.Bool:
// TODO: is anything else considered truthy?
return val == "true", nil
default:
return nil, fmt.Errorf("unsupported path parameter type %T", paramsField.Type)
}
}
// Empty map
return map[string]any{}, nil
}

// Takes the map of path parameters and transforms them into the appropriate type
func mangleQueryParameters(params map[string]any, underlying map[string][]string, ref *schema.Ref, sch *schema.Schema) (any, error) {

paramsField, err := getField("query", ref, sch)
if err != nil {
return nil, err
}

switch paramsField.Type.(type) {

// If the type is Map it might be a map of lists for multi values params
case *schema.Map:
m := paramsField.Type.(*schema.Map)
switch m.Value.(type) {
case *schema.Array:
return params, nil
}
}
// We need to turn them into straight strings
newParams := map[string]any{}
for k, v := range underlying {
if len(v) > 0 {
newParams[k] = v[0]
}
}
return newParams, nil
}

func valueForData(typ schema.Type, data []byte) (any, error) {
switch typ.(type) {
case *schema.Ref:
Expand Down Expand Up @@ -203,12 +284,7 @@ func readRequestBody(r *http.Request) ([]byte, error) {
return bodyData, nil
}

func buildRequestMap(route *dal.IngressRoute, r *http.Request, ref *schema.Ref, sch *schema.Schema) (map[string]any, error) {
requestMap := map[string]any{}
matchSegments(route.Path, r.URL.Path, func(segment, value string) {
requestMap[segment] = value
})

func buildRequestMap(r *http.Request) (map[string]any, error) {
switch r.Method {
case http.MethodPost, http.MethodPut:
var bodyMap map[string]any
Expand All @@ -217,29 +293,10 @@ func buildRequestMap(route *dal.IngressRoute, r *http.Request, ref *schema.Ref,
return nil, fmt.Errorf("HTTP request body is not valid JSON: %w", err)
}

// Merge bodyMap into params
for k, v := range bodyMap {
requestMap[k] = v
}
return bodyMap, nil
default:
symbol, err := sch.ResolveRequestResponseType(ref)
if err != nil {
return nil, err
}

if data, ok := symbol.(*schema.Data); ok {
queryMap, err := parseQueryParams(r.URL.Query(), data)
if err != nil {
return nil, fmt.Errorf("HTTP query params are not valid: %w", err)
}

for key, value := range queryMap {
requestMap[key] = value
}
}
return nil, nil
}

return requestMap, nil
}

func parseQueryParams(values url.Values, data *schema.Data) (map[string]any, error) {
Expand Down
Loading

0 comments on commit b97affd

Please sign in to comment.