Skip to content

Commit

Permalink
feat: add support for HttpRequest[[]byte]
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman committed Jan 31, 2024
1 parent d6204e2 commit 3962960
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 57 deletions.
86 changes: 53 additions & 33 deletions backend/controller/ingress/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
Expand Down Expand Up @@ -116,7 +117,7 @@ func ValidateAndExtractRequestBody(route *dal.IngressRoute, r *http.Request, sch
return nil, fmt.Errorf("verb %s input must be a data structure", verb.Name)
}

bodyMap, err := buildRequest(route, r, request, sch)
httpRequestBody, err := extractHTTPRequestBody(route, r, request, sch)
if err != nil {
return nil, err
}
Expand All @@ -127,7 +128,7 @@ func ValidateAndExtractRequestBody(route *dal.IngressRoute, r *http.Request, sch
requestMap["pathParameters"] = pathParameters
requestMap["query"] = r.URL.Query()
requestMap["headers"] = r.Header
requestMap["body"] = bodyMap
requestMap["body"] = httpRequestBody

requestMap, err = transformAliasedFields(request, sch, requestMap)
if err != nil {
Expand All @@ -149,7 +150,7 @@ func ValidateAndExtractRequestBody(route *dal.IngressRoute, r *http.Request, sch
return nil, fmt.Errorf("verb %s input must be a data structure", verb.Name)
}

requestMap, err := buildRequest(route, r, request, sch)
requestMap, err := buildRequestMap(route, r, request, sch)
if err != nil {
return nil, err
}
Expand All @@ -168,7 +169,46 @@ func ValidateAndExtractRequestBody(route *dal.IngressRoute, r *http.Request, sch
return body, nil
}

func buildRequest(route *dal.IngressRoute, r *http.Request, dataRef *schema.DataRef, sch *schema.Schema) (map[string]any, error) {
func extractHTTPRequestBody(route *dal.IngressRoute, r *http.Request, dataRef *schema.DataRef, sch *schema.Schema) (any, error) {
data, err := sch.ResolveDataRefMonomorphised(dataRef)
if err != nil {
return nil, err
}

var bodyField *schema.Field
for _, field := range data.Fields {
if field.Name == "body" {
bodyField = field
break
}
}

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

switch bodyField.Type.(type) {
case *schema.DataRef:
bodyMap, err := buildRequestMap(route, r, dataRef, sch)
if err != nil {
return nil, err
}
return bodyMap, nil

case *schema.Bytes:
defer r.Body.Close()
bodyData, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("error reading request body: %w", err)
}
return bodyData, nil

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

func buildRequestMap(route *dal.IngressRoute, r *http.Request, dataRef *schema.DataRef, sch *schema.Schema) (map[string]any, error) {
requestMap := map[string]any{}
matchSegments(route.Path, r.URL.Path, func(segment, value string) {
requestMap[segment] = value
Expand All @@ -187,17 +227,9 @@ func buildRequest(route *dal.IngressRoute, r *http.Request, dataRef *schema.Data
requestMap[k] = v
}
default:
data := sch.ResolveDataRef(dataRef)
if data == nil {
return nil, fmt.Errorf("unknown data %v", dataRef)
}

if len(dataRef.TypeParameters) > 0 {
var err error
data, err = data.Monomorphise(dataRef.TypeParameters...)
if err != nil {
return nil, err
}
data, err := sch.ResolveDataRefMonomorphised(dataRef)
if err != nil {
return nil, err
}

queryMap, err := parseQueryParams(r.URL.Query(), data)
Expand All @@ -214,17 +246,9 @@ func buildRequest(route *dal.IngressRoute, r *http.Request, dataRef *schema.Data
}

func validateRequestMap(dataRef *schema.DataRef, path path, request map[string]any, sch *schema.Schema) error {
data := sch.ResolveDataRef(dataRef)
if data == nil {
return fmt.Errorf("unknown data %v", dataRef)
}

if len(dataRef.TypeParameters) > 0 {
var err error
data, err = data.Monomorphise(dataRef.TypeParameters...)
if err != nil {
return err
}
data, err := sch.ResolveDataRefMonomorphised(dataRef)
if err != nil {
return err
}

var errs []error
Expand Down Expand Up @@ -464,13 +488,9 @@ func hasInvalidQueryChars(s string) bool {
}

func transformAliasedFields(dataRef *schema.DataRef, sch *schema.Schema, request map[string]any) (map[string]any, error) {
data := sch.ResolveDataRef(dataRef)
if len(dataRef.TypeParameters) > 0 {
var err error
data, err = data.Monomorphise(dataRef.TypeParameters...)
if err != nil {
return nil, err
}
data, err := sch.ResolveDataRefMonomorphised(dataRef)
if err != nil {
return nil, err
}

for _, field := range data.Fields {
Expand Down
13 changes: 4 additions & 9 deletions backend/schema/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,14 @@ import (
//
// It takes in the full schema in order to resolve and define references.
func DataToJSONSchema(schema *Schema, dataRef DataRef) (*jsonschema.Schema, error) {
data := schema.ResolveDataRef(&dataRef)
data, err := schema.ResolveDataRefMonomorphised(&dataRef)
if err != nil {
return nil, err
}
if data == nil {
return nil, fmt.Errorf("unknown data type %s", dataRef)
}

if len(dataRef.TypeParameters) > 0 {
var err error
data, err = data.Monomorphise(dataRef.TypeParameters...)
if err != nil {
return nil, err
}
}

// Collect all data types.
dataTypes := schema.DataMap()

Expand Down
17 changes: 17 additions & 0 deletions backend/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ func (s *Schema) ResolveDataRef(ref *DataRef) *Data {
return nil
}

func (s *Schema) ResolveDataRefMonomorphised(ref *DataRef) (*Data, error) {
data := s.ResolveDataRef(ref)
if data == nil {
return nil, fmt.Errorf("unknown data %v", ref)
}

if len(ref.TypeParameters) > 0 {
var err error
data, err = data.Monomorphise(ref.TypeParameters...)
if err != nil {
return nil, err
}
}

return data, nil
}

func (s *Schema) ResolveVerbRef(ref *VerbRef) *Verb {
for _, module := range s.Modules {
if module.Name == ref.Module {
Expand Down
50 changes: 35 additions & 15 deletions examples/go/httpingress/httpingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ type GetResponse struct {
Message string `json:"message"`
}

// Example: curl -i http://localhost:8892/ingress/http/users/123/posts?postId=456
//
//ftl:verb
//ftl:ingress http GET /http/users/{userID}/posts/{postID}
//ftl:ingress http GET /http/users/{userID}/posts
func Get(ctx context.Context, req builtin.HttpRequest[GetRequest]) (builtin.HttpResponse[GetResponse], error) {
logger := ftl.LoggerFromContext(ctx)
logger.Infof("Path: %s", req.Path)
logger.Infof("Method: %s", req.Method)
logger.Infof("Query: %s", req.Query)
logger.Infof("Body: %s", req.Body)
logger.Infof("Headers: %s", req.Headers)
logger.Infof("Query: %v", req.Query)
logger.Infof("Body: %v", req.Body)
logger.Infof("Headers: %v", req.Headers)
return builtin.HttpResponse[GetResponse]{
Status: 200,
Headers: map[string][]string{"Get": {"Header from FTL"}},
Expand All @@ -36,21 +38,23 @@ func Get(ctx context.Context, req builtin.HttpRequest[GetRequest]) (builtin.Http
}

type PostRequest struct {
UserID string `json:"userId"`
PostID string `json:"postId"`
UserID int `json:"userId"`
PostID int `json:"postId"`
}

type PostResponse struct{}

// Example: curl -i --json '{"userID": 123, "postID": 345}' http://localhost:8892/ingress/http/users
//
//ftl:verb
//ftl:ingress http POST /http/users
func Post(ctx context.Context, req builtin.HttpRequest[PostRequest]) (builtin.HttpResponse[PostResponse], error) {
logger := ftl.LoggerFromContext(ctx)
logger.Infof("Path: %s", req.Path)
logger.Infof("Method: %s", req.Method)
logger.Infof("Query: %s", req.Query)
logger.Infof("Body: %s", req.Body)
logger.Infof("Headers: %s", req.Headers)
logger.Infof("Query: %v", req.Query)
logger.Infof("Body: %v", req.Body)
logger.Infof("Headers: %v", req.Headers)
return builtin.HttpResponse[PostResponse]{
Status: 201,
Headers: map[string][]string{"Post": {"Header from FTL"}},
Expand All @@ -65,15 +69,17 @@ type PutRequest struct {

type PutResponse struct{}

// Example: curl -X PUT http://localhost:8892/ingress/http/users/123 -d '{"postID": "123"}'
//
//ftl:verb
//ftl:ingress http PUT /http/users/{userID}
func Put(ctx context.Context, req builtin.HttpRequest[PutRequest]) (builtin.HttpResponse[PutResponse], error) {
logger := ftl.LoggerFromContext(ctx)
logger.Infof("Path: %s", req.Path)
logger.Infof("Method: %s", req.Method)
logger.Infof("Query: %s", req.Query)
logger.Infof("Body: %s", req.Body)
logger.Infof("Headers: %s", req.Headers)
logger.Infof("Query: %v", req.Query)
logger.Infof("Body: %v", req.Body)
logger.Infof("Headers: %v", req.Headers)
return builtin.HttpResponse[PutResponse]{
Status: 200,
Headers: map[string][]string{"Put": {"Header from FTL"}},
Expand All @@ -87,15 +93,17 @@ type DeleteRequest struct {

type DeleteResponse struct{}

// Example: curl -X DELETE http://localhost:8892/ingress/http/users/123
//
//ftl:verb
//ftl:ingress http DELETE /http/users/{userID}
func Delete(ctx context.Context, req builtin.HttpRequest[DeleteRequest]) (builtin.HttpResponse[DeleteResponse], error) {
logger := ftl.LoggerFromContext(ctx)
logger.Infof("Path: %s", req.Path)
logger.Infof("Method: %s", req.Method)
logger.Infof("Query: %s", req.Query)
logger.Infof("Body: %s", req.Body)
logger.Infof("Headers: %s", req.Headers)
logger.Infof("Query: %v", req.Query)
logger.Infof("Body: %v", req.Body)
logger.Infof("Headers: %v", req.Headers)
return builtin.HttpResponse[DeleteResponse]{
Status: 200,
Headers: map[string][]string{"Put": {"Header from FTL"}},
Expand All @@ -114,3 +122,15 @@ func Html(ctx context.Context, req builtin.HttpRequest[HtmlRequest]) (builtin.Ht
Body: "<html><body><h1>HTML Page From FTL 🚀!</h1></body></html>",
}, nil
}

// Example: curl -X POST http://localhost:8892/ingress/http/bytes -d 'Your data here'
//
//ftl:verb
//ftl:ingress http POST /http/bytes
func Bytes(ctx context.Context, req builtin.HttpRequest[[]byte]) (builtin.HttpResponse[[]byte], error) {
return builtin.HttpResponse[[]byte]{
Status: 200,
Headers: map[string][]string{"Content-Type": {"application/octet-stream"}},
Body: req.Body,
}, nil
}

0 comments on commit 3962960

Please sign in to comment.