Skip to content

Commit

Permalink
Better walking and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
themue committed Sep 5, 2023
1 parent 375c8a0 commit 88eb56e
Show file tree
Hide file tree
Showing 6 changed files with 487 additions and 2 deletions.
38 changes: 38 additions & 0 deletions genj/genj.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,42 @@ func SetAny(d *Document, v any, path ...ID) error {
return nil
}

// Create adds a new value at the end if the path. If the second last element of the path
// points to an object or array, the last element will be used as key or index. In case of
// an index multiple number higher than the current length of the array the array will be
// extended with nil values. Is the path is longer than existing entries, the missing ones
// will be created as objects or arrays only containing the keys or indexes pointing to the
// next element.
func Create[V ValueConstraint](d *Document, v V, path ...ID) error {
// Create the path.
id, e, err := create(d.root, path)
if err != nil {
return fmt.Errorf("cannot create element: %v", err)
}
// Check the element.
switch et := e.(type) {
case Object:
if _, ok := et[id]; ok {
return fmt.Errorf("cannot create element: element already exists")
}
et[id] = v
case Array:
i, err := strconv.Atoi(id)
if err != nil {
return fmt.Errorf("cannot create element: %v", err)
}
if i < 0 {
return fmt.Errorf("cannot create element: negative index %d", i)
}
if i >= len(et) {
et = append(et, make([]any, i-len(et)+1)...)
}
if et[i] != nil {
return fmt.Errorf("cannot create element: element already exists")
}
et[i] = v
}
return nil
}

// EOF
30 changes: 28 additions & 2 deletions genj/genj_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Tideland Go Stew - Generic JSON - Unit Tests
// Tideland Go Stew - Generic JSON - Private Unit Tests
//
// Copyright (C) 2019-2023 Frank Mueller / Tideland / Oldenburg / Germany
//
Expand Down Expand Up @@ -269,8 +269,34 @@ func TestSetAny(t *testing.T) {
Assert(t, ErrorContains(err, "current element is not allowed to be an object or array"), "array to int")
}

// TestCreate tests the creation of values in an existing JSON document.
func TestCreate(t *testing.T) {
doc, err := genj.Read(bytes.NewReader(createJSON()))
Assert(t, NoError(err), "document must be read w/o error")
Assert(t, NotNil(doc), "document must exist")

// Valid creation.
err = genj.Create(doc, "new value", "string")
Assert(t, NoError(err), "string must be used")

s, err := genj.Get[string](doc, "string")
Assert(t, NoError(err), "string must be accessible")
Assert(t, Equal(s, "new value"), "string must be set correct")

err = genj.Create(doc, 4711, "nested", "0", "x")
Assert(t, NoError(err), "int must be created")

i, err := genj.Get[int](doc, "nested", "0", "x")
Assert(t, NoError(err), "int must be accessible")
Assert(t, Equal(i, 4711), "int must be correct")

// Invalid creation.
err = genj.Create(doc, 4711, "nested", "0", "d")
Assert(t, ErrorContains(err, "current element is not allowed to be an object or array"), "array to int")
}

//--------------------
// TESTS
// HELPER
//--------------------

// createJSON creates a simple JSON test document as bytes.
Expand Down
129 changes: 129 additions & 0 deletions genj/navigation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,93 @@ package genj // import "tideland.dev/go/stew/genj"
import (
"fmt"
"strconv"

"tideland.dev/go/stew/slices"
)

//--------------------
// DOCUMENT TREE NAVIGATION
//--------------------

// moonwalk walks down the path starting at the given element and
// returns the last ID of the whole path as well as the last element found.
// Next value is the rest of the path if it is longer than one element. In
// case of a non-integer path ID for an array the error is returned.
func moonwalk(start Element, path Path) (ID, Element, Path, error) {
var id ID
// Check basic path length.
switch len(path) {
case 0:
return "", start, nil, fmt.Errorf("path is empty")
case 1:
path, id = slices.InitLast(path)
return id, start, path, nil
}
// Start walking.
id = slices.Last(path)
current := start
for {
// Get head and tail of path.
head, tail := slices.HeadTail(path)
// Walk the path.
switch ct := current.(type) {
case Object:
next, ok := ct[head]
if !ok {
// End of walk but not of path.
return id, ct, path, nil
}
current = next
case Array:
i, err := strconv.Atoi(head)
if err != nil {
return id, current, path, fmt.Errorf("invalid array index %q", head)
}
if i < 0 {
return id, current, path, fmt.Errorf("array index %d out of bounds", i)
}
if i >= len(ct) {
// End of walk but not of path.
return id, ct, path, nil
}
current = ct[i]
default:
return id, nil, path, fmt.Errorf("path %q is illegal", path)
}
// Check if we are done.
if len(tail) == 0 {
return id, current, nil, nil
}
// Continue with the tail.
path = tail
}
}

// contains takes an Element and an ID and checks if the element
// is an Object or Array and contains the ID. In this case the found
// Element and nil are returned, otherwise nil and an error.
func contains(elem Element, id ID) (Element, error) {
switch et := elem.(type) {
case Object:
value, ok := et[id]
if !ok {
return nil, fmt.Errorf("element %q not found", id)
}
return value, nil
case Array:
i, err := strconv.Atoi(id)
if err != nil {
return nil, err
}
if i < 0 || i >= len(et) {
return nil, fmt.Errorf("index %d out of bounds", i)
}
value := et[i]
return value, nil
}
return nil, fmt.Errorf("element is no Object or Array")
}

// walk walks down the path starting at the given element and
// returns the found element as head and all elements on the path
// as tail.
Expand Down Expand Up @@ -70,4 +151,52 @@ func walk(start Element, path Path) (Element, []Element, error) {
return head, tail, nil
}

// create walks down the path starting at the given element and ends with
// the last one or creates the missing elements. It return the last ID and
// the last element.
func create(start Element, path Path) (ID, Element, error) {
// Check the path.
switch len(path) {
case 0:
return "", start, fmt.Errorf("path is empty")
case 1:
return path[0], start, nil
}
// Fetch end of path as ID and dive into the tree until the
// path ended or the document tree ended.
begin, _ := slices.InitLast(path)
ph, pt := slices.HeadTail(begin)
head := start
for {
current := head
switch et := current.(type) {
case Object:
v, ok := et[ph]
if !ok {
break
}
head = v
case Array:
i, err := strconv.Atoi(ph)
if err != nil {
return "", nil, err
}
if i < 0 {
return "", nil, fmt.Errorf("negative array index %d", i)
}
if i >= len(et) {
// Enlarge array and break.
for j := len(et); j <= i; j++ {
et = append(et, nil)
}
head = et
break
}
}
ph, pt = slices.HeadTail(pt)
}
// Now create the missing elements.

}

// EOF
Loading

0 comments on commit 88eb56e

Please sign in to comment.