From 76a7f461ebbde70ea0e3d4f9b79c08139acaee7c Mon Sep 17 00:00:00 2001 From: NeonXP Date: Tue, 27 Dec 2022 02:37:02 +0300 Subject: [PATCH] Completely rewrited --- README.md | 123 ++++++++++++++-- factory.go | 46 ++++++ {parser => internal/lexer}/lexer.go | 76 +++++----- {parser => internal/lexer}/lextype_string.go | 28 ++-- {parser => internal/lexer}/scanners.go | 6 +- {parser => internal/lexer}/statefunc.go | 4 +- {parser => internal/lexer}/states.go | 46 +++--- json.go | 106 +++++++++++--- json_test.go | 143 ++++++++++++++----- model/arrayNode.go | 57 -------- model/booleanNode.go | 27 ---- model/node.go | 44 ------ model/node_test.go | 108 -------------- model/nullNode.go | 15 -- model/numberNode.go | 28 ---- model/objectNode.go | 63 -------- model/query.go | 33 ----- model/stringNode.go | 24 ---- parser.go | 133 +++++++++++++++++ parser/parser.go | 126 ---------------- parser/parser_test.go | 71 --------- query.go | 43 ++++++ query_test.go | 65 +++++++++ std/factory.go | 126 ++++++++++++++++ model/types.go => types.go | 9 +- 25 files changed, 798 insertions(+), 752 deletions(-) create mode 100644 factory.go rename {parser => internal/lexer}/lexer.go (71%) rename {parser => internal/lexer}/lextype_string.go (59%) rename {parser => internal/lexer}/scanners.go (82%) rename {parser => internal/lexer}/statefunc.go (81%) rename {parser => internal/lexer}/states.go (77%) delete mode 100644 model/arrayNode.go delete mode 100644 model/booleanNode.go delete mode 100644 model/node.go delete mode 100644 model/node_test.go delete mode 100644 model/nullNode.go delete mode 100644 model/numberNode.go delete mode 100644 model/objectNode.go delete mode 100644 model/query.go delete mode 100644 model/stringNode.go create mode 100644 parser.go delete mode 100644 parser/parser.go delete mode 100644 parser/parser_test.go create mode 100644 query.go create mode 100644 query_test.go create mode 100644 std/factory.go rename model/types.go => types.go (62%) diff --git a/README.md b/README.md index a3cd9a7..a95038f 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file diff --git a/factory.go b/factory.go new file mode 100644 index 0000000..285c235 --- /dev/null +++ b/factory.go @@ -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) +} diff --git a/parser/lexer.go b/internal/lexer/lexer.go similarity index 71% rename from parser/lexer.go rename to internal/lexer/lexer.go index 5034f6a..342864d 100644 --- a/parser/lexer.go +++ b/internal/lexer/lexer.go @@ -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 } diff --git a/parser/lextype_string.go b/internal/lexer/lextype_string.go similarity index 59% rename from parser/lextype_string.go rename to internal/lexer/lextype_string.go index f34eb7c..fe895d2 100644 --- a/parser/lextype_string.go +++ b/internal/lexer/lextype_string.go @@ -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} diff --git a/parser/scanners.go b/internal/lexer/scanners.go similarity index 82% rename from parser/scanners.go rename to internal/lexer/scanners.go index 078f9d3..6181c2d 100644 --- a/parser/scanners.go +++ b/internal/lexer/scanners.go @@ -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() diff --git a/parser/statefunc.go b/internal/lexer/statefunc.go similarity index 81% rename from parser/statefunc.go rename to internal/lexer/statefunc.go index 69d7098..8d0e42a 100644 --- a/parser/statefunc.go +++ b/internal/lexer/statefunc.go @@ -1,6 +1,6 @@ -package parser +package lexer -type stateFunc func(*lexer) stateFunc +type stateFunc func(*Lexer) stateFunc type stateStack []stateFunc diff --git a/parser/states.go b/internal/lexer/states.go similarity index 77% rename from parser/states.go rename to internal/lexer/states.go index 92c80dc..818ccf6 100644 --- a/parser/states.go +++ b/internal/lexer/states.go @@ -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() } diff --git a/json.go b/json.go index 26fcad5..0ffd7e0 100644 --- a/json.go +++ b/json.go @@ -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, + } } diff --git a/json_test.go b/json_test.go index 515f697..b058417 100644 --- a/json_test.go +++ b/json_test.go @@ -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) } }) } diff --git a/model/arrayNode.go b/model/arrayNode.go deleted file mode 100644 index 319f223..0000000 --- a/model/arrayNode.go +++ /dev/null @@ -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 diff --git a/model/booleanNode.go b/model/booleanNode.go deleted file mode 100644 index aba286e..0000000 --- a/model/booleanNode.go +++ /dev/null @@ -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 -} diff --git a/model/node.go b/model/node.go deleted file mode 100644 index fb6bae5..0000000 --- a/model/node.go +++ /dev/null @@ -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{} - } -} diff --git a/model/node_test.go b/model/node_test.go deleted file mode 100644 index 1f46361..0000000 --- a/model/node_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/model/nullNode.go b/model/nullNode.go deleted file mode 100644 index d510828..0000000 --- a/model/nullNode.go +++ /dev/null @@ -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 -} diff --git a/model/numberNode.go b/model/numberNode.go deleted file mode 100644 index b61b63c..0000000 --- a/model/numberNode.go +++ /dev/null @@ -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) -} diff --git a/model/objectNode.go b/model/objectNode.go deleted file mode 100644 index 989fd9e..0000000 --- a/model/objectNode.go +++ /dev/null @@ -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) -} diff --git a/model/query.go b/model/query.go deleted file mode 100644 index 56a0dfe..0000000 --- a/model/query.go +++ /dev/null @@ -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()) -} diff --git a/model/stringNode.go b/model/stringNode.go deleted file mode 100644 index b8dfcfb..0000000 --- a/model/stringNode.go +++ /dev/null @@ -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 -} diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..a82960b --- /dev/null +++ b/parser.go @@ -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") +} diff --git a/parser/parser.go b/parser/parser.go deleted file mode 100644 index dfcd4b4..0000000 --- a/parser/parser.go +++ /dev/null @@ -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") -} diff --git a/parser/parser_test.go b/parser/parser_test.go deleted file mode 100644 index 88a1f8f..0000000 --- a/parser/parser_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/query.go b/query.go new file mode 100644 index 0000000..69b778c --- /dev/null +++ b/query.go @@ -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 +} diff --git a/query_test.go b/query_test.go new file mode 100644 index 0000000..44383de --- /dev/null +++ b/query_test.go @@ -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) + } + }) + } +} diff --git a/std/factory.go b/std/factory.go new file mode 100644 index 0000000..11c3de1 --- /dev/null +++ b/std/factory.go @@ -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" +} diff --git a/model/types.go b/types.go similarity index 62% rename from model/types.go rename to types.go index 72dce5f..090eeac 100644 --- a/model/types.go +++ b/types.go @@ -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 -}