From e7e517b822333188ea34f4a4ba83de2253cfe869 Mon Sep 17 00:00:00 2001 From: Matthew Lemon Date: Sat, 6 Apr 2024 11:48:32 +0100 Subject: Reorganises cmd/ structure for publication on pkg.go.dev Adds key commands to Makefile --- cmd/dbasik-api/datamaps.go | 123 ++++++++++++++++++++++++++++++++++++++++++ cmd/dbasik-api/errors.go | 63 ++++++++++++++++++++++ cmd/dbasik-api/healthcheck.go | 36 +++++++++++++ cmd/dbasik-api/helpers.go | 86 +++++++++++++++++++++++++++++ cmd/dbasik-api/main.go | 80 +++++++++++++++++++++++++++ cmd/dbasik-api/routes.go | 31 +++++++++++ 6 files changed, 419 insertions(+) create mode 100644 cmd/dbasik-api/datamaps.go create mode 100644 cmd/dbasik-api/errors.go create mode 100644 cmd/dbasik-api/healthcheck.go create mode 100644 cmd/dbasik-api/helpers.go create mode 100644 cmd/dbasik-api/main.go create mode 100644 cmd/dbasik-api/routes.go (limited to 'cmd/dbasik-api') diff --git a/cmd/dbasik-api/datamaps.go b/cmd/dbasik-api/datamaps.go new file mode 100644 index 0000000..8d371ab --- /dev/null +++ b/cmd/dbasik-api/datamaps.go @@ -0,0 +1,123 @@ +// dbasik provides a service with which to convert spreadsheets containing +// data to JSON for further processing. + +// Copyright (C) 2024 M R Lemon + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +package main + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" +) + +// datamapLine holds the data parsed from each line of a submitted datamap CSV file. +// The fields need to be exported otherwise they won't be included when encoding +// the struct to json. +type datamapLine struct { + Key string `json:"key"` + Sheet string `json:"sheet"` + DataType string `json:"datatype"` + Cellref string `json:"cellref"` +} + +// datamap includes a slice of datamapLine objects alongside header metadata +type datamap struct { + Name string `json:"name"` + Description string `json:"description"` + Created time.Time `json:"created"` + DMLs []datamapLine `json:"datamap_lines"` +} + +func (app *application) createDatamapHandler(w http.ResponseWriter, r *http.Request) { + // Parse the multipart form + err := r.ParseMultipartForm(10 << 20) // 10Mb max + if err != nil { + app.serverErrorResponse(w, r, err) + } + + // Get form values + dmName := r.FormValue("name") + app.logger.Info("obtain value from form", "name", dmName) + dmDesc := r.FormValue("description") + app.logger.Info("obtain value from form", "description", dmDesc) + + // Get the uploaded file and name + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, "Missing file", http.StatusBadRequest) + return + } + defer file.Close() + + // parse the csv + reader := csv.NewReader(file) + var dmls []datamapLine + var dm datamap + + for { + line, err := reader.Read() + if err != nil { + if err.Error() == "EOF" { + break // end of file + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if len(line) != 4 { + http.Error(w, "Invalid CSV Format", http.StatusBadRequest) + return + } + + dmls = append(dmls, datamapLine{ + Key: line[0], + Sheet: line[1], + DataType: line[2], + Cellref: line[3], + }) + } + dm = datamap{Name: dmName, Description: dmDesc, Created: time.Now(), DMLs: dmls} + + err = app.writeJSONPretty(w, http.StatusOK, envelope{"datamap": dm}, nil) + if err != nil { + app.logger.Debug("writing out csv", "err", err) + app.serverErrorResponse(w, r, err) + } + + // fmt.Fprintf(w, "file successfully uploaded") +} + +func (app *application) showDatamapHandler(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + app.logger.Info("the id requested", "id", id) + id_int, err := strconv.ParseInt(id, 10, 64) + if err != nil || id_int < 1 { + app.notFoundResponse(w, r) + } + fmt.Fprintf(w, "show the details for datamap %d\n", id_int) +} + +func (app *application) createDatamapLine(w http.ResponseWriter, r *http.Request) { + var input datamapLine + err := json.NewDecoder(r.Body).Decode(&input) + if err != nil { + app.errorResponse(w, r, http.StatusBadRequest, err.Error()) + return + } + fmt.Fprintf(w, "%v\n", input) +} diff --git a/cmd/dbasik-api/errors.go b/cmd/dbasik-api/errors.go new file mode 100644 index 0000000..5f5122a --- /dev/null +++ b/cmd/dbasik-api/errors.go @@ -0,0 +1,63 @@ +// dbasik provides a service with which to convert spreadsheets containing +// data to JSON for further processing. + +// Copyright (C) 2024 M R Lemon + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +package main + +import ( + "fmt" + "net/http" +) + +// The logError() method is a generic helper for logging an error message. +func (app *application) logError(r *http.Request, err error) { + app.logger.Info("dbasik error", "error", err) +} + +// The errorResponse() method is a generic helper for sending JSON-formatted error +// messages to the client with a given status code. Because we are using any we +// we have flexibility over the values that we can include in the response. +func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message any) { + env := envelope{"error": message} + + // Write the response using the writeJSON() helper. If it returns + // an error, log it and send the client an empty response with a + // 500 Internal Server status code. + if err := app.writeJSON(w, status, env, nil); err != nil { + app.logError(r, err) + w.WriteHeader(500) + } +} + +// The serverErrorResponse() method will be used when our application encounters an +// unexpected problem at runtime. +func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) { + app.logError(r, err) + message := "the server encountered a problem and could not process your request" + app.errorResponse(w, r, http.StatusInternalServerError, message) +} + +// The notFoundResponse() method will be used to send a 404 status code and JSON response +// to the client. +func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request) { + message := "the requested resource could not be found" + app.errorResponse(w, r, http.StatusNotFound, message) +} + +func (app *application) methodNotAllowed(w http.ResponseWriter, r *http.Request) { + message := fmt.Sprintf("the %s method is not supported for this resource", r.Method) + app.errorResponse(w, r, http.StatusMethodNotAllowed, message) +} diff --git a/cmd/dbasik-api/healthcheck.go b/cmd/dbasik-api/healthcheck.go new file mode 100644 index 0000000..3378c6e --- /dev/null +++ b/cmd/dbasik-api/healthcheck.go @@ -0,0 +1,36 @@ +// dbasik provides a service with which to convert spreadsheets containing +// data to JSON for further processing. + +// Copyright (C) 2024 M R Lemon + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +package main + +import ( + "net/http" +) + +func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) { + env := envelope{ + "status": "available", + "system_info": map[string]string{ + "environment": app.config.env, + "version": version, + }, + } + err := app.writeJSON(w, http.StatusOK, env, nil) + if err != nil { + app.serverErrorResponse(w, r, err) + } +} diff --git a/cmd/dbasik-api/helpers.go b/cmd/dbasik-api/helpers.go new file mode 100644 index 0000000..3b07dc2 --- /dev/null +++ b/cmd/dbasik-api/helpers.go @@ -0,0 +1,86 @@ +// dbasik provides a service with which to convert spreadsheets containing +// data to JSON for further processing. + +// Copyright (C) 2024 M R Lemon + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +package main + +import ( + "encoding/json" + "net/http" +) + +// We want this so that our JSON is nested under a key at the top, e.g. "datamap:"... +type envelope map[string]interface{} + +// writeJSON)Pretty() helper for sending responses - pretty prints output. This takes the destination http.ResponseWriter, the +// HTTP status code to sned, the data to encode to JSON and a header map containing any additional +// HTTP headers we want to include in the response. +func (app *application) writeJSONPretty(w http.ResponseWriter, status int, data envelope, headers http.Header) error { + // Encode the data to JSON, returing the error if there was one. + js, err := json.MarshalIndent(data, "", "\t") + if err != nil { + return err + } + + // Append a newline to make it easier tro view in terminal applications + js = append(js, '\n') + + // We know now that we won't encounter any more errors before writing the response, + // so it's safe to add any headers that we want to include. We loop through the + // header map and add each header to the http.ResponseWriter header map. + // Note that it's okay if the provided header map is nil. Go doesn't throw an error + // if you wanmt to try to range over (or more generally reader from) a nil map. + for key, value := range headers { + w.Header()[key] = value + } + // Add the "Content-Type: application/json" header, then write the status code and + // JSON response. + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + w.Write(js) + + return nil +} + +// writeJSON() helper for sending responses. This takes the destination http.ResponseWriter, the +// HTTP status code to sned, the data to encode to JSON and a header map containing any additional +// HTTP headers we want to include in the response. +func (app *application) writeJSON(w http.ResponseWriter, status int, data any, headers http.Header) error { + // Encode the data to JSON, returing the error if there was one. + js, err := json.Marshal(data) + if err != nil { + return err + } + + // Append a newline to make it easier tro view in terminal applications + js = append(js, '\n') + + // We know now that we won't encounter any more errors before writing the response, + // so it's safe to add any headers that we want to include. We loop through the + // header map and add each header to the http.ResponseWriter header map. + // Note that it's okay if the provided header map is nil. Go doesn't throw an error + // if you wanmt to try to range over (or more generally reader from) a nil map. + for key, value := range headers { + w.Header()[key] = value + } + // Add the "Content-Type: application/json" header, then write the status code and + // JSON response. + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + w.Write(js) + + return nil +} diff --git a/cmd/dbasik-api/main.go b/cmd/dbasik-api/main.go new file mode 100644 index 0000000..fc03d33 --- /dev/null +++ b/cmd/dbasik-api/main.go @@ -0,0 +1,80 @@ +// dbasik provides a service with which to convert spreadsheets containing +// data to JSON for further processing. + +// Copyright (C) 2024 M R Lemon + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +package main + +import ( + "flag" + "fmt" + "log/slog" + "net/http" + "os" + "time" +) + +const version = "0.0.1" + +// holds the config for our application +// We will read this from the command line flags when we run the application +type config struct { + port int + env string +} + +// This application struct holds the dependencies for our HTTP handlers, helpers and +// middleware. +type application struct { + config config + logger *slog.Logger +} + +func main() { + // Instance of config + var cfg config + + // Read the flags into the config struct. Defaults are provided if none given. + flag.IntVar(&cfg.port, "port", 4000, "API server port") + flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") + flag.Parse() + + // Initialize a new structured logger which writes to stdout + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + // An instance of application struct, containing the config struct and the logger + app := &application{ + config: cfg, + logger: logger, + } + + // Declare an http server which listens provided in the config struct and has + // sensible timeout settings and writes log messages to the structured logger at + // Error level. + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", cfg.port), + Handler: app.routes(), + IdleTimeout: time.Minute, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), + } + + logger.Info("starting server", "addr", srv.Addr, "env", cfg.env) + + err := srv.ListenAndServe() + logger.Error(err.Error()) + os.Exit(1) +} diff --git a/cmd/dbasik-api/routes.go b/cmd/dbasik-api/routes.go new file mode 100644 index 0000000..f884855 --- /dev/null +++ b/cmd/dbasik-api/routes.go @@ -0,0 +1,31 @@ +// dbasik provides a service with which to convert spreadsheets containing +// data to JSON for further processing. + +// Copyright (C) 2024 M R Lemon + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +package main + +import "net/http" + +func (app *application) routes() *http.ServeMux { + + mux := http.NewServeMux() + + mux.HandleFunc("GET /v1/healthcheck", app.healthcheckHandler) + mux.HandleFunc("POST /v1/datamaps", app.createDatamapHandler) + mux.HandleFunc("POST /v1/datamapline", app.createDatamapLine) + mux.HandleFunc("GET /v1/datamaps/{id}", app.showDatamapHandler) + return mux +} -- cgit v1.2.3