Skip to content
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

Episode 26 - Creating a CLI for a REST API #113

Merged
merged 14 commits into from Jul 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -23,6 +23,8 @@ glide
linux-amd64
glide-v0.12.0-linux-amd64.tar.gz

.envrc

*.exe
*.test
*.prof
Expand All @@ -46,3 +48,5 @@ episode25/tmp
episode25/tmp/episode25-build
episode25/node_modules
episode25/public/assets

episode26/ds
23 changes: 0 additions & 23 deletions episode20/README.md

This file was deleted.

4 changes: 4 additions & 0 deletions episode26/Makefile
@@ -0,0 +1,4 @@
test:
go test -test.v ./...
build:
go build -o episode20
41 changes: 41 additions & 0 deletions episode26/README.md
@@ -0,0 +1,41 @@
# Consuming a REST API in Go

Go in 5 Minutes, episode 26.

In this screencast, we're going to build a command line client to consume the awesome [Dark Sky API](https://darksky.net/dev/docs).

We'll be using [cobra](https://github.com/spf13/cobra) to build our command line client, and I did a previous episode on that package. If you haven't seen [episode 18](https://www.goin5minutes.com/screencast/episode_18_cli_with_cobra/), you might want to go review that before you look at this one.

Instead of using an already-built Dark Sky API client (there are a [few](https://darksky.net/dev/docs/libraries) for Go), we're going to build our own client according to the API documentation to show some tips and tricks for building clients for any REST API.

In this screencast, we'll use the awesome [gorequest](https://github.com/parnurzeal/gorequest) package to help us build a DarkSky client from scratch.

# Outline

1. Quick primer on Cobra
1. Quick primer on gorequest
1. Let's check out the code!

# How to Run This Code

You'll need Go version 1.11 or above to run this code. If you have an appropriate version, simply run `go build -o darksky .` to build.

Before you run the binary, you'll need an environment variable called `DARKSKY_API_KEY` set to your DarkSky API key (if you don't have one, get it from your [account](https://darksky.net/dev/account), or [create](https://darksky.net/dev/register) an account if you haven't already).

Then, call the binary like so, ensuring that `DARKSKY_API_KEY` is set in your environment:

```console
$ ./darksky temp --lat 45.512230 --long -122.658722
```

The `lat` and `long` flags are set to the latitude and longitude (respectively) of the location for which to get the temperature.

>The latitude and longitude in the above example are set to Portland, OR, USA. If you'd like to try another location, you can use https://www.latlong.net/

# Show Notes

- [Dark Sky API](https://darksky.net/dev/docs)
- [gorequest](https://github.com/parnurzeal/gorequest)
- [Cobra CLI Package](https://github.com/spf13/cobra)
- [Cobra Code Generation CLI](https://github.com/spf13/cobra/blob/master/cobra/README.md)
- A different way to decode JSON: [dcode](https://github.com/go-functional/dcode)
89 changes: 89 additions & 0 deletions episode26/cmd/root.go
@@ -0,0 +1,89 @@
package cmd

import (
"fmt"
"os"

"github.com/spf13/cobra"

homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
)

var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "episode26",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func init() {
apiKey := os.Getenv("DARKSKY_API_KEY")
if apiKey == "" {
fmt.Println("No DARKSKY_API_KEY environment variable set")
os.Exit(1)
}
cobra.OnInitialize(initConfig)

// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.episode26.yaml)")

// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")

tempCmd, err := temp(apiKey)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
rootCmd.AddCommand(tempCmd)
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}

// Search config in home directory with name ".episode26" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".episode26")
}

viper.AutomaticEnv() // read in environment variables that match

// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
40 changes: 40 additions & 0 deletions episode26/cmd/temp.go
@@ -0,0 +1,40 @@
package cmd

import (
"fmt"

"github.com/arschles/go-in-5-minutes/episode26/dsclient"
"github.com/spf13/cobra"
)

func temp(apiKey string) (*cobra.Command, error) {
cl, err := dsclient.New(apiKey)
if err != nil {
return nil, err
}
cmd := &cobra.Command{
Use: "temp",
}
flags := cmd.PersistentFlags()
lat := flags.Float64("lat", 0, "The Latitude to fetch")
long := flags.Float64("long", 0, "The Longitude to fetch")
cmd.RunE = func(*cobra.Command, []string) error {
fmt.Printf("Getting temp for (%f, %f)\n", *lat, *long)
fcast, err := cl.Forecast(*lat, *long)
if err != nil {
return err
}
if len(fcast.Hourly.Data) < 1 {
return fmt.Errorf("No hourly data returned!")
}
hourly := fcast.Hourly.Data[0]
fmt.Printf(
"Temperature for your location (%f, %f): %f\n",
*lat,
*long,
hourly.ApparentTemp, // the API only allows apparent temp on hourly
)
return nil
}
return cmd, nil
}
38 changes: 38 additions & 0 deletions episode26/dsclient/client.go
@@ -0,0 +1,38 @@
package dsclient

import (
"fmt"

multierr "github.com/hashicorp/go-multierror"
"github.com/parnurzeal/gorequest"
)

type Client struct {
apiKey string
cl *gorequest.SuperAgent
}

func New(apiKey string) (*Client, error) {
if apiKey == "" {
return nil, fmt.Errorf("No API key passed")
}
return &Client{apiKey: apiKey, cl: gorequest.New()}, nil
}

func (c *Client) Forecast(lat, long float64) (*Response, error) {
forecastURL := fmt.Sprintf(
"https://api.darksky.net/forecast/%s/%f,%f",
c.apiKey,
lat,
long,
)
resp := &Response{}
httpRes, _, errs := c.cl.Get(forecastURL).EndStruct(resp)
if len(errs) > 0 {
return nil, &multierr.Error{Errors: errs}
}
if httpRes.StatusCode != 200 {
return nil, fmt.Errorf("HTTP Status Code %d returned!", httpRes.StatusCode)
}
return resp, nil
}
9 changes: 9 additions & 0 deletions episode26/dsclient/data_block.go
@@ -0,0 +1,9 @@
package dsclient

type DataBlock struct {
Data []DataPoint `json:"data"`
Summary string `json:"summary"`
Icon string `json:"icon"`
// There are way more fields in a data block.
// see https://darksky.net/dev/docs#data-block for all of them
}
7 changes: 7 additions & 0 deletions episode26/dsclient/data_point.go
@@ -0,0 +1,7 @@
package dsclient

type DataPoint struct {
ApparentTemp float64 `json:"apparentTemperature"`
// There are way more fields in a data point.
// see https://darksky.net/dev/docs#data-point for all of them
}
11 changes: 11 additions & 0 deletions episode26/dsclient/response.go
@@ -0,0 +1,11 @@
package dsclient

type Response struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Timezone string `json:"timezone"`
Currently DataPoint `json:"currently"`
Minutely DataBlock `json:"minutely"`
Hourly DataBlock `json:"hourly"`
Daily DataBlock `json:"daily"`
}
24 changes: 24 additions & 0 deletions episode26/go.mod
@@ -0,0 +1,24 @@
module github.com/arschles/go-in-5-minutes/episode26

go 1.12

require (
github.com/elazarl/goproxy v0.0.0-20190703090003-6125c262ffb0 // indirect
github.com/elazarl/goproxy/ext v0.0.0-20190703090003-6125c262ffb0 // indirect
github.com/hashicorp/go-multierror v1.0.0
github.com/magiconair/properties v1.8.1 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/moul/http2curl v1.0.0 // indirect
github.com/parnurzeal/gorequest v0.2.15
github.com/pelletier/go-toml v1.4.0 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cobra v0.0.5
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.4.0
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect
golang.org/x/text v0.3.2 // indirect
)