Skip to content

Commit 7f00c1e

Browse files
Merge pull request #18 from the-hotels-network/DBE-5763
feat: allow find or set value in JSON using path like `a.b.c`.
2 parents 6780a01 + 7ee998d commit 7f00c1e

File tree

4 files changed

+140
-2
lines changed

4 files changed

+140
-2
lines changed

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
- name: Set up Go
1212
uses: actions/setup-go@v3
1313
with:
14-
go-version: 1.19
14+
go-version: 1.24
1515

1616
- name: Format
1717
run: diff -u <(echo -n) <(gofmt -d ./)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ A [Tinybird](https://www.tinybird.co/) module for Go. Why need this module? It p
77
- Lightweight and fast.
88
- Native Go implementation. No C-bindings, just pure Go
99
- Connection pooling for HTTP.
10+
- Read and write values in nested JSON structures using path-based access.
1011
- Test your code with mocks.
1112
- Allow JSON, [NDJSON](http://ndjson.org/) and CSV between tinybird and this module.
1213
- Parallelize HTTP requests.

data.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package tinybird
22

3-
import "encoding/json"
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"strings"
7+
)
48

59
// Row is a generic map-based structure that can hold fields of any type.
610
// Each key corresponds to a field name, and each value can be any Go type.
@@ -41,3 +45,95 @@ func (d Data) ToString() (out string) {
4145

4246
return string(tmp)
4347
}
48+
49+
// Get retrieves a value from the first row of the Data structure
50+
// by traversing a dot-separated path.
51+
//
52+
// The path represents nested keys within Row maps (e.g. "a.b.c").
53+
// If the Data slice is empty, the path does not exist, or an
54+
// intermediate value is not a Row, Get returns nil.
55+
func (d Data) Get(in string) any {
56+
if len(d) == 0 {
57+
return nil
58+
}
59+
60+
parts := strings.Split(in, ".")
61+
62+
for _, row := range d {
63+
if v, ok := get(row, parts); ok {
64+
return v
65+
}
66+
}
67+
68+
return nil
69+
}
70+
71+
func get(row Row, parts []string) (any, bool) {
72+
var current any = row
73+
74+
for _, part := range parts {
75+
r, ok := current.(Row)
76+
if !ok {
77+
return nil, false
78+
}
79+
80+
current, ok = r[part]
81+
if !ok {
82+
return nil, false
83+
}
84+
}
85+
86+
return current, true
87+
}
88+
89+
// Set assigns a value in the first row of the Data structure
90+
// at the location specified by a dot-separated path.
91+
//
92+
// Intermediate Row nodes are created as needed if they do not exist.
93+
// An error is returned if Data is empty, the path is empty, or
94+
// a non-Row value is encountered while traversing the path.
95+
func (d *Data) Set(in string, value any) error {
96+
if d == nil {
97+
return errors.New("nil Data")
98+
}
99+
100+
parts := strings.Split(in, ".")
101+
102+
for i := range *d {
103+
row := (*d)[i]
104+
105+
if _, ok := get(row, parts[:len(parts)-1]); ok {
106+
return set(row, parts, value)
107+
}
108+
}
109+
110+
return errors.New("path not found")
111+
}
112+
113+
func set(row Row, parts []string, value any) error {
114+
current := row
115+
116+
for i, p := range parts {
117+
if i == len(parts)-1 {
118+
current[p] = value
119+
return nil
120+
}
121+
122+
next, exists := current[p]
123+
if !exists {
124+
child := Row{}
125+
current[p] = child
126+
current = child
127+
continue
128+
}
129+
130+
child, ok := next.(Row)
131+
if !ok {
132+
return errors.New("path collision at " + p)
133+
}
134+
135+
current = child
136+
}
137+
138+
return nil
139+
}

data_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,44 @@ func TestToString_OnNilAndEmpty(t *testing.T) {
7070
assert.Equal(t, "null", dNil.ToString())
7171
assert.Equal(t, "[]", dEmpty.ToString())
7272
}
73+
74+
func TestGet(t *testing.T) {
75+
d := tinybird.Data{
76+
{"a": nil},
77+
{"b": tinybird.Row{"a": 1}},
78+
{"b": tinybird.Row{"b": 2}},
79+
{"b": tinybird.Row{"c": 3}},
80+
{"c": tinybird.Row{"a": 4}},
81+
{"d": tinybird.Row{"a": 5}},
82+
{"d": tinybird.Row{"b": tinybird.Row{"a": 6}}},
83+
{"f": 7},
84+
}
85+
86+
assert.Nil(t, d.Get(""))
87+
assert.Nil(t, d.Get("a"))
88+
assert.Nil(t, d.Get("e"))
89+
assert.Equal(t, 7, d.Get("f"))
90+
assert.Equal(t, 4, d.Get("c.a"))
91+
assert.Equal(t, 6, d.Get("d.b.a"))
92+
assert.Equal(t, tinybird.Row{"a": 4}, d.Get("c"))
93+
}
94+
95+
func TestSet(t *testing.T) {
96+
d := tinybird.Data{
97+
{"a": nil},
98+
{"b": tinybird.Row{"a": 1}},
99+
{"b": tinybird.Row{"b": 2}},
100+
{"b": tinybird.Row{"c": 3}},
101+
{"c": tinybird.Row{"a": 4}},
102+
{"d": tinybird.Row{"a": 5}},
103+
{"d": tinybird.Row{"b": tinybird.Row{"a": 6}}},
104+
{"f": 7},
105+
}
106+
107+
assert.Nil(t, d.Set("a", 0))
108+
assert.Equal(t, 0, d.Get("a"))
109+
assert.Nil(t, d.Set("b.b", 22))
110+
assert.Equal(t, 22, d.Get("b.b"))
111+
assert.Nil(t, d.Set("d.b.a", 66))
112+
assert.Equal(t, 66, d.Get("d.b.a"))
113+
}

0 commit comments

Comments
 (0)