$parent
This commit is contained in:
152
bkl.go
152
bkl.go
@@ -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), ".")
|
||||
|
||||
@@ -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.<ext></inlineFocus> inherits from <inlineFocus>a.<ext></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.<ext>, from a.<ext></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>
|
||||
|
||||
20
merge.go
20
merge.go
@@ -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
2
test
@@ -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
2
tests/parent/a.b.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
$parent: c
|
||||
a: 1
|
||||
1
tests/parent/c.yaml
Normal file
1
tests/parent/c.yaml
Normal file
@@ -0,0 +1 @@
|
||||
b: 2
|
||||
1
tests/parent/cmd
Normal file
1
tests/parent/cmd
Normal file
@@ -0,0 +1 @@
|
||||
bkl -v -f yaml a.b.yaml
|
||||
2
tests/parent/expected
Normal file
2
tests/parent/expected
Normal file
@@ -0,0 +1,2 @@
|
||||
a: 1
|
||||
b: 2
|
||||
Reference in New Issue
Block a user