toolbox

package module
v0.0.0-...-c27e138 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2022 License: MIT Imports: 19 Imported by: 0

README

Built with Go Version Go Docs License

Go Toolbox

A simple reusable Go module with commonly used tools.

The included tools are:

  • Read JSON
  • Write JSON
  • Produce a JSON 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

Installation

go get -u github.com/rossberry/go-toolbox

Usage

package main

import (
	"fmt"
	"github.com/rossberry/go-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/rossberry/go-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/rossberry/go-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/rossberry/go-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

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewTestClient

func NewTestClient(fn RoundTripFunc) *http.Client

NewTestClient returns *http.Client with Transport replaced to avoid making real calls

func TestTools_CreateDirIfNotExist

func TestTools_CreateDirIfNotExist(t *testing.T)

func TestTools_DownloadStaticFile

func TestTools_DownloadStaticFile(t *testing.T)

func TestTools_ErrorJSON

func TestTools_ErrorJSON(t *testing.T)

func TestTools_PushJSONToRemote

func TestTools_PushJSONToRemote(t *testing.T)

func TestTools_RandomString

func TestTools_RandomString(t *testing.T)

func TestTools_ReadJSONAndMarshal

func TestTools_ReadJSONAndMarshal(t *testing.T)

func TestTools_Slugify

func TestTools_Slugify(t *testing.T)

func TestTools_UploadFiles

func TestTools_UploadFiles(t *testing.T)

func TestTools_UploadOneFile

func TestTools_UploadOneFile(t *testing.T)

func TestTools_WriteJSON

func TestTools_WriteJSON(t *testing.T)

func Test_ReadJSON

func Test_ReadJSON(t *testing.T)

Types

type JSONResponse

type JSONResponse struct {
	Error   bool        `json:"error"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

JSONResponse is the type used for sending JSON around.

type RoundTripFunc

type RoundTripFunc func(req *http.Request) *http.Response

RoundTripFunc is used to satisfy the interface requirements for http.Client

func (RoundTripFunc) RoundTrip

func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip is used to satisfy the interface requirements for http.Client

type Tools

type Tools struct {
	MaxJSONSize        int      // maximum size of JSON file we'll process
	MaxFileSize        int      // maximum size of uploaded files in bytes
	AllowedFileTypes   []string // allowed file types for upload (e.g. image/jpeg)
	AllowUnknownFields bool     // if set to true, allow unknown fields in JSON
}

Tools is the type for this package. Create a variable of this type, and you have access to all the exported methods with the receiver type *Tools.

func (*Tools) CreateDirIfNotExist

func (t *Tools) CreateDirIfNotExist(path string) error

CreateDirIfNotExist creates a directory, and all necessary parent directories, if it does not exist.

func (*Tools) DownloadStaticFile

func (t *Tools) DownloadStaticFile(w http.ResponseWriter, r *http.Request, p, file, displayName string)

DownloadStaticFile downloads a file, and tries to force the browser to avoid displaying it in the browser window by setting content-disposition. It also allows specification of the display name.

func (*Tools) ErrorJSON

func (t *Tools) ErrorJSON(w http.ResponseWriter, err error, status ...int) error

ErrorJSON takes an error, and optionally a response status code, and generates and sends a json error response.

func (*Tools) PushJSONToRemote

func (t *Tools) PushJSONToRemote(uri string, data interface{}, client ...*http.Client) (*http.Response, int, error)

PushJSONToRemote posts arbitrary json to some url, and returns the response, the response status code, and error, if any. The final parameter, client, is optional, and will default to the standard http.Client. It exists to make testing possible without an active remote url.

func (*Tools) RandomString

func (t *Tools) RandomString(n int) string

RandomString returns a random string of letters of length n

func (*Tools) ReadJSON

func (t *Tools) ReadJSON(w http.ResponseWriter, r *http.Request, data interface{}) error

ReadJSON tries to read the body of a request and converts it into JSON.

func (*Tools) Slugify

func (t *Tools) Slugify(s string) (string, error)

Slugify is a (very) simple means of creating a slug from a provided string.

func (*Tools) UploadFiles

func (t *Tools) UploadFiles(r *http.Request, uploadDir string, rename ...bool) ([]*UploadedFile, error)

UploadFiles uploads one or more file to a specified directory, and gives the files a random name. It returns a slice containing the newly named files, the original file names, the size of the files, and potentially an error. If the optional last parameter is set to true, then we will not rename the files, but will use the original file names.

func (*Tools) UploadOneFile

func (t *Tools) UploadOneFile(r *http.Request, uploadDir string, rename ...bool) (*UploadedFile, error)

UploadOneFile is just a convenience method that calls UploadFiles, but expects only one file to be in the upload.

func (*Tools) WriteJSON

func (t *Tools) WriteJSON(w http.ResponseWriter, status int, data interface{}, headers ...http.Header) error

WriteJSON takes a response status code and arbitrary data and writes a json response to the client.

type UploadedFile

type UploadedFile struct {
	NewFileName      string
	OriginalFileName string
	FileSize         int64
}

UploadedFile is a struct used for the uploaded file

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL