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( ) @@ -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 (
@@ -132,4 +132,4 @@ class TaskForm extends React.Component { ReactDOM.render( , document.querySelector("#root")); - \ No newline at end of file + 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. 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 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 +// +// 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 +// +// 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 = ` + + + + + Vibrato Tech Test App + + + + + + +
+ +
+
+
+ © Vibrato +
+ + + + + + + +` + +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 = ` - - - - - Vibrato Tech Test App - - - - - - -
- -
-
-
- © Vibrato -
- - - - - - - -` + 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 - } - }) -}