templatex

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Dec 23, 2024 License: MIT Imports: 10 Imported by: 0

README

templatex

[!WARNING]
This library was developed with a very specific use case in mind. It is not a general purpose library. In 99% of cases, you should stick to the default text/template package for templating and/or any of the many 'real' validation libraries for input validation.

This library wraps a subset of go's text/template package to use it for input validation.

Contains zero (0) extra dependencies and no forked code.

Example

Consider you are analyzing the following arbitrary input:

id: "d416e1b0-97b2-4a49-8ad5-2e6b2b46eae0"
static-string: "abc"
invalid-string: def
random-number: 150

Then you can validate it as follows:

package main

import (
	"bytes"
	"errors"
	"fmt"
	templatex "github.com/sleeyax/templatex-go"
	"strconv"
	"strings"
	"text/template"
)

func main() {
	// Create a new Go template instance.
	tpl := template.New("example")

	// Create a new templatex instance.
	tplx, err := templatex.New(tpl).
		// Define custom parsing and validation functions.
		// The parser functions are used to extract the value from the input.
		// The validation functions are used to validate the extracted value (as you would define it on a regular `template.FuncMap` from go's `text/template` lib).
		Funcs(templatex.FuncMap{
			"isUUID": {
				// Parses the UUID from between the quotes "<UUID>".
				Parse: templatex.ParseQuotedString,
				// Validates that the parsed value is a valid UUID.
				Validate: func(uuid string) (string, error) {
					if !isValidUUID(uuid) { // bring your own validation library/implementation; this is just an example.
						return "", errors.New("invalid UUID")
					}

					return uuid, nil
				},
			},
			"inRange": {
				// Parses the value until the first whitespace or newline character. "100 " -> "100".
				Parse: templatex.ParseUntilWhiteSpace,
				// Validates that the parsed value is an integer within the specified range.
				Validate: func(value string, min, max int) (any, error) {
					valueAsNumber, err := strconv.Atoi(value)
					if err != nil {
						return "", err
					}

					if valueAsNumber < min || valueAsNumber > max {
						return "", errors.New("value is not in range")
					}

					return value, nil
				},
			},
		}).
		// Provide input data that should be verified using the template below.
		Input(`
			id: "d416e1b0-97b2-4a49-8ad5-2e6b2b46eae0"
			static-string: "abc"
			invalid-string: def
			random-number: 150
		`).
		// Provide the template that should be used to verify the input data.
		// Keep in mind that it supports only a subset of the Go template syntax.
		// You'll gracefully receive an error if you use unsupported syntax.
		Parse(`
			id: "{{isUUID}}"
			static-string: "abc"
			invalid-string: def
			random-number: {{inRange 100 200}}
		`)

	if err != nil {
		panic(err)
	}

	var buffer bytes.Buffer
	if err = tplx.Execute(&buffer, nil); err != nil {
		panic(err)
	}

	output := buffer.String()
	output = strings.TrimSpace(strings.ReplaceAll(output, "\n\t\t\t", "\n")) // clean the output (only needed for this example to work).

	fmt.Println(output)

	// Output:
	// id: "d416e1b0-97b2-4a49-8ad5-2e6b2b46eae0"
	// static-string: "abc"
	// invalid-string: def
	// random-number: 150
}

To verify that our input works, let's change it to:

id: "d416e1b0-97b2-4a49-8ad5"
static-string: "abc"
invalid-string: def
random-number: 999

And run the example again:

panic: template: example:2:10: executing "example" at <isUUID >: error calling isUUID: invalid UUID

As expected, the UUID is invalid and the program panics!

If you don't want to error early on the fist validation error, you can change your parser function to substitute the value with a default value instead of returning an error:

"isUUID": {
    // ...
    Validate: func(uuid string) (string, error) {
        if !isValidUUID(uuid) {
            return "[INVALID UUID]", nil
        }

        return uuid, nil
    },
},
"inRange": {
    // ...
    Validate: func(value string, min, max int) (any, error) {
        valueAsNumber, err := strconv.Atoi(value)
        if err != nil {
            return "[INVALID NUMBER]", nil
        }

        if valueAsNumber < min || valueAsNumber > max {
            return "[OUT OF RANGE]", nil
        }

        return value, nil
    },
},

Now the output will be:

id: "[INVALID UUID]"
static-string: "abc"
invalid-string: def
random-number: [OUT OF RANGE]

You can then process the output further using a text diffing tool/library of your choice.

Contributions

If you want to add a feature or fix, do so yourself by submitting a PR. I currently do not wish to maintain this library any further than I need to for my own use case.

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrorUnsupportedNode is returned when an unsupported field or node is encountered during parsing.
	ErrorUnsupportedNode = errors.New("unsupported node")
	// ErrorInvalidNode is returned when a supported node is found during parsing but determined to be in an invalid or unsupported format.
	ErrorInvalidNode = errors.New("invalid node")
	// ErrorUnsupportedFunction is returned when an unsupported or unmapped function is encountered during parsing.
	ErrorUnsupportedFunction = errors.New("unsupported or unmapped function")
	// ErrorInputRequired is returned when no input has been provided yet but the Parse method is called.
	ErrorInputRequired = errors.New("input required")
	ErrorDataRequired  = errors.New("data required")
	// ErrorInputValidation is returned when the input doesn't match the template.
	ErrorInputValidation = errors.New("input doesn't match template")
)

Functions

func ReadQuotedString

func ReadQuotedString(reader *bufio.Reader) (string, error)

func ReadUntil

func ReadUntil(reader *bufio.Reader, delimiters []rune) (string, error)

func ReadUntilWhitespace

func ReadUntilWhitespace(reader *bufio.Reader) (string, error)

Types

type Func

type Func struct {
	Parse    ParseFunc
	Validate ValidateFunc
}

type FuncMap

type FuncMap map[string]Func

type OnInputValidationFunc

type OnInputValidationFunc func(actual, expected string)

type ParseFunc

type ParseFunc func(reader *bufio.Reader) ([]string, error)
var ParseQuotedString ParseFunc = func(reader *bufio.Reader) ([]string, error) {
	v, err := ReadQuotedString(reader)
	if err != nil {
		return []string{}, err
	}
	return []string{v}, nil
}
var ParseUntilWhiteSpace ParseFunc = func(reader *bufio.Reader) ([]string, error) {
	v, err := ReadUntilWhitespace(reader)
	if err != nil {
		return []string{}, err
	}
	return []string{v}, nil
}

type Templatex

type Templatex struct {
	// contains filtered or unexported fields
}

func New

func New(tpl *template.Template) *Templatex

func (*Templatex) Data

func (t *Templatex) Data(data any) *Templatex

func (*Templatex) Delims

func (t *Templatex) Delims(left, right string) *Templatex

func (*Templatex) Execute

func (t *Templatex) Execute(wr io.Writer, data any) error

func (*Templatex) Funcs

func (t *Templatex) Funcs(funcMap FuncMap) *Templatex

func (*Templatex) Input

func (t *Templatex) Input(text string) *Templatex

func (*Templatex) OnInputValidationError

func (t *Templatex) OnInputValidationError(fn OnInputValidationFunc) *Templatex

func (*Templatex) Parse

func (t *Templatex) Parse(text string) (*Templatex, error)
Example
// Create a new Go template instance.
tpl := template.New("example")

// Create a new templatex instance.
tplx, err := New(tpl).
	// Define custom parsing and validation functions.
	// The parser functions are used to extract the value from the input.
	// The validation functions are used to validate the extracted value (as you would define it on a regular `template.FuncMap` from go's `text/template` lib).
	Funcs(FuncMap{
		"isUUID": {
			// Parses the UUID from between the quotes "<UUID>".
			Parse: ParseQuotedString,
			// Validates that the parsed value is a valid UUID.
			Validate: func(uuid string) (string, error) {
				if !isValidUUID(uuid) {
					return "", errors.New("invalid UUID")
				}

				return uuid, nil
			},
		},
		"inRange": {
			// Parses the value until the first whitespace or newline character. "100 " -> "100".
			Parse: ParseUntilWhiteSpace,
			// Validates that the parsed value is an integer within the specified range.
			Validate: func(value string, min, max int) (any, error) {
				valueAsNumber, err := strconv.Atoi(value)
				if err != nil {
					return "", err
				}

				if valueAsNumber < min || valueAsNumber > max {
					return "", errors.New("value is not in range")
				}

				return value, nil
			},
		},
	}).
	// Provide input data that should be verified using the template below.
	Input(`
			id: "d416e1b0-97b2-4a49-8ad5-2e6b2b46eae0"
			static-string: "abc"
			invalid-string: def
			random-number: 150
		`).
	// Provide the template that should be used to verify the input data.
	// Keep in mind that it supports only a subset of the Go template syntax.
	// You'll gracefully receive an error if you use unsupported syntax.
	Parse(`
			id: "{{isUUID}}"
			static-string: "abc"
			invalid-string: def
			random-number: {{inRange 100 200}}
		`)

if err != nil {
	panic(err)
}

var buffer bytes.Buffer
if err = tplx.Execute(&buffer, nil); err != nil {
	panic(err)
}

output := buffer.String()
output = strings.TrimSpace(strings.ReplaceAll(output, "\n\t\t\t", "\n")) // clean the output, only needed for the example.

fmt.Println(output)
Output:

id: "d416e1b0-97b2-4a49-8ad5-2e6b2b46eae0"
static-string: "abc"
invalid-string: def
random-number: 150

func (*Templatex) Template

func (t *Templatex) Template() *template.Template

type ValidateFunc

type ValidateFunc any

Jump to

Keyboard shortcuts

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