Writing a Go CLI to complete the DigitalOcean Functions Challenge
Tags: go, digitalocean, functions, api
Published: 2022-06-09 15:39:23

---
title: Writing a Go CLI to complete the DigitalOcean Functions Challenge
published: true
description: A quick write-up on writing a CLI to complete the DigitalOcean Functions Challenge
tags: go, golang, digitalocean, functions, api
series: DigitalOcean Functions
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
---

# The Challenge

DigitalOcean released their version of [serverless functions](https://docs.digitalocean.com/products/functions) and [released a challenge](https://functionschallenge.digitalocean.com) recently to showcase them as well. Being one that doesn't like to turn down a challenge, I took DigitalOcean up on their challenge with a Go CLI.

# Getting started

First, it was pretty simple to read the challenge on the [challenge website](https://functionschallenge.digitalocean.com), but it left a few questions to be answered.

1. The `Content-Type` and `Accepted` headers tell me that this application accepts and sends back JSON data, but the directions in the overall documentation don't really say that other than that a parameters file is a JSON document, but I went ahead and wrote my CLI as thought the API did accept and send back JSON. I was correct, but the documentation here wasn't entirely clear.

2. Once you have deployed you "Sammy" (or your DigitalOcean mascot). You have to have a keen eye to catch it. If you don't believe it's there, you can easily inspect the page elements for something that looks like the Sammy that you created.

# The Code

In this case, as this was just a throw away app, I just wrote everything into a `main.go` file and turns on modules with `go mod init <github.com/your_handle/project_name>`.

The first thing to do was to start with a CLI that would accept the `name` and `type` parameters as described in the instructions. I opted to put these flags in the `init` function and load a few module variables so I didn't really have to do much with them and I expected them to run each time the app was run.

```go
// main.go

package main

import (
    "flag"
)

var (
    sammyname string
    sammytype string
)

const (
    API_URL = "https://functionschallenge.digitalocean.com/api/sammy"
)

func init() {
    flag.StringVar(&sammyname, "name", "", "The name to give your new Sammy.")
    flag.StringVar(&sammytype, "type", "", "The type to assign to your new Sammy.")
    flag.Parse()
}

func main() {}
```

Running our app with `go run main.go` now gives us our command line flags.

After looking again at the docs, the Sammy Type parameter is really a enum, so I created a type and a few helper functions and used them to set the variable appropriately.

```go
// main.go
...

import (
    "fmt"
    "log"
    "flag"
    "strings"
)

var (
    sammyname string
    sammytype sammyType
)

type (
    sammyType string
)

const (
    API_URL = "https://functionschallenge.digitalocean.com/api/sammy"

    sammyType_Sammy sammyType = "sammy"
    sammyType_Punk sammyType = "punk"
    sammyType_Dinosaur sammyType = "dinosaur"
    sammyType_Retro sammyType = "retro"
    sammyType_Pizza sammyType = "pizza"
    sammyType_Robot sammyType = "robot"
    sammyType_Pony sammyType = "pony"
    sammyType_Bootcamp sammyType = "bootcamp"
    sammyType_XRay sammyType = "xray"
)

func NewSammyType(s string) (sammyType, error) {
    var t sammyType
    switch strings.ToLower(s) {
    case "sammy":
        t = sammyType_Sammy
    case "punk":
        t = sammyType_Punk
    case "dinosaur":
        t = sammyType_Dinosaur
    case "retro":
        t = sammyType_Retro
    case "pizza":
        t = sammyType_Pizza
    case "robot":
        t = sammyType_Robot
    case "pony":
        t = sammyType_Pony
    case "bootcamp":
        t = sammyType_Bootcamp
    case "xray":
        t = sammyType_XRay
    default:
        return "", fmt.Errorf("%s is an invalid sammyType", s)
    }
    return t, nil
}

func (s sammyType) String() string {
    return string(s)
}

func init() {
    flag.StringVar(&sammyname, "name", "", "The name to give your new Sammy.")
    st := flag.String("type", "", "The type to assign to your new Sammy.")
    flag.Parse()

    sammyType, err := NewSammyType(*st)
    if err != nil {
        log.Fatal(err)
    }
}

...
```

Now, we had something that is type correct for the API.
Next, I simply wrote in a struct to represent the request, as well as a few helper functions there as well.

```go
// main.go
...

import (
    "fmt"
    "log"
    "flag"
    "strings"
    "bytes"
    "encoding/json"
    "io"
    "io/ioutil"
)

...

type (
    sammyType string

    sharksRequest struct {
        Name string `json:"name"`
        Type string `json:"name"`
    }
)

// func NewSharksRequest() is something that I normally write, but there was really no point in this case.

func (req *sharksRequest) setName(name string) *sharksRequest  {
    req.Name = name
}

func (req *sharksRequest) setType(t sammyType) *sharksRequest {
    req.Type = t.String()
}

func (req *sharksRequest) marshalJSON() ([]byte, error) {
    return json.Marshal(req)
} 

...
```

Now we had something to send to the API. So it was just a matter of sending a request, and seeing what the output was as the output wasn't documented at all (that I could see).

```go
// main.go
...

func main() {
    // Set some variables to use:
    // Mostly we need the set up the base request struct and
    // the http client.
    var (
        c := http.DefaultClient
        r := &sharksRequest{
            Name: sammyname,
        }
    )

    // We can use our helper function to make sure we are
    // getting correct values here.
    r.setType(NewSammyType(sammytype))

    // marshal the struct to JSON
    rb, err := r.marshalJSON()
    if err != nil {
        log.Fatal(err)
    }
    
    // Build the new http request
    req, err := http.NewRequest(http.MethodPost, API_URL, bytes.NewBuffer(rb))
    if err != nil {
        log.Fatal(err)
    }

    // Set the required headers (from the docs).
    req.Header = map[string][]string{
        "Accept": []string{"application/json"},
        "Content-Type": []string{"application/json"},
    }

    // Send the request
    resp, err := c.Do(req)
    if err != nil {
        log.Fatal(err)
    }

    // Because we don't know the structure of the output
    // we can just output the response to the terminal.
    defer resp.Body.Close()

    out, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    log.Println(string(out))
}
```

Running our code via go run, `go run main.go -name <myname> -type <mytype>` actually works at this point, but I don't like the raw byte string that we are outputting. In a real API handler, we want to put the response into a struct, so now that I can see some output, I can build a struct.

The output for me looked something like this for a success:

`{"message":"Shark created successfully! Congrats on successfully completing the functions challenge!"}`

and this for a failure:

`{"message":"The name has already been taken.","errors":{"name":["The name has already been taken."]}}`

So, with this info in mind, I wrote another struct and some more helper methods to load that info into:

```go
// main.go
...

type (
    sammyType string

    sharksRequest struct {
        Name string `json:"name"`
        Type string `json:"name"`
    }

    sharksResponse struct {
        // Message seems pretty strait forward and exists 
        // in both the responses, so this is a pretty safe
        // bet
        Message string `json:"message"`
        // Errors on the other hand, I don't have enough
        // info to strongly type the response, but the
        // map type here works for now. I can change it
        // later if there is more I want to do with the
        // errors.
        Errors map[string][]string `json:"errors"`
    }

    // func NewSharksResponse() is something again that I would normally write, but it isn't necessary in this example

    func (resp *sharksResponse) unmarshalJSON(body io.ReadCloser) error {
        b, err := ioutil.ReadAll(body)
        if err != nil {
            return fmt.Errorf("unable to read http body: %w", err)
        }

        return json.Unmarshal(b, resp)
    }
)

...
```

Now that we have this, we can add it to our main function:

```go
// main.go
...

func main() {
    // Set some variables to use:
    // Mostly we need the set up the base request struct, 
    // the response struct, and the http client.
    var (
        c := http.DefaultClient
        r := &sharksRequest{
            Name: sammyname,
        }
        R := &sharksResponse{}
    )

    // We can use our helper function to make sure we are
    // getting correct values here.
    r.setType(NewSammyType(sammytype))

    // marshal the struct to JSON
    rb, err := r.marshalJSON()
    if err != nil {
        log.Fatal(err)
    }
    
    // Build the new http request
    req, err := http.NewRequest(http.MethodPost, API_URL, bytes.NewBuffer(rb))
    if err != nil {
        log.Fatal(err)
    }

    // Set the required headers (from the docs).
    req.Header = map[string][]string{
        "Accept": []string{"application/json"},
        "Content-Type": []string{"application/json"},
    }

    // Send the request
    resp, err := c.Do(req)
    if err != nil {
        log.Fatal(err)
    }

    // We have a structure to unmarshal to now, so lets use
    // it.
    defer resp.Body.Close()

    if err := R.unmarshalJSON(resp.Body); err != nil {
        log.Fatal(err)
    }

    // Now we can send some better info to the terminal
    if len(R.Errors) > 0 {
        log.Fatalf("An error occurred. Message: %s, Errors: %v", R.Message, R.Errors)
    }

    log.Println(R.Message)
}

```

And with that, I would say we have a successful application. To see the full code, feel free to check out my repository for the challenge here: https://github.com/j4ng5y/digitalocean-functions-challenge

I will put more languages in this repo too, so follow the instructions in the README to look at other examples (as I create them). I will have similar write-ups for those other languages as well.