diff options
author | NeonXP <i@neonxp.dev> | 2022-11-16 05:11:19 +0300 |
---|---|---|
committer | NeonXP <i@neonxp.dev> | 2022-11-16 05:11:19 +0300 |
commit | a321bfe7b2f6db5078de7b2e5ed5ddcccd65f319 (patch) | |
tree | d11c187bceee610a7843463949df128569142680 /model |
initial commit
Diffstat (limited to 'model')
-rw-r--r-- | model/array.go | 32 | ||||
-rw-r--r-- | model/map.go | 43 | ||||
-rw-r--r-- | model/node.go | 115 | ||||
-rw-r--r-- | model/node_test.go | 108 | ||||
-rw-r--r-- | model/query.go | 33 | ||||
-rw-r--r-- | model/types.go | 20 |
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 |