From e68309450cf1811be54aadd0b1b4a20416fbbee9 Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sun, 30 Jun 2024 11:02:09 -0700 Subject: [PATCH] Add client-only example and improve JSON CLI --- README.md | 69 ++++++++++++------------ client.go | 6 +-- examples/pokemon-client/README.md | 21 ++++++++ examples/pokemon-client/main.go | 88 +++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 37 deletions(-) create mode 100644 examples/pokemon-client/README.md create mode 100644 examples/pokemon-client/main.go diff --git a/README.md b/README.md index 119b9bd..0257d4b 100644 --- a/README.md +++ b/README.md @@ -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 + ``` Simple Example @@ -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 | | | [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 | | | [SQL](./examples/sql/) | This example shows how you can build an API with a custom implementation of `babyapi.Storage` using [`sqlc`](https://sqlc.dev) | | +| [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 | | 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). diff --git a/client.go b/client.go index cb2ec03..df1bb71 100644 --- a/client.go +++ b/client.go @@ -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) @@ -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") diff --git a/examples/pokemon-client/README.md b/examples/pokemon-client/README.md new file mode 100644 index 0000000..83b0681 --- /dev/null +++ b/examples/pokemon-client/README.md @@ -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 +``` diff --git a/examples/pokemon-client/main.go b/examples/pokemon-client/main.go new file mode 100644 index 0000000..e87e89f --- /dev/null +++ b/examples/pokemon-client/main.go @@ -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) + } +}