The Challenge
DigitalOcean released their version of serverless functions and released a challenge 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, but it left a few questions to be answered.
The
Content-Type
andAccepted
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.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.
// 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.
// 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.
// 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).
// 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:
// 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:
// 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.
Top comments (0)