Skip to content
This repository was archived by the owner on Apr 10, 2024. It is now read-only.

Commit 16d9668

Browse files
committed
Initial spike on dot notation
Adds simple implementation of dot notation extraction for nested data structures in mongo data objects. Allows for flattening of: ``` {"name": {"first": "Jane", "last": "Doe"}} ``` By using "name.first" as the accessor for "Jane". This is accomplished by using gjson's accessors of JSON strings.
1 parent 0916d7e commit 16d9668

File tree

4 files changed

+46
-33
lines changed

4 files changed

+46
-33
lines changed

glide.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

glide.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ import:
3636
- package: github.com/heroku/rollrus
3737
- package: github.com/pkg/errors
3838
- package: github.com/stvp/roll
39+
- package: github.com/tidwall/gjson

utils.go

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99
"time"
1010

1111
log "github.com/Sirupsen/logrus"
12+
"github.com/tidwall/gjson"
1213

1314
rollus "github.com/heroku/rollrus"
1415
"github.com/rwynn/gtm"
15-
"gopkg.in/mgo.v2/bson"
1616
)
1717

1818
func FetchEnvsAndFlags() (e Env) {
@@ -116,47 +116,34 @@ func isActionableOperation(filters ...func() bool) bool {
116116
}
117117

118118
// SanitizeData handles type inconsistency between mongo and pg
119+
// and flattens the data from a potentially nested data struct
120+
// into a flattened struct using gjson.
119121
func SanitizeData(pgFields Fields, op *gtm.Op) map[string]interface{} {
120122
if !IsInsertUpdateDelete(op) {
121123
return make(map[string]interface{})
122124
}
123125

124-
newData := op.Data
126+
newData, err := json.Marshal(op.Data)
127+
parsed := gjson.ParseBytes(newData)
128+
output := make(map[string]interface{})
129+
if err != nil {
130+
log.Errorf("Failed to marshal op.Data into json %s", err.Error())
131+
}
125132
// Normalize data map to always include the Id with conversion
126133
if op.Id != nil {
127-
newData["_id"] = op.Id
134+
output["_id"] = op.Id
128135
}
136+
129137
for k, v := range pgFields {
130-
// Guard against nil values sneaking into dataset
131-
if v.Mongo.Type == "id" && newData[k] != nil {
132-
newData[k] = newData[k].(bson.ObjectId).Hex()
133-
} else if sym, ok := newData[k].(bson.ObjectId); ok {
134-
newData[k] = sym.Hex()
135-
} else if sym, ok := newData[k].(bson.Symbol); ok {
136-
// Handle bson.Symbols which are unknown types for SQL driver
137-
newData[k] = string(sym)
138-
} else if sym, ok := newData[k].(map[string]interface{}); ok {
139-
// Convert hashes to json strings
140-
js, err := json.Marshal(sym)
141-
if err == nil {
142-
newData[k] = js
143-
}
144-
} else if sym, ok := newData[k].([]interface{}); ok {
145-
// Convert array objects to json strings
146-
js, err := json.Marshal(sym)
147-
if err == nil {
148-
newData[k] = js
149-
}
150-
} else if v.Mongo.Type == "object" {
151-
// Convert objects to json strings
152-
// TODO: deprecate this since we type check elsewhere
153-
js, err := json.Marshal(newData[k])
154-
if err == nil {
155-
newData[k] = js
156-
}
138+
// Dot notation extraction
139+
maybe := parsed.Get(k)
140+
if !maybe.Exists() {
141+
continue
157142
}
143+
144+
output[v.Postgres.Name] = maybe.Value()
158145
}
159-
return newData
146+
return output
160147
}
161148

162149
func createFanKey(db string, collection string) string {

utils_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,29 @@ func (s *MySuite) TestSanitizeData(c *C) {
2727
actual := m.SanitizeData(BuildFields("_id", "name", "age", "location_id"), t.op)
2828
c.Check(actual, DeepEquals, t.result)
2929
}
30+
31+
// Test nested data structures
32+
test1Mongo := m.Mongo{}
33+
test1Postgres := m.Postgres{}
34+
test1Mongo.Name = "name.first"
35+
test1Postgres.Name = "name_first"
36+
field := m.Field{}
37+
field.Mongo = test1Mongo
38+
field.Postgres = test1Postgres
39+
nameFirst := m.Fields{"name.first": field}
40+
singleNested := map[string]interface{}{"name": map[string]interface{}{"first": "John", "last": "Doe"}}
41+
singleNestedResult := map[string]interface{}{"name_first": "John"}
42+
var nested = []struct {
43+
op *gtm.Op
44+
fields m.Fields
45+
result map[string]interface{}
46+
}{
47+
{&gtm.Op{Operation: "i", Data: singleNested}, nameFirst, singleNestedResult},
48+
}
49+
for _, t := range nested {
50+
actual := m.SanitizeData(t.fields, t.op)
51+
c.Check(actual, DeepEquals, t.result)
52+
}
3053
}
3154

3255
func (s *MySuite) TestEnsureOpHasAllFieldsWhenEmpty(c *C) {

0 commit comments

Comments
 (0)