From a7bed04885c03aa2b793490f676eb9b054d86745 Mon Sep 17 00:00:00 2001 From: Richard Kosegi Date: Thu, 13 Jun 2024 20:58:57 +0200 Subject: [PATCH] New: Path abstraction Signed-off-by: Richard Kosegi --- path/builder.go | 48 +++++++++++++++++++++++++++++ path/builder_test.go | 62 +++++++++++++++++++++++++++++++++++++ path/component.go | 46 ++++++++++++++++++++++++++++ path/path.go | 73 ++++++++++++++++++++++++++++++++++++++++++++ path/types.go | 65 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 294 insertions(+) create mode 100644 path/builder.go create mode 100644 path/builder_test.go create mode 100644 path/component.go create mode 100644 path/path.go create mode 100644 path/types.go diff --git a/path/builder.go b/path/builder.go new file mode 100644 index 0000000..d125f66 --- /dev/null +++ b/path/builder.go @@ -0,0 +1,48 @@ +/* +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 path + +type builder struct { + components []component +} + +func (b *builder) Reset() Builder { + b.components = nil + return b +} + +func (b *builder) Build() Path { + c := make([]component, len(b.components)) + copy(c, b.components) + return &path{components: c} +} + +func (b *builder) Append(opts ...AppendOpt) Builder { + 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 +} + +func NewBuilder() Builder { + return &builder{} +} diff --git a/path/builder_test.go b/path/builder_test.go new file mode 100644 index 0000000..add27e8 --- /dev/null +++ b/path/builder_test.go @@ -0,0 +1,62 @@ +/* +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 path + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBuilder(t *testing.T) { + b := NewBuilder(). + Append(Simple("root")). + Append(Wildcard()). + Append(Numeric(1)). + Append(AfterLast()) + p := b.Build() + + pc := p.Components() + + assert.False(t, p.IsEmpty()) + assert.Equal(t, 4, len(pc)) + assert.Equal(t, "root", pc[0].Value()) + assert.True(t, pc[1].IsWildcard()) + assert.True(t, pc[2].IsNumeric()) + assert.Equal(t, "1", pc[2].Value()) + assert.Equal(t, 1, pc[2].NumericValue()) + assert.True(t, pc[3].IsInsertAfterLast()) + assert.False(t, p.Last().IsNumeric()) + + b.Reset() + assert.True(t, b.Build().IsEmpty()) +} + +func TestBuilderAppendNoOption(t *testing.T) { + defer func() { + recover() + }() + NewBuilder().Append() + assert.Fail(t, "should not be here") +} + +func TestPathGetLastEmpty(t *testing.T) { + defer func() { + recover() + }() + NewBuilder().Build().Last() + assert.Fail(t, "should not be here") +} diff --git a/path/component.go b/path/component.go new file mode 100644 index 0000000..e34aa72 --- /dev/null +++ b/path/component.go @@ -0,0 +1,46 @@ +/* +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 path + +type component struct { + value string + // rfc6901 - pointer after last list item "-" + afterLast bool + wildcard bool + num int + isNumeric bool +} + +func (c component) IsInsertAfterLast() bool { + return c.afterLast +} + +func (c component) IsNumeric() bool { + return c.isNumeric +} + +func (c component) NumericValue() int { + return c.num +} + +func (c component) IsWildcard() bool { + return c.wildcard +} + +func (c component) Value() string { + return c.value +} diff --git a/path/path.go b/path/path.go new file mode 100644 index 0000000..8d18c50 --- /dev/null +++ b/path/path.go @@ -0,0 +1,73 @@ +/* +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 path + +import "strconv" + +type path struct { + components []component +} + +func (p path) Last() Component { + if len(p.components) == 0 { + panic("empty path") + } + return p.components[len(p.components)-1] +} + +func (p path) IsEmpty() bool { + return len(p.components) == 0 +} + +func (p path) Components() []Component { + c := make([]Component, len(p.components)) + for i := range p.components { + c[i] = p.components[i] + } + return c +} + +func AfterLast() AppendOpt { + return func(c *component) { + c.afterLast = true + c.value = "-" + } +} + +func Wildcard() AppendOpt { + return func(c *component) { + c.wildcard = true + c.isNumeric = false + c.afterLast = false + } +} + +func Numeric(val int) AppendOpt { + return func(c *component) { + c.value = strconv.Itoa(val) + c.isNumeric = true + c.wildcard = false + c.afterLast = false + c.num = val + } +} + +func Simple(value string) AppendOpt { + return func(c *component) { + c.value = value + } +} diff --git a/path/types.go b/path/types.go new file mode 100644 index 0000000..3d321d3 --- /dev/null +++ b/path/types.go @@ -0,0 +1,65 @@ +/* +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 path + +// Builder provides convenient way to construct Path using fluent builder pattern. +type Builder interface { + // Append adds path component to the end of path. + Append(opts ...AppendOpt) Builder + + // Reset clears internal state of builder. + // Path instances created using this builder previously are unaffected by this operation. + Reset() Builder + + // Build creates Path using current state. + // It's safe to call this function multiple times or re-use it afterward, + // it will always create fresh Path everytime. + Build() Path +} + +type Component interface { + // IsInsertAfterLast returns true if this path component points to non-existent item after last element in the list. + // This is required by JSON pointer (rfc6901) during append to the end of list. + IsInsertAfterLast() bool + + // IsNumeric returns true if name is numeric value according to rfc6901 + IsNumeric() bool + + // NumericValue gets number that points to array element with the zero-based index. + // Only valid if IsNumeric returns true. + NumericValue() int + + // IsWildcard returns true if this path element represent wildcard match, ie equal to any value. + // This is used e.g. in property paths such as "x.y.z.*.w" + IsWildcard() bool + + // Value returns canonical value of this component. + Value() string +} + +type Path interface { + // Components returns copy of path components in this path + Components() []Component + + // IsEmpty returns true if Path does not have any components. + IsEmpty() bool + + // Last gets very last path Component, panics if path is empty. + Last() Component +} + +type AppendOpt func(*component)