Better interface
This commit is contained in:
parent
4934a51c69
commit
4054a50ce4
18 changed files with 203 additions and 370 deletions
59
README.md
59
README.md
|
@ -20,61 +20,4 @@ func Query(json string, query string) (*model.Node, error)
|
|||
func QueryArray(json string, query []string) (*model.Node, error)
|
||||
```
|
||||
|
||||
## Node methods
|
||||
|
||||
```go
|
||||
package model // import "go.neonxp.dev/json/model"
|
||||
|
||||
// Node of JSON tree
|
||||
type Node struct {
|
||||
Type NodeType
|
||||
}
|
||||
|
||||
// NewNode creates new node from value
|
||||
func NewNode(value any) *Node
|
||||
|
||||
// Get node from object by key
|
||||
func (n *Node) Get(key string) (*Node, error)
|
||||
|
||||
// Index returns node by index from array
|
||||
func (n *Node) Index(idx int) (*Node, error)
|
||||
|
||||
// Set node to object by key
|
||||
func (n *Node) Set(key string, value *Node) error
|
||||
|
||||
// SetIndex sets node to array by index
|
||||
func (n *Node) SetIndex(idx int, value *Node) error
|
||||
|
||||
// SetValue to node
|
||||
func (n *Node) SetValue(value any)
|
||||
|
||||
// Map callback to each key value pair of object
|
||||
func (n *Node) Map(cb func(key string, value *Node) (*Node, error)) error
|
||||
|
||||
// Each applies callback to each element of array
|
||||
func (n *Node) Each(cb func(idx int, value *Node) error) error
|
||||
|
||||
// Query returns node by array query
|
||||
func (n *Node) Query(query []string) (*Node, error)
|
||||
|
||||
// Value returns value of node
|
||||
func (n *Node) Value() any
|
||||
|
||||
// MarshalJSON to []byte
|
||||
func (n *Node) MarshalJSON() ([]byte, error)
|
||||
|
||||
// Merge two object or array nodes
|
||||
func (n *Node) Merge(node *Node) error
|
||||
|
||||
// Len returns length of object or array nodes
|
||||
func (n *Node) Len() (int, error)
|
||||
|
||||
// Compare current node with another node
|
||||
func (n *Node) Compare(op Operand, node *Node) bool
|
||||
|
||||
// Remove by key from object
|
||||
func (n *Node) Remove(key string) error
|
||||
|
||||
// RemoveIndex from array
|
||||
func (n *Node) RemoveIndex(idx int) error
|
||||
```
|
||||
Other methods: https://pkg.go.dev/go.neonxp.dev/json
|
12
json.go
12
json.go
|
@ -8,29 +8,29 @@ import (
|
|||
)
|
||||
|
||||
// Marshal Node tree to []byte
|
||||
func Marshal(node *model.Node) ([]byte, error) {
|
||||
func Marshal(node model.Node) ([]byte, error) {
|
||||
return node.MarshalJSON()
|
||||
}
|
||||
|
||||
// Unmarshal data to Node tree
|
||||
func Unmarshal(data []byte) (*model.Node, error) {
|
||||
func Unmarshal(data []byte) (model.Node, error) {
|
||||
return parser.Parse(string(data))
|
||||
}
|
||||
|
||||
// Query returns node by query string (dot notation)
|
||||
func Query(json string, query string) (*model.Node, error) {
|
||||
func Query(json string, query string) (model.Node, error) {
|
||||
n, err := parser.Parse(json)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n.Query(strings.Split(query, "."))
|
||||
return model.Query(n, strings.Split(query, "."))
|
||||
}
|
||||
|
||||
// QueryArray returns node by array query
|
||||
func QueryArray(json string, query []string) (*model.Node, error) {
|
||||
func QueryArray(json string, query []string) (model.Node, error) {
|
||||
n, err := parser.Parse(json)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n.Query(query)
|
||||
return model.Query(n, query)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ func TestQuery(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *model.Node
|
||||
want model.Node
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
package model
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Index returns node by index from array
|
||||
func (n *Node) Index(idx int) (*Node, error) {
|
||||
arrlen := len(n.ArrayValue)
|
||||
if idx >= arrlen {
|
||||
return nil, fmt.Errorf("index %d out of range (len=%d)", idx, arrlen)
|
||||
}
|
||||
return n.ArrayValue[idx], nil
|
||||
}
|
||||
|
||||
// SetIndex sets node to array by index
|
||||
func (n *Node) SetIndex(idx int, value *Node) error {
|
||||
arrlen := len(n.ArrayValue)
|
||||
if idx >= arrlen {
|
||||
return fmt.Errorf("index %d out of range (len=%d)", idx, arrlen)
|
||||
}
|
||||
n.ArrayValue[idx] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
// Each applies callback to each element of array
|
||||
func (n *Node) Each(cb func(idx int, value *Node) error) error {
|
||||
for i, v := range n.ArrayValue {
|
||||
if err := cb(i, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveIndex from array
|
||||
func (n *Node) RemoveIndex(idx int) error {
|
||||
arrlen := len(n.ArrayValue)
|
||||
if idx >= arrlen {
|
||||
return fmt.Errorf("index %d out of range (len=%d)", idx, arrlen)
|
||||
}
|
||||
n.ArrayValue = append(n.ArrayValue[:idx], n.ArrayValue[idx:]...)
|
||||
return nil
|
||||
}
|
48
model/arrayNode.go
Normal file
48
model/arrayNode.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ArrayNode struct {
|
||||
Value NodeArrayValue
|
||||
}
|
||||
|
||||
func (n ArrayNode) Type() NodeType {
|
||||
return ArrayType
|
||||
}
|
||||
|
||||
func (n *ArrayNode) MarshalJSON() ([]byte, error) {
|
||||
result := make([][]byte, 0, len(n.Value))
|
||||
for _, v := range n.Value {
|
||||
b, err := v.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, b)
|
||||
}
|
||||
return bytes.Join(
|
||||
[][]byte{
|
||||
[]byte("["),
|
||||
bytes.Join(result, []byte(", ")),
|
||||
[]byte("]"),
|
||||
}, []byte("")), nil
|
||||
}
|
||||
|
||||
func (n *ArrayNode) Index(idx int) (Node, error) {
|
||||
if len(n.Value) <= idx {
|
||||
return nil, fmt.Errorf("index %d out of range [0...%d]", idx, len(n.Value)-1)
|
||||
}
|
||||
return n.Value[idx], nil
|
||||
}
|
||||
|
||||
func (n *ArrayNode) Merge(n2 *ArrayNode) {
|
||||
n.Value = append(n.Value, n2.Value...)
|
||||
}
|
||||
|
||||
func (n *ArrayNode) Len() int {
|
||||
return len(n.Value)
|
||||
}
|
||||
|
||||
type NodeArrayValue []Node
|
16
model/booleanNode.go
Normal file
16
model/booleanNode.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package model
|
||||
|
||||
type BooleanNode struct {
|
||||
Value bool
|
||||
}
|
||||
|
||||
func (n BooleanNode) Type() NodeType {
|
||||
return BooleanType
|
||||
}
|
||||
|
||||
func (n *BooleanNode) MarshalJSON() ([]byte, error) {
|
||||
if n.Value {
|
||||
return []byte("true"), nil
|
||||
}
|
||||
return []byte("false"), nil
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package model
|
||||
|
||||
// Compare current node with another node
|
||||
func (n *Node) Compare(op Operand, node *Node) bool {
|
||||
switch op {
|
||||
case OpEq:
|
||||
return n.Value() == node.Value()
|
||||
case OpNeq:
|
||||
return n.Value() != node.Value()
|
||||
case OpLess:
|
||||
return less(n, node)
|
||||
case OpGt:
|
||||
return less(node, n)
|
||||
case OpLessEq:
|
||||
return less(n, node) || n.Value() == node.Value()
|
||||
case OpGtEq:
|
||||
return less(node, n) || n.Value() == node.Value()
|
||||
case OpIn:
|
||||
if n.Type != ArrayNode {
|
||||
return false
|
||||
}
|
||||
for _, v := range n.ArrayValue {
|
||||
if v.Value() == node.Value() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func less(n1 *Node, n2 *Node) bool {
|
||||
if n1.Type != n2.Type {
|
||||
return false
|
||||
}
|
||||
switch n1.Type {
|
||||
case NumberNode:
|
||||
return n1.NumberValue < n2.NumberValue
|
||||
case StringNode:
|
||||
return n1.StringValue < n2.StringValue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type Operand int
|
||||
|
||||
const (
|
||||
OpEq Operand = iota
|
||||
OpNeq
|
||||
OpLess
|
||||
OpLessEq
|
||||
OpGt
|
||||
OpGtEq
|
||||
OpIn
|
||||
)
|
52
model/map.go
52
model/map.go
|
@ -1,52 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Get node from object by key
|
||||
func (n *Node) Get(key string) (*Node, error) {
|
||||
if n.Type != ObjectNode {
|
||||
return nil, fmt.Errorf("node must be object, got %s", n.Type)
|
||||
}
|
||||
node, ok := n.ObjectValue[key]
|
||||
if !ok {
|
||||
keys := make([]string, 0, len(n.ObjectValue))
|
||||
for k := range n.ObjectValue {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return nil, fmt.Errorf("field '%s' does not exist in object (keys %s)", key, strings.Join(keys, ", "))
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// Set node to object by key
|
||||
func (n *Node) Set(key string, value Node) error {
|
||||
if n.Type != ObjectNode {
|
||||
return fmt.Errorf("node must be object, got %s", n.Type)
|
||||
}
|
||||
n.ObjectValue[key] = &value
|
||||
return nil
|
||||
}
|
||||
|
||||
// Map callback to each key value pair of object
|
||||
func (n *Node) Map(cb func(key string, value *Node) (*Node, error)) error {
|
||||
for k, v := range n.ObjectValue {
|
||||
newNode, err := cb(k, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.ObjectValue[k] = newNode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove by key from object
|
||||
func (n *Node) Remove(key string) error {
|
||||
if n.Type != ObjectNode {
|
||||
return fmt.Errorf("node must be object, got %s", n.Type)
|
||||
}
|
||||
delete(n.ObjectValue, key)
|
||||
return nil
|
||||
}
|
159
model/node.go
159
model/node.go
|
@ -1,154 +1,39 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Node of JSON tree
|
||||
type Node struct {
|
||||
Type NodeType
|
||||
Meta NodeObjectValue
|
||||
StringValue string
|
||||
NumberValue float64
|
||||
ObjectValue NodeObjectValue
|
||||
ArrayValue NodeArrayValue
|
||||
BooleanValue bool
|
||||
type Node interface {
|
||||
Type() NodeType
|
||||
MarshalJSON() ([]byte, error)
|
||||
}
|
||||
|
||||
// NewNode creates new node from value
|
||||
func NewNode(value any) *Node {
|
||||
n := new(Node)
|
||||
n.SetValue(value)
|
||||
return n
|
||||
}
|
||||
|
||||
// Value returns value of node
|
||||
func (n *Node) Value() any {
|
||||
switch n.Type {
|
||||
case StringNode:
|
||||
return n.StringValue
|
||||
case NumberNode:
|
||||
return n.NumberValue
|
||||
case ObjectNode:
|
||||
return n.ObjectValue
|
||||
case ArrayNode:
|
||||
return n.ArrayValue
|
||||
case BooleanNode:
|
||||
return n.BooleanValue
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetValue to node
|
||||
func (n *Node) SetValue(value any) {
|
||||
func NewNode(value any) Node {
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
n.Type = StringNode
|
||||
n.StringValue = value
|
||||
return &StringNode{
|
||||
Value: value,
|
||||
}
|
||||
case float64:
|
||||
n.Type = NumberNode
|
||||
n.NumberValue = value
|
||||
return &NumberNode{
|
||||
Value: value,
|
||||
}
|
||||
case int:
|
||||
n.Type = NumberNode
|
||||
n.NumberValue = float64(value)
|
||||
return &NumberNode{
|
||||
Value: float64(value),
|
||||
}
|
||||
case NodeObjectValue:
|
||||
n.Type = ObjectNode
|
||||
meta, hasMeta := value["@"]
|
||||
if hasMeta {
|
||||
n.Meta = meta.ObjectValue
|
||||
delete(value, "@")
|
||||
return &ObjectNode{
|
||||
Value: value,
|
||||
}
|
||||
n.ObjectValue = value
|
||||
case NodeArrayValue:
|
||||
n.Type = ArrayNode
|
||||
n.ArrayValue = value
|
||||
return &ArrayNode{
|
||||
Value: value,
|
||||
}
|
||||
case bool:
|
||||
n.Type = BooleanNode
|
||||
n.BooleanValue = value
|
||||
return &BooleanNode{
|
||||
Value: value,
|
||||
}
|
||||
default:
|
||||
n.Type = NullNode
|
||||
return NullNode{}
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON to []byte
|
||||
func (n *Node) MarshalJSON() ([]byte, error) {
|
||||
switch n.Type {
|
||||
case StringNode:
|
||||
return []byte(`"` + n.StringValue + `"`), nil
|
||||
case NumberNode:
|
||||
return []byte(strconv.FormatFloat(n.NumberValue, 'g', -1, 64)), nil
|
||||
case ObjectNode:
|
||||
result := make([][]byte, 0, len(n.ObjectValue))
|
||||
for k, v := range n.ObjectValue {
|
||||
b, err := v.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, []byte(fmt.Sprintf("\"%s\": %s", k, b)))
|
||||
}
|
||||
return bytes.Join(
|
||||
[][]byte{
|
||||
[]byte("{"),
|
||||
bytes.Join(result, []byte(", ")),
|
||||
[]byte("}"),
|
||||
}, []byte("")), nil
|
||||
case ArrayNode:
|
||||
result := make([][]byte, 0, len(n.ArrayValue))
|
||||
for _, v := range n.ArrayValue {
|
||||
b, err := v.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, b)
|
||||
}
|
||||
return bytes.Join(
|
||||
[][]byte{
|
||||
[]byte("["),
|
||||
bytes.Join(result, []byte(", ")),
|
||||
[]byte("]"),
|
||||
}, []byte("")), nil
|
||||
case BooleanNode:
|
||||
if n.BooleanValue {
|
||||
return []byte("true"), nil
|
||||
}
|
||||
return []byte("false"), nil
|
||||
default:
|
||||
return []byte("null"), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Merge two object or array nodes
|
||||
func (n *Node) Merge(node *Node) error {
|
||||
if n.Type != node.Type {
|
||||
return fmt.Errorf("can't merge nodes of different types")
|
||||
}
|
||||
switch n.Type {
|
||||
case ObjectNode:
|
||||
for k, v := range node.ObjectValue {
|
||||
n.ObjectValue[k] = v
|
||||
}
|
||||
case ArrayNode:
|
||||
n.ArrayValue = append(n.ArrayValue, node.ArrayValue...)
|
||||
default:
|
||||
return fmt.Errorf("merge not implemented for type %s", n.Type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len returns length of object or array nodes
|
||||
func (n *Node) Len() (int, error) {
|
||||
switch n.Type {
|
||||
case ObjectNode:
|
||||
return len(n.ObjectValue), nil
|
||||
case ArrayNode:
|
||||
return len(n.ArrayValue), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("merge not implemented for type %s", n.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Meta represents node metadata
|
||||
type Meta map[string]any
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
func TestNode_MarshalJSON(t *testing.T) {
|
||||
type fields struct {
|
||||
node *Node
|
||||
node Node
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
11
model/nullNode.go
Normal file
11
model/nullNode.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package model
|
||||
|
||||
type NullNode struct{}
|
||||
|
||||
func (n NullNode) Type() NodeType {
|
||||
return NullType
|
||||
}
|
||||
|
||||
func (n NullNode) MarshalJSON() ([]byte, error) {
|
||||
return []byte("null"), nil
|
||||
}
|
15
model/numberNode.go
Normal file
15
model/numberNode.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package model
|
||||
|
||||
import "strconv"
|
||||
|
||||
type NumberNode struct {
|
||||
Value float64
|
||||
}
|
||||
|
||||
func (n NumberNode) Type() NodeType {
|
||||
return NumberType
|
||||
}
|
||||
|
||||
func (n *NumberNode) MarshalJSON() ([]byte, error) {
|
||||
return []byte(strconv.FormatFloat(n.Value, 'g', -1, 64)), nil
|
||||
}
|
53
model/objectNode.go
Normal file
53
model/objectNode.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ObjectNode struct {
|
||||
Value map[string]Node
|
||||
}
|
||||
|
||||
func (n ObjectNode) Type() NodeType {
|
||||
return ObjectType
|
||||
}
|
||||
|
||||
func (n *ObjectNode) MarshalJSON() ([]byte, error) {
|
||||
result := make([][]byte, 0, len(n.Value))
|
||||
for k, v := range n.Value {
|
||||
b, err := v.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, []byte(fmt.Sprintf("\"%s\": %s", k, b)))
|
||||
}
|
||||
return bytes.Join(
|
||||
[][]byte{
|
||||
[]byte("{"),
|
||||
bytes.Join(result, []byte(", ")),
|
||||
[]byte("}"),
|
||||
}, []byte("")), nil
|
||||
}
|
||||
|
||||
func (n *ObjectNode) Set(k string, v any) {
|
||||
n.Value[k] = NewNode(v)
|
||||
}
|
||||
|
||||
func (n *ObjectNode) Get(k string) (Node, error) {
|
||||
child, ok := n.Value[k]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("field %s not found", k)
|
||||
}
|
||||
return child, nil
|
||||
}
|
||||
|
||||
func (n *ObjectNode) Merge(n2 *ObjectNode) {
|
||||
for k, v := range n2.Value {
|
||||
n.Value[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (n *ObjectNode) Len() int {
|
||||
return len(n.Value)
|
||||
}
|
|
@ -6,13 +6,13 @@ import (
|
|||
)
|
||||
|
||||
// Query returns node by array query
|
||||
func (n *Node) Query(query []string) (*Node, error) {
|
||||
func Query(n Node, query []string) (Node, error) {
|
||||
if len(query) == 0 {
|
||||
return n, nil
|
||||
}
|
||||
head, rest := query[0], query[1:]
|
||||
switch n.Type {
|
||||
case ArrayNode:
|
||||
switch n := n.(type) {
|
||||
case *ArrayNode:
|
||||
idx, err := strconv.Atoi(head)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("index must be a number, got %s", head)
|
||||
|
@ -21,13 +21,13 @@ func (n *Node) Query(query []string) (*Node, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return next.Query(rest)
|
||||
case ObjectNode:
|
||||
return Query(next, rest)
|
||||
case *ObjectNode:
|
||||
next, err := n.Get(head)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return next.Query(rest)
|
||||
return Query(next, rest)
|
||||
}
|
||||
return nil, fmt.Errorf("can't get %s from node type %s", head, n.Type)
|
||||
return nil, fmt.Errorf("can't get %s from node type %s", head, n.Type())
|
||||
}
|
||||
|
|
13
model/stringNode.go
Normal file
13
model/stringNode.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package model
|
||||
|
||||
type StringNode struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (n StringNode) Type() NodeType {
|
||||
return StringType
|
||||
}
|
||||
|
||||
func (n *StringNode) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + n.Value + `"`), nil
|
||||
}
|
|
@ -3,18 +3,16 @@ package model
|
|||
type NodeType string
|
||||
|
||||
const (
|
||||
StringNode NodeType = "string"
|
||||
NumberNode NodeType = "number"
|
||||
ObjectNode NodeType = "object"
|
||||
ArrayNode NodeType = "array"
|
||||
BooleanNode NodeType = "boolean"
|
||||
NullNode NodeType = "null"
|
||||
StringType NodeType = "string"
|
||||
NumberType NodeType = "number"
|
||||
ObjectType NodeType = "object"
|
||||
ArrayType NodeType = "array"
|
||||
BooleanType NodeType = "boolean"
|
||||
NullType NodeType = "null"
|
||||
)
|
||||
|
||||
type NodeObjectValue map[string]*Node
|
||||
type NodeObjectValue map[string]Node
|
||||
|
||||
func (n NodeObjectValue) Set(k string, v any) {
|
||||
n[k] = NewNode(v)
|
||||
}
|
||||
|
||||
type NodeArrayValue []*Node
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"go.neonxp.dev/json/model"
|
||||
)
|
||||
|
||||
func Parse(json string) (*model.Node, error) {
|
||||
func Parse(json string) (model.Node, error) {
|
||||
l := newLexer(json)
|
||||
go l.Run(initJson)
|
||||
n, err := parse(l.Output)
|
||||
|
|
|
@ -14,7 +14,7 @@ func TestParse(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *model.Node
|
||||
want model.Node
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue