Better interface

This commit is contained in:
NeonXP 2022-11-21 04:31:56 +03:00
parent 4934a51c69
commit 4054a50ce4
No known key found for this signature in database
GPG key ID: B0DA6283C40CB2CB
18 changed files with 203 additions and 370 deletions

View file

@ -20,61 +20,4 @@ func Query(json string, query string) (*model.Node, error)
func QueryArray(json string, query []string) (*model.Node, error)
```
## Node methods
```go
package model // import "go.neonxp.dev/json/model"
// Node of JSON tree
type Node struct {
Type NodeType
}
// NewNode creates new node from value
func NewNode(value any) *Node
// Get node from object by key
func (n *Node) Get(key string) (*Node, error)
// Index returns node by index from array
func (n *Node) Index(idx int) (*Node, error)
// Set node to object by key
func (n *Node) Set(key string, value *Node) error
// SetIndex sets node to array by index
func (n *Node) SetIndex(idx int, value *Node) error
// SetValue to node
func (n *Node) SetValue(value any)
// Map callback to each key value pair of object
func (n *Node) Map(cb func(key string, value *Node) (*Node, error)) error
// Each applies callback to each element of array
func (n *Node) Each(cb func(idx int, value *Node) error) error
// Query returns node by array query
func (n *Node) Query(query []string) (*Node, error)
// Value returns value of node
func (n *Node) Value() any
// MarshalJSON to []byte
func (n *Node) MarshalJSON() ([]byte, error)
// Merge two object or array nodes
func (n *Node) Merge(node *Node) error
// Len returns length of object or array nodes
func (n *Node) Len() (int, error)
// Compare current node with another node
func (n *Node) Compare(op Operand, node *Node) bool
// Remove by key from object
func (n *Node) Remove(key string) error
// RemoveIndex from array
func (n *Node) RemoveIndex(idx int) error
```
Other methods: https://pkg.go.dev/go.neonxp.dev/json

12
json.go
View file

@ -8,29 +8,29 @@ import (
)
// Marshal Node tree to []byte
func Marshal(node *model.Node) ([]byte, error) {
func Marshal(node model.Node) ([]byte, error) {
return node.MarshalJSON()
}
// Unmarshal data to Node tree
func Unmarshal(data []byte) (*model.Node, error) {
func Unmarshal(data []byte) (model.Node, error) {
return parser.Parse(string(data))
}
// Query returns node by query string (dot notation)
func Query(json string, query string) (*model.Node, error) {
func Query(json string, query string) (model.Node, error) {
n, err := parser.Parse(json)
if err != nil {
return nil, err
}
return n.Query(strings.Split(query, "."))
return model.Query(n, strings.Split(query, "."))
}
// QueryArray returns node by array query
func QueryArray(json string, query []string) (*model.Node, error) {
func QueryArray(json string, query []string) (model.Node, error) {
n, err := parser.Parse(json)
if err != nil {
return nil, err
}
return n.Query(query)
return model.Query(n, query)
}

View file

@ -15,7 +15,7 @@ func TestQuery(t *testing.T) {
tests := []struct {
name string
args args
want *model.Node
want model.Node
wantErr bool
}{
{

View file

@ -1,42 +0,0 @@
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
}
// RemoveIndex from array
func (n *Node) RemoveIndex(idx int) error {
arrlen := len(n.ArrayValue)
if idx >= arrlen {
return fmt.Errorf("index %d out of range (len=%d)", idx, arrlen)
}
n.ArrayValue = append(n.ArrayValue[:idx], n.ArrayValue[idx:]...)
return nil
}

48
model/arrayNode.go Normal file
View file

@ -0,0 +1,48 @@
package model
import (
"bytes"
"fmt"
)
type ArrayNode struct {
Value NodeArrayValue
}
func (n ArrayNode) Type() NodeType {
return ArrayType
}
func (n *ArrayNode) MarshalJSON() ([]byte, error) {
result := make([][]byte, 0, len(n.Value))
for _, v := range n.Value {
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
}
func (n *ArrayNode) Index(idx int) (Node, error) {
if len(n.Value) <= idx {
return nil, fmt.Errorf("index %d out of range [0...%d]", idx, len(n.Value)-1)
}
return n.Value[idx], nil
}
func (n *ArrayNode) Merge(n2 *ArrayNode) {
n.Value = append(n.Value, n2.Value...)
}
func (n *ArrayNode) Len() int {
return len(n.Value)
}
type NodeArrayValue []Node

16
model/booleanNode.go Normal file
View file

@ -0,0 +1,16 @@
package model
type BooleanNode struct {
Value bool
}
func (n BooleanNode) Type() NodeType {
return BooleanType
}
func (n *BooleanNode) MarshalJSON() ([]byte, error) {
if n.Value {
return []byte("true"), nil
}
return []byte("false"), nil
}

View file

@ -1,55 +0,0 @@
package model
// Compare current node with another node
func (n *Node) Compare(op Operand, node *Node) bool {
switch op {
case OpEq:
return n.Value() == node.Value()
case OpNeq:
return n.Value() != node.Value()
case OpLess:
return less(n, node)
case OpGt:
return less(node, n)
case OpLessEq:
return less(n, node) || n.Value() == node.Value()
case OpGtEq:
return less(node, n) || n.Value() == node.Value()
case OpIn:
if n.Type != ArrayNode {
return false
}
for _, v := range n.ArrayValue {
if v.Value() == node.Value() {
return true
}
}
}
return false
}
func less(n1 *Node, n2 *Node) bool {
if n1.Type != n2.Type {
return false
}
switch n1.Type {
case NumberNode:
return n1.NumberValue < n2.NumberValue
case StringNode:
return n1.StringValue < n2.StringValue
default:
return false
}
}
type Operand int
const (
OpEq Operand = iota
OpNeq
OpLess
OpLessEq
OpGt
OpGtEq
OpIn
)

View file

@ -1,52 +0,0 @@
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
}
// Remove by key from object
func (n *Node) Remove(key string) error {
if n.Type != ObjectNode {
return fmt.Errorf("node must be object, got %s", n.Type)
}
delete(n.ObjectValue, key)
return nil
}

View file

@ -1,154 +1,39 @@
package model
import (
"bytes"
"fmt"
"strconv"
)
// Node of JSON tree
type Node struct {
Type NodeType
Meta NodeObjectValue
StringValue string
NumberValue float64
ObjectValue NodeObjectValue
ArrayValue NodeArrayValue
BooleanValue bool
type Node interface {
Type() NodeType
MarshalJSON() ([]byte, error)
}
// 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) {
func NewNode(value any) Node {
switch value := value.(type) {
case string:
n.Type = StringNode
n.StringValue = value
return &StringNode{
Value: value,
}
case float64:
n.Type = NumberNode
n.NumberValue = value
return &NumberNode{
Value: value,
}
case int:
n.Type = NumberNode
n.NumberValue = float64(value)
return &NumberNode{
Value: float64(value),
}
case NodeObjectValue:
n.Type = ObjectNode
meta, hasMeta := value["@"]
if hasMeta {
n.Meta = meta.ObjectValue
delete(value, "@")
return &ObjectNode{
Value: value,
}
n.ObjectValue = value
case NodeArrayValue:
n.Type = ArrayNode
n.ArrayValue = value
return &ArrayNode{
Value: value,
}
case bool:
n.Type = BooleanNode
n.BooleanValue = value
return &BooleanNode{
Value: value,
}
default:
n.Type = NullNode
return 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
}
}
// Merge two object or array nodes
func (n *Node) Merge(node *Node) error {
if n.Type != node.Type {
return fmt.Errorf("can't merge nodes of different types")
}
switch n.Type {
case ObjectNode:
for k, v := range node.ObjectValue {
n.ObjectValue[k] = v
}
case ArrayNode:
n.ArrayValue = append(n.ArrayValue, node.ArrayValue...)
default:
return fmt.Errorf("merge not implemented for type %s", n.Type)
}
return nil
}
// Len returns length of object or array nodes
func (n *Node) Len() (int, error) {
switch n.Type {
case ObjectNode:
return len(n.ObjectValue), nil
case ArrayNode:
return len(n.ArrayValue), nil
default:
return 0, fmt.Errorf("merge not implemented for type %s", n.Type)
}
}
// Meta represents node metadata
type Meta map[string]any

View file

@ -8,7 +8,7 @@ import (
func TestNode_MarshalJSON(t *testing.T) {
type fields struct {
node *Node
node Node
}
tests := []struct {
name string

11
model/nullNode.go Normal file
View file

@ -0,0 +1,11 @@
package model
type NullNode struct{}
func (n NullNode) Type() NodeType {
return NullType
}
func (n NullNode) MarshalJSON() ([]byte, error) {
return []byte("null"), nil
}

15
model/numberNode.go Normal file
View file

@ -0,0 +1,15 @@
package model
import "strconv"
type NumberNode struct {
Value float64
}
func (n NumberNode) Type() NodeType {
return NumberType
}
func (n *NumberNode) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatFloat(n.Value, 'g', -1, 64)), nil
}

53
model/objectNode.go Normal file
View file

@ -0,0 +1,53 @@
package model
import (
"bytes"
"fmt"
)
type ObjectNode struct {
Value map[string]Node
}
func (n ObjectNode) Type() NodeType {
return ObjectType
}
func (n *ObjectNode) MarshalJSON() ([]byte, error) {
result := make([][]byte, 0, len(n.Value))
for k, v := range n.Value {
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
}
func (n *ObjectNode) Set(k string, v any) {
n.Value[k] = NewNode(v)
}
func (n *ObjectNode) Get(k string) (Node, error) {
child, ok := n.Value[k]
if !ok {
return nil, fmt.Errorf("field %s not found", k)
}
return child, nil
}
func (n *ObjectNode) Merge(n2 *ObjectNode) {
for k, v := range n2.Value {
n.Value[k] = v
}
}
func (n *ObjectNode) Len() int {
return len(n.Value)
}

View file

@ -6,13 +6,13 @@ import (
)
// Query returns node by array query
func (n *Node) Query(query []string) (*Node, error) {
func Query(n Node, query []string) (Node, error) {
if len(query) == 0 {
return n, nil
}
head, rest := query[0], query[1:]
switch n.Type {
case ArrayNode:
switch n := n.(type) {
case *ArrayNode:
idx, err := strconv.Atoi(head)
if err != nil {
return nil, fmt.Errorf("index must be a number, got %s", head)
@ -21,13 +21,13 @@ func (n *Node) Query(query []string) (*Node, error) {
if err != nil {
return nil, err
}
return next.Query(rest)
case ObjectNode:
return Query(next, rest)
case *ObjectNode:
next, err := n.Get(head)
if err != nil {
return nil, err
}
return next.Query(rest)
return Query(next, rest)
}
return nil, fmt.Errorf("can't get %s from node type %s", head, n.Type)
return nil, fmt.Errorf("can't get %s from node type %s", head, n.Type())
}

13
model/stringNode.go Normal file
View file

@ -0,0 +1,13 @@
package model
type StringNode struct {
Value string
}
func (n StringNode) Type() NodeType {
return StringType
}
func (n *StringNode) MarshalJSON() ([]byte, error) {
return []byte(`"` + n.Value + `"`), nil
}

View file

@ -3,18 +3,16 @@ package model
type NodeType string
const (
StringNode NodeType = "string"
NumberNode NodeType = "number"
ObjectNode NodeType = "object"
ArrayNode NodeType = "array"
BooleanNode NodeType = "boolean"
NullNode NodeType = "null"
StringType NodeType = "string"
NumberType NodeType = "number"
ObjectType NodeType = "object"
ArrayType NodeType = "array"
BooleanType NodeType = "boolean"
NullType NodeType = "null"
)
type NodeObjectValue map[string]*Node
type NodeObjectValue map[string]Node
func (n NodeObjectValue) Set(k string, v any) {
n[k] = NewNode(v)
}
type NodeArrayValue []*Node

View file

@ -8,7 +8,7 @@ import (
"go.neonxp.dev/json/model"
)
func Parse(json string) (*model.Node, error) {
func Parse(json string) (model.Node, error) {
l := newLexer(json)
go l.Run(initJson)
n, err := parse(l.Output)

View file

@ -14,7 +14,7 @@ func TestParse(t *testing.T) {
tests := []struct {
name string
args args
want *model.Node
want model.Node
wantErr bool
}{
{