From 2d5d45d8e95e3f52d4718da1f9da95fe6c3ad553 Mon Sep 17 00:00:00 2001 From: Lokesh Jain Date: Tue, 22 Nov 2022 18:24:41 +0530 Subject: [PATCH] Added support for byteArray, default values and Object Processing along with JSON Array --- jsonq.go | 141 ++++++++++++++++++++++++++++++++++--------------- jsonq_test.go | 77 ++++++++++++++++++++++++++- option.go | 13 +++++ option_test.go | 21 ++++++++ result.go | 2 +- 5 files changed, 209 insertions(+), 45 deletions(-) diff --git a/jsonq.go b/jsonq.go index 3fb90ef..8d59a93 100644 --- a/jsonq.go +++ b/jsonq.go @@ -8,6 +8,12 @@ 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{ @@ -15,6 +21,7 @@ func New(options ...OptionFunc) *JSONQ { option: option{ decoder: &DefaultDecoder{}, separator: defaultSeparator, + defaults: make(map[string]interface{}), }, } for _, option := range options { @@ -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 @@ -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 @@ -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) @@ -310,7 +323,10 @@ 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 @@ -318,8 +334,7 @@ func (j *JSONQ) findInArray(aa []interface{}) []interface{} { // 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 @@ -327,7 +342,7 @@ func (j *JSONQ) findInMap(vm map[string]interface{}) []interface{} { 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 { @@ -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 } @@ -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) @@ -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 } @@ -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) @@ -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 } @@ -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...) @@ -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() @@ -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 diff --git a/jsonq_test.go b/jsonq_test.go index ca34902..cd88aaf 100644 --- a/jsonq_test.go +++ b/jsonq_test.go @@ -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) } @@ -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 @@ -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"). @@ -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") @@ -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) diff --git a/option.go b/option.go index 9a568ec..2caacb0 100644 --- a/option.go +++ b/option.go @@ -6,6 +6,7 @@ import "errors" type option struct { decoder Decoder separator string + defaults map[string]interface{} } // OptionFunc represents a contract for option func, it basically set options to jsonq instance options @@ -44,3 +45,15 @@ func WithSeparator(s string) OptionFunc { return nil } } + +// WithDefaults set all the default values for the attributes/node +func WithDefaults(defaults map[string]interface{}) OptionFunc { + return func(j *JSONQ) error { + if defaults == nil { + return errors.New("defaults can not be empty") + } + + j.option.defaults = defaults + return nil + } +} diff --git a/option_test.go b/option_test.go index 8ccf40e..3e6cc42 100644 --- a/option_test.go +++ b/option_test.go @@ -60,3 +60,24 @@ func TestSetSeparator_with_nil_expecting_an_error(t *testing.T) { t.Error("failed to catch nil in SetSeparator") } } + +func TestWithDefaults(t *testing.T) { + tests := []struct { + name string + defaults map[string]interface{} + wantError bool + }{ + {name: "Nil defaults", wantError: true, defaults: nil}, + {name: "Empty defaults", wantError: false, defaults: map[string]interface{}{}}, + {name: "with defaults value", wantError: false, defaults: map[string]interface{}{"1": 1}}, + {name: "with defaults array", wantError: false, defaults: map[string]interface{}{"1": []int{1, 2, 3}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jq := New(WithDefaults(tt.defaults)) + if err := jq.Error(); err != nil && !tt.wantError { + t.Errorf("WithDefaults() = Expected Error but got %v", err) + } + }) + } +} diff --git a/result.go b/result.go index e8385a5..5cee243 100644 --- a/result.go +++ b/result.go @@ -13,7 +13,7 @@ const errMessage = "gojsonq: wrong method call for %v" var ( ErrExpectsPointer = fmt.Errorf("gojsonq: failed to unmarshal, expects pointer") ErrImmutable = fmt.Errorf("gojsonq: failed to unmarshal, target is not mutable") - ErrTypeMismatch = fmt.Errorf("gojsonq: failed to unmarshal, target type misatched") + ErrTypeMismatch = fmt.Errorf("gojsonq: failed to unmarshal, target type mismatched") ) // NewResult return an instance of Result