Skip to content

Add support for byteArray, default values and Object Processing along with JSON Array #100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 98 additions & 43 deletions jsonq.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ import (
"io/ioutil"
)

// Available named error values
var (
ErrUnsupportedType = fmt.Errorf("gojsonq: Unsupported Type")
ErrNoRecordFound = fmt.Errorf("gojsonq: No Record Found")
)

// New returns a new instance of JSONQ
func New(options ...OptionFunc) *JSONQ {
jq := &JSONQ{
queryMap: defaultQueries(),
option: option{
decoder: &DefaultDecoder{},
separator: defaultSeparator,
defaults: make(map[string]interface{}),
},
}
for _, option := range options {
Expand Down Expand Up @@ -46,7 +53,7 @@ type JSONQ struct {
jsonContent interface{} // copy of original decoded json data for further processing
queryIndex int // contains number of orWhere query call
queries [][]query // nested queries
attributes []string // select attributes that will be available in final resuls
attributes []string // select attributes that will be available in final results
offsetRecords int // number of records that will be skipped in final result
limitRecords int // number of records that will be available in final result
distinctProperty string // contain the distinct attribute name
Expand All @@ -55,7 +62,7 @@ type JSONQ struct {

// String satisfies stringer interface
func (j *JSONQ) String() string {
return fmt.Sprintf("\nContent: %s\nQueries:%v\n", string(j.raw), j.queries)
return fmt.Sprintf("\nContent: %s\nQueries: %v\nAttributes: %v", string(j.raw), j.queries, j.attributes)
}

// decode decodes the raw message to Go data structure
Expand Down Expand Up @@ -97,6 +104,12 @@ func (j *JSONQ) FromString(str string) *JSONQ {
return j.decode() // handle error
}

// FromByteArray reads the content from valid json/xml/csv/yml string
func (j *JSONQ) FromByteArray(bytes []byte) *JSONQ {
j.raw = bytes
return j.decode() // handle error
}

// Reader reads the json content from io reader
func (j *JSONQ) Reader(r io.Reader) *JSONQ {
buf := new(bytes.Buffer)
Expand Down Expand Up @@ -310,24 +323,26 @@ func (j *JSONQ) findInArray(aa []interface{}) []interface{} {
result := make([]interface{}, 0)
for _, a := range aa {
if m, ok := a.(map[string]interface{}); ok {
result = append(result, j.findInMap(m)...)
r := j.findInMap(m)
if r != empty {
result = append(result, r)
}
}
}
return result
}

// findInMap traverses through a map and returns the matched value list.
// This helps to process Where/OrWhere queries
func (j *JSONQ) findInMap(vm map[string]interface{}) []interface{} {
result := make([]interface{}, 0)
func (j *JSONQ) findInMap(vm map[string]interface{}) interface{} {
orPassed := false
for _, qList := range j.queries {
andPassed := true
for _, q := range qList {
cf, ok := j.queryMap[q.operator]
if !ok {
j.addError(fmt.Errorf("invalid operator %s", q.operator))
return result
return empty
}
nv, errnv := getNestedValue(vm, q.key, j.option.separator)
if errnv != nil {
Expand All @@ -344,15 +359,18 @@ func (j *JSONQ) findInMap(vm map[string]interface{}) []interface{} {
orPassed = orPassed || andPassed
}
if orPassed {
result = append(result, vm)
return vm
}
return result
return empty
}

// processQuery makes the result
func (j *JSONQ) processQuery() *JSONQ {
if aa, ok := j.jsonContent.([]interface{}); ok {
j.jsonContent = j.findInArray(aa)
switch v := j.jsonContent.(type) {
case []interface{}:
j.jsonContent = j.findInArray(v)
case map[string]interface{}:
j.jsonContent = j.findInMap(v)
}
return j
}
Expand All @@ -376,8 +394,9 @@ func (j *JSONQ) prepare() *JSONQ {
func (j *JSONQ) GroupBy(property string) *JSONQ {
j.prepare()

dt := map[string][]interface{}{}
if aa, ok := j.jsonContent.([]interface{}); ok {
dt := map[string][]interface{}{}

for _, a := range aa {
if vm, ok := a.(map[string]interface{}); ok {
v, err := getNestedValue(vm, property, j.option.separator)
Expand All @@ -388,9 +407,11 @@ func (j *JSONQ) GroupBy(property string) *JSONQ {
}
}
}

// replace the new result with the previous result
j.jsonContent = dt
}
// replace the new result with the previous result
j.jsonContent = dt

return j
}

Expand Down Expand Up @@ -439,9 +460,9 @@ func (j *JSONQ) Distinct(property string) *JSONQ {

// distinct builds distinct value using provided attribute/column/property
func (j *JSONQ) distinct() *JSONQ {
m := map[string]bool{}
var dt = make([]interface{}, 0)
if aa, ok := j.jsonContent.([]interface{}); ok {
m := map[string]bool{}
var dt = make([]interface{}, 0)
for _, a := range aa {
if vm, ok := a.(map[string]interface{}); ok {
v, err := getNestedValue(vm, j.distinctProperty, j.option.separator)
Expand All @@ -455,9 +476,9 @@ func (j *JSONQ) distinct() *JSONQ {
}
}
}
// replace the new result with the previous result
j.jsonContent = dt
}
// replace the new result with the previous result
j.jsonContent = dt
return j
}

Expand Down Expand Up @@ -488,29 +509,53 @@ func (j *JSONQ) sortBy(property string, asc bool) *JSONQ {
return j
}

// only return selected properties in result
func (j *JSONQ) only(properties ...string) interface{} {
// only return selected properties in result from array
func (j *JSONQ) onlyFromArray(input []interface{}, properties ...string) interface{} {
var result = make([]interface{}, 0)
if aa, ok := j.jsonContent.([]interface{}); ok {
for _, am := range aa {
tmap := map[string]interface{}{}
for _, prop := range properties {
node, alias := makeAlias(prop, j.option.separator)
rv, errV := getNestedValue(am, node, j.option.separator)
if errV != nil {
j.addError(errV)
continue
}
tmap[alias] = rv
}
if len(tmap) > 0 {
result = append(result, tmap)
for _, am := range input {
tmap := j.onlyFromMap(am, properties...)
if len(tmap) > 0 {
result = append(result, tmap)
}
}

return result
}

// only return selected properties in result from interface
func (j *JSONQ) onlyFromMap(input interface{}, properties ...string) map[string]interface{} {
result := map[string]interface{}{}
for _, prop := range properties {
node, alias := makeAlias(prop, j.option.separator)
rv, errV := getNestedValue(input, node, j.option.separator)
if rv == nil {
defaultValue, ok := j.option.defaults[node]
if !ok && errV != nil {
j.addError(errV)
continue
}
rv = defaultValue
}
result[alias] = rv
}

return result
}

// only return selected properties in result
func (j *JSONQ) only(properties ...string) interface{} {
if j.jsonContent == empty {
j.errors = append(j.errors, ErrNoRecordFound)
return j
}

if aa, ok := j.jsonContent.([]interface{}); ok {
return j.onlyFromArray(aa, properties...)
}

return j.onlyFromMap(j.jsonContent, properties...)
}

// Only collects the properties from a list of object
func (j *JSONQ) Only(properties ...string) interface{} {
return j.prepare().only(properties...)
Expand All @@ -525,6 +570,19 @@ func (j *JSONQ) OnlyR(properties ...string) (*Result, error) {
return NewResult(v), nil
}

// Pluck build an array of values form a property of a list of objects
func (j *JSONQ) pluckFromArray(input []interface{}, property string) interface{} {
var result = make([]interface{}, 0)
for _, am := range input {
if mv, ok := am.(map[string]interface{}); ok {
if v, ok := mv[property]; ok {
result = append(result, v)
}
}
}
return result
}

// Pluck build an array of values form a property of a list of objects
func (j *JSONQ) Pluck(property string) interface{} {
j.prepare()
Expand All @@ -534,17 +592,14 @@ func (j *JSONQ) Pluck(property string) interface{} {
if j.limitRecords != 0 {
j.limit()
}
var result = make([]interface{}, 0)
if aa, ok := j.jsonContent.([]interface{}); ok {
for _, am := range aa {
if mv, ok := am.(map[string]interface{}); ok {
if v, ok := mv[property]; ok {
result = append(result, v)
}
}
}

switch v := j.jsonContent.(type) {
case []interface{}:
return j.pluckFromArray(v, property)
case map[string]interface{}:
return v[property]
}
return result
return empty
}

// PluckR build an array of values form a property of a list of objects and return as Result instance
Expand Down
77 changes: 76 additions & 1 deletion jsonq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestNew(t *testing.T) {

func TestJSONQ_String(t *testing.T) {
jq := New()
expected := fmt.Sprintf("\nContent: %s\nQueries:%v\n", string(jq.raw), jq.queries)
expected := fmt.Sprintf("\nContent: %s\nQueries: %v\nAttributes: %v", string(jq.raw), jq.queries, jq.distinct().attributes)
if out := jq.String(); out != expected {
t.Errorf("Expected: %v\n Got: %v", expected, out)
}
Expand Down Expand Up @@ -149,6 +149,31 @@ func TestJSONQ_FromString(t *testing.T) {
}
}

func TestJSONQ_FromByteArray(t *testing.T) {
testCases := []struct {
tag string
input []byte
errExpect bool
}{
{
tag: "valid json",
input: []byte(`{"name": "John Doe", "age": 30}`),
errExpect: false,
},
{
tag: "invalid json should return error",
input: []byte(`{"name": "John Doe", "age": 30, "only_key"}`),
errExpect: true,
},
}

for _, tc := range testCases {
if err := New().FromByteArray(tc.input).Error(); err != nil && !tc.errExpect {
t.Errorf("failed %s", tc.tag)
}
}
}

func TestJSONQ_Reader(t *testing.T) {
testCases := []struct {
tag string
Expand Down Expand Up @@ -587,6 +612,26 @@ func TestJSONQ_WhereStrictContains_expecting_empty_result(t *testing.T) {
assertJSON(t, out, expected, "WhereContains expecting empty result")
}

func TestJSONQ_WhereMap_expecting_result(t *testing.T) {
jq := New().FromString(`{"name":"computers","description":"List of computer products","vendor":{"name":"Star Trek","email":"info@example.com","website":"www.example.com","items":{"id":1,"name":"MacBook Pro 13 inch retina","price":1350}}}`).
From("vendor.items").
WhereStrictContains("name", "retina")
expected := `{"id":1,"name":"MacBook Pro 13 inch retina","price":1350}`
out := jq.Get()
assertJSON(t, out, expected, "WhereContains expecting result")
}

func TestJSONQ_WhereMap_expecting_empty_result(t *testing.T) {
jq := New().FromString(`{"name":"computers","description":"List of computer products","vendor":{"name":"Star Trek","email":"info@example.com","website":"www.example.com","items":{"id":1,"name":"MacBook Pro 13 inch retina","price":1350}}}`).
From("vendor.items").
WhereStrictContains("name", "RetinA")
out := jq.Get()
if out != nil {
t.Errorf("WhereContains expecting empty result")

}
}

func TestJSONQ_GroupBy(t *testing.T) {
jq := New().FromString(jsonStr).
From("vendor.items").
Expand Down Expand Up @@ -781,6 +826,20 @@ func TestJSONQ_Only(t *testing.T) {
assertJSON(t, out, expected)
}

func TestJSONQ_OnlyMap(t *testing.T) {
jq := New().FromString(jsonStr)
expected := `{"name":"computers","vendor":"Star Trek"}`
out := jq.Only("name", "vendor.name as vendor")
assertJSON(t, out, expected)
}

func TestJSONQ_Only_Default(t *testing.T) {
jq := New(WithDefaults(map[string]interface{}{"id": 1000})).FromString(jsonStr).From("vendor.items")
expected := `[{"id":1,"price":1350},{"id":2,"price":1700},{"id":3,"price":1200},{"id":4,"price":850},{"id":5,"price":850},{"id":6,"price":950},{"id":1000,"price":850}]`
out := jq.Only("id", "price")
assertJSON(t, out, expected)
}

func TestJSONQ_Only_with_distinct(t *testing.T) {
jq := New().FromString(jsonStr).
From("vendor.items").Distinct("price")
Expand Down Expand Up @@ -1032,6 +1091,22 @@ func TestJSONQ_Pluck_expecting_empty_list_of_float64(t *testing.T) {
assertJSON(t, out, expected, "Pluck expecting empty list from list of objects, because of invalid property name")
}

func TestJSONQ_Pluck_Map_expecting_float64(t *testing.T) {
jq := New().FromString(jsonStr)
out := jq.Pluck("name")
expected := `"computers"`
assertJSON(t, out, expected, "Pluck expecting prices from list of objects")
}

func TestJSONQ_Pluck_Map_expecting_empty_float64(t *testing.T) {
jq := New().FromString(jsonStr)
out := jq.Pluck("invalid_prop")

if out != nil {
t.Errorf("Pluck expecting empty nil, because of invalid property name")
}
}

func TestJSONQ_Pluck_expecting_with_distinct(t *testing.T) {
jq := New().FromString(jsonStr).
From("vendor.items").Distinct("price").Limit(3)
Expand Down
Loading