confl

package module
v0.2.4 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2023 License: MIT Imports: 19 Imported by: 43

README

Yet another Config Parser for go

This is a config parser most similar to Nginx, supports Json format, but with line-breaks, comments, etc. Also, like Nginx is more lenient.

Uses same syntax as https://github.com/vstakhov/libucl

Code Coverage GoDoc Build Status Go ReportCard

Use SublimeText Nginx Plugin for formatting.

Credit to BurntSushi/Toml and Apcera/Gnatsd from which this was derived.

Example

# nice, a config with comments!

# support the name = value format
title = "conf Example"
# support json semicolon
title2 : "conf example2"
# support omitting = or : because key starts a line
title3 "conf example"
# note, we do not have to have quotes
title4 = Without Quotes

# for Sections we can use brackets
hand {
  name = "Tyrion"
  organization = "Lannisters"
  bio = "Imp"                 // comments on fields
  dob = 1979-05-27T07:32:00Z  # dates, and more comments on fields
}

// Note, double-slash comment
// section name/value that is quoted and json valid, including commas
address : {
  "street"  : "1 Sky Cell",
  "city"    : "Eyre",
  "region"  : "Vale of Arryn",
  "country" : "Westeros"
}

# sections can omit the colon, equal before bracket 
seenwith {
  # nested section
  # can be spaces or tabs for nesting
  jaime : {
    season = season1
    episode = "episode1"
  }

  cersei = {
    season = season1
    episode = "episode1"
  }

}


# Line breaks are OK when inside arrays
seasons = [
  "season1",
  "season2",
  "season3",
  "season4",
  "???"
]


# long strings can use parens to allow multi-line
description (
    we possibly
    can have
    multi line text with a block paren
    block ends with end paren on new line
)



And the corresponding Go types are:

type Config struct {
	Title       string
	Hand        HandOfKing
	Location    *Address `confl:"address"`
	Seenwith    map[string]Character
	Seasons     []string
	Description string
}

type HandOfKing struct {
	Name     string
	Org      string `json:"organization"`  // Reads either confl, or json attributes
	Bio      string
	DOB      time.Time
	Deceased bool
}

type Address struct {
	Street  string
	City    string
	Region  string
	ZipCode int
}

type Character struct {
	Episode string
	Season  string
}

Note that a case insensitive match will be tried if an exact match can't be found.

A working example of the above can be found in _examples/example.{go,conf}.

Examples

This package works similarly to how the Go standard library handles XML and JSON. Namely, data is loaded into Go values via reflection.

For the simplest example, consider a file as just a list of keys and values:

// Comments in Config
Age = 25
# another comment
Cats = [ "Cauchy", "Plato" ]
# now, using quotes on key
"Pi" = 3.14
Perfection = [ 6, 28, 496, 8128 ]
DOB = 1987-07-05T05:45:00Z

Which could be defined in Go as:

type Config struct {
  Age int
  Cats []string
  Pi float64
  Perfection []int
  DOB time.Time 
}

And then decoded with:

var conf Config
if err := confl.Unmarshal(byteData, &conf); err != nil {
  // handle error
}

You can also use struct tags if your struct field name doesn't map to a confl key value directly:

some_key_NAME = "wat"
type Config struct {
  ObscureKey string `confl:"some_key_NAME"`
}

Using the encoding.TextUnmarshaler interface

Here's an example that automatically parses duration strings into time.Duration values:

song [
	{
		name = "Thunder Road"
		duration = "4m49s"
	},
	{
		name = "Stairway to Heaven"
		duration = "8m03s"
	}
]

Which can be decoded with:

type song struct {
  Name     string
  Duration duration
}
type songs struct {
  Song []song
}
var favorites songs
if err := confl.Unmarshal(blob, &favorites); err != nil {
  log.Fatal(err)
}

for _, s := range favorites.Song {
  fmt.Printf("%s (%s)\n", s.Name, s.Duration)
}

And you'll also need a duration type that satisfies the encoding.TextUnmarshaler interface:

type duration struct {
	time.Duration
}

func (d *duration) UnmarshalText(text []byte) error {
	var err error
	d.Duration, err = time.ParseDuration(string(text))
	return err
}

Documentation

Overview

Package confl provides facilities for decoding and encoding TOML/NGINX configuration files via reflection.

Conf is a configuration file format used by gnatsd. It is a flexible format that combines the best of traditional configuration formats and newer styles such as JSON and YAML.

Example (Unmarshaler)

Example Unmarshaler shows how to decode strings into your own custom data type.

rawData := `
song [
	{
		name = "Thunder Road"
		duration = "4m49s"
	},
	{
		name = "Stairway to Heaven"
		duration = "8m03s"
	}
]

`
type song struct {
	Name     string
	Duration duration
}
type songs struct {
	Song []song
}
var favorites songs
if _, err := Decode(rawData, &favorites); err != nil {
	log.Fatal(err)
}

// Code to implement the TextUnmarshaler interface for `duration`:
//
// type duration struct {
// 	time.Duration
// }
//
// func (d *duration) UnmarshalText(text []byte) error {
// 	var err error
// 	d.Duration, err = time.ParseDuration(string(text))
// 	return err
// }

for _, s := range favorites.Song {
	fmt.Printf("%s (%s)\n", s.Name, s.Duration)
}
Output:

Thunder Road (4m49s)
Stairway to Heaven (8m3s)

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// IdentityChars Which Identity Characters are allowed?
	IdentityChars = "_."
)
View Source
var (
	KeyEqElement = ":"
)

Functions

func Marshal

func Marshal(v interface{}) ([]byte, error)

Marshal a go struct into bytes

func Parse

func Parse(data string) (map[string]interface{}, error)

func SafeKey

func SafeKey(key string) string

SafeKey key

func Unmarshal

func Unmarshal(bs []byte, v interface{}) error

Types

type Decoder

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

func NewDecoder

func NewDecoder(r io.Reader) *Decoder

func (*Decoder) Decode

func (dec *Decoder) Decode(v interface{}) error

type Encoder

type Encoder struct {
	// A single indentation level. By default it is two spaces.
	Indent string

	KeyEqElement string
	// contains filtered or unexported fields
}

Encoder controls the encoding of Go values to a document to some io.Writer.

The indentation level can be controlled with the Indent field.

func NewEncoder

func NewEncoder(w io.Writer) *Encoder

NewEncoder returns a encoder that encodes Go values to the io.Writer given. By default, a single indentation level is 2 spaces.

func (*Encoder) Encode

func (enc *Encoder) Encode(v interface{}) error

Encode writes a representation of the Go value to the underlying io.Writer. If the value given cannot be encoded to a valid document, then an error is returned.

The mapping between Go values and values should be precisely the same as for the Decode* functions. Similarly, the TextMarshaler interface is supported by encoding the resulting bytes as strings. (If you want to write arbitrary binary data then you will need to use something like base64 since does not have any binary types.)

When encoding hashes (i.e., Go maps or structs), keys without any sub-hashes are encoded first.

If a Go map is encoded, then its keys are sorted alphabetically for deterministic output. More control over this behavior may be provided if there is demand for it.

Encoding Go values without a corresponding representation---like map types with non-string keys---will cause an error to be returned. Similarly for mixed arrays/slices, arrays/slices with nil elements, embedded non-struct types and nested slices containing maps or structs. (e.g., [][]map[string]string is not allowed but []map[string]string is OK and so is []map[string][]string.)

Example
date, _ := time.Parse(time.RFC822, "14 Mar 10 18:00 UTC")
var config = map[string]interface{}{
	"date":   date,
	"counts": []int{1, 1, 2, 3, 5, 8},
	"hash": map[string]string{
		"key1": "val1",
		"key2": "val2",
	},
}
buf := new(bytes.Buffer)
if err := NewEncoder(buf).Encode(config); err != nil {
	u.Errorf("could not encode: %v", err)
	log.Fatal(err)
}
fmt.Println(buf.String())
Output:

counts = [1, 1, 2, 3, 5, 8]
date = 2010-03-14T18:00:00Z
hash {
  key1 = "val1"
  key2 = "val2"
}

type Key

type Key []string

Key is the type of any key, including key groups. Use (MetaData).Keys to get values of this type.

func (Key) String

func (k Key) String() string

type MetaData

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

MetaData allows access to meta information about data that may not be inferrable via reflection. In particular, whether a key has been defined and the type of a key.

func Decode

func Decode(data string, v interface{}) (MetaData, error)

Decode will decode the contents of `data` in confl format into a pointer `v`.

confl hashes correspond to Go structs or maps. (Dealer's choice. They can be used interchangeably.)

confl arrays of tables correspond to either a slice of structs or a slice of maps.

confl datetimes correspond to Go `time.Time` values.

All other confl types (float, string, int, bool and array) correspond to the obvious Go types.

An exception to the above rules is if a type implements the encoding.TextUnmarshaler interface. In this case, any primitive confl value (floats, strings, integers, booleans and datetimes) will be converted to a byte string and given to the value's UnmarshalText method. See the Unmarshaler example for a demonstration with time duration strings.

Key mapping

confl keys can map to either keys in a Go map or field names in a Go struct. The special `confl` struct tag may be used to map confl keys to struct fields that don't match the key name exactly. (See the example.) A case insensitive match to struct names will be tried if an exact match can't be found.

The mapping between confl values and Go values is loose. That is, there may exist confl values that cannot be placed into your representation, and there may be parts of your representation that do not correspond to confl values. This loose mapping can be made stricter by using the IsDefined and/or Undecoded methods on the MetaData returned.

This decoder will not handle cyclic types. If a cyclic type is passed, `Decode` will not terminate.

Example
var rawData = `
# Some comments.
alpha {
	ip = "10.0.0.1"
	// config section
	config {
		Ports = [ 8001, 8002 ]
		Location = "Toronto"
		Created = 1987-07-05T05:45:00Z
	}
}


beta {
	ip = "10.0.0.2"

	config {
		Ports = [ 9001, 9002 ]
		Location = "New Jersey"
		Created = 1887-01-05T05:55:00Z
	}
}

`

type serverConfig struct {
	Ports    []int
	Location string
	Created  time.Time
}

type server struct {
	IP     string       `confl:"ip"`
	Config serverConfig `confl:"config"`
}

type servers map[string]server

var config servers
if _, err := Decode(rawData, &config); err != nil {
	log.Fatal(err)
}

for _, name := range []string{"alpha", "beta"} {
	s := config[name]
	fmt.Printf("Server: %s (ip: %s) in %s created on %s\n",
		name, s.IP, s.Config.Location,
		s.Config.Created.Format("2006-01-02"))
	fmt.Printf("Ports: %v\n", s.Config.Ports)
}
Output:

Server: alpha (ip: 10.0.0.1) in Toronto created on 1987-07-05
Ports: [8001 8002]
Server: beta (ip: 10.0.0.2) in New Jersey created on 1887-01-05
Ports: [9001 9002]

func DecodeFile

func DecodeFile(fpath string, v interface{}) (MetaData, error)

DecodeFile is just like Decode, except it will automatically read the contents of the file at `fpath` and decode it for you.

func DecodeReader

func DecodeReader(r io.Reader, v interface{}) (MetaData, error)

DecodeReader is just like Decode, except it will consume all bytes from the reader and decode it for you.

func (*MetaData) IsDefined

func (md *MetaData) IsDefined(key ...string) bool

IsDefined returns true if the key given exists in the data. The key should be specified hierarchially. e.g.,

// access the key 'a.b.c'
IsDefined("a", "b", "c")

IsDefined will return false if an empty key given. Keys are case sensitive.

func (*MetaData) Keys

func (md *MetaData) Keys() []Key

Keys returns a slice of every key in the data, including key groups. Each key is itself a slice, where the first element is the top of the hierarchy and the last is the most specific.

The list will have the same order as the keys appeared in the data.

All keys returned are non-empty.

func (*MetaData) PrimitiveDecode

func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error

PrimitiveDecode is just like the other `Decode*` functions, except it decodes a confl value that has already been parsed. Valid primitive values can *only* be obtained from values filled by the decoder functions, including this method. (i.e., `v` may contain more `Primitive` values.)

Meta data for primitive values is included in the meta data returned by the `Decode*` functions with one exception: keys returned by the Undecoded method will only reflect keys that were decoded. Namely, any keys hidden behind a Primitive will be considered undecoded. Executing this method will update the undecoded keys in the meta data. (See the example.)

Example
var md MetaData
var err error

var rawData = `

ranking = ["Springsteen", "JGeils"]

bands {
	Springsteen { 
		started = 1973
		albums = ["Greetings", "WIESS", "Born to Run", "Darkness"]
	}

	JGeils {
		started = 1970
		albums = ["The J. Geils Band", "Full House", "Blow Your Face Out"]
	}
}
`

type band struct {
	Started int
	Albums  []string
}
type classics struct {
	Ranking []string
	Bands   map[string]Primitive
}

// Do the initial decode. Reflection is delayed on Primitive values.
var music classics
if md, err = Decode(rawData, &music); err != nil {
	log.Fatal(err)
}

// MetaData still includes information on Primitive values.
fmt.Printf("Is `bands.Springsteen` defined? %v\n",
	md.IsDefined("bands", "Springsteen"))

// Decode primitive data into Go values.
for _, artist := range music.Ranking {
	// A band is a primitive value, so we need to decode it to get a
	// real `band` value.
	primValue := music.Bands[artist]

	var aBand band
	if err = md.PrimitiveDecode(primValue, &aBand); err != nil {
		u.Warnf("failed on: ")
		log.Fatalf("Failed on %v  %v", primValue, err)
	}
	fmt.Printf("%s started in %d.\n", artist, aBand.Started)
}
// Check to see if there were any fields left undecoded.
// Note that this won't be empty before decoding the Primitive value!
fmt.Printf("Undecoded: %q\n", md.Undecoded())
Output:

Is `bands.Springsteen` defined? true
Springsteen started in 1973.
JGeils started in 1970.
Undecoded: []

func (*MetaData) Type

func (md *MetaData) Type(key ...string) string

Type returns a string representation of the type of the key specified.

Type will return the empty string if given an empty key or a key that does not exist. Keys are case sensitive.

func (*MetaData) Undecoded

func (md *MetaData) Undecoded() []Key

Undecoded returns all keys that have not been decoded in the order in which they appear in the original document.

This includes keys that haven't been decoded because of a Primitive value. Once the Primitive value is decoded, the keys will be considered decoded.

Also note that decoding into an empty interface will result in no decoding, and so no keys will be considered decoded.

In this sense, the Undecoded keys correspond to keys in the document that do not have a concrete type in your representation.

type Primitive

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

Primitive is a value that hasn't been decoded into a Go value. When using the various `Decode*` functions, the type `Primitive` may be given to any value, and its decoding will be delayed.

A `Primitive` value can be decoded using the `PrimitiveDecode` function.

The underlying representation of a `Primitive` value is subject to change. Do not rely on it.

N.B. Primitive values are still parsed, so using them will only avoid the overhead of reflection. They can be useful when you don't know the exact type of data until run time.

type TextMarshaler

type TextMarshaler encoding.TextMarshaler

TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here so that Go 1.1 can be supported.

type TextUnmarshaler

type TextUnmarshaler encoding.TextUnmarshaler

TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here so that Go 1.1 can be supported.

Notes

Bugs

  • The behavior here is incorrect whenever a Go type satisfies the encoding.TextUnmarshaler interface but also corresponds to a conf hash or array. In particular, the unmarshaler should only be applied to primitive conf values. But at this point, it will be applied to all kinds of values and produce an incorrect error whenever those values are hashes or arrays (including arrays of tables).

  • I honestly don't understand how this works. I can't seem to find a way to make this fail. I figured this would fail on invalid UTF-8 characters like U+DCFF, but it doesn't.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

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