/toolbox

An example of creating a simple go module

Primary LanguageGoMIT LicenseMIT

Version Built with GoLang License Go Report Card Tests Go Coverage

Toolbox

A simple example of how to create a reusable Go module with commonly used tools.

The included tools are:

  • Read JSON
  • Write JSON
  • Produce a JSON encoded error response
  • Write XML
  • Read XML
  • Produce an XML encoded error response
  • Upload a file to a specified directory
  • Download a static file
  • Get a random string of length n
  • Post JSON to a remote service
  • Create a directory, including all parent directories, if it does not already exist
  • Create a URL safe slug from a string
  • ContainsElement checks if a value exists in a slice
  • Creating a QUERY map from a SQL file

Installation

go get -u github.com/tsawler/toolbox

Usage

package main

import (
	"fmt"
	"github.com/tsawler/toolbox"
)

func main() {
	// create a variable of type toolbox.Tools, so we can use this variable
	// to call the methods on that type
	var tools toolbox.Tools

	// get a random string
	rnd := tools.RandomString(10)
	fmt.Println(rnd)
}

Working with JSON

In a handler, for example:

// JSONPayload is the type for JSON data that we receive
type JSONPayload struct {
    Name string `json:"name"`
    Data string `json:"data"`
}

// SomeHandler is the handler to accept a post request consisting of json payload
func (app *Config) SomeHandler(w http.ResponseWriter, r *http.Request) {
    var tools toolbox.Tools
    
    // read json into var
    var requestPayload JSONPayload
    _ = tools.ReadJSON(w, r, &requestPayload)
	
    // do something with the data here...
    
    // create the response we'll send back as JSON
    resp := toolbox.JSONResponse{
        Error:   false,
        Message: "logged",
    }
    
    // write the response back as JSON
    _ = tools.WriteJSON(w, http.StatusAccepted, resp)
}

Download a file

To download a static file, and force it to download instead of displaying in a browser:

// DownloadAFile downloads an arbitrary file
func (app *Config) DownloadAFile(w http.ResponseWriter, r *http.Request) {
    var tools toolbox.Tools

    tools.DownloadStaticFile(w, r, "./data", "file.pdf", "file.pdf")
}

Creating a directory

To create a directory if it does not already exist:

// SomeHandler is some kind of handler
func (app *Config) SomeHandler(w http.ResponseWriter, r *http.Request) {
    var tools toolbox.Tools

    err := tools.CreateDirIfNotExist("./myDir")
    if err != nil {
        // do something with the error...
    }
	
    // keep going in the handler...
}

Uploading a File:

To upload a file to a specific directory, with this for HTML:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
    <title>Upload test</title>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col">
            <h1 class="mt-2">Upload a file</h1>
            <hr>

            <form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">

                <div class="mb-3">
                    <label for="fileUpload" class="form-label">Choose file(s) to upload...</label>
                    <input class="form-control" type="file" id="fileUpload" name="uploaded" multiple>
                </div>


                <input class="btn btn-primary" type="submit" value="Upload file">
            </form>

        </div>
    </div>
</div>
</body>
</html>

And this for a Go application:

package main

import (
	"fmt"
	"github.com/tsawler/toolbox"
	"log"
	"net/http"
)

func main() {

	// handle html route (http://localhost:8080/)
	http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("."))))

	// Post handler 
	http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != "POST" {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}

		t := toolbox.Tools{
			MaxFileSize:      1024 * 1024 * 1024,
			AllowedFileTypes: []string{"image/gif", "image/png", "image/jpeg"},
		}
		

		// Upload the file(s). Note that if you don't want the files to be renamed,
		// you can add an optional final parameter -- true will rename the files (the default)
		// and false will preserve the original filenames, for example:
		// files, err := t.UploadFiles(r, "./uploads", false)
		// n.b.: if the "./uploads" directory does not exist, we attempt to create it.
		files, err := t.UploadFiles(r, "./uploads")
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		// the returned variable, files, will be a slice of the type toolbox.UploadedFile
		_, _ = w.Write([]byte(fmt.Sprintf("Uploaded %d file(s) to the uploads folder", len(files))))	
	})

	// print a log message
	log.Println("Starting server on port 8080")

	// start the server
	http.ListenAndServe(":8080", nil)
}

Calling a Remote API

To make a JSON post to a remote URI, with this html:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
          crossorigin="anonymous">

    <title>JSON functionality</title>
    <style>
        label{
            font-weight: bold;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col">
            <h1 class="mt-2">JSON functionality</h1>
            <hr>

            <form>


                <div class="mb-3">
                    <label for="json" class="form-label">JSON to Send:</label>
                    <textarea style="font-family: Courier,sans-serif" class="form-control"
                              id="json" name="json" rows="5">
{
    "action": "some action",
    "message": "some message"
}
                    </textarea>
                </div>


                <a id="pushBtn" class="btn btn-primary">Push JSON</a>
            </form>
            <hr>
            <p><strong>Response from server:</strong></p>
            <div style="outline: 1px solid silver; padding: 2em">
                <pre id="response">No response from server yet...</pre>
            </div>

        </div>
    </div>
</div>

<script>
    let pushBtn = document.getElementById("pushBtn");
    let jsonPayload = document.getElementById("json")
    let serverResponse = document.getElementById("response");

    pushBtn.addEventListener("click", function () {
        const payload = jsonPayload.value;
        const headers = new Headers();

        const body = {
            method: 'POST',
            body: payload,
            headers: headers,
        }

        headers.append("Content-Type", "application/json");

        fetch("http://localhost:8081/receive-post", body)
            .then((response) => response.json())
            .then((data) => {
                serverResponse.innerHTML = JSON.stringify(data, undefined, 4);
            })
            .catch((error) => {
                serverResponse.innerHTML = "<br><br>Error: " + error;
            })
    })
</script>
</body>
</html>

You can use this kind of Go code:

package main

import (
	"github.com/tsawler/toolbox"
	"log"
	"net/http"
)

func main() {
	// create a default server mux
	mux := http.NewServeMux()

	// register routes
	mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("."))))
	mux.HandleFunc("/receive-post", receivePost)
	mux.HandleFunc("/remote-service", remoteService)

	// print a log message
	log.Println("Starting server on port 8081")

	// start the server
	err := http.ListenAndServe(":8081", mux)
	if err != nil {
		log.Fatal(err)
	}
}

// RequestPayload describes the JSON that this service accepts as an HTTP Post request
type RequestPayload struct {
	Action  string `json:"action"`
	Message string `json:"message"`
}

// ResponsePayload is the structure used for sending a JSON response
type ResponsePayload struct {
	Message    string `json:"message"`
	StatusCode int    `json:"status_code,omitempty"`
}

func receivePost(w http.ResponseWriter, r *http.Request) {
	// get the posted json and decode it
	var requestPayload RequestPayload
	var t toolbox.Tools

	err := t.ReadJSON(w, r, &requestPayload)
	if err != nil {
		_ = t.ErrorJSON(w, err)
		return
	}

	// Call remote service. Note that we are ignoring the first return parameter, which is the 
	// entire response from the remote service, but you have access to it if you need it.
	_, statusCode, err := t.PushJSONToRemote("http://localhost:8081/remote-service", requestPayload)
	if err != nil {
		_ = t.ErrorJSON(w, err)
		return
	}

	// send response
	payload := ResponsePayload{
		Message:    "hit the service ok",
		StatusCode: statusCode,
	}

	err = t.WriteJSON(w, http.StatusAccepted, payload)
	if err != nil {
		log.Println(err)
	}
}

// remoteService just simulates calling some remote API
func remoteService(w http.ResponseWriter, r *http.Request) {
	payload := ResponsePayload{
		Message: "OK",
	}
	var t toolbox.Tools

	_ = t.WriteJSON(w, http.StatusOK, payload)
}

Create a slug from a string

To slugify a string, we simply remove all non URL safe characters and return the original string with a hyphen where spaces would be. Example:

package main

import (
	"fmt"
	"github.com/tsawler/toolbox"
)

func main() {
	toSlugify := "hello, world! These are unsafe chars: こんにちは世界*!&^%"
	fmt.Println("To slugify:", toSlugify)
	var tools toolbox.Tools

	slug, err := tools.Slugify(toSlugify)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println("Slugified:", slug)
}

Output from this is:

To slugify: hello, world! These are unsafe chars: こんにちは世界*!&^%
Slugified: hello-world-these-are-unsafe-chars

Value exists in a slice

It is a method we often use when writing code. Is the value we are looking for present in the slice? The type of this slice can be of any type. Example:

package main

import(
	"fmt"
	"github.com/tsawler/toolbox"
)

func main(){
	var tools toolbox.Tools

	tests := []test{{name: "abc"}, {name: "def"}}
	t1 := test{name: "def"}
	t2 := test{name: "xyz"}
	
	if tools.ContainsElement(t1, tests) {
		fmt.Println("This slice contains the key you are looking for.")
    }

	if !tools.ContainsElement(t2, tests) {
		fmt.Println("This slice does not contain the key you are looking for.")
	}
}

Creating a QUERY map from a SQL file

This tool aims to facilitate the use of the go language's database/sql standard library. Writing SQL queries directly in the code can make it messy, so writing SQL queries in .sql files and then calling them from the code helps prevent code clutter, allowing SQL queries to be centralized in one place for better organization. Example:

package main

import(
	"database/sql"
	"encoding/json"
	"log"
	"github.com/tsawler/toolbox"
)

type City struct {
	ID        int      `json:"id,omitempty"`
	Name      string   `json:"name,omitempty"`
	CountryID int      `json:"country_id,omitempty"`
}

var DB *sql.DB
var QUERY map[string]string

func main(){
	var tools toolbox.Tools
	
	// Load Sql
	QUERY = make(map[string]string)
	if query, err := tools.LoadSQLQueries("./testdata/test.sql"); err != nil {
		log.Fatalf("Load Sql Error: %v", err)
	} else {
		QUERY = query
	}

	var cities []City
	rows, err := DB.Query(QUERY["CITIES"])
	if err != nil {
		log.Printf("GET Cities Error %v", err)
		return
	}
	defer func(rows *sql.Rows) {
		_ = rows.Close()
	}(rows)
	for rows.Next() {
		var city City
		_ = rows.Scan(&city.ID, &city.Name, &city.CountryID)
		cities = append(cities, city)
	}
	
	if marshal, err := json.MarshalIndent(cities, "", "  "); err != nil {
		return
	}else{
		log.Println(string(marshal))
	}
}