From 76a7f461ebbde70ea0e3d4f9b79c08139acaee7c Mon Sep 17 00:00:00 2001 From: NeonXP Date: Tue, 27 Dec 2022 02:37:02 +0300 Subject: Completely rewrited --- README.md | 121 +++++++++++++++++++++++--- factory.go | 46 ++++++++++ internal/lexer/lexer.go | 182 +++++++++++++++++++++++++++++++++++++++ internal/lexer/lextype_string.go | 34 ++++++++ internal/lexer/scanners.go | 32 +++++++ internal/lexer/statefunc.go | 17 ++++ internal/lexer/states.go | 110 +++++++++++++++++++++++ json.go | 102 ++++++++++++++++++---- 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 ------ model/types.go | 19 ---- parser.go | 133 ++++++++++++++++++++++++++++ parser/lexer.go | 182 --------------------------------------- parser/lextype_string.go | 34 -------- parser/parser.go | 126 --------------------------- parser/parser_test.go | 71 --------------- parser/scanners.go | 32 ------- parser/statefunc.go | 17 ---- parser/states.go | 110 ----------------------- query.go | 43 +++++++++ query_test.go | 65 ++++++++++++++ std/factory.go | 126 +++++++++++++++++++++++++++ types.go | 12 +++ 31 files changed, 1101 insertions(+), 1055 deletions(-) create mode 100644 factory.go create mode 100644 internal/lexer/lexer.go create mode 100644 internal/lexer/lextype_string.go create mode 100644 internal/lexer/scanners.go create mode 100644 internal/lexer/statefunc.go create mode 100644 internal/lexer/states.go 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 delete mode 100644 model/types.go create mode 100644 parser.go delete mode 100644 parser/lexer.go delete mode 100644 parser/lextype_string.go delete mode 100644 parser/parser.go delete mode 100644 parser/parser_test.go delete mode 100644 parser/scanners.go delete mode 100644 parser/statefunc.go delete mode 100644 parser/states.go create mode 100644 query.go create mode 100644 query_test.go create mode 100644 std/factory.go create mode 100644 types.go 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) +// Запрос по получившемуся дереву узлов +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 +} -// QueryArray returns node by array query -func QueryArray(json string, query []string) (*model.Node, error) +// Если узел имплементирует этот интерфейс то вызывается метод 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/internal/lexer/lexer.go b/internal/lexer/lexer.go new file mode 100644 index 0000000..342864d --- /dev/null +++ b/internal/lexer/lexer.go @@ -0,0 +1,182 @@ +package lexer + +import ( + "fmt" + "strings" + "unicode/utf8" +) + +const eof rune = -1 + +type Lexem struct { + Type lexType // Type of Lexem. + Value string // Value of Lexem. + Start int // Start position at input string. + End int // End position at input string. +} + +//go:generate stringer -type=lexType +type lexType int + +const ( + LEOF lexType = iota + LError + LObjectStart + LObjectEnd + LObjectKey + LObjectValue + LArrayStart + LArrayEnd + LString + LNumber + LBoolean + LNull +) + +// 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. + 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{ + Input: input, + Start: 0, + Pos: 0, + Output: make(chan Lexem, 2), + width: 0, + } +} + +// Run lexing. +func (l *Lexer) Run(init stateFunc) { + for state := init; state != nil; { + state = state(l) + } + close(l.Output) +} + +// PopState returns previous state function. +func (l *Lexer) PopState() stateFunc { + return l.states.Pop() +} + +// PushState pushes state before going deeper states. +func (l *Lexer) PushState(s stateFunc) { + l.states.Push(s) +} + +// Emit current lexem to output. +func (l *Lexer) Emit(typ lexType) { + l.Output <- Lexem{ + Type: typ, + Value: l.Input[l.Start:l.Pos], + Start: l.Start, + End: l.Pos, + } + l.Start = l.Pos +} + +// Errorf produces error lexem and stops scanning. +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, + } + return nil +} + +// Next rune from input. +func (l *Lexer) Next() (r rune) { + if int(l.Pos) >= len(l.Input) { + l.width = 0 + return eof + } + r, l.width = utf8.DecodeRuneInString(l.Input[l.Pos:]) + l.Pos += l.width + return r +} + +// Back move position to previos rune. +func (l *Lexer) Back() { + l.Pos -= l.width +} + +// Ignore previosly buffered text. +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) { + 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 { + if strings.ContainsRune(valid, l.Next()) { + return true + } + l.Back() + return false +} + +// AcceptString returns true if given string was at position. +func (l *Lexer) AcceptString(s string, caseInsentive bool) bool { + input := l.Input[l.Start:] + if caseInsentive { + input = strings.ToLower(input) + s = strings.ToLower(s) + } + if strings.HasPrefix(input, s) { + l.width = 0 + l.Pos += len(s) + return true + } + return false +} + +// AcceptAnyOf substrings. Retuns true if any of substrings was found. +func (l *Lexer) AcceptAnyOf(s []string, caseInsentive bool) bool { + for _, substring := range s { + if l.AcceptString(substring, caseInsentive) { + return true + } + } + return false +} + +// AcceptWhile passing symbols from input while they at `valid` string. +func (l *Lexer) AcceptWhile(valid string) bool { + isValid := false + for l.Accept(valid) { + isValid = true + } + return isValid +} + +// AcceptWhileNot passing symbols from input while they NOT in `invalid` string. +func (l *Lexer) AcceptWhileNot(invalid string) bool { + isValid := false + for !strings.ContainsRune(invalid, l.Next()) { + isValid = true + } + l.Back() + return isValid +} + +// AtStart returns true if current lexem not empty +func (l *Lexer) AtStart() bool { + return l.Pos == l.Start +} diff --git a/internal/lexer/lextype_string.go b/internal/lexer/lextype_string.go new file mode 100644 index 0000000..fe895d2 --- /dev/null +++ b/internal/lexer/lextype_string.go @@ -0,0 +1,34 @@ +// Code generated by "stringer -type=lexType"; DO NOT EDIT. + +package lexer + +import "strconv" + +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] +} + +const _lexType_name = "LEOFLErrorLObjectStartLObjectEndLObjectKeyLObjectValueLArrayStartLArrayEndLStringLNumberLBooleanLNull" + +var _lexType_index = [...]uint8{0, 4, 10, 22, 32, 42, 54, 65, 74, 81, 88, 96, 101} + +func (i lexType) String() string { + if i < 0 || i >= lexType(len(_lexType_index)-1) { + return "lexType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _lexType_name[_lexType_index[i]:_lexType_index[i+1]] +} diff --git a/internal/lexer/scanners.go b/internal/lexer/scanners.go new file mode 100644 index 0000000..6181c2d --- /dev/null +++ b/internal/lexer/scanners.go @@ -0,0 +1,32 @@ +package lexer + +func scanNumber(l *Lexer) bool { + l.AcceptWhile("0123456789") + if l.AtStart() { + // not found any digit + return false + } + l.Accept(".") + l.AcceptWhile("0123456789") + return !l.AtStart() +} + +func scanQuotedString(l *Lexer, quote rune) bool { + start := l.Pos + if l.Next() != quote { + l.Back() + return false + } + for { + ch := l.Next() + switch ch { + case eof: + l.Pos = start // Return position to start + return false // Unclosed quote string? + case '\\': + l.Next() // Skip next char + case quote: + return true // Closing quote + } + } +} diff --git a/internal/lexer/statefunc.go b/internal/lexer/statefunc.go new file mode 100644 index 0000000..8d0e42a --- /dev/null +++ b/internal/lexer/statefunc.go @@ -0,0 +1,17 @@ +package lexer + +type stateFunc func(*Lexer) stateFunc + +type stateStack []stateFunc + +func (ss *stateStack) Push(s stateFunc) { + *ss = append(*ss, s) +} + +func (ss *stateStack) Pop() (s stateFunc) { + if len(*ss) == 0 { + return nil + } + *ss, s = (*ss)[:len(*ss)-1], (*ss)[len(*ss)-1] + return s +} diff --git a/internal/lexer/states.go b/internal/lexer/states.go new file mode 100644 index 0000000..818ccf6 --- /dev/null +++ b/internal/lexer/states.go @@ -0,0 +1,110 @@ +package lexer + +func InitJson(l *Lexer) stateFunc { + ignoreWhiteSpace(l) + switch { + case l.Accept("{"): + l.Emit(LObjectStart) + return stateInObject + case l.Accept("["): + l.Emit(LArrayStart) + case l.Peek() == eof: + return nil + } + return l.Errorf("Unknown token: %s", string(l.Peek())) +} + +func stateInObject(l *Lexer) stateFunc { + // we in object, so we expect field keys and values + ignoreWhiteSpace(l) + if l.Accept("}") { + l.Emit(LObjectEnd) + // If meet close object return to previous state (including initial) + return l.PopState() + } + ignoreWhiteSpace(l) + l.Accept(",") + ignoreWhiteSpace(l) + if !scanQuotedString(l, '"') { + return l.Errorf("Unknown token: %s", string(l.Peek())) + } + l.Emit(LObjectKey) + ignoreWhiteSpace(l) + if !l.Accept(":") { + return l.Errorf("Expected ':'") + } + ignoreWhiteSpace(l) + l.Emit(LObjectValue) + switch { + case scanQuotedString(l, '"'): + l.Emit(LString) + ignoreWhiteSpace(l) + l.Accept(",") + l.Ignore() + ignoreWhiteSpace(l) + return stateInObject + case scanNumber(l): + l.Emit(LNumber) + ignoreWhiteSpace(l) + l.Accept(",") + l.Ignore() + ignoreWhiteSpace(l) + return stateInObject + case l.AcceptAnyOf([]string{"true", "false"}, true): + l.Emit(LBoolean) + ignoreWhiteSpace(l) + l.Accept(",") + l.Ignore() + ignoreWhiteSpace(l) + return stateInObject + case l.AcceptString("null", true): + l.Emit(LNull) + ignoreWhiteSpace(l) + l.Accept(",") + l.Ignore() + ignoreWhiteSpace(l) + return stateInObject + case l.Accept("{"): + l.Emit(LObjectStart) + l.PushState(stateInObject) + return stateInObject + case l.Accept("["): + l.Emit(LArrayStart) + l.PushState(stateInObject) + return stateInArray + } + return l.Errorf("Unknown token: %s", string(l.Peek())) +} + +func stateInArray(l *Lexer) stateFunc { + ignoreWhiteSpace(l) + l.Accept(",") + ignoreWhiteSpace(l) + switch { + case scanQuotedString(l, '"'): + l.Emit(LString) + case scanNumber(l): + l.Emit(LNumber) + case l.AcceptAnyOf([]string{"true", "false"}, true): + l.Emit(LBoolean) + case l.AcceptString("null", true): + l.Emit(LNull) + case l.Accept("{"): + l.Emit(LObjectStart) + l.PushState(stateInArray) + return stateInObject + case l.Accept("["): + l.Emit(LArrayStart) + l.PushState(stateInArray) + return stateInArray + case l.Accept("]"): + l.Emit(LArrayEnd) + return l.PopState() + } + return stateInArray +} + +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) +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 { - return nil, err + panic(err) + } + return n +} + +func New(factory NodeFactory) *JSON { + return &JSON{ + Factory: factory, } - return model.Query(n, query) } 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", - { - "status": "invalid" - }, + 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": "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/model/types.go b/model/types.go deleted file mode 100644 index 72dce5f..0000000 --- a/model/types.go +++ /dev/null @@ -1,19 +0,0 @@ -package model - -type NodeType string - -const ( - StringType NodeType = "string" - NumberType NodeType = "number" - ObjectType NodeType = "object" - ArrayType NodeType = "array" - 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 -} 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/lexer.go b/parser/lexer.go deleted file mode 100644 index 5034f6a..0000000 --- a/parser/lexer.go +++ /dev/null @@ -1,182 +0,0 @@ -package parser - -import ( - "fmt" - "strings" - "unicode/utf8" -) - -const eof rune = -1 - -type lexem struct { - Type lexType // Type of Lexem. - Value string // Value of Lexem. - Start int // Start position at input string. - End int // End position at input string. -} - -//go:generate stringer -type=lexType -type lexType int - -const ( - lEOF lexType = iota - lError - lObjectStart - lObjectEnd - lObjectKey - lObjectValue - lArrayStart - lArrayEnd - lString - lNumber - lBoolean - lNull -) - -// 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. - 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{ - Input: input, - Start: 0, - Pos: 0, - Output: make(chan lexem, 2), - width: 0, - } -} - -// Run lexing. -func (l *lexer) Run(init stateFunc) { - for state := init; state != nil; { - state = state(l) - } - close(l.Output) -} - -// PopState returns previous state function. -func (l *lexer) PopState() stateFunc { - return l.states.Pop() -} - -// PushState pushes state before going deeper states. -func (l *lexer) PushState(s stateFunc) { - l.states.Push(s) -} - -// Emit current lexem to output. -func (l *lexer) Emit(typ lexType) { - l.Output <- lexem{ - Type: typ, - Value: l.Input[l.Start:l.Pos], - Start: l.Start, - End: l.Pos, - } - l.Start = l.Pos -} - -// Errorf produces error lexem and stops scanning. -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, - } - return nil -} - -// Next rune from input. -func (l *lexer) Next() (r rune) { - if int(l.Pos) >= len(l.Input) { - l.width = 0 - return eof - } - r, l.width = utf8.DecodeRuneInString(l.Input[l.Pos:]) - l.Pos += l.width - return r -} - -// Back move position to previos rune. -func (l *lexer) Back() { - l.Pos -= l.width -} - -// Ignore previosly buffered text. -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) { - 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 { - if strings.ContainsRune(valid, l.Next()) { - return true - } - l.Back() - return false -} - -// AcceptString returns true if given string was at position. -func (l *lexer) AcceptString(s string, caseInsentive bool) bool { - input := l.Input[l.Start:] - if caseInsentive { - input = strings.ToLower(input) - s = strings.ToLower(s) - } - if strings.HasPrefix(input, s) { - l.width = 0 - l.Pos += len(s) - return true - } - return false -} - -// AcceptAnyOf substrings. Retuns true if any of substrings was found. -func (l *lexer) AcceptAnyOf(s []string, caseInsentive bool) bool { - for _, substring := range s { - if l.AcceptString(substring, caseInsentive) { - return true - } - } - return false -} - -// AcceptWhile passing symbols from input while they at `valid` string. -func (l *lexer) AcceptWhile(valid string) bool { - isValid := false - for l.Accept(valid) { - isValid = true - } - return isValid -} - -// AcceptWhileNot passing symbols from input while they NOT in `invalid` string. -func (l *lexer) AcceptWhileNot(invalid string) bool { - isValid := false - for !strings.ContainsRune(invalid, l.Next()) { - isValid = true - } - l.Back() - return isValid -} - -// AtStart returns true if current lexem not empty -func (l *lexer) AtStart() bool { - return l.Pos == l.Start -} diff --git a/parser/lextype_string.go b/parser/lextype_string.go deleted file mode 100644 index f34eb7c..0000000 --- a/parser/lextype_string.go +++ /dev/null @@ -1,34 +0,0 @@ -// Code generated by "stringer -type=lexType"; DO NOT EDIT. - -package parser - -import "strconv" - -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] -} - -const _lexType_name = "lEOFlErrorlObjectStartlObjectEndlObjectKeylObjectValuelArrayStartlArrayEndlStringlNumberlBooleanlNull" - -var _lexType_index = [...]uint8{0, 4, 10, 22, 32, 42, 54, 65, 74, 81, 88, 96, 101} - -func (i lexType) String() string { - if i < 0 || i >= lexType(len(_lexType_index)-1) { - return "lexType(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _lexType_name[_lexType_index[i]:_lexType_index[i+1]] -} 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/parser/scanners.go b/parser/scanners.go deleted file mode 100644 index 078f9d3..0000000 --- a/parser/scanners.go +++ /dev/null @@ -1,32 +0,0 @@ -package parser - -func scanNumber(l *lexer) bool { - l.AcceptWhile("0123456789") - if l.AtStart() { - // not found any digit - return false - } - l.Accept(".") - l.AcceptWhile("0123456789") - return !l.AtStart() -} - -func scanQuotedString(l *lexer, quote rune) bool { - start := l.Pos - if l.Next() != quote { - l.Back() - return false - } - for { - ch := l.Next() - switch ch { - case eof: - l.Pos = start // Return position to start - return false // Unclosed quote string? - case '\\': - l.Next() // Skip next char - case quote: - return true // Closing quote - } - } -} diff --git a/parser/statefunc.go b/parser/statefunc.go deleted file mode 100644 index 69d7098..0000000 --- a/parser/statefunc.go +++ /dev/null @@ -1,17 +0,0 @@ -package parser - -type stateFunc func(*lexer) stateFunc - -type stateStack []stateFunc - -func (ss *stateStack) Push(s stateFunc) { - *ss = append(*ss, s) -} - -func (ss *stateStack) Pop() (s stateFunc) { - if len(*ss) == 0 { - return nil - } - *ss, s = (*ss)[:len(*ss)-1], (*ss)[len(*ss)-1] - return s -} diff --git a/parser/states.go b/parser/states.go deleted file mode 100644 index 92c80dc..0000000 --- a/parser/states.go +++ /dev/null @@ -1,110 +0,0 @@ -package parser - -func initJson(l *lexer) stateFunc { - ignoreWhiteSpace(l) - switch { - case l.Accept("{"): - l.Emit(lObjectStart) - return stateInObject - case l.Accept("["): - l.Emit(lArrayStart) - case l.Peek() == eof: - return nil - } - return l.Errorf("Unknown token: %s", string(l.Peek())) -} - -func stateInObject(l *lexer) stateFunc { - // we in object, so we expect field keys and values - ignoreWhiteSpace(l) - if l.Accept("}") { - l.Emit(lObjectEnd) - // If meet close object return to previous state (including initial) - return l.PopState() - } - ignoreWhiteSpace(l) - l.Accept(",") - ignoreWhiteSpace(l) - if !scanQuotedString(l, '"') { - return l.Errorf("Unknown token: %s", string(l.Peek())) - } - l.Emit(lObjectKey) - ignoreWhiteSpace(l) - if !l.Accept(":") { - return l.Errorf("Expected ':'") - } - ignoreWhiteSpace(l) - l.Emit(lObjectValue) - switch { - case scanQuotedString(l, '"'): - l.Emit(lString) - ignoreWhiteSpace(l) - l.Accept(",") - l.Ignore() - ignoreWhiteSpace(l) - return stateInObject - case scanNumber(l): - l.Emit(lNumber) - ignoreWhiteSpace(l) - l.Accept(",") - l.Ignore() - ignoreWhiteSpace(l) - return stateInObject - case l.AcceptAnyOf([]string{"true", "false"}, true): - l.Emit(lBoolean) - ignoreWhiteSpace(l) - l.Accept(",") - l.Ignore() - ignoreWhiteSpace(l) - return stateInObject - case l.AcceptString("null", true): - l.Emit(lNull) - ignoreWhiteSpace(l) - l.Accept(",") - l.Ignore() - ignoreWhiteSpace(l) - return stateInObject - case l.Accept("{"): - l.Emit(lObjectStart) - l.PushState(stateInObject) - return stateInObject - case l.Accept("["): - l.Emit(lArrayStart) - l.PushState(stateInObject) - return stateInArray - } - return l.Errorf("Unknown token: %s", string(l.Peek())) -} - -func stateInArray(l *lexer) stateFunc { - ignoreWhiteSpace(l) - l.Accept(",") - ignoreWhiteSpace(l) - switch { - case scanQuotedString(l, '"'): - l.Emit(lString) - case scanNumber(l): - l.Emit(lNumber) - case l.AcceptAnyOf([]string{"true", "false"}, true): - l.Emit(lBoolean) - case l.AcceptString("null", true): - l.Emit(lNull) - case l.Accept("{"): - l.Emit(lObjectStart) - l.PushState(stateInArray) - return stateInObject - case l.Accept("["): - l.Emit(lArrayStart) - l.PushState(stateInArray) - return stateInArray - case l.Accept("]"): - l.Emit(lArrayEnd) - return l.PopState() - } - return stateInArray -} - -func ignoreWhiteSpace(l *lexer) { - l.AcceptWhile(" \n\t") // ignore whitespaces - l.Ignore() -} 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/types.go b/types.go new file mode 100644 index 0000000..090eeac --- /dev/null +++ b/types.go @@ -0,0 +1,12 @@ +package json + +type NodeType string + +const ( + StringType NodeType = "string" + NumberType NodeType = "number" + ObjectType NodeType = "object" + ArrayType NodeType = "array" + BooleanType NodeType = "boolean" + NullType NodeType = "null" +) -- cgit v1.2.3