-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: HttpRequest path and query mapping #2422
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,7 @@ func TestHttpIngress(t *testing.T) { | |
in.Run(t, | ||
in.CopyModule("httpingress"), | ||
in.Deploy("httpingress"), | ||
in.HttpCall(http.MethodGet, "/users/123/posts/456", nil, in.JsonData(t, in.Obj{}), func(t testing.TB, resp *in.HTTPResponse) { | ||
in.HttpCall(http.MethodGet, "/users/123/posts/456", nil, nil, func(t testing.TB, resp *in.HTTPResponse) { | ||
assert.Equal(t, 200, resp.Status) | ||
assert.Equal(t, []string{"Header from FTL"}, resp.Headers["Get"]) | ||
assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.Headers["Content-Type"]) | ||
|
@@ -82,23 +82,23 @@ func TestHttpIngress(t *testing.T) { | |
assert.Equal(t, nil, resp.BodyBytes) | ||
}), | ||
|
||
in.HttpCall(http.MethodGet, "/string", nil, []byte("Hello, World!"), func(t testing.TB, resp *in.HTTPResponse) { | ||
in.HttpCall(http.MethodPost, "/string", nil, []byte("Hello, World!"), func(t testing.TB, resp *in.HTTPResponse) { | ||
assert.Equal(t, 200, resp.Status) | ||
assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.Headers["Content-Type"]) | ||
assert.Equal(t, []byte("Hello, World!"), resp.BodyBytes) | ||
}), | ||
|
||
in.HttpCall(http.MethodGet, "/int", nil, []byte("1234"), func(t testing.TB, resp *in.HTTPResponse) { | ||
in.HttpCall(http.MethodPost, "/int", nil, []byte("1234"), func(t testing.TB, resp *in.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) | ||
}), | ||
in.HttpCall(http.MethodGet, "/float", nil, []byte("1234.56789"), func(t testing.TB, resp *in.HTTPResponse) { | ||
in.HttpCall(http.MethodPost, "/float", nil, []byte("1234.56789"), func(t testing.TB, resp *in.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) | ||
}), | ||
in.HttpCall(http.MethodGet, "/bool", nil, []byte("true"), func(t testing.TB, resp *in.HTTPResponse) { | ||
in.HttpCall(http.MethodPost, "/bool", nil, []byte("true"), func(t testing.TB, resp *in.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) | ||
|
@@ -108,7 +108,7 @@ func TestHttpIngress(t *testing.T) { | |
assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.Headers["Content-Type"]) | ||
assert.Equal(t, []byte("Error from FTL"), resp.BodyBytes) | ||
}), | ||
in.HttpCall(http.MethodGet, "/array/string", nil, in.JsonData(t, []string{"hello", "world"}), func(t testing.TB, resp *in.HTTPResponse) { | ||
in.HttpCall(http.MethodPost, "/array/string", nil, in.JsonData(t, []string{"hello", "world"}), func(t testing.TB, resp *in.HTTPResponse) { | ||
assert.Equal(t, 200, resp.Status) | ||
assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.Headers["Content-Type"]) | ||
assert.Equal(t, in.JsonData(t, []string{"hello", "world"}), resp.BodyBytes) | ||
|
@@ -118,7 +118,7 @@ func TestHttpIngress(t *testing.T) { | |
assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.Headers["Content-Type"]) | ||
assert.Equal(t, in.JsonData(t, []in.Obj{{"item": "a"}, {"item": "b"}}), resp.BodyBytes) | ||
}), | ||
in.HttpCall(http.MethodGet, "/typeenum", nil, in.JsonData(t, in.Obj{"name": "A", "value": "hello"}), func(t testing.TB, resp *in.HTTPResponse) { | ||
in.HttpCall(http.MethodPost, "/typeenum", nil, in.JsonData(t, in.Obj{"name": "A", "value": "hello"}), func(t testing.TB, resp *in.HTTPResponse) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the method changes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah because of the request body in the test. |
||
assert.Equal(t, 200, resp.Status) | ||
assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.Headers["Content-Type"]) | ||
assert.Equal(t, in.JsonData(t, in.Obj{"name": "A", "value": "hello"}), resp.BodyBytes) | ||
|
@@ -134,12 +134,12 @@ func TestHttpIngress(t *testing.T) { | |
assert.Equal(t, nil, resp.Headers["Access-Control-Allow-Methods"]) | ||
assert.Equal(t, nil, resp.Headers["Access-Control-Allow-Headers"]) | ||
}), | ||
in.HttpCall(http.MethodGet, "/external", nil, in.JsonData(t, in.Obj{"message": "hello"}), func(t testing.TB, resp *in.HTTPResponse) { | ||
in.HttpCall(http.MethodPost, "/external", nil, in.JsonData(t, in.Obj{"message": "hello"}), func(t testing.TB, resp *in.HTTPResponse) { | ||
assert.Equal(t, 200, resp.Status) | ||
assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.Headers["Content-Type"]) | ||
assert.Equal(t, in.JsonData(t, in.Obj{"message": "hello"}), resp.BodyBytes) | ||
}), | ||
in.HttpCall(http.MethodGet, "/external2", nil, in.JsonData(t, in.Obj{"message": "hello"}), func(t testing.TB, resp *in.HTTPResponse) { | ||
in.HttpCall(http.MethodPost, "/external2", nil, in.JsonData(t, in.Obj{"message": "hello"}), func(t testing.TB, resp *in.HTTPResponse) { | ||
assert.Equal(t, 200, resp.Status) | ||
assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.Headers["Content-Type"]) | ||
assert.Equal(t, in.JsonData(t, in.Obj{"Message": "hello"}), resp.BodyBytes) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
} | ||
|
@@ -56,6 +60,10 @@ func BuildRequestBody(route *dal.IngressRoute, r *http.Request, sch *schema.Sche | |
queryMap[key] = valuesAny | ||
} | ||
|
||
finalQueryParams, err := mangleQueryParameters(queryMap, r.URL.Query(), request, sch) | ||
if err != nil { | ||
return nil, err | ||
} | ||
headerMap := make(map[string]any) | ||
for key, values := range r.Header { | ||
valuesAny := make([]any, len(values)) | ||
|
@@ -69,15 +77,11 @@ 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) | ||
} | ||
|
||
requestMap, err = schema.TransformFromAliasedFields(request, sch, requestMap) | ||
|
@@ -102,15 +106,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) | ||
} | ||
} | ||
|
||
|
@@ -122,6 +126,76 @@ 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 | ||
default: | ||
} | ||
// 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, nil | ||
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, nil | ||
case *schema.Bool: | ||
// TODO: is anything else considered truthy? | ||
return val == "true", nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use an LLM here to decide? |
||
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 | ||
} | ||
|
||
if m, ok := paramsField.Type.(*schema.Map); ok { | ||
if _, ok := m.Value.(*schema.Array); ok { | ||
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] | ||
stuartwdouglas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
return newParams, nil | ||
} | ||
|
||
func valueForData(typ schema.Type, data []byte) (any, error) { | ||
switch typ.(type) { | ||
case *schema.Ref: | ||
|
@@ -203,12 +277,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 | ||
|
@@ -217,29 +286,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) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe later we should code-gen some type aliases to eradicate some of the boilerplate, it's pretty verbose.
Something like
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the same thought, we just need to figure out which ones we want to provide and what to name them.