Skip to content

Commit

Permalink
Path: Add new interfaces
Browse files Browse the repository at this point in the history
Also expose component builder to outside

Signed-off-by: Richard Kosegi <richard.kosegi@gmail.com>
  • Loading branch information
rkosegi committed Jul 4, 2024
1 parent 074b38f commit 86595b4
Show file tree
Hide file tree
Showing 14 changed files with 482 additions and 81 deletions.
55 changes: 53 additions & 2 deletions dom/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package dom

import (
"fmt"
"github.com/rkosegi/yaml-toolkit/path"
"github.com/rkosegi/yaml-toolkit/utils"
"gopkg.in/yaml.v3"
"io"
Expand Down Expand Up @@ -70,8 +71,7 @@ func flattenContainer(node Container, path string, ret *map[string]Leaf) {

func (c *containerImpl) Flatten() map[string]Leaf {
ret := make(map[string]Leaf)
path := ""
flattenContainer(c, path, &ret)
flattenContainer(c, "", &ret)
return ret
}

Expand Down Expand Up @@ -146,6 +146,25 @@ func (c *containerImpl) Serialize(writer io.Writer, mappingFunc NodeMappingFunc,
return encFn(writer, mappingFunc(c))
}

func (c *containerImpl) Get(path path.Path) Node {
if path.IsEmpty() {
return nil
}
c.ensureChildren()
var current Container
current = c
pc := path.Components()
for _, p := range pc[0 : len(pc)-1] {
x := current.Child(p.Value())
if x == nil || !x.IsContainer() {
return nil
} else {
current = x.(Container)
}
}
return current.Child(path.Last().Value())
}

func (c *containerImpl) Lookup(path string) Node {
if path == "" {
return nil
Expand Down Expand Up @@ -268,11 +287,43 @@ func (c *containerBuilderImpl) ancestorOf(path string, create bool) (ContainerBu
return node, cp[len(cp)-1]
}

// TODO
func (c *containerBuilderImpl) ancestorPath(path path.Path, create bool) (ContainerBuilder, string) {
c.ensureChildren()
var node ContainerBuilder
node = c
pc := path.Components()
for _, p := range pc[0 : len(pc)-1] {
x := node.Child(p.Value())
if x == nil || !x.IsContainer() {
if create {
node = c.addChild(node, p.Value())
} else {
return nil, ""
}
} else {
node = x.(ContainerBuilder)
}
}
return node, path.Last().Value()
}

func (c *containerBuilderImpl) Set(path path.Path, value Node) {
node, p := c.ancestorPath(path, true)
node.AddValue(p, value)
}

func (c *containerBuilderImpl) AddValueAt(path string, value Node) {
node, p := c.ancestorOf(path, true)
node.AddValue(p, value)
}

func (c *containerBuilderImpl) Delete(path path.Path) {
if node, p := c.ancestorPath(path, false); node != nil {
node.Remove(p)
}
}

func (c *containerBuilderImpl) RemoveAt(path string) {
if node, p := c.ancestorOf(path, false); node != nil {
node.Remove(p)
Expand Down
30 changes: 30 additions & 0 deletions dom/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package dom

import (
"bytes"
"github.com/rkosegi/yaml-toolkit/path"
"github.com/stretchr/testify/assert"
"os"
"slices"
Expand Down Expand Up @@ -120,6 +121,35 @@ func TestLookup(t *testing.T) {
assert.Equal(t, "leaf1", doc.Lookup("level1.level2a.level3a").(Leaf).Value())
}

func TestGet(t *testing.T) {
data, err := os.ReadFile("../testdata/doc1.yaml")
assert.Nil(t, err)
doc, err := b.FromReader(bytes.NewReader(data), DefaultYamlDecoder)
assert.Nil(t, err)

assert.NotNil(t, doc.Get(path.NewBuilder().Append(path.Simple("level1")).Build()))
assert.Nil(t, doc.Get(path.NewBuilder().Append(path.Simple("level1a")).Build()))
assert.Nil(t, doc.Get(path.NewBuilder().Build()))
assert.Nil(t, doc.Get(path.NewBuilder().
Append(path.Simple("level1")).
Append(path.Simple("level2b")).
Append(path.Simple("level3")).
Build()))
assert.Equal(t, "leaf1", doc.Get(path.NewBuilder().
Append(path.Simple("level1")).
Append(path.Simple("level2a")).
Append(path.Simple("level3a")).
Build()).(Leaf).Value())
}

func TestGetWithDot(t *testing.T) {
c := b.Container()
c.AddValue("application.yaml", LeafNode("/tmp/1.yaml"))
assert.Equal(t, "/tmp/1.yaml", c.Get(path.NewBuilder().Append(
path.Simple("application.yaml")).
Build()).(Leaf).Value())
}

func TestFlatten(t *testing.T) {
data, err := os.ReadFile("../testdata/doc1.yaml")
assert.Nil(t, err)
Expand Down
15 changes: 13 additions & 2 deletions dom/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package dom
import (
"encoding/json"
"github.com/google/go-cmp/cmp"
"github.com/rkosegi/yaml-toolkit/path"
"github.com/rkosegi/yaml-toolkit/utils"
"gopkg.in/yaml.v3"
"io"
Expand Down Expand Up @@ -116,6 +117,8 @@ type Container interface {
Child(name string) Node
// Lookup attempts to find child Node at given path
Lookup(path string) Node
// Get attempts to find child Node at given path.Path
Get(path path.Path) Node
// Flatten flattens this Container into list of leaves
Flatten() map[string]Leaf
// Search finds all paths where Node's value is equal to given value according to provided SearchValueFunc.
Expand All @@ -126,18 +129,26 @@ type Container interface {
// ContainerBuilder is mutable extension of Container
type ContainerBuilder interface {
Container
// AddValue adds Node value into this Container
// AddValue adds Node value into this Container.
// Deprecated. Use Set.
AddValue(name string, value Node)
// AddValueAt adds Leaf value into this Container at given path.
// AddValueAt adds Node value into this Container at given path.
// Child nodes are creates as needed.
// Deprecated. Use Set.
AddValueAt(path string, value Node)
// Set sets Node into this Container at given path.
// Child nodes are creates as needed.
Set(path path.Path, value Node)
// Delete removes Node at given path.
Delete(path path.Path)
// AddContainer adds child Container into this Container
AddContainer(name string) ContainerBuilder
// AddList adds child List into this Container
AddList(name string) ListBuilder
// Remove removes direct child Node.
Remove(name string)
// RemoveAt removes child Node at given path.
// Deprecated. Use Delete.
RemoveAt(path string)
// Walk walks whole document tree, visiting every node
Walk(fn WalkFn)
Expand Down
21 changes: 21 additions & 0 deletions patch/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package patch
import (
"fmt"
"github.com/rkosegi/yaml-toolkit/dom"
"github.com/rkosegi/yaml-toolkit/path"
"strconv"
"strings"
)
Expand All @@ -31,6 +32,7 @@ func (ps PathSegment) IsNumeric() (int, bool) {
return i, err == nil
}

// Deprecated: migrate to path.Path
type Path []PathSegment

func (p Path) Parent() Path {
Expand Down Expand Up @@ -172,3 +174,22 @@ func (p Path) Eval(target dom.Container) (dom.NodeList, dom.Node) {
}
return res, curr
}

type parserAdapter struct {
}

func (pa parserAdapter) Parse(in string) (path.Path, error) {
p, err := ParsePath(in)
if err != nil {
return nil, err
}
b := path.NewBuilder()
for _, pc := range p {
b.Append(path.Simple(string(pc)))
}
return b.Build(), nil
}

func NewPathParser() path.Parser {
return parserAdapter{}
}
17 changes: 17 additions & 0 deletions patch/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package patch

import (
"github.com/rkosegi/yaml-toolkit/dom"
"github.com/rkosegi/yaml-toolkit/path"
"github.com/stretchr/testify/assert"
"testing"
)
Expand Down Expand Up @@ -140,3 +141,19 @@ func TestPathLastSegment(t *testing.T) {
p, _ = ParsePath("")
assert.Equal(t, "", string(p.LastSegment()))
}

func TestNewPathParser(t *testing.T) {
var (
p path.Path
err error
)
p, err = NewPathParser().Parse("/x/y/z")
assert.NoError(t, err)
assert.NotNil(t, p)
assert.Equal(t, 3, len(p.Components()))
assert.Equal(t, "z", p.Last().Value())

p, err = NewPathParser().Parse("invalid")
assert.Error(t, err)
assert.Nil(t, p)
}
17 changes: 13 additions & 4 deletions path/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,26 @@ func (b *builder) Build() Path {
}

func (b *builder) Append(opts ...AppendOpt) Builder {
b.components = append(b.components, *buildComponent(opts...))
return b
}

// NewBuilder creates new Builder
func NewBuilder() Builder {
return &builder{}
}

func buildComponent(opts ...AppendOpt) *component {
if len(opts) == 0 {
panic("no append option provided by caller")
}
c := &component{}
for _, opt := range opts {
opt(c)
}
b.components = append(b.components, *c)
return b
return c
}

func NewBuilder() Builder {
return &builder{}
func BuildComponent(opts ...AppendOpt) Component {
return buildComponent(opts...)
}
2 changes: 1 addition & 1 deletion path/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestBuilderAppendNoOption(t *testing.T) {
defer func() {
recover()
}()
NewBuilder().Append()
BuildComponent()
assert.Fail(t, "should not be here")
}

Expand Down
12 changes: 12 additions & 0 deletions path/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,15 @@ type Path interface {
}

type AppendOpt func(*component)

// Parser interface is implemented by different Path syntax parsers.
type Parser interface {
// Parse parses source string into Path
Parse(string) (Path, error)
}

// Serializer is interface that allows to serialize Path into lexical form
type Serializer interface {
// Serialize serializes path into lexical representation
Serialize(Path) string
}
86 changes: 86 additions & 0 deletions pipeline/path_support.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
Copyright 2024 Richard Kosegi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package pipeline

import (
"errors"
"fmt"
"github.com/rkosegi/yaml-toolkit/patch"
"github.com/rkosegi/yaml-toolkit/path"
"github.com/rkosegi/yaml-toolkit/props"
"gopkg.in/yaml.v3"
"reflect"
)

type PathSyntax string

const (
// PathSyntaxProps is using syntax of Java properties
PathSyntaxProps = PathSyntax("properties")
// PathSyntaxJsonPointer syntax according to https://datatracker.ietf.org/doc/html/rfc6901
PathSyntaxJsonPointer = PathSyntax("rfc6901")
)

var pathParsersMap = map[PathSyntax]path.Parser{
PathSyntaxProps: props.NewPathParser(),
PathSyntaxJsonPointer: patch.NewPathParser(),
}

type UniversalPath struct {
Value path.Path
Syntax PathSyntax
}

func (u *UniversalPath) UnmarshalYAML(node *yaml.Node) error {
var (
val string
ok bool
x interface{}
)
u.Syntax = PathSyntaxProps

switch node.Kind {
case yaml.ScalarNode:
val = node.Value
case yaml.MappingNode:
m := make(map[string]interface{})
// TODO: how to provoke error from this call?
_ = node.Decode(&m)
if x, ok = m["value"]; !ok {
return errors.New("missing 'value' field under path")
}
if val, ok = x.(string); !ok {
return fmt.Errorf("'value' field is not a string (actual type: %v)", reflect.TypeOf(x))
}
if syn, ok := m["syntax"]; ok {
u.Syntax = PathSyntax(syn.(string))
}
default:
return fmt.Errorf("node kind is not supported: %v", node.Kind)
}

if pf, ok := pathParsersMap[u.Syntax]; ok {
p, err := pf.Parse(val)
if err != nil {
return err
}
u.Value = p
} else {
return fmt.Errorf("unrecognized path syntax: %s", u.Syntax)
}
return nil
}
Loading

0 comments on commit 86595b4

Please sign in to comment.