Skip to content
Permalink
Browse files

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

* Creating a CLI for a REST API

* moving to ep 26

* re-add episode 20 Makefile

* spacing

* move to episode 26

* progress!

* mod tidy

* fixes

* more readme-ing

* Add reference to dcode

* better show notes

* adding ignore for ds binary

* Adding a web page for this episode

YouTube ID to come - still uploading

* YT video ID
  • Loading branch information...
arschles committed Jul 9, 2019
1 parent f3d9e07 commit c5287aa61c6db248b775300c579e4e88203cb638
@@ -23,6 +23,8 @@ glide
linux-amd64
glide-v0.12.0-linux-amd64.tar.gz

.envrc

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

episode26/ds

This file was deleted.

@@ -0,0 +1,4 @@
test:
go test -test.v ./...
build:
go build -o episode20
@@ -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)
@@ -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())
}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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"`
}
@@ -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
)

0 comments on commit c5287aa

Please sign in to comment.
You can’t perform that action at this time.