aboutsummaryrefslogtreecommitdiff
path: root/model
diff options
context:
space:
mode:
authorNeonXP <i@neonxp.dev>2022-11-16 05:11:19 +0300
committerNeonXP <i@neonxp.dev>2022-11-16 05:11:19 +0300
commita321bfe7b2f6db5078de7b2e5ed5ddcccd65f319 (patch)
treed11c187bceee610a7843463949df128569142680 /model
initial commit
Diffstat (limited to 'model')
-rw-r--r--model/array.go32
-rw-r--r--model/map.go43
-rw-r--r--model/node.go115
-rw-r--r--model/node_test.go108
-rw-r--r--model/query.go33
-rw-r--r--model/types.go20
6 files changed, 351 insertions, 0 deletions
diff --git a/model/array.go b/model/array.go
new file mode 100644
index 0000000..b3d2586
--- /dev/null
+++ b/model/array.go
@@ -0,0 +1,32 @@
+package model
+
+import "fmt"
+
+// Index returns node by index from array
+func (n *Node) Index(idx int) (*Node, error) {
+ arrlen := len(n.arrayValue)
+ if idx >= arrlen {
+ return nil, fmt.Errorf("index %d out of range (len=%d)", idx, arrlen)
+ }
+ return n.arrayValue[idx], nil
+}
+
+// SetIndex sets node to array by index
+func (n *Node) SetIndex(idx int, value *Node) error {
+ arrlen := len(n.arrayValue)
+ if idx >= arrlen {
+ return fmt.Errorf("index %d out of range (len=%d)", idx, arrlen)
+ }
+ n.arrayValue[idx] = value
+ return nil
+}
+
+// Each applies callback to each element of array
+func (n *Node) Each(cb func(idx int, value *Node) error) error {
+ for i, v := range n.arrayValue {
+ if err := cb(i, v); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/model/map.go b/model/map.go
new file mode 100644
index 0000000..59ba2d8
--- /dev/null
+++ b/model/map.go
@@ -0,0 +1,43 @@
+package model
+
+import (
+ "fmt"
+ "strings"
+)
+
+// Get node from object by key
+func (n *Node) Get(key string) (*Node, error) {
+ if n.Type != ObjectNode {
+ return nil, fmt.Errorf("node must be object, got %s", n.Type)
+ }
+ node, ok := n.objectValue[key]
+ if !ok {
+ keys := make([]string, 0, len(n.objectValue))
+ for k := range n.objectValue {
+ keys = append(keys, k)
+ }
+ return nil, fmt.Errorf("field '%s' does not exist in object (keys %s)", key, strings.Join(keys, ", "))
+ }
+ return node, nil
+}
+
+// Set node to object by key
+func (n *Node) Set(key string, value *Node) error {
+ if n.Type != ObjectNode {
+ return fmt.Errorf("node must be object, got %s", n.Type)
+ }
+ n.objectValue[key] = value
+ return nil
+}
+
+// Map callback to each key value pair of object
+func (n *Node) Map(cb func(key string, value *Node) (*Node, error)) error {
+ for k, v := range n.objectValue {
+ newNode, err := cb(k, v)
+ if err != nil {
+ return err
+ }
+ n.objectValue[k] = newNode
+ }
+ return nil
+}
diff --git a/model/node.go b/model/node.go
new file mode 100644
index 0000000..1483eaf
--- /dev/null
+++ b/model/node.go
@@ -0,0 +1,115 @@
+package model
+
+import (
+ "bytes"
+ "fmt"
+ "strconv"
+)
+
+// Node of JSON tree
+type Node struct {
+ Type NodeType
+ stringValue string
+ numberValue float64
+ objectValue NodeObjectValue
+ arrayValue NodeArrayValue
+ booleanValue bool
+}
+
+// NewNode creates new node from value
+func NewNode(value any) *Node {
+ n := new(Node)
+ n.SetValue(value)
+ return n
+}
+
+// Value returns value of node
+func (n *Node) Value() any {
+ switch n.Type {
+ case StringNode:
+ return n.stringValue
+ case NumberNode:
+ return n.numberValue
+ case ObjectNode:
+ return n.objectValue
+ case ArrayNode:
+ return n.arrayValue
+ case BooleanNode:
+ return n.booleanValue
+ default:
+ return nil
+ }
+}
+
+// SetValue to node
+func (n *Node) SetValue(value any) {
+ switch value := value.(type) {
+ case string:
+ n.Type = StringNode
+ n.stringValue = value
+ case float64:
+ n.Type = NumberNode
+ n.numberValue = value
+ case int:
+ n.Type = NumberNode
+ n.numberValue = float64(value)
+ case NodeObjectValue:
+ n.Type = ObjectNode
+ n.objectValue = value
+ case NodeArrayValue:
+ n.Type = ArrayNode
+ n.arrayValue = value
+ case bool:
+ n.Type = BooleanNode
+ n.booleanValue = value
+ default:
+ n.Type = NullNode
+ }
+}
+
+// MarshalJSON to []byte
+func (n *Node) MarshalJSON() ([]byte, error) {
+ switch n.Type {
+ case StringNode:
+ return []byte(`"` + n.stringValue + `"`), nil
+ case NumberNode:
+ return []byte(strconv.FormatFloat(n.numberValue, 'g', -1, 64)), nil
+ case ObjectNode:
+ result := make([][]byte, 0, len(n.objectValue))
+ for k, v := range n.objectValue {
+ b, err := v.MarshalJSON()
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, []byte(fmt.Sprintf("\"%s\": %s", k, b)))
+ }
+ return bytes.Join(
+ [][]byte{
+ []byte("{"),
+ bytes.Join(result, []byte(", ")),
+ []byte("}"),
+ }, []byte("")), nil
+ case ArrayNode:
+ result := make([][]byte, 0, len(n.arrayValue))
+ for _, v := range n.arrayValue {
+ b, err := v.MarshalJSON()
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, b)
+ }
+ return bytes.Join(
+ [][]byte{
+ []byte("["),
+ bytes.Join(result, []byte(", ")),
+ []byte("]"),
+ }, []byte("")), nil
+ case BooleanNode:
+ if n.booleanValue {
+ return []byte("true"), nil
+ }
+ return []byte("false"), nil
+ default:
+ return []byte("null"), nil
+ }
+}
diff --git a/model/node_test.go b/model/node_test.go
new file mode 100644
index 0000000..268afa5
--- /dev/null
+++ b/model/node_test.go
@@ -0,0 +1,108 @@
+package model
+
+import (
+ stdJSON "encoding/json"
+ "reflect"
+ "testing"
+)
+
+func TestNode_MarshalJSON(t *testing.T) {
+ type fields struct {
+ node *Node
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want []byte
+ wantErr bool
+ }{
+ {
+ name: "empty",
+ fields: fields{
+ node: NewNode(nil),
+ },
+ want: []byte(`null`),
+ },
+ {
+ name: "string",
+ fields: fields{
+ node: NewNode("this is a string"),
+ },
+ want: []byte(`"this is a string"`),
+ },
+ {
+ name: "int",
+ fields: fields{
+ node: NewNode(123),
+ },
+ want: []byte(`123`),
+ },
+ {
+ name: "float",
+ fields: fields{
+ node: NewNode(123.321),
+ },
+ want: []byte(`123.321`),
+ },
+ {
+ name: "booleant",
+ fields: fields{
+ node: NewNode(true),
+ },
+ want: []byte(`true`),
+ },
+ {
+ name: "booleanf",
+ fields: fields{
+ node: NewNode(false),
+ },
+ want: []byte(`false`),
+ },
+ {
+ name: "complex",
+ fields: fields{
+ node: NewNode(
+ NodeObjectValue{
+ "string key": NewNode("string value"),
+ "number key": NewNode(1337),
+ "float key": NewNode(123.3),
+ "object key": NewNode(NodeObjectValue{
+ "ab": NewNode("cd"),
+ }),
+ "array key": NewNode(NodeArrayValue{
+ NewNode(1), NewNode(2), NewNode("three"),
+ }),
+ "boolean key": NewNode(true),
+ "null key": NewNode(nil),
+ },
+ ),
+ },
+ want: []byte(
+ `{"string key": "string value", "number key": 1337, "float key": 123.3, "object key": {"ab": "cd"}, "array key": [1, 2, "three"], "boolean key": true, "null key": null}`,
+ ),
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var (
+ gotObj any
+ wantObj any
+ )
+
+ got, err := tt.fields.node.MarshalJSON()
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Node.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ err = stdJSON.Unmarshal(got, &gotObj) // TODO use own unmarshaller
+ if err != nil {
+ t.Errorf("Generated invalid json = %s, error = %v", got, err)
+ }
+ _ = stdJSON.Unmarshal(tt.want, &wantObj) // I belive, test is correct
+ if !reflect.DeepEqual(gotObj, wantObj) {
+ t.Errorf("Node.MarshalJSON() = %s, want %s", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/model/query.go b/model/query.go
new file mode 100644
index 0000000..75ee6b0
--- /dev/null
+++ b/model/query.go
@@ -0,0 +1,33 @@
+package model
+
+import (
+ "fmt"
+ "strconv"
+)
+
+// Query returns node by array query
+func (n *Node) Query(query []string) (*Node, error) {
+ if len(query) == 0 {
+ return n, nil
+ }
+ head, rest := query[0], query[1:]
+ switch n.Type {
+ case ArrayNode:
+ idx, err := strconv.Atoi(head)
+ if err != nil {
+ return nil, fmt.Errorf("index must be a number, got %s", head)
+ }
+ next, err := n.Index(idx)
+ if err != nil {
+ return nil, err
+ }
+ return next.Query(rest)
+ case ObjectNode:
+ next, err := n.Get(head)
+ if err != nil {
+ return nil, err
+ }
+ return next.Query(rest)
+ }
+ return nil, fmt.Errorf("can't get %s from node type %s", head, n.Type)
+}
diff --git a/model/types.go b/model/types.go
new file mode 100644
index 0000000..10e60fa
--- /dev/null
+++ b/model/types.go
@@ -0,0 +1,20 @@
+package model
+
+type NodeType string
+
+const (
+ StringNode NodeType = "string"
+ NumberNode NodeType = "number"
+ ObjectNode NodeType = "object"
+ ArrayNode NodeType = "array"
+ BooleanNode NodeType = "boolean"
+ NullNode NodeType = "null"
+)
+
+type NodeObjectValue map[string]*Node
+
+func (n NodeObjectValue) Set(k string, v any) {
+ n[k] = NewNode(v)
+}
+
+type NodeArrayValue []*Node