bkl

package module
v1.0.41 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2024 License: Apache-2.0 Imports: 16 Imported by: 0

README

bkl

bkl (short for Baklava because it has layers) is a templating configuration language without the templates. It's designed to be simple to read and write with obvious behavior.

Write your configuration in your favorite format: JSON, YAML, or TOML. Layer configurations on top of each other, even from different file formats. Use filenames to define the inheritance. Have as many layers as you like. bkl merges your layers together with sane default behavior that you can override. Export your results in any supported format for human or machine consumption. Use the CLI directly or in scripts or automate with the library.

Go Reference

Example

service.yaml
addr: 127.0.0.1
name: myService
port: 8080
service.test.toml
port = 8081
Run it!
$ bkl service.test.toml
addr = '127.0.0.1'
name = 'myService'
port = 8081

bkl knows that service.test.toml inherits from service.yaml by the filename pattern (override with $parent) and uses filename extensions to determine formats.

Install

$ go install github.com/gopatchy/bkl/...@latest

Verify that ~/go/bin is in your $PATH.

You can also download binaries directly here.

Documentation

Overview

Package bkl implements a layered configuration language parser.

Example
package main

import (
	"os"

	"github.com/gopatchy/bkl"
)

func main() {
	// import "github.com/gopatchy/bkl"

	b := bkl.New()

	err := b.MergeFileLayers("tests/example1/a.b.toml")
	if err != nil {
		panic(err)
	}

	err = b.OutputToWriter(os.Stdout, "json")
	if err != nil {
		panic(err)
	}
}
Output:

{"addr":"127.0.0.1","name":"myService","port":8081}

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// Base error; every error in bkl inherits from this
	Err = fmt.Errorf("bkl error")

	// Format and system errors
	ErrCircularRef       = fmt.Errorf("circular reference (%w)", Err)
	ErrConflictingParent = fmt.Errorf("conflicting $parent (%w)", Err)
	ErrExtraEntries      = fmt.Errorf("extra entries (%w)", Err)
	ErrExtraKeys         = fmt.Errorf("extra keys (%w)", Err)
	ErrInvalidArguments  = fmt.Errorf("invalid arguments (%w)", Err)
	ErrInvalidDirective  = fmt.Errorf("invalid directive (%w)", Err)
	ErrInvalidIndex      = fmt.Errorf("invalid index (%w)", Err)
	ErrInvalidFilename   = fmt.Errorf("invalid filename (%w)", Err)
	ErrInvalidType       = fmt.Errorf("invalid type (%w)", Err)
	ErrInvalidParent     = fmt.Errorf("invalid $parent (%w)", Err)
	ErrInvalidRepeat     = fmt.Errorf("invalid $repeat (%w)", Err)
	ErrMarshal           = fmt.Errorf("encoding error (%w)", Err)
	ErrRefNotFound       = fmt.Errorf("reference not found (%w)", Err)
	ErrMissingEnv        = fmt.Errorf("missing environment variable (%w)", Err)
	ErrMissingFile       = fmt.Errorf("missing file (%w)", Err)
	ErrMissingMatch      = fmt.Errorf("missing $match (%w)", Err)
	ErrMultiMatch        = fmt.Errorf("multiple documents $match (%w)", Err)
	ErrNoMatchFound      = fmt.Errorf("no document/entry matched $match (%w)", Err)
	ErrNoCloneFound      = fmt.Errorf("no document/entry matched $clone (%w)", Err)
	ErrOutputFile        = fmt.Errorf("error opening output file (%w)", Err)
	ErrRequiredField     = fmt.Errorf("required field not set (%w)", Err)
	ErrUnknownFormat     = fmt.Errorf("unknown format (%w)", Err)
	ErrUnmarshal         = fmt.Errorf("decoding error (%w)", Err)
	ErrUselessOverride   = fmt.Errorf("useless override (%w)", Err)
	ErrVariableNotFound  = fmt.Errorf("variable not found (%w)", Err)
)

Functions

func FileMatch added in v1.0.2

func FileMatch(path string) (string, string, error)

FileMatch attempts to find a file with the same base name as path, but possibly with a different supported extension. It is intended to support "virtual" filenames that auto-convert from the format of the underlying real file.

Returns the real filename and the requested output format, or ("", "", error).

Types

type Document added in v1.0.25

type Document struct {
	ID      string
	Parents []*Document
	Data    any
	Vars    map[string]any
}

func NewDocument added in v1.0.25

func NewDocument(id string) *Document

func NewDocumentWithData added in v1.0.25

func NewDocumentWithData(id string, data any) *Document

func (*Document) AddParents added in v1.0.25

func (d *Document) AddParents(parents ...*Document)

func (*Document) AllParents added in v1.0.25

func (d *Document) AllParents() map[string]*Document

func (*Document) Clone added in v1.0.36

func (d *Document) Clone(suffix string) (*Document, error)

func (*Document) DataAsMap added in v1.0.25

func (d *Document) DataAsMap() map[string]any

func (*Document) PopMapValue added in v1.0.25

func (d *Document) PopMapValue(key string) (bool, any)

func (*Document) Process added in v1.0.39

func (d *Document) Process(mergeFromDocs []*Document) ([]*Document, error)

func (*Document) String added in v1.0.25

func (d *Document) String() string

type Format added in v1.0.7

type Format struct {
	MarshalStream   func([]any) ([]byte, error)
	UnmarshalStream func([]byte) ([]any, error)
}

func GetFormat added in v1.0.7

func GetFormat(name string) (*Format, error)

type Parser

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

A Parser reads input documents, merges layers, and generates outputs.

Terminology

  • Each Parser can read multiple files
  • Each file represents a single layer
  • Each file contains one or more documents
  • Each document generates one or more outputs

Directive Evaluation Order

Directive evaluation order can matter, e.g. if you $merge a subtree that contains an $output directive.

Phase 1

  • $parent

Phase 2

  • $delete
  • $replace: true

Phase 3

  • $merge
  • $replace: map
  • $replace: string

Phase 4

  • $repeat: int

Phase 5

  • $""
  • $encode
  • $env
  • $repeat
  • $value

Phase 6

  • $output

Document Layer Matching Logic

When applying a new document to internal state, it may be merged into one or more existing documents or appended as a new document. To select merge targets, Parser considers (in order):

  • If $match:
  • $match: null -> append
  • $match within parent documents -> merge
  • $match any documents -> merge
  • No matching documents -> error
  • If parent documents -> merge into all parents
  • If no parent documents -> append
Example
package main

import (
	"os"

	"github.com/gopatchy/bkl"
)

func main() {
	b := bkl.New()

	// Also parses tests/example1/a.yaml
	err := b.MergeFileLayers("tests/example1/a.b.toml")
	if err != nil {
		panic(err)
	}

	if err = b.OutputToWriter(os.Stdout, "json"); err != nil {
		panic(err)
	}
}
Output:

{"addr":"127.0.0.1","name":"myService","port":8081}

func New

func New() *Parser

New creates and returns a new Parser with an empty starting document set.

New always succeeds and returns a Parser instance.

Example
package main

import (
	"fmt"

	"github.com/gopatchy/bkl"
)

func main() {
	b := bkl.New()
	docs := b.Documents()
	fmt.Println(docs)
}
Output:

[]

func (*Parser) Documents added in v1.0.14

func (p *Parser) Documents() []*Document

Documents returns the parsed, merged (but not processed) trees for all documents.

Example
package main

import (
	"fmt"

	"github.com/gopatchy/bkl"
)

func main() {
	b := bkl.New()

	if err := b.MergeFileLayers("tests/example1/a.b.toml"); err != nil {
		panic(err)
	}

	docs := b.Documents()
	fmt.Println(docs[0].Data)
}
Output:

map[addr:127.0.0.1 name:myService port:8081]

func (*Parser) MergeDocument added in v1.0.25

func (p *Parser) MergeDocument(patch *Document) error

MergeDocument applies the supplied Document to the Parser's current internal document state using bkl's merge semantics. If expand is true, documents without $match will append; otherwise this is an error.

Example
package main

import (
	"os"

	"github.com/gopatchy/bkl"
)

func main() {
	b := bkl.New()

	doc1 := bkl.NewDocumentWithData("a", map[string]any{"a": 1})
	doc2 := bkl.NewDocumentWithData("b", map[string]any{"b": 2})
	doc2.AddParents(doc1)

	err := b.MergeDocument(doc1)
	if err != nil {
		panic(err)
	}

	err = b.MergeDocument(doc2)
	if err != nil {
		panic(err)
	}

	if err = b.OutputToWriter(os.Stdout, "json"); err != nil {
		panic(err)
	}
}
Output:

{"a":1,"b":2}

func (*Parser) MergeFile

func (p *Parser) MergeFile(path string) error

MergeFile parses the file at path and merges its contents into the Parser's document state using bkl's merge semantics.

Example
package main

import (
	"os"

	"github.com/gopatchy/bkl"
)

func main() {
	// Compare to Parser.MergeFileLayers example.

	b := bkl.New()

	// Does *not* parse tests/example1/a.yaml
	err := b.MergeFile("tests/example1/a.b.toml")
	if err != nil {
		panic(err)
	}

	if err = b.OutputToWriter(os.Stdout, "json"); err != nil {
		panic(err)
	}
}
Output:

{"port":8081}

func (*Parser) MergeFileLayers

func (p *Parser) MergeFileLayers(path string) error

MergeFileLayers determines relevant layers from the supplied path and merges them in order.

Example
package main

import (
	"os"

	"github.com/gopatchy/bkl"
)

func main() {
	// Compare to Parser.MergeFile example.

	b := bkl.New()

	// Also parses tests/example1/a.yaml
	err := b.MergeFileLayers("tests/example1/a.b.toml")
	if err != nil {
		panic(err)
	}

	if err = b.OutputToWriter(os.Stdout, "json"); err != nil {
		panic(err)
	}
}
Output:

{"addr":"127.0.0.1","name":"myService","port":8081}

func (*Parser) Output

func (p *Parser) Output(format string) ([]byte, error)

Output returns all documents encoded in the specified format and merged into a stream.

Example
package main

import (
	"os"

	"github.com/gopatchy/bkl"
)

func main() {
	b := bkl.New()

	if err := b.MergeFileLayers("tests/output-multi/a.yaml"); err != nil {
		panic(err)
	}

	blob, err := b.Output("yaml")
	if err != nil {
		panic(err)
	}

	os.Stdout.Write(blob)
}
Output:

a: 1
b: 2
---
c: 3
Example (Literal_dollar)
package main

import (
	"os"

	"github.com/gopatchy/bkl"
)

func main() {
	b := bkl.New()

	if err := b.MergeFileLayers("tests/literal-dollar/a.yaml"); err != nil {
		panic(err)
	}

	blob, err := b.Output("json")
	if err != nil {
		panic(err)
	}

	os.Stdout.Write(blob)
}
Output:

{"listKey":["$instance",{"nested":{"$key":"$value"}}],"mapKey":"$pod"}

func (*Parser) OutputDocuments added in v1.0.7

func (p *Parser) OutputDocuments() ([]any, error)

OutputDocuments returns the output objects generated by all documents.

func (*Parser) OutputToFile

func (p *Parser) OutputToFile(path, format string) error

OutputToFile encodes all documents in the specified format and writes them to the specified output path.

If format is "", it is inferred from path's file extension.

Example
package main

import (
	"fmt"
	"io"
	"os"

	"github.com/gopatchy/bkl"
)

func main() {
	b := bkl.New()

	if err := b.MergeFileLayers("tests/output-multi/a.yaml"); err != nil {
		panic(err)
	}

	f, err := os.CreateTemp("", "example")
	if err != nil {
		panic(err)
	}

	defer os.Remove(f.Name())

	err = b.OutputToFile(f.Name(), "toml")
	if err != nil {
		panic(err)
	}

	blob, err := io.ReadAll(f)
	if err != nil {
		panic(err)
	}

	fmt.Println(string(blob))
}
Output:

a = 1
b = 2
---
c = 3

func (*Parser) OutputToWriter

func (p *Parser) OutputToWriter(fh io.Writer, format string) error

OutputToWriter encodes all documents in the specified format and writes them to the specified io.Writer.

If format is "", it defaults to "json-pretty".

Example
package main

import (
	"os"

	"github.com/gopatchy/bkl"
)

func main() {
	b := bkl.New()

	if err := b.MergeFileLayers("tests/output-multi/a.yaml"); err != nil {
		panic(err)
	}

	err := b.OutputToWriter(os.Stdout, "yaml")
	if err != nil {
		panic(err)
	}
}
Output:

a: 1
b: 2
---
c: 3

func (*Parser) SetDebug

func (p *Parser) SetDebug(debug bool)

SetDebug enables or disables debug log output to stderr.

Example
package main

import (
	"log"
	"os"

	"github.com/gopatchy/bkl"
)

func main() {
	log.Default().SetFlags(0)
	log.Default().SetOutput(os.Stdout)

	b := bkl.New()

	b.SetDebug(true)

	if err := b.MergeFileLayers("tests/example1/a.b.toml"); err != nil {
		panic(err)
	}
}
Output:

Directories

Path Synopsis
cmd
bkl

Jump to

Keyboard shortcuts

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