Completely rewrited

This commit is contained in:
NeonXP 2022-12-27 02:37:02 +03:00
parent 6f1d1df79f
commit 76a7f461eb
No known key found for this signature in database
GPG Key ID: B0DA6283C40CB2CB
25 changed files with 798 additions and 752 deletions

123
README.md
View File

@ -1,23 +1,120 @@
# JSON parsing library
This library is an marshaler/unmarshaler for JSON in a tree of nodes. Also allows you to make queries over these trees.
Библиотека для разбора JSON в дерево объектов. Так же позволяет выполнять поисковые запросы над ними.
## Library interface
## Использование
```go
package json // import "go.neonxp.dev/json"
import "go.neonxp.dev/json"
// Marshal Node tree to []byte
func Marshal(node *model.Node) ([]byte, error)
jsonString := `{
"string key": "string value",
"number key": 123.321,
"bool key": true,
"object": {
"one": "two",
"object 2": {
"three": "four"
}
},
"array": [
"one",
2,
true,
null,
{
"five": "six"
}
]
}`
// Unmarshal data to Node tree
func Unmarshal(data []byte) (*model.Node, error)
j := json.New(std.Factory) // в качестве фабрики можно передавать имплементацию интерфейса NodeFactory
rootNode, err := j.Unmarshal(jsonString)
// Query returns node by query string (dot notation)
func Query(json string, query string) (*model.Node, error)
// QueryArray returns node by array query
func QueryArray(json string, query []string) (*model.Node, error)
// Запрос по получившемуся дереву узлов
found := json.MustQuery(rootNode, []string{ "array", "4", "five" }) // == six
```
В результате `rootNode` будет содержать:
```go
std.ObjectNode{
"string key": &std.StringNode{ "string value" },
"number key": &std.NumberNode{ 123.321 },
"bool key": &std.BoolNode{ true },
"object": std.ObjectNode{
"one": &std.StringNode{ "two" },
"object 2": std.ObjectNode{
"three": &std.StringNode{ "four" },
},
},
"array": &std.ArrayNode{
&std.StringNode{ "one" },
&std.NumberNode{ 2 },
&std.BoolNode{ true },
&std.NullNode{},
std.ObjectNode{
"five": &std.StringNode{ "six" },
},
},
},
```
## Своя фабрика
```
// Непосредственно фабрика возвращающая заготовки нужного типа
type NodeFactory func(typ NodeType) (Node, error)
type Node interface {
String() string
}
// Имплементация узла объекта
type ObjectNode interface {
Node
SetKetValue(k string, v Node)
GetByKey(k string) (Node, bool)
}
// Имлементация узла массива
type ArrayNode interface {
Node
Append(v Node)
Index(i int) Node
Len() int
}
// Имплементация узла строки
type StringNode interface {
Node
SetString(v string)
GetString() string
}
// Имплементация узла числа
type NumberNode interface {
Node
SetNumber(v float64)
GetNumber() float64
}
// Имплементация узла булевого типа
type BooleanNode interface {
Node
SetBool(v bool)
GetBool() bool
}
// Имплементация null
type NullNode interface {
Node
}
// Если узел имплементирует этот интерфейс то вызывается метод Parent передающий родительский узел
type AcceptParent interface {
Parent(n Node)
}
```
Other methods: https://pkg.go.dev/go.neonxp.dev/json

46
factory.go Normal file
View File

@ -0,0 +1,46 @@
package json
type NodeFactory func(typ NodeType) (Node, error)
type Node interface {
String() string
}
type ObjectNode interface {
Node
SetKeyValue(k string, v Node)
GetByKey(k string) (Node, bool)
}
type ArrayNode interface {
Node
Append(v Node)
Index(i int) Node
Len() int
}
type StringNode interface {
Node
SetString(v string)
GetString() string
}
type NumberNode interface {
Node
SetNumber(v float64)
GetNumber() float64
}
type BooleanNode interface {
Node
SetBool(v bool)
GetBool() bool
}
type NullNode interface {
Node
}
type AcceptParent interface {
Parent(n Node)
}

View File

@ -1,4 +1,4 @@
package parser
package lexer
import (
"fmt"
@ -8,7 +8,7 @@ import (
const eof rune = -1
type lexem struct {
type Lexem struct {
Type lexType // Type of Lexem.
Value string // Value of Lexem.
Start int // Start position at input string.
@ -19,43 +19,43 @@ type lexem struct {
type lexType int
const (
lEOF lexType = iota
lError
lObjectStart
lObjectEnd
lObjectKey
lObjectValue
lArrayStart
lArrayEnd
lString
lNumber
lBoolean
lNull
LEOF lexType = iota
LError
LObjectStart
LObjectEnd
LObjectKey
LObjectValue
LArrayStart
LArrayEnd
LString
LNumber
LBoolean
LNull
)
// lexer holds current scanner state.
type lexer struct {
// Lexer holds current scanner state.
type Lexer struct {
Input string // Input string.
Start int // Start position of current lexem.
Pos int // Pos at input string.
Output chan lexem // Lexems channel.
Output chan Lexem // Lexems channel.
width int // Width of last rune.
states stateStack // Stack of states to realize PrevState.
}
// newLexer returns new scanner for input string.
func newLexer(input string) *lexer {
return &lexer{
func NewLexer(input string) *Lexer {
return &Lexer{
Input: input,
Start: 0,
Pos: 0,
Output: make(chan lexem, 2),
Output: make(chan Lexem, 2),
width: 0,
}
}
// Run lexing.
func (l *lexer) Run(init stateFunc) {
func (l *Lexer) Run(init stateFunc) {
for state := init; state != nil; {
state = state(l)
}
@ -63,18 +63,18 @@ func (l *lexer) Run(init stateFunc) {
}
// PopState returns previous state function.
func (l *lexer) PopState() stateFunc {
func (l *Lexer) PopState() stateFunc {
return l.states.Pop()
}
// PushState pushes state before going deeper states.
func (l *lexer) PushState(s stateFunc) {
func (l *Lexer) PushState(s stateFunc) {
l.states.Push(s)
}
// Emit current lexem to output.
func (l *lexer) Emit(typ lexType) {
l.Output <- lexem{
func (l *Lexer) Emit(typ lexType) {
l.Output <- Lexem{
Type: typ,
Value: l.Input[l.Start:l.Pos],
Start: l.Start,
@ -84,9 +84,9 @@ func (l *lexer) Emit(typ lexType) {
}
// Errorf produces error lexem and stops scanning.
func (l *lexer) Errorf(format string, args ...interface{}) stateFunc {
l.Output <- lexem{
Type: lError,
func (l *Lexer) Errorf(format string, args ...interface{}) stateFunc {
l.Output <- Lexem{
Type: LError,
Value: fmt.Sprintf(format, args...),
Start: l.Start,
End: l.Pos,
@ -95,7 +95,7 @@ func (l *lexer) Errorf(format string, args ...interface{}) stateFunc {
}
// Next rune from input.
func (l *lexer) Next() (r rune) {
func (l *Lexer) Next() (r rune) {
if int(l.Pos) >= len(l.Input) {
l.width = 0
return eof
@ -106,25 +106,25 @@ func (l *lexer) Next() (r rune) {
}
// Back move position to previos rune.
func (l *lexer) Back() {
func (l *Lexer) Back() {
l.Pos -= l.width
}
// Ignore previosly buffered text.
func (l *lexer) Ignore() {
func (l *Lexer) Ignore() {
l.Start = l.Pos
l.width = 0
}
// Peek rune at current position without moving position.
func (l *lexer) Peek() (r rune) {
func (l *Lexer) Peek() (r rune) {
r = l.Next()
l.Back()
return r
}
// Accept any rune from valid string. Returns true if Next rune was in valid string.
func (l *lexer) Accept(valid string) bool {
func (l *Lexer) Accept(valid string) bool {
if strings.ContainsRune(valid, l.Next()) {
return true
}
@ -133,7 +133,7 @@ func (l *lexer) Accept(valid string) bool {
}
// AcceptString returns true if given string was at position.
func (l *lexer) AcceptString(s string, caseInsentive bool) bool {
func (l *Lexer) AcceptString(s string, caseInsentive bool) bool {
input := l.Input[l.Start:]
if caseInsentive {
input = strings.ToLower(input)
@ -148,7 +148,7 @@ func (l *lexer) AcceptString(s string, caseInsentive bool) bool {
}
// AcceptAnyOf substrings. Retuns true if any of substrings was found.
func (l *lexer) AcceptAnyOf(s []string, caseInsentive bool) bool {
func (l *Lexer) AcceptAnyOf(s []string, caseInsentive bool) bool {
for _, substring := range s {
if l.AcceptString(substring, caseInsentive) {
return true
@ -158,7 +158,7 @@ func (l *lexer) AcceptAnyOf(s []string, caseInsentive bool) bool {
}
// AcceptWhile passing symbols from input while they at `valid` string.
func (l *lexer) AcceptWhile(valid string) bool {
func (l *Lexer) AcceptWhile(valid string) bool {
isValid := false
for l.Accept(valid) {
isValid = true
@ -167,7 +167,7 @@ func (l *lexer) AcceptWhile(valid string) bool {
}
// AcceptWhileNot passing symbols from input while they NOT in `invalid` string.
func (l *lexer) AcceptWhileNot(invalid string) bool {
func (l *Lexer) AcceptWhileNot(invalid string) bool {
isValid := false
for !strings.ContainsRune(invalid, l.Next()) {
isValid = true
@ -177,6 +177,6 @@ func (l *lexer) AcceptWhileNot(invalid string) bool {
}
// AtStart returns true if current lexem not empty
func (l *lexer) AtStart() bool {
func (l *Lexer) AtStart() bool {
return l.Pos == l.Start
}

View File

@ -1,6 +1,6 @@
// Code generated by "stringer -type=lexType"; DO NOT EDIT.
package parser
package lexer
import "strconv"
@ -8,21 +8,21 @@ func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[lEOF-0]
_ = x[lError-1]
_ = x[lObjectStart-2]
_ = x[lObjectEnd-3]
_ = x[lObjectKey-4]
_ = x[lObjectValue-5]
_ = x[lArrayStart-6]
_ = x[lArrayEnd-7]
_ = x[lString-8]
_ = x[lNumber-9]
_ = x[lBoolean-10]
_ = x[lNull-11]
_ = x[LEOF-0]
_ = x[LError-1]
_ = x[LObjectStart-2]
_ = x[LObjectEnd-3]
_ = x[LObjectKey-4]
_ = x[LObjectValue-5]
_ = x[LArrayStart-6]
_ = x[LArrayEnd-7]
_ = x[LString-8]
_ = x[LNumber-9]
_ = x[LBoolean-10]
_ = x[LNull-11]
}
const _lexType_name = "lEOFlErrorlObjectStartlObjectEndlObjectKeylObjectValuelArrayStartlArrayEndlStringlNumberlBooleanlNull"
const _lexType_name = "LEOFLErrorLObjectStartLObjectEndLObjectKeyLObjectValueLArrayStartLArrayEndLStringLNumberLBooleanLNull"
var _lexType_index = [...]uint8{0, 4, 10, 22, 32, 42, 54, 65, 74, 81, 88, 96, 101}

View File

@ -1,6 +1,6 @@
package parser
package lexer
func scanNumber(l *lexer) bool {
func scanNumber(l *Lexer) bool {
l.AcceptWhile("0123456789")
if l.AtStart() {
// not found any digit
@ -11,7 +11,7 @@ func scanNumber(l *lexer) bool {
return !l.AtStart()
}
func scanQuotedString(l *lexer, quote rune) bool {
func scanQuotedString(l *Lexer, quote rune) bool {
start := l.Pos
if l.Next() != quote {
l.Back()

View File

@ -1,6 +1,6 @@
package parser
package lexer
type stateFunc func(*lexer) stateFunc
type stateFunc func(*Lexer) stateFunc
type stateStack []stateFunc

View File

@ -1,24 +1,24 @@
package parser
package lexer
func initJson(l *lexer) stateFunc {
func InitJson(l *Lexer) stateFunc {
ignoreWhiteSpace(l)
switch {
case l.Accept("{"):
l.Emit(lObjectStart)
l.Emit(LObjectStart)
return stateInObject
case l.Accept("["):
l.Emit(lArrayStart)
l.Emit(LArrayStart)
case l.Peek() == eof:
return nil
}
return l.Errorf("Unknown token: %s", string(l.Peek()))
}
func stateInObject(l *lexer) stateFunc {
func stateInObject(l *Lexer) stateFunc {
// we in object, so we expect field keys and values
ignoreWhiteSpace(l)
if l.Accept("}") {
l.Emit(lObjectEnd)
l.Emit(LObjectEnd)
// If meet close object return to previous state (including initial)
return l.PopState()
}
@ -28,83 +28,83 @@ func stateInObject(l *lexer) stateFunc {
if !scanQuotedString(l, '"') {
return l.Errorf("Unknown token: %s", string(l.Peek()))
}
l.Emit(lObjectKey)
l.Emit(LObjectKey)
ignoreWhiteSpace(l)
if !l.Accept(":") {
return l.Errorf("Expected ':'")
}
ignoreWhiteSpace(l)
l.Emit(lObjectValue)
l.Emit(LObjectValue)
switch {
case scanQuotedString(l, '"'):
l.Emit(lString)
l.Emit(LString)
ignoreWhiteSpace(l)
l.Accept(",")
l.Ignore()
ignoreWhiteSpace(l)
return stateInObject
case scanNumber(l):
l.Emit(lNumber)
l.Emit(LNumber)
ignoreWhiteSpace(l)
l.Accept(",")
l.Ignore()
ignoreWhiteSpace(l)
return stateInObject
case l.AcceptAnyOf([]string{"true", "false"}, true):
l.Emit(lBoolean)
l.Emit(LBoolean)
ignoreWhiteSpace(l)
l.Accept(",")
l.Ignore()
ignoreWhiteSpace(l)
return stateInObject
case l.AcceptString("null", true):
l.Emit(lNull)
l.Emit(LNull)
ignoreWhiteSpace(l)
l.Accept(",")
l.Ignore()
ignoreWhiteSpace(l)
return stateInObject
case l.Accept("{"):
l.Emit(lObjectStart)
l.Emit(LObjectStart)
l.PushState(stateInObject)
return stateInObject
case l.Accept("["):
l.Emit(lArrayStart)
l.Emit(LArrayStart)
l.PushState(stateInObject)
return stateInArray
}
return l.Errorf("Unknown token: %s", string(l.Peek()))
}
func stateInArray(l *lexer) stateFunc {
func stateInArray(l *Lexer) stateFunc {
ignoreWhiteSpace(l)
l.Accept(",")
ignoreWhiteSpace(l)
switch {
case scanQuotedString(l, '"'):
l.Emit(lString)
l.Emit(LString)
case scanNumber(l):
l.Emit(lNumber)
l.Emit(LNumber)
case l.AcceptAnyOf([]string{"true", "false"}, true):
l.Emit(lBoolean)
l.Emit(LBoolean)
case l.AcceptString("null", true):
l.Emit(lNull)
l.Emit(LNull)
case l.Accept("{"):
l.Emit(lObjectStart)
l.Emit(LObjectStart)
l.PushState(stateInArray)
return stateInObject
case l.Accept("["):
l.Emit(lArrayStart)
l.Emit(LArrayStart)
l.PushState(stateInArray)
return stateInArray
case l.Accept("]"):
l.Emit(lArrayEnd)
l.Emit(LArrayEnd)
return l.PopState()
}
return stateInArray
}
func ignoreWhiteSpace(l *lexer) {
func ignoreWhiteSpace(l *Lexer) {
l.AcceptWhile(" \n\t") // ignore whitespaces
l.Ignore()
}

106
json.go
View File

@ -1,36 +1,100 @@
package json
import (
"strings"
"fmt"
"go.neonxp.dev/json/model"
"go.neonxp.dev/json/parser"
"go.neonxp.dev/json/internal/lexer"
)
// Marshal Node tree to []byte
func Marshal(node model.Node) ([]byte, error) {
return node.MarshalJSON()
type JSON struct {
Factory NodeFactory
}
// Unmarshal data to Node tree
func Unmarshal(data []byte) (model.Node, error) {
return parser.Parse(string(data))
func (j *JSON) Unmarshal(input string) (Node, error) {
lex := lexer.NewLexer(input)
go lex.Run(lexer.InitJson)
return j.parse(lex.Output)
}
// Query returns node by query string (dot notation)
func Query(json string, query string) (model.Node, error) {
n, err := parser.Parse(json)
func (j *JSON) MustUnmarshal(input string) Node {
n, err := j.Unmarshal(input)
if err != nil {
return nil, err
panic(err)
}
return model.Query(n, strings.Split(query, "."))
return n
}
// QueryArray returns node by array query
func QueryArray(json string, query []string) (model.Node, error) {
n, err := parser.Parse(json)
if err != nil {
return nil, err
}
return model.Query(n, query)
func (j *JSON) Marshal(n Node) string {
return n.String()
}
func (j *JSON) Node(value any) (Node, error) {
switch value := value.(type) {
case string:
n, err := j.Factory(StringType)
if err != nil {
return nil, err
}
n.(StringNode).SetString(value)
return n, nil
case float64:
n, err := j.Factory(NumberType)
if err != nil {
return nil, err
}
n.(NumberNode).SetNumber(value)
return n, nil
case int:
n, err := j.Factory(NumberType)
if err != nil {
return nil, err
}
n.(NumberNode).SetNumber(float64(value))
return n, nil
case bool:
n, err := j.Factory(BooleanType)
if err != nil {
return nil, err
}
n.(BooleanNode).SetBool(value)
return n, nil
case nil:
return j.Factory(NullType)
case map[string]Node:
n, err := j.Factory(ObjectType)
if err != nil {
return nil, err
}
on := n.(ObjectNode)
for k, v := range value {
on.SetKeyValue(k, v)
}
return on, nil
case []Node:
n, err := j.Factory(ArrayType)
if err != nil {
return nil, err
}
an := n.(ArrayNode)
for _, v := range value {
an.Append(v)
}
return an, nil
default:
return nil, fmt.Errorf("invalid type %t", value)
}
}
func (j *JSON) MustNode(value any) Node {
n, err := j.Node(value)
if err != nil {
panic(err)
}
return n
}
func New(factory NodeFactory) *JSON {
return &JSON{
Factory: factory,
}
}

View File

@ -1,66 +1,141 @@
package json
package json_test
import (
"reflect"
"testing"
"go.neonxp.dev/json/model"
"go.neonxp.dev/json"
"go.neonxp.dev/json/std"
)
func TestQuery(t *testing.T) {
func TestJSON_Unmarshal(t *testing.T) {
j := &json.JSON{
Factory: std.Factory,
}
type args struct {
json string
query string
input string
}
tests := []struct {
name string
args args
want model.Node
want json.Node
wantErr bool
}{
{
name: "Complex",
name: "object with strings",
args: args{
json: `{
"key1": "value1",
"key2": [
"item 1",
"item 2",
"item 3",
"item 4",
"item 5",
"item 6",
input: `{
"hello": "world",
}`,
},
want: std.ObjectNode{
"hello": &std.StringNode{Value: "world"},
},
wantErr: false,
},
{
name: "complex object",
args: args{
input: `{
"string key": "string value",
"number key": 123.321,
"bool key": true,
"object": {
"one": "two",
"object 2": {
"three": "four"
}
},
"array": [
"one",
2,
true,
null,
{
"status": "invalid"
},
{
"status": "valid",
"embededArray": [
"not target",
"not target",
"not target",
"target",
"not target",
"not target",
]
"five": "six"
}
]
}`,
query: "key2.7.embededArray.3",
},
want: model.NewNode("target"),
wantErr: false,
want: std.ObjectNode{
"string key": j.MustNode("string value"),
"number key": j.MustNode(123.321),
"bool key": j.MustNode(true),
"object": std.ObjectNode{
"one": j.MustNode("two"),
"object 2": std.ObjectNode{
"three": j.MustNode("four"),
},
},
"array": &std.ArrayNode{
j.MustNode("one"),
j.MustNode(2),
j.MustNode(true),
j.MustNode(nil),
std.ObjectNode{
"five": j.MustNode("six"),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Query(tt.args.json, tt.args.query)
got, err := j.Unmarshal(tt.args.input)
if (err != nil) != tt.wantErr {
t.Errorf("Query() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("JSON.Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Query() = %v, want %v", got, tt.want)
t.Errorf("JSON.Unmarshal() = %v, want %v", got, tt.want)
}
})
}
}
func TestJSON_Marshal(t *testing.T) {
j := &json.JSON{
Factory: std.Factory,
}
type args struct {
n json.Node
}
tests := []struct {
name string
args args
want string
}{
{
name: "complex object",
args: args{
n: std.ObjectNode{
"string key": j.MustNode("string value"),
"number key": j.MustNode(123.321),
"bool key": j.MustNode(true),
"object": std.ObjectNode{
"one": j.MustNode("two"),
"object 2": std.ObjectNode{
"three": j.MustNode("four"),
},
},
"array": &std.ArrayNode{
j.MustNode("one"),
j.MustNode(2),
j.MustNode(true),
j.MustNode(nil),
std.ObjectNode{
"five": j.MustNode("six"),
},
},
},
},
want: `{"string key":"string value","number key":123.321,"bool key":true,"object":{"one":"two","object 2":{"three":"four"}},"array":["one",2,true,null,{"five":"six"}]}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := j.Marshal(tt.args.n); len(got) != len(tt.want) {
t.Errorf("JSON.Marshal() = %v, want %v", got, tt.want)
}
})
}

View File

@ -1,57 +0,0 @@
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) Set(v any) error {
val, ok := v.(NodeArrayValue)
if !ok {
return fmt.Errorf("%v is not array", v)
}
n.Value = val
return 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

View File

@ -1,27 +0,0 @@
package model
import "fmt"
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
}
func (n *BooleanNode) Set(v any) error {
val, ok := v.(bool)
if !ok {
return fmt.Errorf("%v is not boolean", v)
}
n.Value = val
return nil
}

View File

@ -1,44 +0,0 @@
package model
// Node of JSON tree
type Node interface {
Type() NodeType
MarshalJSON() ([]byte, error)
Set(v any) error
}
// NewNode creates new node from value
func NewNode(value any) Node {
if value, ok := value.(Node); ok {
return value
}
switch value := value.(type) {
case string:
return &StringNode{
Value: value,
}
case float64:
return &NumberNode{
Value: value,
}
case int:
return &NumberNode{
Value: float64(value),
}
case NodeObjectValue:
return &ObjectNode{
Value: value,
Meta: make(map[string]any),
}
case NodeArrayValue:
return &ArrayNode{
Value: value,
}
case bool:
return &BooleanNode{
Value: value,
}
default:
return NullNode{}
}
}

View File

@ -1,108 +0,0 @@
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)
}
})
}
}

View File

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

View File

@ -1,28 +0,0 @@
package model
import (
"fmt"
"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
}
func (n *NumberNode) Set(v any) error {
switch v := v.(type) {
case float64:
n.Value = v
case int:
n.Value = float64(v)
}
return fmt.Errorf("%v is not number", v)
}

View File

@ -1,63 +0,0 @@
package model
import (
"bytes"
"fmt"
)
type ObjectNode struct {
Value NodeObjectValue
Meta map[string]any
}
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) 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)
}
func (n *ObjectNode) Set(v any) error {
val, ok := v.(NodeObjectValue)
if !ok {
return fmt.Errorf("%v is not object", v)
}
n.Value = val
return nil
}
func (n *ObjectNode) Remove(key string) {
delete(n.Value, key)
}

View File

@ -1,33 +0,0 @@
package model
import (
"fmt"
"strconv"
)
// Query returns node by array query
func Query(n Node, query []string) (Node, error) {
if len(query) == 0 {
return n, nil
}
head, rest := query[0], query[1:]
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)
}
next, err := n.Index(idx)
if err != nil {
return nil, err
}
return Query(next, rest)
case *ObjectNode:
next, err := n.Get(head)
if err != nil {
return nil, err
}
return Query(next, rest)
}
return nil, fmt.Errorf("can't get %s from node type %s", head, n.Type())
}

View File

@ -1,24 +0,0 @@
package model
import "fmt"
type StringNode struct {
Value string
}
func (n StringNode) Type() NodeType {
return StringType
}
func (n *StringNode) MarshalJSON() ([]byte, error) {
return []byte(`"` + n.Value + `"`), nil
}
func (n *StringNode) Set(v any) error {
val, ok := v.(string)
if !ok {
return fmt.Errorf("%v is not string", v)
}
n.Value = val
return nil
}

133
parser.go Normal file
View File

@ -0,0 +1,133 @@
package json
import (
"fmt"
"strconv"
"strings"
"go.neonxp.dev/json/internal/lexer"
)
func (j *JSON) parse(ch chan lexer.Lexem) (Node, error) {
prefix := <-ch
return j.createChild(nil, prefix, ch)
}
func (j *JSON) createChild(parent Node, l lexer.Lexem, ch chan lexer.Lexem) (Node, error) {
switch l.Type {
case lexer.LString:
c, err := j.Factory(StringType)
if err != nil {
return nil, err
}
if c, ok := c.(AcceptParent); ok {
c.Parent(parent)
}
child := c.(StringNode)
child.SetString(strings.Trim(l.Value, `"`))
return child, nil
case lexer.LNumber:
num, err := strconv.ParseFloat(l.Value, 64)
if err != nil {
return nil, err
}
c, err := j.Factory(NumberType)
if err != nil {
return nil, err
}
if c, ok := c.(AcceptParent); ok {
c.Parent(parent)
}
child := c.(NumberNode)
child.SetNumber(num)
return child, nil
case lexer.LBoolean:
b := strings.ToLower(l.Value) == "true"
c, err := j.Factory(BooleanType)
if err != nil {
return nil, err
}
if c, ok := c.(AcceptParent); ok {
c.Parent(parent)
}
child := c.(BooleanNode)
child.SetBool(b)
return child, nil
case lexer.LObjectStart:
child, err := j.parseObject(parent, ch)
if err != nil {
return nil, err
}
return child, nil
case lexer.LArrayStart:
child, err := j.parseArray(parent, ch)
if err != nil {
return nil, err
}
return child, nil
case lexer.LNull:
c, err := j.Factory(NullType)
if err != nil {
return nil, err
}
if c, ok := c.(AcceptParent); ok {
c.Parent(parent)
}
return c.(NullNode), nil
default:
return nil, fmt.Errorf("ivalid token: '%s' type=%s", l.Value, l.Type.String())
}
}
func (j *JSON) parseObject(parent Node, ch chan lexer.Lexem) (ObjectNode, error) {
c, err := j.Factory(ObjectType)
if err != nil {
return nil, err
}
if c, ok := c.(AcceptParent); ok {
c.Parent(parent)
}
n := c.(ObjectNode)
nextKey := ""
for l := range ch {
switch l.Type {
case lexer.LObjectKey:
nextKey = strings.Trim(l.Value, `"`)
case lexer.LObjectEnd:
return n, nil
case lexer.LObjectValue:
continue
default:
child, err := j.createChild(n, l, ch)
if err != nil {
return nil, err
}
n.SetKeyValue(nextKey, child)
}
}
return nil, fmt.Errorf("unexpected end of object")
}
func (j *JSON) parseArray(parent Node, ch chan lexer.Lexem) (ArrayNode, error) {
c, err := j.Factory(ArrayType)
if err != nil {
return nil, err
}
if c, ok := c.(AcceptParent); ok {
c.Parent(parent)
}
n := c.(ArrayNode)
for l := range ch {
switch l.Type {
case lexer.LArrayEnd:
return n, nil
default:
child, err := j.createChild(n, l, ch)
if err != nil {
return nil, err
}
n.Append(child)
}
}
return nil, fmt.Errorf("unexpected end of object")
}

View File

@ -1,126 +0,0 @@
package parser
import (
"fmt"
"strconv"
"strings"
"go.neonxp.dev/json/model"
)
func Parse(json string) (model.Node, error) {
l := newLexer(json)
go l.Run(initJson)
n, err := parse(l.Output)
if err != nil {
return nil, err
}
return model.NewNode(n), nil
}
func parse(ch chan lexem) (any, error) {
prefix := <-ch
switch prefix.Type {
case lObjectStart:
return parseObject(ch)
case lArrayStart:
return parseArray(ch)
case lString:
return strings.Trim(prefix.Value, `"`), nil
case lNumber:
num, err := strconv.ParseFloat(prefix.Value, 64)
if err != nil {
return nil, err
}
return num, nil
case lBoolean:
if strings.ToLower(prefix.Value) == "true" {
return true, nil
}
return false, nil
case lNull:
return nil, nil
}
return nil, fmt.Errorf("ivalid token: '%s' type=%s", prefix.Value, prefix.Type.String())
}
func parseObject(ch chan lexem) (model.NodeObjectValue, error) {
m := model.NodeObjectValue{}
nextKey := ""
for l := range ch {
switch l.Type {
case lObjectKey:
nextKey = strings.Trim(l.Value, `"`)
case lString:
m.Set(nextKey, strings.Trim(l.Value, `"`))
case lNumber:
num, err := strconv.ParseFloat(l.Value, 64)
if err != nil {
return nil, err
}
m.Set(nextKey, num)
case lBoolean:
if strings.ToLower(l.Value) == "true" {
m.Set(nextKey, true)
continue
}
m.Set(nextKey, false)
case lNull:
m.Set(nextKey, nil)
case lObjectStart:
obj, err := parseObject(ch)
if err != nil {
return nil, err
}
m.Set(nextKey, obj)
case lArrayStart:
arr, err := parseArray(ch)
if err != nil {
return nil, err
}
m.Set(nextKey, arr)
case lObjectEnd:
return m, nil
}
}
return nil, fmt.Errorf("unexpected end of object")
}
func parseArray(ch chan lexem) (model.NodeArrayValue, error) {
m := model.NodeArrayValue{}
for l := range ch {
switch l.Type {
case lString:
m = append(m, model.NewNode(strings.Trim(l.Value, `"`)))
case lNumber:
num, err := strconv.ParseFloat(l.Value, 64)
if err != nil {
return nil, err
}
m = append(m, model.NewNode(num))
case lBoolean:
if strings.ToLower(l.Value) == "true" {
m = append(m, model.NewNode(true))
continue
}
m = append(m, model.NewNode(false))
case lNull:
m = append(m, model.NewNode(nil))
case lObjectStart:
obj, err := parseObject(ch)
if err != nil {
return nil, err
}
m = append(m, model.NewNode(obj))
case lArrayStart:
arr, err := parseArray(ch)
if err != nil {
return nil, err
}
m = append(m, model.NewNode(arr))
case lArrayEnd:
return m, nil
}
}
return nil, fmt.Errorf("unexpected end of object")
}

View File

@ -1,71 +0,0 @@
package parser
import (
"reflect"
"testing"
"go.neonxp.dev/json/model"
)
func TestParse(t *testing.T) {
type args struct {
json string
}
tests := []struct {
name string
args args
want model.Node
wantErr bool
}{
{
name: "complex",
args: args{
json: `{
"string key": "string value",
"number key": 1337,
"float key": 123.3,
"object key": {
"ab": "cd"
},
"array key": [
1,
2,
"three"
],
"null key":null,
"boolean key":true
}`,
},
want: model.NewNode(
model.NodeObjectValue{
"string key": model.NewNode("string value"),
"number key": model.NewNode(1337),
"float key": model.NewNode(123.3),
"object key": model.NewNode(model.NodeObjectValue{
"ab": model.NewNode("cd"),
}),
"array key": model.NewNode(model.NodeArrayValue{
model.NewNode(1),
model.NewNode(2),
model.NewNode("three"),
}),
"null key": model.NewNode(nil),
"boolean key": model.NewNode(true),
},
),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.args.json)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Parse() = %v, want %v", got, tt.want)
}
})
}
}

43
query.go Normal file
View File

@ -0,0 +1,43 @@
package json
import (
"fmt"
"strconv"
"strings"
)
func Query(parent Node, path []string) (Node, error) {
if len(path) == 0 {
return parent, nil
}
head, rest := path[0], path[1:]
switch parent := parent.(type) {
case ObjectNode:
next, ok := parent.GetByKey(head)
if !ok {
return nil, fmt.Errorf("key %s not found at object %v", head, parent)
}
return Query(next, rest)
case ArrayNode:
stringIdx := strings.Trim(head, "[]")
idx, err := strconv.Atoi(stringIdx)
if err != nil {
return nil, fmt.Errorf("key %s is invalid index: %w", stringIdx, err)
}
if idx >= parent.Len() {
return nil, fmt.Errorf("index %d is out of range (len=%d)", idx, parent.Len())
}
next := parent.Index(idx)
return Query(next, rest)
default:
return nil, fmt.Errorf("can't get key=%s from node type = %t", head, parent)
}
}
func MustQuery(parent Node, path []string) Node {
n, err := Query(parent, path)
if err != nil {
panic(err)
}
return n
}

65
query_test.go Normal file
View File

@ -0,0 +1,65 @@
package json_test
import (
"reflect"
"testing"
"go.neonxp.dev/json"
"go.neonxp.dev/json/std"
)
func TestMustQuery(t *testing.T) {
jsonString := `{
"string key": "string value",
"number key": 123.321,
"bool key": true,
"object": {
"one": "two",
"object 2": {
"three": "four"
}
},
"array": [
"one",
2,
true,
null,
{
"five": "six"
}
]
}`
type args struct {
parent json.Node
path []string
}
tests := []struct {
name string
args args
want json.Node
}{
{
name: "find in object",
args: args{
parent: json.New(std.Factory).MustUnmarshal(jsonString),
path: []string{"object", "object 2", "three"},
},
want: &std.StringNode{Value: "four"},
},
{
name: "find in array",
args: args{
parent: json.New(std.Factory).MustUnmarshal(jsonString),
path: []string{"array", "[4]", "five"},
},
want: &std.StringNode{Value: "six"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := json.MustQuery(tt.args.parent, tt.args.path); !reflect.DeepEqual(got, tt.want) {
t.Errorf("MustQuery() = %v, want %v", got, tt.want)
}
})
}
}

126
std/factory.go Normal file
View File

@ -0,0 +1,126 @@
package std
import (
"fmt"
"strconv"
"strings"
"go.neonxp.dev/json"
)
func Factory(typ json.NodeType) (json.Node, error) {
switch typ {
case json.ObjectType:
return ObjectNode{}, nil
case json.ArrayType:
return &ArrayNode{}, nil
case json.StringType:
return &StringNode{}, nil
case json.NumberType:
return &NumberNode{}, nil
case json.BooleanType:
return &BooleanNode{}, nil
case json.NullType:
return NullNode{}, nil
}
return nil, fmt.Errorf("unknown type: %s", typ)
}
type ObjectNode map[string]json.Node
func (o ObjectNode) SetKeyValue(k string, v json.Node) {
o[k] = v
}
func (o ObjectNode) GetByKey(k string) (json.Node, bool) {
v, ok := o[k]
return v, ok
}
func (o ObjectNode) String() string {
res := make([]string, 0, len(o))
for k, n := range o {
res = append(res, fmt.Sprintf(`"%s":%s`, k, n.String()))
}
return fmt.Sprintf(`{%s}`, strings.Join(res, ","))
}
type ArrayNode []json.Node
func (o *ArrayNode) Append(v json.Node) {
na := append(*o, v)
*o = na
}
func (o *ArrayNode) Index(i int) json.Node {
return (*o)[i]
}
func (o *ArrayNode) Len() int {
return len(*o)
}
func (o *ArrayNode) String() string {
res := make([]string, 0, len(*o))
for _, v := range *o {
res = append(res, v.String())
}
return fmt.Sprintf(`[%s]`, strings.Join(res, ","))
}
type StringNode struct {
Value string
}
func (o *StringNode) SetString(v string) {
o.Value = v
}
func (o *StringNode) GetString() string {
return o.Value
}
func (o *StringNode) String() string {
return `"` + o.Value + `"`
}
type NumberNode struct {
Value float64
}
func (o *NumberNode) SetNumber(v float64) {
o.Value = v
}
func (o *NumberNode) GetNumber() float64 {
return o.Value
}
func (o *NumberNode) String() string {
return strconv.FormatFloat(float64(o.Value), 'g', 15, 64)
}
type BooleanNode struct {
Value bool
}
func (o *BooleanNode) SetBool(v bool) {
o.Value = v
}
func (o *BooleanNode) GetBool() bool {
return o.Value
}
func (o BooleanNode) String() string {
if o.Value {
return "true"
}
return "false"
}
type NullNode struct{}
func (o NullNode) String() string {
return "null"
}

View File

@ -1,4 +1,4 @@
package model
package json
type NodeType string
@ -10,10 +10,3 @@ const (
BooleanType NodeType = "boolean"
NullType NodeType = "null"
)
type NodeObjectValue map[string]Node
func (n NodeObjectValue) Set(k string, v any) error {
n[k] = NewNode(v)
return nil
}