Skip to content

Commit

Permalink
Merge pull request #70 from calvinmclean/example/client-only
Browse files Browse the repository at this point in the history
Add client-only example and improve JSON CLI
  • Loading branch information
calvinmclean committed Jun 30, 2024
2 parents edaa67a + e683094 commit 7e987ea
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 37 deletions.
69 changes: 35 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,55 +23,55 @@ Implement custom request/response handling by implemented `Renderer` and `Binder

1. Create a new Go module:

```shell
mkdir babyapi-example
cd babyapi-example
go mod init babyapi-example
```
```shell
mkdir babyapi-example
cd babyapi-example
go mod init babyapi-example
```

2. Write `main.go` to create a `TODO` struct and initialize `babyapi.API`:

```go
package main
```go
package main

import "github.com/calvinmclean/babyapi"
import "github.com/calvinmclean/babyapi"

type TODO struct {
babyapi.DefaultResource
type TODO struct {
babyapi.DefaultResource

Title string
Description string
Completed bool
}
Title string
Description string
Completed bool
}

func main() {
api := babyapi.NewAPI(
"TODOs", "/todos",
func() *TODO { return &TODO{} },
)
api.RunCLI()
}
```
func main() {
api := babyapi.NewAPI(
"TODOs", "/todos",
func() *TODO { return &TODO{} },
)
api.RunCLI()
}
```

3. Run!

```shell
go mod tidy
go run main.go serve
```
```shell
go mod tidy
go run main.go serve
```

4. Use the built-in CLI to interact with the API:

```shell
# Create a new TODO
go run main.go client todos post --data '{"title": "use babyapi!"}'
```shell
# Create a new TODO
go run main.go client todos post --data '{"title": "use babyapi!"}'

# Get all TODOs
go run main.go client todos list
# Get all TODOs
go run main.go client todos list

# Get TODO by ID (use ID from previous responses)
go run main.go client todos get cljvfslo4020kglbctog
```
# Get TODO by ID (use ID from previous responses)
go run main.go client todos get cljvfslo4020kglbctog
```

<img alt="Simple Example" src="examples/simple/simple.gif" width="600" />

Expand Down Expand Up @@ -152,6 +152,7 @@ The `babyapi.EndDateable` interface can be implemented to enable soft-delete wit
| [Multiple APIs](./examples/multiple-apis/) | This example shows how multiple top-level (or any level) sibling APIs can be served, and have CLI functionality, under one root API | <ul><li>Use `NewRootAPI` to create a root API</li><li>Add multiple children to create siblings</li></ul> |
| [Background Worker](./examples/background-worker/) | This example shows how you can use `babyapi` in an application alongside background workers and have runtime control over all goroutines | <ul><li>Use `WithContext` to add a context to an API so the API will stop when the context is cancelled</li><li>Use `api.Done()` to have other goroutines stop when the API is stopped</li></ul> |
| [SQL](./examples/sql/) | This example shows how you can build an API with a custom implementation of `babyapi.Storage` using [`sqlc`](https://sqlc.dev) | <ul><li>Implement an Extension using a custom implementation of `babyapi.Storage`</li><li>Use `api.Done()` to clean up DB resources</li><li>Extend the built-in CLI to add flags or other customizations</li></ul> |
| [Pokemon Client](./examples/pokemon-client/) | This example shows how you can leverage the client and CLI features of `babyapi` to create a client for an external API | <ul><li>Add custom CLI command</li><li>Use just the client for an external API</li></ul> |

Also see a full example of an application implementing a REST API using `babyapi` in my [`automated-garden` project](https://github.com/calvinmclean/automated-garden/tree/main/garden-app).

Expand Down
6 changes: 3 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func newResponse[T any](resp *http.Response, expectedStatusCode int) (*Response[
return nil, httpErr
}

if result.ContentType == "application/json" {
if strings.Contains(result.ContentType, "application/json") {
err := json.Unmarshal([]byte(result.Body), &result.Data)
if err != nil {
return nil, fmt.Errorf("error decoding response body %q: %w", result.Body, err)
Expand All @@ -67,8 +67,8 @@ func (sr *Response[T]) Fprint(out io.Writer, pretty bool) error {
}

var err error
switch sr.ContentType {
case "application/json":
switch {
case strings.Contains(sr.ContentType, "application/json"):
encoder := json.NewEncoder(out)
if pretty {
encoder.SetIndent("", "\t")
Expand Down
21 changes: 21 additions & 0 deletions examples/pokemon-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Pokemon Client

This is an example of how babyapi can be used just to create a client for an external API, like the [PokeAPI](https://pokeapi.co/docs/v2).

Although it works with the default commands, this example adds an extra command to better demonstrate the Pokemon struct since the default commands will print the raw JSON response.

## How To

Use the custom `get` command to pretty-print Pokemon details
```shell
> go run main.go get pikachu
Name: pikachu
Types: [electric]
2 Abilities
105 Moves
```

Use the default `client get` command to print raw JSON data
```shell
go run main.go client --address "https://pokeapi.co" pokemon get pikachu
```
88 changes: 88 additions & 0 deletions examples/pokemon-client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/calvinmclean/babyapi"
"github.com/spf13/cobra"
)

type Pokemon struct {
ID int `json:"id"`
Name string `json:"name"`
Abilities []any `json:"abilities"`
Moves []any `json:"moves"`
Types []struct {
Type struct {
Name string `json:"name"`
} `json:"type"`
} `json:"types"`
}

func (p *Pokemon) String() string {
var sb strings.Builder

writef := func(format string, a ...any) {
sb.WriteString(fmt.Sprintf(format, a...))
}

var types []string
for _, t := range p.Types {
types = append(types, t.Type.Name)
}

writef("Name: %s\n", p.Name)
writef("Types: %s\n", types)
writef("%d Abilities\n", len(p.Abilities))
writef("%d Moves\n", len(p.Moves))

return sb.String()
}

func (p *Pokemon) GetID() string {
return fmt.Sprint(p.ID)
}

func (*Pokemon) Bind(*http.Request) error {
return nil
}

func (*Pokemon) Render(http.ResponseWriter, *http.Request) error {
return nil
}

func main() {
api := babyapi.NewAPI("pokemon", "/api/v2/pokemon/", func() *Pokemon { return &Pokemon{} })

cmd := api.Command()

// Add a custom get command because the regular get command will just print raw JSON
cmd.AddCommand(&cobra.Command{
Use: "get",
RunE: func(cmd *cobra.Command, args []string) error {
address, _ := cmd.Flags().GetString("address")
if address == "" {
address = "https://pokeapi.co"
}

resp, err := api.Client(address).Get(context.Background(), args[0])
if err != nil {
return err
}

if resp.Data != nil {
fmt.Println(resp.Data.String())
}

return nil
},
})

err := cmd.Execute()
if err != nil {
fmt.Printf("error: %v\n", err)
}
}

0 comments on commit 7e987ea

Please sign in to comment.