This commit is contained in:
Ian Gulliver
2023-07-06 22:19:29 +01:00
parent b657e19eb1
commit 8581564a3c
8 changed files with 175 additions and 38 deletions

152
bkl.go
View File

@@ -9,15 +9,18 @@ import (
"log"
"os"
"path/filepath"
"slices"
"strings"
)
var (
Err = fmt.Errorf("bkl error")
Err = fmt.Errorf("bkl error")
ErrEncode = fmt.Errorf("encoding error (%w)", Err)
ErrDecode = fmt.Errorf("decoding error (%w)", Err)
ErrInvalidIndex = fmt.Errorf("invalid index (%w)", Err)
ErrInvalidDirective = fmt.Errorf("invalid directive (%w)", Err)
ErrInvalidFilename = fmt.Errorf("invalid filename (%w)", Err)
ErrInvalidType = fmt.Errorf("invalid type (%w)", Err)
ErrMissingFile = fmt.Errorf("missing file (%w)", Err)
ErrNoMatchFound = fmt.Errorf("no document matched $match (%w)", Err)
@@ -25,6 +28,7 @@ var (
ErrUnknownFormat = fmt.Errorf("unknown format (%w)", Err)
ErrInvalidMergeType = fmt.Errorf("invalid $merge type (%w)", ErrInvalidDirective)
ErrInvalidParentType = fmt.Errorf("invalid $parent type (%w)", ErrInvalidDirective)
ErrInvalidPatchType = fmt.Errorf("invalid $patch type (%w)", ErrInvalidDirective)
ErrInvalidPatchValue = fmt.Errorf("invalid $patch value (%w)", ErrInvalidDirective)
ErrInvalidReplaceType = fmt.Errorf("invalid $replace type (%w)", ErrInvalidDirective)
@@ -195,30 +199,28 @@ func (p *Parser) MergeFile(path string) error {
// MergeFileLayers determines relevant layers from the supplied path and merges
// them in order.
func (p *Parser) MergeFileLayers(path string) error {
dir := filepath.Dir(path)
base := filepath.Base(path)
paths := []string{
path,
}
parts := strings.Split(base, ".")
for i := 1; i < len(parts); i++ {
layerPath := filepath.Join(dir, strings.Join(parts[:i], "."))
extPath := FindFile(layerPath)
if extPath == "" {
return fmt.Errorf("%s: %w", layerPath, ErrMissingFile)
for {
parent, err := GetParent(path)
if err != nil {
return err
}
dest, _ := os.Readlink(extPath)
if dest != "" {
err := p.MergeFileLayers(dest)
if err != nil {
return err
}
continue
if parent == "" {
break
}
err := p.MergeFile(extPath)
path = parent
paths = append(paths, path)
}
slices.Reverse(paths)
for _, path := range paths {
err := p.MergeFile(path)
if err != nil {
return err
}
@@ -248,7 +250,7 @@ func (p *Parser) GetOutputIndex(index int, ext string) ([][]byte, error) {
return nil, err
}
obj, err = PostMerge(obj, obj)
obj, err = PostMerge(obj)
if err != nil {
return nil, err
}
@@ -316,6 +318,114 @@ func (p *Parser) log(format string, v ...any) {
log.Printf(format, v...)
}
func GetParent(path string) (string, error) {
// TODO: Needs a different API so it can specify no parent
parent, err := GetParentFromOverride(path)
if err != nil {
return "", err
}
if parent != "" {
return parent, nil
}
parent, err = GetParentFromSymlink(path)
if err != nil {
return "", err
}
if parent != "" {
return parent, nil
}
return GetParentFromFilename(path)
}
func GetParentFromOverride(path string) (string, error) {
fh, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("%s: %w", path, err)
}
defer fh.Close()
b, err := io.ReadAll(fh)
if err != nil {
return "", err
}
ext := Ext(path)
f, found := formatByExtension[ext]
if !found {
return "", fmt.Errorf("%s: %w", ext, ErrUnknownFormat)
}
patch, err := f.decode(b)
if err != nil {
return "", fmt.Errorf("%w / %w", err, ErrDecode)
}
patchMap, ok := patch.(map[string]any)
if !ok {
return "", nil
}
if parent, found := patchMap["$parent"]; found {
parentStr, ok := parent.(string)
if !ok {
return "", fmt.Errorf("%T: %w", parent, ErrInvalidParentType)
}
parentPath := FindFile(parentStr)
if parentPath == "" {
return "", fmt.Errorf("%s: %w", parentStr, ErrMissingFile)
}
return parentPath, nil
}
return "", nil
}
func GetParentFromSymlink(path string) (string, error) {
dest, _ := os.Readlink(path)
if dest == "" {
// Not a link
return "", nil
}
return GetParentFromFilename(dest)
}
func GetParentFromFilename(path string) (string, error) {
dir := filepath.Dir(path)
base := filepath.Base(path)
parts := strings.Split(base, ".")
// Last part is file extension
switch {
case len(parts) < 2:
return "", fmt.Errorf("%s: %w", path, ErrInvalidFilename)
case len(parts) == 2:
// Base template
return "", nil
default:
layerPath := filepath.Join(dir, strings.Join(parts[:len(parts)-2], "."))
extPath := FindFile(layerPath)
if extPath == "" {
return "", fmt.Errorf("%s: %w", layerPath, ErrMissingFile)
}
return extPath, nil
}
}
// Ext returns the file extension for path, or "".
func Ext(path string) string {
return strings.TrimPrefix(filepath.Ext(path), ".")

View File

@@ -220,10 +220,6 @@ op {
font-size: 30px;
}
ref {
color: var(--green2);
}
code.labeled {
border-radius: 0 5px 5px 5px;
}
@@ -255,6 +251,8 @@ focus, focus > * {
inlineFocus {
background: var(--neutral8);
color: var(--red6);
display: inline-block;
padding: 0 3px 0 3px;
white-space: nowrap;
}
</style>
@@ -299,7 +297,7 @@ inlineFocus {
<vSpace></vSpace>
<p>bkl knows that <ref>service.test.toml</ref> inherits from <ref>service.yaml</ref> by the filename pattern, and uses filename extensions to determine formats.</p>
<p>bkl knows that <inlineFocus>service.test.toml</inlineFocus> inherits from <inlineFocus>service.yaml</inlineFocus> by the filename pattern (<a href="#inheritance">override with <inlineFocus>$parent</inlineFocus></a>), and uses filename extensions to determine formats.</p>
@@ -357,19 +355,19 @@ inlineFocus {
<h2><a name="advanced-input">Advanced Input</a></h2>
<h2><a name="multiple-input-files">Multiple Input Files</a></h2>
<label>Multiple Files</label>
<code class="labeled">$ <cmd>bkl</cmd> <focus><string>a.b.yaml</string> <string>c.d.yaml</string></focus> # (a.yaml + a.b.yaml) + (c.yaml + c.d.yaml)</code>
<code>$ <cmd>bkl</cmd> <focus><string>a.b.yaml</string> <string>c.d.yaml</string></focus> # (a.yaml + a.b.yaml) + (c.yaml + c.d.yaml)</code>
<vSpace></vSpace>
<p>Specifying multiple input files evaluates them as normal, then merges them onto each other in order.</p>
<vSpace></vSpace>
<label>Symlinks</label>
<code class="labeled">$ <cmd>ln</cmd> <string>-s</string> <focus><string>a.b.yaml</string> <string>c.yaml</string></focus>
<h2><a name="symlinks">Symlinks</a></h2>
<code>$ <cmd>ln</cmd> <string>-s</string> <focus><string>a.b.yaml</string> <string>c.yaml</string></focus>
$ <cmd>bkl</cmd> <focus><string>c.d.yaml</string></focus> # a.yaml + a.b.yaml (c.yaml) + c.d.yaml</code>
<vSpace></vSpace>
@@ -378,6 +376,19 @@ $ <cmd>bkl</cmd> <focus><string>c.d.yaml</string></focus> # a.yaml + a.b.yaml
<h2><a name="inheritance">Inheritance</a></h2>
<p>Inheritance is determined using filenames by default. After stripping the extension, the remaining filename is split on <inlineFocus>.</inlineFocus> and treated as an inheritance hierarchy (e.g. <inlineFocus>a.b.c.yaml</inlineFocus> inherits from <inlineFocus>a.b.&lt;ext&gt;</inlineFocus> inherits from <inlineFocus>a.&lt;ext&gt;</inlineFocus>). Parent templates may have any supported file extension.</p>
<label>Override Inheritance</label>
<code class="labeled"><focus><key>$parent</key>: <string>a.b</string></focus> # inherits from a.b.&lt;ext&gt;, from a.&lt;ext&gt;</code>
<vSpace></vSpace>
<p>bkl will still check for all supported endings of a manually-specified parent file, and will still evaluate template layers of the parent in the normal order.</p>
<h2><a name="streams">Streams</a></h2>
<p>bkl understands input streams (multi-document YAML files delimited with <inlineFocus>---</inlineFocus>). To layer them, it has to match documents between layers. It does this using document ordering within layer files by default, but can be overridden with <inlineFocus>$match</inlineFocus>.</p>

View File

@@ -114,7 +114,16 @@ func MergeList(dst []any, src any) (any, error) {
}
}
func PostMerge(root any, obj any) (any, error) {
func PostMerge(root any) (any, error) {
switch rootType := root.(type) {
case map[string]any:
delete(rootType, "$parent")
}
return PostMergeInt(root, root)
}
func PostMergeInt(root any, obj any) (any, error) {
switch objType := obj.(type) {
case map[string]any:
if path, found := objType["$merge"]; found {
@@ -135,7 +144,7 @@ func PostMerge(root any, obj any) (any, error) {
return nil, err
}
return PostMerge(root, next)
return PostMergeInt(root, next)
}
if path, found := objType["$replace"]; found {
@@ -151,11 +160,11 @@ func PostMerge(root any, obj any) (any, error) {
return nil, fmt.Errorf("%s: (%w)", pathVal, ErrReplaceRefNotFound)
}
return PostMerge(root, next)
return PostMergeInt(root, next)
}
for k, v := range objType {
v2, err := PostMerge(root, v)
v2, err := PostMergeInt(root, v)
if err != nil {
return nil, err
}
@@ -167,7 +176,7 @@ func PostMerge(root any, obj any) (any, error) {
case []any:
for i, v := range objType {
v2, err := PostMerge(root, v)
v2, err := PostMergeInt(root, v)
if err != nil {
return nil, err
}
@@ -192,6 +201,7 @@ func FindOutputs(obj any) []any {
ret = append(ret, obj)
}
// TODO: Sort by key so output order is stable
for _, v := range objType {
ret = append(ret, FindOutputs(v)...)
}

2
test
View File

@@ -11,7 +11,7 @@ function cleanup {
trap cleanup EXIT
go build ./cmd/bkl
export PATH=$PATH:/opt/homebrew/share/git-core/contrib/diff-highlight:$ROOT
export PATH=$ROOT:/opt/homebrew/share/git-core/contrib/diff-highlight:$PATH
for TEST in tests/*; do
echo TEST $TEST

2
tests/parent/a.b.yaml Normal file
View File

@@ -0,0 +1,2 @@
$parent: c
a: 1

1
tests/parent/c.yaml Normal file
View File

@@ -0,0 +1 @@
b: 2

1
tests/parent/cmd Normal file
View File

@@ -0,0 +1 @@
bkl -v -f yaml a.b.yaml

2
tests/parent/expected Normal file
View File

@@ -0,0 +1,2 @@
a: 1
b: 2