Browse Source

Pushed Repo

Apollo 1 year ago
parent
commit
59a6f83bc2
5 changed files with 558 additions and 1 deletions
  1. 23 1
      README.md
  2. 3 0
      go.mod
  3. 359 0
      node.go
  4. 136 0
      node_test.go
  5. 37 0
      utilities.go

+ 23 - 1
README.md

@@ -1,3 +1,25 @@
 # node
 
-A go structure for handling dynamic JSON.
+A go structure for handling dynamic JSON.
+
+## Feature List
+
+- Primary Goal: Unmarshal JSON into walkable structure(s) for easier access to data. (i.e. no predefining structure(s) or unmarshaling into a map of string interface{} or any)
+- Secondary Goal: Support Marshaling into valid JSON (the order may differ) but remain technically the same JSON payload.
+
+## How it works
+
+1. We unmarshal the JSON into a `map[string]any` (or `map[string]interface{}` as `any` is an alias for `interface{}`)
+2. Then we walk thru each element calling reflect to determine the dynamic type under run-time: \*
+    - Maps/Maplikes (`map[string]any`/`map[string]interface{}`) will be walked thru determining it's dynamic type \*
+    - Arrays/Slices/Arraylikes (`[]any`/`[]interface{}`) will be walked thru determining it's dynamic type \*
+    - Strings/Integers/Floats/Values (`any`/`interface{}`) will be assigned to either a named structure or a un-named structure (un-named are used when the previous depth or "parent" is determined to be Arraylike (Arrays/Slices), while named structures are used when the previous depth or "parent" is determined to be Maplike (Maps))
+3. Once the structures have been populated, all the JSON has technically been walked over, and thus the Node tree or JSON structure is now available.
+
+\* This process is recursive, it will iterate over it's elements determining their type and can possibly cause it to iterate over that.
+
+This concept uses pointers to structures heavily for a few reasons:
+
+- Pointers are lighter than structures (as they are just pointing to them, so they exist in memory only once, with 1 or more pointers to them)
+- Pointers are like C++'s "Reference" (we can modify the "parent" or "children" without fear of copies and other ugly memory issues)
+- So long as there is 1 reference to the pointer, it won't be freed/forgotten (This is quite similar to all the "parent" and "children" being C++ "Smart Pointers" in that they keep a "reference count", when it's 0, then it's released)

+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module git.red-green.com/david/node
+
+go 1.21.0

+ 359 - 0
node.go

@@ -0,0 +1,359 @@
+package node
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+)
+
+type Node struct {
+	name     string
+	data     any
+	parent   *Node
+	children []*Node
+}
+
+func NewNode() *Node {
+	return &Node{}
+}
+
+func NewNodeWithName(name string) *Node {
+	return &Node{name: name}
+}
+
+func NewNodeWithData(data any) *Node {
+	return &Node{data: data}
+}
+
+func NewNodeWithNameAndData(name string, data any) *Node {
+	return &Node{name: name, data: data}
+}
+
+func NewNodeWithDataAndName(data any, name string) *Node {
+	return &Node{name: name, data: data}
+}
+
+func (n *Node) Name(name ...string) string {
+	if len(name) != 0 {
+		n.name = name[0]
+	}
+	return n.name
+}
+
+func (n *Node) Data(data ...any) any {
+	if len(data) != 0 {
+		n.data = data[0]
+	}
+	return n.data
+}
+
+func (n *Node) Len() int {
+	return len(n.children)
+}
+
+func (n *Node) Parent(parent ...*Node) *Node {
+	if len(parent) != 0 {
+		n.parent = parent[0]
+	}
+	return n.parent
+}
+
+func (n *Node) Root() *Node {
+	if n.Parent() == nil {
+		return n
+	}
+	at := n.Parent()
+	for at.Parent() != nil {
+		at = at.Parent()
+	}
+	return at
+}
+
+func (n *Node) Depth() int {
+	if n.Parent() == nil {
+		return 0
+	}
+	var (
+		depth int   = 1
+		at    *Node = n.Parent()
+	)
+	for at.Parent() != nil {
+		depth++
+		at = at.Parent()
+	}
+	return depth
+}
+
+func (n *Node) Kids() []*Node {
+	return n.children
+}
+
+func (n *Node) AddKid(kid *Node) bool {
+	for _, k := range n.children {
+		if k == kid {
+			return false
+		}
+	}
+	kid.Parent(n)
+	n.children = append(n.children, kid)
+	return true
+}
+
+func (n *Node) NewKid() *Node {
+	k := &Node{}
+	n.AddKid(k)
+	return k
+}
+
+func (n *Node) NewKidWithName(name string) *Node {
+	k := n.NewKid()
+	k.Name(name)
+	return k
+}
+
+func (n *Node) NewKidWithData(data any) *Node {
+	k := n.NewKid()
+	k.Data(data)
+	return k
+}
+
+func (n *Node) NewKidWithNameAndData(name string, data any) *Node {
+	k := n.NewKid()
+	k.Name(name)
+	k.Data(data)
+	return k
+}
+
+func (n *Node) NewKidWithDataAndName(data any, name string) *Node {
+	k := n.NewKid()
+	k.Name(name)
+	k.Data(data)
+	return k
+}
+
+func (n *Node) Index() int {
+	if n.Parent() == nil { // There is no parent, so it's not known
+		return -1
+	}
+	// Iterate thru the children of the parent to find ourselves
+	for i, kid := range n.Parent().Kids() {
+		if kid == n {
+			return i
+		}
+	}
+	// Ok we for some reason are not in that list, so it's not known
+	return -1
+}
+
+func (n *Node) Kid(index int) *Node {
+	if n == nil || index > n.Len() || index < 0 {
+		return nil
+	}
+	return n.children[index]
+}
+
+func (n *Node) KidByName(name string, recurse ...bool) *Node {
+	for _, kid := range n.children {
+		if kid.name == name {
+			return kid
+		}
+	}
+	if len(recurse) != 0 {
+		if recurse[0] {
+			for _, kid := range n.children {
+				r := kid.KidByName(name, recurse...)
+				if r != nil {
+					return r
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (n *Node) RemoveKid(index int) bool {
+	if index > n.Len() || index < 0 {
+		return false
+	}
+	n.children[index].Parent(nil)
+	_, ok := arrayDelete(&n.children, index)
+	return ok
+}
+
+func (n *Node) RemoveAllKids() {
+	for _, kid := range n.children {
+		kid.Parent(nil)
+	}
+	n.children = []*Node{}
+}
+
+func (n *Node) Detach() bool {
+	if n.Parent() != nil {
+		return false
+	}
+	index := n.Index()
+	if index == -1 {
+		return false
+	}
+	return n.Parent().RemoveKid(index)
+}
+
+func (n *Node) Destroy() {
+	if n.Parent() != nil {
+		n.Detach()
+	}
+	n.Data(nil)
+	n.Name("")
+	for _, kid := range n.children {
+		kid.Destroy()
+	}
+}
+
+// Checks if the children of this node are better represented as a map or an array
+//
+// This is determined by:
+//
+// * Totaling number of children without names
+//
+// * Comparing the percent of children without names to total children (if less than 50% use a map, if more than 50% use an array)
+func (n *Node) IsMapLike() bool {
+	if n.Len() == 0 {
+		return false
+	}
+	var (
+		total  int = n.Len() // number of children
+		noname int = 0       // number of children with no name
+	)
+	for _, kid := range n.Kids() {
+		if len(kid.Name()) == 0 { // When it has no name it's not map-like
+			noname += 1
+		}
+	}
+	// Form a percent of no name children and compare it to less than 50%
+	return int((float32(noname)/float32(total))*100.0) <= 50
+}
+
+func (n *Node) from(A any) {
+	switch reflect.TypeOf(A).Kind() {
+	case reflect.Map:
+		// Ok, key, value
+		// key == name, value if not a map, array, slice is data
+		m := A.(map[string]any)
+		for k, v := range m {
+			switch reflect.TypeOf(v).Kind() {
+			case reflect.Map, reflect.Array, reflect.Slice:
+				// New kid, with name, and recurse
+				kid := n.NewKidWithName(k)
+				kid.from(v)
+			default:
+				// New kid, with name, and data
+				n.NewKidWithNameAndData(k, v)
+			}
+		}
+	case reflect.Array, reflect.Slice:
+		// Ok, values
+		// no name (use index), value if not a map, array, slice is data
+		a := A.([]any)
+		for idx, v := range a {
+			switch reflect.TypeOf(v).Kind() {
+			case reflect.Map, reflect.Array, reflect.Slice:
+				kid := n.NewKidWithName(fmt.Sprintf("%d", idx))
+				kid.from(v)
+			default:
+				n.NewKidWithNameAndData(fmt.Sprintf("%d", idx), v)
+			}
+		}
+	default:
+		// Ok it's just a single value (most likely)
+		n.Data(A)
+	}
+}
+
+func (n *Node) ToMap() any {
+	if len(n.Name()) != 0 && n.Len() != 0 { // Name + Children
+		if n.IsMapLike() {
+			// Map
+			m := map[string]any{}
+			for i, kid := range n.children {
+				if len(kid.Name()) != 0 {
+					m[kid.Name()] = kid.ToMap()
+				} else {
+					m[fmt.Sprint(i)] = kid.ToMap()
+				}
+			}
+			if n.Depth() != 0 {
+				return m
+			} else {
+				return map[string]any{
+					n.Name(): m,
+				}
+			}
+		} else {
+			// Array
+			a := []any{}
+			for _, kid := range n.children {
+				a = append(a, kid.ToMap())
+			}
+			if n.Depth() != 0 {
+				return a
+			} else {
+				return map[string]any{
+					n.Name(): a,
+				}
+			}
+		}
+	} else if len(n.Name()) == 0 && n.Len() != 0 { // No Name + Children
+		if n.IsMapLike() {
+			// Map
+			m := map[string]any{}
+			for i, kid := range n.children {
+				if len(kid.Name()) != 0 {
+					m[kid.Name()] = kid.ToMap()
+				} else {
+					m[fmt.Sprint(i)] = kid.ToMap()
+				}
+			}
+			return m
+		} else {
+			// Array
+			a := []any{}
+			for _, kid := range n.children {
+				a = append(a, kid.ToMap())
+			}
+			if n.Depth() != 0 {
+				return a
+			} else {
+				return map[string]any{
+					fmt.Sprint(n.Index()): a,
+				}
+			}
+		}
+	} else if n.Data() != nil {
+		if n.Depth() != 0 || len(n.Name()) == 0 {
+			return n.Data()
+		} else {
+			return map[string]any{
+				n.Name(): n.Data(),
+			}
+		}
+	}
+	return nil
+}
+
+// Marshals the results from ToMap
+func (n *Node) MarshalJSON() ([]byte, error) {
+	return json.Marshal(n.ToMap())
+}
+
+// Unmarshal function
+func (n *Node) UnmarshalJSON(pay []byte) error {
+	var m map[string]any
+	err := json.Unmarshal(pay, &m)
+	if err != nil {
+		return err
+	}
+	n.Destroy()
+	// In this case, the struct isn't in a invalid state, it's just a reset
+	n.from(m)
+	return nil
+}

+ 136 - 0
node_test.go

@@ -0,0 +1,136 @@
+package node_test
+
+import (
+	"testing"
+
+	"git.red-green.com/david/node"
+)
+
+func TestNewNode(t *testing.T) {
+	n := node.NewNode()
+	if n.Name() != "" || n.Data() != nil || n.Parent() != nil || n.Len() != 0 {
+		t.Fail()
+		t.Logf("NewNode should return an empty struct, not %#v", n)
+	}
+	n = node.NewNodeWithName("taco")
+	if n.Name() != "taco" || n.Data() != nil || n.Parent() != nil || n.Len() != 0 {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("NewNodeWithName should set name, not %#v", n)
+	}
+	n = node.NewNodeWithData("taco")
+	if n.Name() != "" || n.Data() != "taco" || n.Parent() != nil || n.Len() != 0 {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("NewNodeWithData should set data, not %#v", n)
+	}
+	n = node.NewNodeWithNameAndData("taco", 2)
+	if n.Name() != "taco" || n.Data() != 2 || n.Parent() != nil || n.Len() != 0 {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("NewNodeWithNameAndData should set name and data, not %#v", n)
+	}
+	n = node.NewNodeWithDataAndName(3, "taco")
+	if n.Name() != "taco" || n.Data() != 3 || n.Parent() != nil || n.Len() != 0 {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("NewNodeWithDataAndName should set name and data, not %#v", n)
+	}
+}
+
+func TestNameAndData(t *testing.T) {
+	n := node.NewNodeWithNameAndData("taco", 1)
+	if n.Name() != "taco" || n.Data() != 1 {
+		t.Fail()
+		t.Logf("NewNodeWithNameAndData should set name and data, not %#v", n)
+	}
+	n.Name("cat")
+	if n.Name() != "cat" || n.Data() != 1 {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("Name should set name, not %#v", n)
+	}
+	n.Data(2)
+	if n.Name() != "cat" || n.Data() != 2 {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("Data should set data, not %#v", n)
+	}
+}
+
+func TestNodeTree1(t *testing.T) {
+	n := node.NewNodeWithName("root")
+	n.NewKidWithNameAndData("kid1", true)
+	k2 := n.NewKidWithName("kid2")
+	k2.NewKidWithNameAndData("kid2-sub1", 1)
+	k2.NewKidWithNameAndData("kid2-sub2", 2.5)
+	k3 := n.NewKidWithName("kid3")
+	k3.NewKidWithNameAndData("kid3-sub1", '\u003f') // ?
+
+	if n.Len() != 3 {
+		t.Fail()
+		t.Logf("Len should return 3, got %d from %#v", n.Len(), n)
+	}
+	if n.Kid(0).Len() != 0 {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("Kid1 should have no children, got %d from %#v", n.Kid(0).Len(), n.Kid(0))
+	}
+	if n.Kid(1).Len() != 2 {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("Kid2 should have 2 children, got %d from %#v", n.Kid(1).Len(), n.Kid(1))
+	}
+	if n.Kid(2).Len() != 1 {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("Kid3 should have 1 child, got %d from %#v", n.Kid(2).Len(), n.Kid(2))
+	}
+
+	if n.Kid(0).Data() != true {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("Kid1 should have data value true, got %#v from %#v", n.Kid(0).Data(), n.Kid(0))
+	}
+	if n.Kid(1).Data() != nil {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("Kid2 should have no data thus nil, got %#v from %#v", n.Kid(1).Data(), n.Kid(1))
+	}
+	if n.Kid(2).Data() != nil {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("Kid3 should have no data thus nil, got %#v from %#v", n.Kid(2).Data(), n.Kid(2))
+	}
+
+	if n.Kid(1).Kid(0).Name() != "kid2-sub1" {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("kid2-sub1 has invalid name, got '%s' from %#v", n.Kid(1).Kid(0).Name(), n.Kid(1).Kid(0))
+	}
+	if n.Kid(2).Kid(0).Name() != "kid3-sub1" {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("kid3-sub1 has invalid name, got '%s' from %#v", n.Kid(2).Kid(0).Name(), n.Kid(2).Kid(0))
+	}
+	if n.Kid(1).Kid(1).Name() != "kid2-sub2" {
+		if !t.Failed() {
+			t.Fail()
+		}
+		t.Logf("kid2-sub2 has invalid name, got '%s' from %#v", n.Kid(1).Kid(1).Name(), n.Kid(1).Kid(1))
+	}
+}

+ 37 - 0
utilities.go

@@ -0,0 +1,37 @@
+package node
+
+// Delete an item from an array, returning item and true on success.
+func arrayDelete[T any](stack *[]T, pos int) (T, bool) {
+	// log.Printf("Array %d, %d\n", len(*stack), cap(*stack))
+	var result T
+	/*
+	   https://stackoverflow.com/questions/33834742/remove-and-adding-elements-to-array-in-go-lang
+	   https://github.com/golang/go/wiki/SliceTricks
+	*/
+	if pos < 0 || pos > len(*stack) {
+		return result, false
+	}
+	result = (*stack)[pos]
+	copy((*stack)[pos:], (*stack)[pos+1:])
+	// var temp T
+	// (*stack)[len(*stack)-1] = temp
+	*stack = (*stack)[:len(*stack)-1]
+	return result, true
+}
+
+// Pop items from head of array, return true on success.
+func arrayPop[T any](stack *[]T, count int) bool {
+	/*
+	   https://stackoverflow.com/questions/33834742/remove-and-adding-elements-to-array-in-go-lang
+	   https://github.com/golang/go/wiki/SliceTricks
+	*/
+	if count < 0 || count > len(*stack) {
+		return false
+	}
+
+	copy((*stack)[0:], (*stack)[count:])
+	// var temp T
+	// (*stack)[len(*stack)-1] = temp
+	*stack = (*stack)[:len(*stack)-count]
+	return true
+}