cdl

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

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

Go to latest
Published: Aug 11, 2015 License: MIT Imports: 8 Imported by: 0

README

cdl Build Status GoDoc GitHub release

cdl is a configuration definition language for Go

There are several ways to import configuration files into Go, from unmarshalling your own JSON using encoding/json to projects such as Viper which will read the configuration in JSON, YAML, TOML or from etcd, the command line, or possibly other sources. All of these have in common that in the end they produce something that looks in Go like

map[string]interface{}

However, these have the issue that they don't validate the configuration. If the supplied configuration omits mandatory keys, or puts in extra ones, or does both by misspelling one key, or puts the right bit of configuration at the wrong level, or any other error is made that doesn't actually prevent the JSON, YAML etc. parsing, then it's left for you to detect that manually in your program.

cdl makes this all much easier. Simply supply a cdl template, compile it (once) using

ct := cdl.Compile(...)

then validate using

err := ct.Validate(object, nil)

If the validation fails, you will get an error return with a context that will allow a user to discover the error in his file.

So what was that nil parameter to cdt.Validate about? cdl also permits you to pass a configurator in, so that you can store the values retrieved in appropriate places.

cdl templates

cdl templates are themselves a

map[string]interface{}

but are flat (i.e. only one level deep). The key represents a point in in the hierarchy to parse, and the value specifies what may appear at that point. The value is normally a string, but may be a pointer to a validation function you supply.

For example:

	template := cdl.Template{
		"/":     "{}apple peach? lemon",
		"apple": "float64",
		"peach": isOneOrTwo,
	}

Here:

  • The root level is specified to be a map ('{}'), which may consist of the elements apple, peach and lemon.

  • There must be an apple element and lemon element, but the peach element is optional

  • The apple element must be a float64

  • In order to validate the peach element, your own validator function (isOneOrTwo) is called. If this returns a cdl.CdlError, that error will be passed to the user (as an error). If it returns nil, then validation will continue.

  • There is no validation at all on peach

Let's take a more complicated example:

	template := cdl.Template{
		"/":          "{}apple peach? pear* plum+ raspberry{1,3} strawberry! kiwi{1,4}? guava!{1,2} orange?{2,31}",
		"apple":      "float64",
		"peach":      isOneOrTwo,
		"strawberry": "[nectarine]{1,3}",
		"nectarine":  "string",
		"raspberry":  "string"
	}

Here we have allowed in the root level:

  • strawberry: The ! indicates it is mandatory; this is the default, so the ! is unnecessary. Each strawberry must be an array of nectarine with between 1 and 3 components, and each nectarine must be a string.

  • rasbperry: This is a shorthand for writing the same thing as above, i.e. an array of between 1 and 3 raspberry, each of which must be a string.

  • pear: An array of zero or more items. Note the empty array must be there (if the array itself is optional, write pear?*).

  • plum: An array of one or more entries.

  • kiwi: An optionally present array of between 1 and 4 entries

  • guava: A mandatory array of between 1 and 2 entries

Template syntax in detail
  1. Each key must either be / (for the root key) or consist of word characters (i.e. matching \w+ in regexp terms)

  2. Each key must have a value, which may be either:

  • A validator function;
  • A cdl.EnumType (in which case the data will be validated against that EnumType); or
  • A validation instruction in the form of a string
  1. A validator function is a function with the signature func(obj interface{}) (err *CdlError)

  2. Each validation instruction is a quoted string, and may be either

  • The Go name of a type (not a slice), e.g. bool, string etc. (in quotes as it's a string);
  • A pseudotype (e.g. number, integer) in quotes - see below;
  • An array specifier, having a form beginning []; or
  • A map specifier, having a form beginning {}.
  1. Each pseudotype may be either
  • The word number which indicates any numerical type (not bool)
  • The word integer which indicates any numerical type where the value is an integer (useful for parsing JSON with json/encoding which presents these as float64)
  • The word ipport for an IP port pair which is successfully decoded by net.SplitHostPort
  1. An array specifier has the form []key optionally followed by a range specifier
  • The key (key above) consists of word characters.
  • The key need not be specified within the template (if it isn't, no validation will be done on it).
  1. A range specifier takes the form
  • {n,m} (meaning between n and m) or
  • {n,} (meaning at least n).
  1. A map specifier has the form {} followed by zero or more space-separated map elements

  2. A map element consists of a key (key) followed by zero or more modifiers

  • The key consists of word characters.
  • The key need not be specified within the template (if it isn't, no validation will be done on it).
  1. Permitted modifiers are:
  • ? means the key is optional
  • ! means the key is mandatory (the default)
  • * means the key is an array of 0 or more elements
  • + means the key is an array of 1 or more elements
  • A range specifier (see above), i.e.
    • {n,m} (meaning between n and m) or
    • {n,} (meaning at least n)
Validator Functions

Where the validator is passed, it is a function with signature:

func (o interface{}) *cdl.CdlError

Here's an example showing how it can return an error and send supplementary data back to the user. Note that cdl itself will add the appropriate context.

func isOneOrTwo(o interface{}) *cdl.CdlError {
	if v, ok := o.(float64); !ok {
		return cdl.NewError(cdl.ErrBadValue).SetSupplementary("is not a float64")
	} else {
		if v != 1 && v != 2 {
			return cdl.NewError(cdl.ErrBadValue).SetSupplementary("is not 1 or 2")
		}
	}
	return nil
}

cdl Configurators

A cdl configurator may optionally be passed to the Validate function. The configurator allows you to consume the configuration in your program now you know that is validated.

The configurator consists of a map of keys to items. Each item should be either

  • a pointer to the variable to be set; or
  • a pointer to a configuration function.

If a pointer to a variable is used, the variable must be of the same type as the item in the configuration, or an error will be issued; therefore as a type check is performed here, it is unnecessary in this case to require a specific type in the template. If a specific type is require, a type check is done twice. Certain pseudo-types being required will cause a type conversion:

  1. If you required the pseudo-type number, you will be always be given a float64

  2. If you required the pseudo-type integer, you will always be given an int

If a pointer to an Enum is given, a string value is expected in the data, and it will be validated against that Enum.

If a pointer configuration function is used, it has a ConfiguratorFunc type (or a function with a similar signature), which looks like this:

type ConfiguratorFunc func(obj interface{}, path Path) (err *CdlError)

This function is guaranteed to be called for each item in the tree (if it's key is present in the configurator) after it and all of its children have been validated. It may return an error (just like a validator function).

The object passed will be the validated object from the configuration tree. It is guaranteed to be of the correct type, which means the type you asked for save for the following exceptions:

  1. If you asked for the pseudo-type number, you will always be given a float64.

  2. If you asked for the pseudo-type integer, you will always be given an int.

As a trivial example:

var i int
err := ct.Validate(object, cdl.Configurator{
	"i": func(o interface{}, p cdl.Path) *cdl.CdlError {
		i = o.(int)
		return nil
	},
})

Here the parameter named "i" in the template will be stored in variable i.

Installation

The recommended way to install cdl is:

go get github.com/abligh/cdl

Examples

How import the package

import "github.com/abligh/cdl"

Example of basic usage


package main

import (
	"encoding/json"
	"github.com/abligh/cdl"
	"log"
)

func isOneOrTwo(o interface{}) *cdl.CdlError {
	if v, ok := o.(float64); !ok {
		return cdl.NewError(cdl.ErrBadValue).SetSupplementary("is not a float64")
	} else {
		if v != 1 && v != 2 {
			return cdl.NewError(cdl.ErrBadValue).SetSupplementary("is not 1 or 2")
		}
	}
	return nil
}

func main() {

	// here's our template
	template := cdl.Template{
		"/":     "{}apple peach? pear* plum+ raspberry{1,3} strawberry! kiwi{1,4}? guava!{1,2} orange?{2,31}",
		"apple": "float64",
		"peach": isOneOrTwo,
	}

	if ct, err := cdl.Compile(template); err != nil {
		log.Fatalf("Error on compile: %v", err)
	} else {

		// Unmarshal some JSON
		var m interface{}

		j := `
		     {
			"apple" : 3,
			"pear" : [],
			"plum" : [ 1 ],
			"raspberry" : [ "a", "b" ],
			"strawberry" : "here",
			"guava": [ "c", "d" ]
		     }`

		if err := json.Unmarshal([]byte(j), &m); err != nil {
			log.Fatalf("Cannot unmarshal JSON: %v", err)
		}

		// Validate it
		if err := ct.Validate(m, nil); err != nil {
			log.Fatalf("Validation error: %v", err)
		}
		
		log.Println("Success!")
	}
}
								  `

License

MIT, see LICENSE

Documentation

Overview

Package cdl provides a configuration definition language for Go

There are several ways to import configuration files into Go, from unmarshalling your own JSON using `encoding/json` to projects such as Viper (http://spf13.com/project/viper) which will read the configuration in JSON, YAML, TOML or from etcd, the command line, or possibly other sources. All of these have in common that in the end they produce something that looks in Go like

map[string]interface{}

However, these have the issue that they don't validate the configuration. If the supplied configuration omits mandatory keys, or puts in extra ones, or does both by misspelling one key, or puts the right bit of configuration at the wrong level, or any other error is made that doesn't actually prevent the JSON, YAML etc. parsing, then it's left for you to detect that manually in your program.

cdl makes this all much easier. Simply supply a cdl template, compile it (once) using

ct := cdl.Compile(...)

then validate using

err := ct.Validate(object, nil)

If the validation fails, you will get an `error` return with a context that will allow a user to discover the error in his file.

So what was that `nil` parameter to `cdt.Validate` about? cdl also permits you to pass a configurator in, so that you can store the values retrieved in appropriate places.

Templates

cdl templates are themselves a

map[string]interface{}

but are flat (i.e. only one level deep). The key represents a point in in the hierarchy to parse, and the value specifies what may appear at that point. The value is normally a `string`, but may be a pointer to a validation function you supply.

For example:

    template := cdl.Template{
		"/":     "{}apple peach? lemon",
		"apple": "float64",
		"peach": isOneOrTwo,
	}

Here:

  • The root level is specified to be a map ('`{}`'), which may consist of the elements `apple`, `peach` and `lemon`.

  • There must be an `apple` element and `lemon` element, but the `peach` element is optional.

  • The `apple` element must be a `float64`

  • In order to validate the `peach` element, your own validator function (`isOneOrTwo`) is called. If this returns a `cdl.CdlError`, that error will be passed to the user (as an `error`). If it returns `nil`, then validation will continue.

  • There is no validation at all on `peach`

Let's take a more complicated example:

template := cdl.Template{
	"/":          "{}apple peach? pear* plum+ raspberry{1,3} strawberry! kiwi{1,4}? guava!{1,2} orange?{2,31}",
	"apple":      "float64",
	"peach":      isOneOrTwo,
	"strawberry": "[nectarine]{1,3}",
	"nectarine":  "string",
	"raspberry":  "string"
}

Here we have allowed in the root level:

  • `strawberry`: The `!` indicates it is mandatory; this is the default, so the `!` is unnecessary. Each `strawberry` must be an array of `nectarine` with between 1 and 3 components, and each `nectarine` must be a `string`.

  • `rasbperry`: This is a shorthand for writing the same thing as above, i.e. an array of between 1 and 3 `raspberry`, each of which must be a string.

  • `pear`: An array of zero or more items. Note the empty array must be there (if the array itself is optional, write `pear?*`).

  • `plum`: An array of one or more entries.

  • `kiwi`: An optionally present array of between 1 and 4 entries

  • `guava`: A mandatory array of between 1 and 2 entries

Template syntax in detail

1. Each key must either be `/` (for the root key) or consist of word characters (i.e. matching `\w+` in regexp terms)

2. Each key must have a value, which may be either:

  • A validator function;
  • A `cdl.EnumType` (in which case the data will be validated against that `EnumType`); or
  • A validation instruction in the form of a `string`
  1. A validator function is a function with the signature func(obj interface{}) (err *CdlError)`

4. Each validation instruction is a quoted string and may be either

  • The Go name of a type (not a slice), e.g. `bool`, `string` etc. (in quotes as it's a `string`)
  • A pseudotype (e.g. `number`, `integer`) - see below
  • An array specifier, having a form beginning `[]`
  • A map specifier, having a form beginning `{}`

5. Each pseudotype may be either

  • The word `number` which indicates any numerical type (not `bool`)
  • The word `integer` which indicates any numerical type where the value is an integer (useful for parsing JSON with `json/encoding` which presents these as `float64`)
  • The word `ipport` for an IP port pair which is successfully decoded by `net.SplitHostPort`

6. An array specifier has the form `[]key` optionally followed by a range specifier

  • The key (`key` above) consists of word characters.
  • The key need not be specified within the template (if it isn't, no validation will be done on it).

7. A range specifier takes the form

  • `{n,m}` (meaning between `n` and `m`) or
  • `{n,}` (meaning at least `n`).
  1. A map specifier has the form `{}` followed by zero or more space-separated map elements

9. A map element consists of a key (`key`) followed by zero or more modifiers

  • The key consists of word characters.
  • The key need not be specified within the template (if it isn't, no validation will be done on it).

10. Permitted modifiers are:

  • `?` means the key is optional
  • `!` means the key is mandatory (the default)
  • `*` means the key is an array of 0 or more elements
  • `+` means the key is an array of 1 or more elements
  • A range specifier (see above), i.e.
  • `{n,m}` (meaning between `n` and `m`) or
  • `{n,}` (meaning at least `n`)

Validator Functions

Where the validator is passed, it is a function with signature:

func (o interface{}) *cdl.CdlError

Here's an example showing how it can return an error and send supplementary data back to the user. Note that cdl itself will add the appropriate context.

func isOneOrTwo(o interface{}) *cdl.CdlError {
	if v, ok := o.(float64); !ok {
		return cdl.NewError(cdl.ErrBadValue).SetSupplementary("is not a float64")
	} else {
		if v != 1 && v != 2 {
			return cdl.NewError(cdl.ErrBadValue).SetSupplementary("is not 1 or 2")
		}
	}
	return nil
}

Configurators

A cdl configurator may optionally be passed to the `Validate` function. The configurator allows you to consume the configuration in your program now you know that is validated.

The configurator consists of a map of keys to items. Each item should be either

  • a pointer to the variable to be set; or
  • a pointer to a configuration function.

If a pointer to a variable is used, the variable must be of the same type as the item in the configuration, or an error will be issued; therefore as a type check is performed here, it is unnecessary in this case to require a specific type in the template. If a specific type is require, a type check is done twice. Certain pseudo-types being required will cause a type conversion:

1. If you required the pseudo-type `number`, you will be always be given a `float64`

2. If you required the pseudo-type `integer`, you will always be given an `int`

If a pointer to an `Enum` is given, a `string` value is expected in the data, and it will be validated against that `Enum`.

If a pointer configuration function is used, it has a `ConfiguratorFunc` type (or a function with a similar signature), which looks like this:

type ConfiguratorFunc func(obj interface{}, path Path) (err *CdlError)

This function is guaranteed to be called for each item in the tree (if it's key is present in the configurator) after it and all of its children have been validated. It may return an error (just like a validator function).

The object passed will be the validated object from the configuration tree. It is guaranteed to be of the correct type, which means the type you asked for save for the following exceptions:

1. If you asked for the pseudo-type `number`, you will always be given a `float64`.

2. If you asked for the pseudo-type `integer`, you will always be given an `int`.

As a trivial example:

    var i int
    err := ct.Validate(object, cdl.Configurator{
        "i": func(o interface{}, p cdl.Path) *cdl.CdlError {
		        i = o.(int)
		        return nil
	        },
    })

Here the parameter named `"i"` in the template will be stored in variable `i`.

Example (CdlCompile)
package main

import (
	"fmt"
	"github.com/abligh/cdl"
	"log"
)

func main() {

	// here's our template
	template := cdl.Template{
		"/":     "{}apple peach? pear* plum+ raspberry{1,3} strawberry! kiwi{1,4}? guava!{1,2} orange?{2,31}",
		"apple": "float64",
	}

	if ct, err := cdl.Compile(template); err != nil {
		log.Fatalf("Error on compile: %v", err)
	} else {

		// use ct here
		_ = ct
	}

	fmt.Println("Success!")
}
Output:

Success!
Example (CdlValidate)
package main

import (
	"encoding/json"
	"fmt"
	"github.com/abligh/cdl"
	"log"
)

func main() {

	// here's our template
	template := cdl.Template{
		"/":     "{}apple peach? pear* plum+ raspberry{1,3} strawberry! kiwi{1,4}? guava!{1,2} orange?{2,31}",
		"apple": "float64",
		"peach": func(o interface{}) *cdl.CdlError {
			if v, ok := o.(float64); !ok {
				return cdl.NewError("ErrBadValue").SetSupplementary("is not a float64")
			} else {
				if v != 1 && v != 2 {
					return cdl.NewError("ErrBadValue").SetSupplementary("is not 1 or 2")
				}
			}
			return nil
		},
	}

	if ct, err := cdl.Compile(template); err != nil {
		log.Fatalf("Error on compile: %v", err)
	} else {

		var strawberry string

		// here's our configurator
		configurator := cdl.Configurator{

			// First an easy example using a pointer
			"strawberry": &strawberry,

			// Now a more complex example using a string
			"apple": func(o interface{}, p cdl.Path) *cdl.CdlError {
				fmt.Printf("Apple is %1.0f - ", o.(float64))
				return nil
			},
		}

		// Unmarshal some JSON
		var m interface{}

		j := `
		     {
				"apple" : 3,
				"pear" : [],
				"peach" : 2,
				"plum" : [ 1 ],
				"raspberry" : [ "a", "b" ],
				"strawberry" : "here",
				"guava": [ "c", "d" ]
		     }`

		if err := json.Unmarshal([]byte(j), &m); err != nil {
			log.Fatalf("Cannot unmarshal JSON: %v", err)
		}

		// Validate it
		if err := ct.Validate(m, configurator); err != nil {
			log.Fatalf("Validation error: %v", err)
		}

		if strawberry != "here" {
			log.Fatal("Strawberry variable not set correctly")
		}

		fmt.Println("Success!")

	}
}
Output:

Apple is 3 - Success!

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrorEnum = NewEnumTypeWithText(map[string]string{
		"ErrInternal":                    "Internal error",
		"ErrMissingRoot":                 "No root key in template",
		"ErrBadOptionValue":              "Bad option value",
		"ErrBadRangeOptionModifier":      "Bad range option modifer",
		"ErrBadRangeOptionModifierValue": "Bad range option modifier value",
		"ErrBadOptionModifier":           "Bad option modifier",
		"ErrBadKey":                      "Bad key",
		"ErrBadValue":                    "Bad value",
		"ErrUnknownKey":                  "Unknown key",
		"ErrExpectedMap":                 "Expected map",
		"ErrExpectedArray":               "Expected array",
		"ErrOutOfRange":                  "Number of array items outside permissible range",
		"ErrBadType":                     "Bad type",
		"ErrMissingMandatory":            "Missing mandatory key",
		"ErrBadConfigurator":             "Bad configurator",
		"ErrBadEnumValue":                "Bad option",
	})
)

var ErrorEnum is the Enum containing cdl errors.

Functions

This section is empty.

Types

type CdlError

type CdlError struct {
	Type          Enum
	Supplementary string
	Context       []string
}

func NewError

func NewError(t string) *CdlError

func NewError returns a new CdlError of a given type.

The type should be a type starting with `Err` in the constants section.

func NewErrorContext

func NewErrorContext(t string, c string) *CdlError

func NewErrorContext creates a new CdlError with the specified context string.

The type should be a type starting with `Err` in the constants section.

func NewErrorContextQuoted

func NewErrorContextQuoted(t string, c string) *CdlError

func NewErrorContext creates a new CdlError with the specified context string.

The type should be a type starting with `Err` in the constants section. The context string will be quoted.

func (*CdlError) AddContext

func (e *CdlError) AddContext(c string) *CdlError

func AddContext adds the specified context to an existing cdl error.

func (*CdlError) AddContextQuoted

func (e *CdlError) AddContextQuoted(c string) *CdlError

func AddContextQuoted adds the specified context to an existing cdl error.

The context will be quoted.

func (CdlError) Error

func (e CdlError) Error() string

func Error implements the Error() function of the error interface.

An error string is returned in context.

func (*CdlError) SetSupplementary

func (e *CdlError) SetSupplementary(s string) *CdlError

func SetSupplementary adds the specified supplementary data to an existing cdl error.

type CompiledTemplate

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

type CompiledTemplate is a compiled template.

It is opaque to the user in operations.

func Compile

func Compile(t Template) (*CompiledTemplate, error)

func Compile compiles a specified cdl template.

func MustCompile

func MustCompile(t Template) *CompiledTemplate

MustCompile is like Compile but panics if the expression cannot be parsed. It simplifies safe initialization of global variables holding compiled templates

func (*CompiledTemplate) Validate

func (ct *CompiledTemplate) Validate(o interface{}, configurator Configurator) error

func Validate validates an object against a cdl template.

Optionally a configurator may be passed. This can be nil if you do not need configurator functions calling

type Configurator

type Configurator map[string]interface{}

type Configurator is a map of Configurator functions

type ConfiguratorFunc

type ConfiguratorFunc func(obj interface{}, path Path) (err *CdlError)

type ConfiguratorFunc allows user specified configurator functions to be passed to cdl.

type Enum

type Enum struct {
	Type *EnumType
	// contains filtered or unexported fields
}

func (*Enum) Has

func (e *Enum) Has(v string) bool

func Has determines whether an Enum could be set to a value

returns true if the value is valid, else false

func (*Enum) Set

func (e *Enum) Set(v string) bool

func Set sets the value of an Enum to a specific value

returns true if setting the value to v succeeded, else false

func (Enum) String

func (e Enum) String() string

func String produces the string representation of an Enum

func (Enum) Text

func (e Enum) Text() string

func String produces the text representation of an Enum

If no text has been specified, the text is the string representation of the item

type EnumType

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

type EnumType represents an enum type within cdl

Each enum type must be initialised exactly once. To initialise use something like

var myEnumType = cdl.NewEnumType("DEFAULT_VALUE", "ONE_VALUE", "ANOTHER_VALUE")

func NewEnumType

func NewEnumType(values ...string) EnumType

func NewEnumType produces a new EnumType for a given list of enumeration constants

func NewEnumTypeWithText

func NewEnumTypeWithText(values map[string]string) EnumType

func NewEnumType produces a new EnumType for a given list of enumeration constants

func (*EnumType) Has

func (et *EnumType) Has(v string) bool

func Has determines whether an EnumType's instance could be set to a value

returns true if the value is valid, else false

func (*EnumType) New

func (et *EnumType) New(v string) Enum

func New creates a new enum value

type Path

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

type Path is an array of items constituting the path to an item to be checked for configuration

func (*Path) Slice

func (p *Path) Slice() []interface{}

func Slice returns a slice of objects representing the path.

The objects may be strings or integers

func (Path) String

func (p Path) String() string

func String produces a string representation of a path

The path elements are separated by '/'

func (*Path) StringSlice

func (p *Path) StringSlice() []string

func StringSlice returns a slice of strings representing a path

type Template

type Template map[string]interface{}

type Template is a user-provided uncompiled template.

See the overview for how these work.

type ValidatorFunc

type ValidatorFunc func(obj interface{}) (err *CdlError)

type ValidatorFunc allows user specified validation functions to be passed to cdl.

Jump to

Keyboard shortcuts

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