Skip to content

Commit

Permalink
Support for Raw/File Responses (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
robsignorelli authored Jun 4, 2021
1 parent 0d6b756 commit 8bf319e
Show file tree
Hide file tree
Showing 27 changed files with 1,403 additions and 174 deletions.
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ with as little fuss as possible.
* [Example](https://github.com/monadicstack/frodo#example)
* [Customize HTTP Route, Status, etc](https://github.com/monadicstack/frodo#doc-options-custom-urls-status-etc)
* [Error Handling](https://github.com/monadicstack/frodo#error-handling)
* [HTTP Redirects](https://github.com/monadicstack/frodo#http-redirects)
* [Middleware](https://github.com/monadicstack/frodo#middleware)

* [HTTP Redirects](https://github.com/monadicstack/frodo#http-redirects)
* [Request Scoped Metadata](https://github.com/monadicstack/frodo#request-scoped-metadata)
* [Create a JavaScript Client](https://github.com/monadicstack/frodo#creating-a-javascript-client)
* [Create a Dart/Flutter Client](https://github.com/monadicstack/frodo#creating-a-dartflutter-client)
Expand Down Expand Up @@ -353,6 +354,51 @@ documentation for [github.com/monadicstack/respond](https://github.com/monadicst
to see how you can roll your own custom errors, but still
drive which 4XX/5XX status your service generates.

## Returning Raw File Data

Let's say that you're writing `ProfilePictureService`. One of the
operations you might want is the ability to return the raw JPG data
for a user's profile picture. You do this the same way that you
handle JSON-based responses; just implement some interfaces so that
Frodo knows to treat it a little different:

```go
type ServeResponse struct {
file *io.File
}

// By implementing io.Reader, that tells Frodo to respond w/ raw
// data rather than JSON. Whatever it reads from here, that's what
// the caller will receive.
func (res ServeResponse) Read(b []byte) (int, error) {
return res.file.Read(b)
}

// By implementing ContentTypeSpecifier, this lets you dictate the
// underlying HTTP Content-Type header. Without this Frodo will have
// nothing to go on and assume "application/octet-stream".
func (res ServeResponse) ContentType() string {
return "image/jpeg"
}

// --- and now in your service ---

func (svc ProfilePictureService) Serve(ctx context.Context, req *ServeRequest) (*ServeResponse, error) {
// Ignore the fact that you probably don't store profile pictures on the
// hard drive of your service boxes...
f, err := os.Open("./pictures/" + req.UserID + ".jpg")
if err != nil {
return nil, errors.NotFound("no profile picture for user %s", req.UserID)
}
return &ServeResponse{file: f}, nil
}
```

Since `ServeResponse` implements `io.Reader`, the raw JPG bytes will
be sent to the caller instead of the JSON-marshaled version of the
result. Also, since it implements the `ContentType()` function, the
caller will see it as an "image/jpg" rather than "application/octet-stream".

## HTTP Redirects

It's fairly common to have a service call that does some work
Expand Down
2 changes: 2 additions & 0 deletions example/basic/calc/calculator_service_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
// are only included to show how you can return specific types of 4XX errors in a readable/maintainable fashion.
type CalculatorServiceHandler struct{}

// Add accepts two integers and returns a result w/ their sum.
func (c CalculatorServiceHandler) Add(_ context.Context, req *AddRequest) (*AddResponse, error) {
sum := req.A + req.B
if sum == 12345 {
Expand All @@ -20,6 +21,7 @@ func (c CalculatorServiceHandler) Add(_ context.Context, req *AddRequest) (*AddR
return &AddResponse{Result: sum}, nil
}

// Sub accepts two integers and returns a result w/ their difference.
func (c CalculatorServiceHandler) Sub(_ context.Context, req *SubRequest) (*SubResponse, error) {
if req.A < req.B {
return nil, errors.BadRequest("calculator service does not support negative numbers")
Expand Down
2 changes: 2 additions & 0 deletions example/multiservice/games/game_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type GameServiceHandler struct {
Repo Repo
}

// GetByID looks up a game record given its unique id.
func (svc *GameServiceHandler) GetByID(ctx context.Context, req *GetByIDRequest) (*GetByIDResponse, error) {
if req.ID == "" {
return nil, errors.BadRequest("id is required")
Expand All @@ -28,6 +29,7 @@ func (svc *GameServiceHandler) GetByID(ctx context.Context, req *GetByIDRequest)
return &response, nil
}

// Register adds another game record to our gaming database.
func (svc *GameServiceHandler) Register(ctx context.Context, req *RegisterRequest) (*RegisterResponse, error) {
if req.Name == "" {
return nil, errors.BadRequest("create: name is required")
Expand Down
2 changes: 2 additions & 0 deletions example/multiservice/scores/score_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type ScoreServiceHandler struct {
Repo Repo
}

// NewHighScore captures a player's high score for the given game.
func (svc *ScoreServiceHandler) NewHighScore(ctx context.Context, request *NewHighScoreRequest) (*NewHighScoreResponse, error) {
if request.GameID == "" {
return nil, errors.BadRequest("high scores: game id is required")
Expand Down Expand Up @@ -58,6 +59,7 @@ func (svc *ScoreServiceHandler) NewHighScore(ctx context.Context, request *NewHi
return &response, nil
}

// HighScoresForGame fetches the top "N" high scores achieved by any player
func (svc ScoreServiceHandler) HighScoresForGame(ctx context.Context, request *HighScoresForGameRequest) (*HighScoresForGameResponse, error) {
if request.GameID == "" {
return nil, errors.BadRequest("high scores: game id is required")
Expand Down
Loading

0 comments on commit 8bf319e

Please sign in to comment.