From 95d680ee0c7fa553311d29fd2df4f5c8db918601 Mon Sep 17 00:00:00 2001
From: Jamie Lennox <jamie@vibrato.com.au>
Date: Tue, 26 Feb 2019 09:47:42 +1100
Subject: [PATCH 1/2] Add swagger json and UI to application

We're going to use this application as the base of a take home front-end
test in which the applicant is expected to develop a new front end for
the api. This means we need to much better document the API.

Add a swagger generator and all the tagging required to make that work,
and a swagger UI so that we can point users at the interface for the API
documentation.
---
 Dockerfile        |  14 +++-
 assets/js/app.jsx |  38 +++++------
 db/db.go          |   4 +-
 main.go           |   8 +++
 model/task.go     |  22 ++++--
 ui/api.go         | 168 ++++++++++++++++++++++++++++++++++++++++++++++
 ui/index.go       |  82 ++++++++++++++++++++++
 ui/ui.go          | 139 +++-----------------------------------
 8 files changed, 319 insertions(+), 156 deletions(-)
 create mode 100644 ui/api.go
 create mode 100644 ui/index.go

diff --git a/Dockerfile b/Dockerfile
index e5914e56..5b116dfa 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,18 @@
 FROM golang:alpine AS build
 
-RUN apk add --no-cache curl git
+RUN apk add --no-cache curl git gcc linux-headers musl-dev
 
 RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
 
+ARG GO_SWAGGER_VERSION=0.18.0
+ARG SWAGGER_UI_VERSION=3.20.9
+
+RUN curl -sfL -o /usr/local/bin/swagger https://github.com/go-swagger/go-swagger/releases/download/v$GO_SWAGGER_VERSION/swagger_linux_amd64 \
+    && chmod +x /usr/local/bin/swagger \
+    && curl -sfL https://github.com/swagger-api/swagger-ui/archive/v$SWAGGER_UI_VERSION.tar.gz | tar xz -C /tmp/ \
+    && mv /tmp/swagger-ui-$SWAGGER_UI_VERSION /tmp/swagger \
+    && sed -i 's#"https://petstore\.swagger\.io/v2/swagger\.json"#"./swagger.json"#g' /tmp/swagger/dist/index.html
+
 WORKDIR $GOPATH/src/github.com/vibrato/TechTestApp
 
 COPY Gopkg.toml Gopkg.lock $GOPATH/src/github.com/vibrato/TechTestApp/
@@ -13,6 +22,7 @@ RUN dep ensure -vendor-only -v
 COPY . .
 
 RUN go build -o /TechTestApp
+RUN swagger generate spec -o /swagger.json
 
 FROM alpine:latest
 
@@ -21,6 +31,8 @@ WORKDIR /TechTestApp
 COPY assets ./assets
 COPY conf.toml ./conf.toml
 
+COPY --from=build /tmp/swagger/dist ./assets/swagger
+COPY --from=build /swagger.json ./assets/swagger/swagger.json
 COPY --from=build /TechTestApp TechTestApp
 
 ENTRYPOINT [ "./TechTestApp",  "serve" ]
diff --git a/assets/js/app.jsx b/assets/js/app.jsx
index 4d978742..f93c6974 100644
--- a/assets/js/app.jsx
+++ b/assets/js/app.jsx
@@ -7,10 +7,10 @@ class TaskContainer extends React.Component {
 
         this.state ={
             tasks: [{
-                Id: 0, 
-                Title: "Loading...", 
-                Completed: false, 
-                Priority: 0
+                id: 0,
+                title: "Loading...",
+                completed: false,
+                priority: 0
             }]
         };
     }
@@ -30,22 +30,22 @@ class TaskContainer extends React.Component {
     deleteTask(task) {
 
         console.log(task)
-        
+
         var filteredTasks = this.state.tasks.filter(function (item) {
-            return (item.ID !== task.ID);
+            return (item.id !== task.id);
           });
 
         console.log(filteredTasks)
-        
-        this.setState({tasks: filteredTasks}); 
 
-        fetch("/api/task/" + task.ID + "/", {
+        this.setState({tasks: filteredTasks});
+
+        fetch("/api/task/" + task.id + "/", {
             method: "DELETE",
         })
 
 
     }
-    
+
     render() {
 
       return (
@@ -70,8 +70,8 @@ class TaskList extends React.Component {
     render() {
         return(
             <ul className="theList">
-            {this.props.tasks.map(task => 
-                <li key={task.Id}><span className="delete" onClick={() => this.delete(task)}><i className="fas fa-trash"></i></span><span className="title">{task.Title}</span></li>
+            {this.props.tasks.map(task =>
+                <li key={task.id}><span className="delete" onClick={() => this.delete(task)}><i className="fas fa-trash"></i></span><span className="title">{task.title}</span></li>
             )}
             </ul>
         )
@@ -86,10 +86,10 @@ class TaskForm extends React.Component {
     }
 
     state = {
-        Title: "",
-        Priority: 1000,
-        Completed: false,
-        Id: 0,
+        title: "",
+        priority: 1000,
+        completed: false,
+        id: 0,
     };
 
     onChange = (e) => {
@@ -104,7 +104,7 @@ class TaskForm extends React.Component {
         event.preventDefault();
 
         const data = this.state
-        
+
         console.log(data)
 
         if (data.Title === "") {
@@ -119,7 +119,7 @@ class TaskForm extends React.Component {
         .then(data => this.props.onAddTask(data))
         .then(o => this.setState({Title: ""}));
     }
-    
+
     render() {
 		return (
 			<form onSubmit={this.handleSubmit} className="taskForm">
@@ -132,4 +132,4 @@ class TaskForm extends React.Component {
 
 
 ReactDOM.render( <TaskContainer/>, document.querySelector("#root"));
-  
\ No newline at end of file
+
diff --git a/db/db.go b/db/db.go
index 07a9adf4..6aad7c0f 100644
--- a/db/db.go
+++ b/db/db.go
@@ -69,7 +69,7 @@ func RebuildDb(cfg Config) error {
 	}
 
 	query = fmt.Sprintf(`CREATE DATABASE %s
-WITH 
+WITH
 OWNER = %s
 ENCODING = 'UTF8'
 LC_COLLATE = 'en_US.utf8'
@@ -194,7 +194,7 @@ func getSeedTasks() []model.Task {
 	return tasks
 }
 
-// GetAllTasks lists ass tasks in the database
+// GetAllTasks lists all tasks in the database
 func GetAllTasks(cfg Config) ([]model.Task, error) {
 
 	var tasks []model.Task
diff --git a/main.go b/main.go
index ca8da3d2..ad9e7b93 100644
--- a/main.go
+++ b/main.go
@@ -18,6 +18,14 @@
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 
+// Vibrato Tech Test Application
+//
+// This application is used as part of the Vibrato Technical Assessment.
+//
+//     Version: 1.0
+//     Contact: Thomas Winsnes<tom@vibrato.com.au> https://www.vibrato.com.au
+//
+// swagger:meta
 package main
 
 import "github.com/vibrato/TechTestApp/cmd"
diff --git a/model/task.go b/model/task.go
index 8d175ed3..8b3372b2 100644
--- a/model/task.go
+++ b/model/task.go
@@ -20,9 +20,23 @@
 
 package model
 
+// A task
+// swagger:model
 type Task struct {
-	ID       int
-	Priority int
-	Title    string
-	Complete bool
+	// the id of the task
+	// required: true
+	// min: 0
+	ID int `json:"id"`
+
+	// Where the task fits in the list
+	// required: true
+	// min: 0
+	Priority int `json:"priority"`
+
+	// The task name or description
+	// required: true
+	Title string `json:"title"`
+
+	// Is the task finished
+	Complete bool `json:"complete"`
 }
diff --git a/ui/api.go b/ui/api.go
new file mode 100644
index 00000000..7d5e031c
--- /dev/null
+++ b/ui/api.go
@@ -0,0 +1,168 @@
+// Copyright © 2018 Thomas Winsnes <tom@vibrato.com.au>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package ui
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"strconv"
+
+	"github.com/gorilla/mux"
+	"github.com/vibrato/TechTestApp/db"
+	"github.com/vibrato/TechTestApp/model"
+)
+
+// TaskID parameter.
+//
+// swagger:parameters deleteTask
+type TaskID struct {
+	// The ID of the task
+	//
+	// in: path
+	// min: 0
+	// required: true
+	ID int `json:"id"`
+}
+
+// Sucessful Task Array Response
+//
+// swagger:response allTasks
+type allTasks struct {
+	// in: body
+	// The tasks being returned
+	// required: true
+	Tasks []model.Task `json:"tasks"`
+}
+
+// swagger:route GET /api/task/ getTasks
+//
+// Fetch all tasks
+//
+//    Produces:
+//      - application/json
+//
+//    Responses:
+//      200: allTasks
+//
+func getTasks(cfg Config) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		output, _ := db.GetAllTasks(cfg.DB)
+		js, _ := json.Marshal(output)
+		w.Header().Set("Content-Type", "application/json")
+		fmt.Fprintf(w, string(js))
+	})
+}
+
+// Sucessful Single Task Response
+//
+// swagger:response aTask
+type aTask struct {
+	// in: body
+	// The tasks being returned
+	// required: true
+	Task model.Task `json:"task"`
+}
+
+// swagger:parameters addTask
+type taskParameter struct {
+	// in:body
+	Task model.Task `json:"task"`
+}
+
+// swagger:route POST /api/task/ addTask
+//
+// Add a new task to the list.
+//
+//    Produces:
+//      - application/json
+//
+//    Responses:
+//      200: aTask
+//      400:
+//      500:
+//
+func addTask(cfg Config) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		decoder := json.NewDecoder(r.Body)
+		var task model.Task
+
+		err := decoder.Decode(&task)
+
+		if err != nil {
+			log.Println(err)
+			http.Error(w, err.Error(), 400)
+			return
+		}
+
+		newTask, err := db.AddTask(cfg.DB, task)
+
+		if err != nil {
+			log.Println(err)
+			http.Error(w, err.Error(), 500)
+			return
+		}
+
+		js, _ := json.Marshal(newTask)
+		w.Header().Set("Content-Type", "application/json")
+		fmt.Fprintf(w, string(js))
+	})
+}
+
+// swagger:route DELETE /api/task/{id}/ deleteTask
+//
+// Delete a Task by ID
+//
+// Responses:
+//    204:
+//    404:
+//    500:
+//
+func deleteTask(cfg Config) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		vars := mux.Vars(r)
+
+		id, err := strconv.Atoi(vars["id"])
+
+		if err != nil {
+			fmt.Print(err)
+			http.Error(w, err.Error(), 500)
+			return
+		}
+
+		err = db.DeleteTask(cfg.DB, model.Task{ID: id})
+
+		if err != nil {
+			fmt.Print(err)
+			http.Error(w, err.Error(), 500)
+			return
+		}
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+func apiHandler(cfg Config, router *mux.Router) {
+	router.Handle("/task/{id:[0-9]+}/", deleteTask(cfg)).Methods("DELETE")
+	router.Handle("/task/", getTasks(cfg)).Methods("GET")
+	router.Handle("/task/", addTask(cfg)).Methods("POST")
+}
diff --git a/ui/index.go b/ui/index.go
new file mode 100644
index 00000000..4155e589
--- /dev/null
+++ b/ui/index.go
@@ -0,0 +1,82 @@
+// Copyright © 2018 Thomas Winsnes <tom@vibrato.com.au>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package ui
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gorilla/mux"
+)
+
+const (
+	cdnReact           = "https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.min.js"
+	cdnReactDom        = "https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js"
+	cdnBabelStandalone = "https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.24.0/babel.min.js"
+	cdnAxios           = "https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.1/axios.min.js"
+)
+
+const indexHTML = `
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="utf-8">
+	<title>Vibrato Tech Test App</title>
+	<link rel="stylesheet" href="/css/site.css" type="text/css" />
+	<link href="https://fonts.googleapis.com/css?family=Arimo" rel="stylesheet">
+	<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
+	<script defer src="https://use.fontawesome.com/releases/v5.0.6/js/all.js"></script>
+  </head>
+  <body>
+  	<header>
+    	<img src="/images/VIBRATO_Logo_dark-cropped-200.png" width="100" height="85"/>
+    </header>
+    <div id='root'></div>
+	<footer>
+        &COPY; Vibrato
+	</footer>
+	<script src="` + cdnReact + `"></script>
+    <script src="` + cdnReactDom + `"></script>
+    <script src="` + cdnBabelStandalone + `"></script>
+    <script src="` + cdnAxios + `"></script>
+	<script src="/js/app.jsx" type="text/babel"></script>
+  </body>
+</html>
+`
+
+func indexHandler() http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, indexHTML)
+	})
+}
+
+func assetHandler(cfg Config) http.Handler {
+	// so not secure!
+	return http.FileServer(cfg.Assets)
+}
+
+func uiHandler(cfg Config, router *mux.Router) {
+	router.PathPrefix("/js/").Handler(assetHandler(cfg))
+	router.PathPrefix("/css/").Handler(assetHandler(cfg))
+	router.PathPrefix("/images/").Handler(assetHandler(cfg))
+	router.PathPrefix("/swagger/").Handler(assetHandler(cfg))
+	router.Handle("/", indexHandler())
+}
diff --git a/ui/ui.go b/ui/ui.go
index ce180a30..4ba998d1 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -21,17 +21,13 @@
 package ui
 
 import (
-	"encoding/json"
 	"fmt"
-	"log"
 	"net"
 	"net/http"
-	"strconv"
 	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/vibrato/TechTestApp/db"
-	"github.com/vibrato/TechTestApp/model"
 )
 
 // Config configuration for ui package
@@ -42,63 +38,23 @@ type Config struct {
 
 // Start - start web server and handle web requets
 func Start(cfg Config, listener net.Listener) {
-
 	server := &http.Server{
 		ReadTimeout:    60 * time.Second,
 		WriteTimeout:   60 * time.Second,
-		MaxHeaderBytes: 1 << 16}
+		MaxHeaderBytes: 1 << 16,
+	}
 
-	mainRouter := mux.NewRouter().PathPrefix("/").Subrouter()
-	mainRouter.PathPrefix("/js/").Handler(assetHandler(cfg))
-	mainRouter.PathPrefix("/css/").Handler(assetHandler(cfg))
-	mainRouter.PathPrefix("/images/").Handler(assetHandler(cfg))
-	mainRouter.Handle("/api/task/{id:[0-9]+}/", deleteTask(cfg)).Methods("DELETE")
-	mainRouter.Handle("/api/task/", allTasksHandler(cfg))
+	mainRouter := mux.NewRouter()
 	mainRouter.Handle("/healthcheck/", healthcheckHandler(cfg))
-	mainRouter.Handle("/", indexHandler())
-	http.Handle("/", mainRouter)
-
-	go server.Serve(listener)
-}
 
-const (
-	cdnReact           = "https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.min.js"
-	cdnReactDom        = "https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js"
-	cdnBabelStandalone = "https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.24.0/babel.min.js"
-	cdnAxios           = "https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.1/axios.min.js"
-)
+	apiRouter := mainRouter.PathPrefix("/api").Subrouter()
+	apiHandler(cfg, apiRouter)
 
-const indexHTML = `
-<!DOCTYPE HTML>
-<html>
-  <head>
-    <meta charset="utf-8">
-	<title>Vibrato Tech Test App</title>
-	<link rel="stylesheet" href="/css/site.css" type="text/css" />
-	<link href="https://fonts.googleapis.com/css?family=Arimo" rel="stylesheet">
-	<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
-	<script defer src="https://use.fontawesome.com/releases/v5.0.6/js/all.js"></script>
-  </head>
-  <body>
-  	<header>
-    	<img src="/images/VIBRATO_Logo_dark-cropped-200.png" width="100" height="85"/>
-    </header>
-    <div id='root'></div>
-	<footer>
-        &COPY; Vibrato
-	</footer>
-	<script src="` + cdnReact + `"></script>
-    <script src="` + cdnReactDom + `"></script>
-    <script src="` + cdnBabelStandalone + `"></script>
-    <script src="` + cdnAxios + `"></script>
-	<script src="/js/app.jsx" type="text/babel"></script>
-  </body>
-</html>
-`
+	uiRouter := mainRouter.PathPrefix("/").Subrouter()
+	uiHandler(cfg, uiRouter)
 
-func assetHandler(cfg Config) http.Handler {
-	// so not secure!
-	return http.FileServer(cfg.Assets)
+	http.Handle("/", mainRouter)
+	go server.Serve(listener)
 }
 
 func healthcheckHandler(cfg Config) http.Handler {
@@ -113,80 +69,3 @@ func healthcheckHandler(cfg Config) http.Handler {
 		fmt.Fprintf(w, "OK")
 	})
 }
-
-func indexHandler() http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprintf(w, indexHTML)
-	})
-}
-
-func allTasksHandler(cfg Config) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
-		switch r.Method {
-		case ("GET"):
-			getTasks(cfg, w)
-			break
-		case ("POST"):
-			addTask(cfg, w, r)
-		case ("PATCH"):
-			updateTask(cfg, w, r)
-		}
-	})
-}
-
-func getTasks(cfg Config, w http.ResponseWriter) {
-	output, _ := db.GetAllTasks(cfg.DB)
-	js, _ := json.Marshal(output)
-	fmt.Fprintf(w, string(js))
-}
-
-func addTask(cfg Config, w http.ResponseWriter, r *http.Request) {
-	decoder := json.NewDecoder(r.Body)
-	var task model.Task
-
-	err := decoder.Decode(&task)
-	if err != nil {
-		log.Println(err)
-		http.Error(w, err.Error(), 400)
-		return
-	}
-
-	newTask, err := db.AddTask(cfg.DB, task)
-
-	if err != nil {
-		log.Println(err)
-		http.Error(w, err.Error(), 500)
-		return
-	}
-
-	js, _ := json.Marshal(newTask)
-
-	fmt.Fprintf(w, string(js))
-}
-
-func updateTask(cfg Config, w http.ResponseWriter, r *http.Request) {
-
-}
-
-func deleteTask(cfg Config) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		vars := mux.Vars(r)
-
-		id, err := strconv.Atoi(vars["id"])
-
-		if err != nil {
-			fmt.Print(err)
-			http.Error(w, err.Error(), 500)
-			return
-		}
-
-		err = db.DeleteTask(cfg.DB, model.Task{ID: id})
-
-		if err != nil {
-			fmt.Print(err)
-			http.Error(w, err.Error(), 500)
-			return
-		}
-	})
-}

From b77b86f0950f9717b2606278b7380572da44fd86 Mon Sep 17 00:00:00 2001
From: Jamie Lennox <jamie@vibrato.com.au>
Date: Tue, 5 Mar 2019 11:46:55 +1100
Subject: [PATCH 2/2] Bump version to 0.4.0

New features, new minor version.
---
 cmd/root.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmd/root.go b/cmd/root.go
index d7c2541b..49a8b720 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -54,7 +54,7 @@ var rootCmd = &cobra.Command{
 This application is used as part of testing potential candiates at Vibrato.
 
 Please visit http://vibrato.com.au for more details`,
-	Version: "0.3.7",
+	Version: "0.4.0",
 }
 
 // Execute adds all child commands to the root command and sets flags appropriately.