Skip to content

Commit

Permalink
resolve path long nested qualified tags
Browse files Browse the repository at this point in the history
  • Loading branch information
mandelsoft committed May 29, 2021
1 parent 6fe60c7 commit 96863ab
Show file tree
Hide file tree
Showing 10 changed files with 406 additions and 114 deletions.
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ Contents:
- [(( &tag:name(value) ))](#-tagnamevalue-)
- [(( name::path ))](#-namepath-)
- [(( name::. ))](#-name-)
- [Path Resolution for Tags](#path-resolution-for-tags)
- [Tags in Multi-Document Streams](#tags-in-multi-document-streams)
- [Templates](#templates)
- [<<: (( &template ))](#--template-)
Expand Down Expand Up @@ -4822,6 +4823,59 @@ tagref: (( alice::. ))

resolves `tagref` to `25`

### Path Resolution for Tags

A tag reference always contains a tag name and a path separated by a double
colon (`::`).
The standard usecase is to describe a dedicated sub node for a tagged
node value.

for example, if the tag `X` describes the value

```yaml
data:
alice: 25
bob: 24
```

the tagged reference `tag::data.alice` describes the value `25`.

For tagged reference with a path other the `.` (the whole tag value),
structured tags feature a more sophisticated resolution mechanism. A structured
tag consist of multiple tag components separated by a colon (`:`), for
example `lib:mylib`.
Evaluation of a path reference for a tag tries to resolve the path in the
first unique nested tag, if it cannot be resolved directly by the given tag.

For example:

```yaml
tags:
- <<: (( &tag:lib:alice ))
data: alice.alice
- <<: (( &tag:lib:alice:v1))
data: alice.v1
- <<: (( &tag:lib:bob))
other: bob
usage:
data: (( lib::data ))
```

effectively resolves `usage.data` to `lib:alice::data` and therefore to the value
`alice.alice`.

To achieve this all matching sub tags are orderd by their number of
tag components. The first sub-level tag containing such a
given path is selected. For this level, the matching tag must be non-ambigious.
There must only be one tag with this level containing a matching path.
If there are multiple ones the evaluation fails. In the above example this would
be the case if tag `lib:bob` would contain a field `data` instead of or
additional to `other`.

This feature can be used in library stubs to provide qualified names for their
elements that can be used with merging the containing document nodes into
the template.

### Tags in Multi-Document Streams

If the template file is a multi-document stream the tags are preserved during
Expand Down
11 changes: 8 additions & 3 deletions cmd/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,18 +173,23 @@ func merge(stdin bool, templateFilePath string, opts flow.Options, json, split b
if i <= 0 {
log.Fatalln(fmt.Sprintf("tag file must be preceeded by a tag (<tag>:<path>)"))
}
tagName := tagDef[:i]
err := dynaml.CheckTagName(tagName)
if err != nil {
log.Fatalln(fmt.Sprintf("invalid tag name [%s]:", path.Clean(tagName)), err)
}
tagFilePath := tagDef[i+1:]
tagFile, err := ReadFile(tagFilePath)
if err != nil {
log.Fatalln(fmt.Sprintf("error reading tag gile [%s]:", path.Clean(tagFilePath)), err)
log.Fatalln(fmt.Sprintf("error reading tag file [%s]:", path.Clean(tagFilePath)), err)
}

tagYAML, err := yaml.Parse(tagFilePath, tagFile)
if err != nil {
log.Fatalln(fmt.Sprintf("error parsing tag gile [%s]:", path.Clean(tagFilePath)), err)
log.Fatalln(fmt.Sprintf("error parsing tag file [%s]:", path.Clean(tagFilePath)), err)
}

tags = append(tags, dynaml.NewTag(tagDef[:i], tagYAML, nil, dynaml.TAG_SCOPE_GLOBAL))
tags = append(tags, dynaml.NewTag(tagName, tagYAML, nil, dynaml.TAG_SCOPE_GLOBAL))
}

if stubs == nil {
Expand Down
5 changes: 3 additions & 2 deletions dynaml/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package dynaml

import (
"fmt"
"github.com/mandelsoft/spiff/yaml"
"strings"

"github.com/mandelsoft/spiff/yaml"

"github.com/mandelsoft/spiff/debug"
)

Expand Down Expand Up @@ -58,7 +59,7 @@ func (e CallExpr) Evaluate(binding Binding, locally bool) (interface{}, Evaluati
var info EvaluationInfo

ref, okf := e.Function.(ReferenceExpr)
if okf && len(ref.Path) == 1 && ref.Path[0] != "" && ref.Path[0] != "_" {
if okf && ref.Tag == "" && len(ref.Path) == 1 && ref.Path[0] != "" && ref.Path[0] != "_" {
funcName = ref.Path[0]
} else {
value, info, okf = ResolveExpressionOrPushEvaluation(&e.Function, &resolved, &info, binding, false)
Expand Down
84 changes: 1 addition & 83 deletions dynaml/expression.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package dynaml

import (
"fmt"

"github.com/mandelsoft/vfs/pkg/vfs"

"github.com/mandelsoft/spiff/yaml"
Expand All @@ -18,87 +16,6 @@ type SourceProvider interface {
SourceName() string
}

func CheckTagName(name string) error {
l := 0
for _, c := range name {
switch c {
case ':':
if l == 0 {
return fmt.Errorf("empty tag component not allowed")
}
l = 0
default:
l++
if c >= '0' && c <= '9' {
if l == 1 {
return fmt.Errorf("tag component must start with alnum rune")
}
continue
}
if c >= 'a' && c <= 'z' {
continue
}
if c >= 'A' && c <= 'Z' {
continue
}
return fmt.Errorf("invalid character %q in tag component", string(c))
}
}
return nil
}

const TAG_LOCAL = TagScope(0x01)
const TAG_SCOPE = TagScope(0x06)
const TAG_SCOPE_GLOBAL = TagScope(0x00)
const TAG_SCOPE_STREAM = TagScope(0x02)

type TagScope int

type Tag struct {
name string
node yaml.Node
path []string
scope TagScope
}

func NewTag(name string, node yaml.Node, path []string, scope TagScope) *Tag {
return &Tag{name, node, path, scope}
}

func (t *Tag) Name() string {
return t.name
}

func (t *Tag) Node() yaml.Node {
return t.node
}

func (t *Tag) Path() []string {
return t.path
}

func (t *Tag) Scope() TagScope {
return t.scope
}

func (t *Tag) IsLocal() bool {
return t.scope&TAG_LOCAL != 0
}

func (t *Tag) IsStream() bool {
return t.scope&TAG_SCOPE == TAG_SCOPE_STREAM
}

func (t *Tag) IsGlobal() bool {
return t.scope&TAG_SCOPE == TAG_SCOPE_GLOBAL
}

func (t *Tag) ResetLocal() {
if t.IsLocal() {
t.scope &= ^TAG_LOCAL
}
}

type State interface {
GetTempName(data []byte) (string, error)
GetFileContent(file string, cached bool) ([]byte, error)
Expand All @@ -110,6 +27,7 @@ type State interface {
InterpolationEnabled() bool
SetTag(name string, node yaml.Node, path []string, scope TagScope) error
GetTag(name string) *Tag
GetTags(name string) []*TagInfo

EnableInterpolation()
}
Expand Down
12 changes: 12 additions & 0 deletions dynaml/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ var _ = Describe("parsing", func() {
})
})

Describe("tagged expressions", func() {
It("parses tagged function", func() {
parsesAs(
`lib::func(1)`,
CallExpr{
Function: ReferenceExpr{Tag: "lib", Path: []string{"func"}},
Arguments: []Expression{IntegerExpr{Value: 1}},
},
)
})
})

Describe("concatenation", func() {
It("parses adjacent nodes as concatenation", func() {
parsesAs(
Expand Down
74 changes: 57 additions & 17 deletions dynaml/reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,67 @@ func (e ReferenceExpr) Evaluate(binding Binding, locally bool) (interface{}, Eva
fromRoot := e.Path[0] == ""

debug.Debug("reference: (%s)%v\n", e.Tag, e.Path)
sel := func(end int, path []string) (yaml.Node, bool) {
if fromRoot {
start := 0
if e.Path[0] == "" {
start = 1
}
return binding.FindFromRoot(path[start : end+1])
} else {
if tag != nil {
return yaml.Find(tag.Node(), path...)
}
return binding.FindReference(path[:end+1])
}
}

if e.Tag != "" {
info := DefaultInfo()
if e.Tag != "doc:0" {
tag = binding.GetState().GetTag(e.Tag)
if tag == nil {
tags := binding.GetState().GetTags(e.Tag)
if len(tags) == 0 {
return info.Error("tag '%s' not found", e.Tag)
}
if len(e.Path) == 1 && e.Path[0] == "" {
return tag.Node().Value(), info, true
if len(tags) == 1 || tags[0].Name() == e.Tag {
return tags[0].Node().Value(), info, true
}
return info.Error("found multiple tags for '%s': %s", e.Tag, tagList(tags))
}
var val interface{}
var info EvaluationInfo
var found *TagInfo
for _, t := range tags {
tag = t.Tag()
if found != nil && found.Level() < t.Level() {
break
}
val1, info1, ok1 := e.find(sel, binding, locally)
if ok1 {
if tag.Name() == e.Tag {
return val1, info1, ok1
}
if found != nil {
if found.Level() == t.Level() {
return info.Error("ambigious tag resolution for %s: %s <-> %s", e.String(),
found.Name(), t.Name())
}
}
found = t
val = val1
info = info1
}
}
return val, info, found != nil
} else {
if len(e.Path) == 1 && e.Path[0] == "" {
return info.Error("no reference to actual document possible")
}
fromRoot = true
}
}
return e.find(func(end int, path []string) (yaml.Node, bool) {
if fromRoot {
start := 0
if e.Path[0] == "" {
start = 1
}
return binding.FindFromRoot(path[start : end+1])
} else {
if tag != nil {
return yaml.Find(tag.Node(), path...)
}
return binding.FindReference(path[:end+1])
}
}, binding, locally)
return e.find(sel, binding, locally)
}

func (e ReferenceExpr) String() string {
Expand Down Expand Up @@ -103,3 +133,13 @@ func (e ReferenceExpr) find(f func(int, []string) (node yaml.Node, x bool), bind
info.KeyName = step.KeyName()
return value(yaml.ReferencedNode(step)), info, true
}

func tagList(list []*TagInfo) string {
s := ""
sep := ""
for _, l := range list {
s = s + sep + l.Name()
sep = ", "
}
return s
}
Loading

0 comments on commit 96863ab

Please sign in to comment.