Skip to content

Commit

Permalink
Merge pull request #31 from zoncoen/option
Browse files Browse the repository at this point in the history
feat: add options
  • Loading branch information
zoncoen authored Nov 13, 2022
2 parents ca9166f + 8a51bea commit bac45fe
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 20 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ The query syntax understood by this package when parsing is as follows.
```txt
$ the root element
.key extracts by a key of map or field name of struct ("." can be omitted if the head of query)
['key'] same as the ".key" (if the key contains "\" or "'", these characters must be escaped like "\\", "\'")
[0] extracts by a index of array or slice
['key'] same as the ".key"
```
2 changes: 1 addition & 1 deletion doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The query syntax understood by this package when parsing is as follows.
$ the root element
.key extracts by a key of map or field name of struct ("." can be omitted if the head of query)
['key'] same as the ".key" (if the key contains "\" or "'", these characters must be escaped like "\\", "\'")
[0] extracts by a index of array or slice
['key'] same as the ".key"
*/
package query
39 changes: 39 additions & 0 deletions example_option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,45 @@ type Person struct {
Name string `json:"name,omitempty"`
}

func ExampleCaseInsensitive() {
person := Person{
Name: "Alice",
}
q := query.New(query.CaseInsensitive()).Key("NAME")
name, _ := q.Extract(person)
fmt.Println(name)
// Output:
// Alice
}

func ExampleExtractByStructTag() {
person := Person{
Name: "Alice",
}
q := query.New(query.ExtractByStructTag("json")).Key("name")
name, _ := q.Extract(person)
fmt.Println(name)
// Output:
// Alice
}

func ExampleCustomExtractFunc() {
person := Person{
Name: "Alice",
}
q := query.New(
query.CustomExtractFunc(func(f query.ExtractFunc) query.ExtractFunc {
return func(v reflect.Value) (reflect.Value, bool) {
return reflect.ValueOf("Bob"), true
}
}),
).Key("name")
name, _ := q.Extract(person)
fmt.Println(name)
// Output:
// Bob
}

// getFieldNameByJSONTag returns the JSON field tag as field name if exists.
func getFieldNameByJSONTag(field reflect.StructField) string {
tag, ok := field.Tag.Lookup("json")
Expand Down
10 changes: 6 additions & 4 deletions example_parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import (
)

type S struct {
Maps []map[string]string
Maps []map[string]map[string]string
}

func ExampleParseString() {
q, err := query.ParseString("Maps[0].key")
q, err := query.ParseString(`$.Maps[0].key['.key\'']`)
if err == nil {
v, _ := q.Extract(&S{
Maps: []map[string]string{
{"key": "value"},
Maps: []map[string]map[string]string{
{"key": map[string]string{
".key'": "value",
}},
},
})
fmt.Println(v)
Expand Down
49 changes: 45 additions & 4 deletions key.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type KeyExtractor interface {
// Key represents an extractor to access the value by key.
type Key struct {
key string
caseInsensitive bool
structTags []string
fieldNameGetter func(f reflect.StructField) string
}

Expand All @@ -39,15 +41,54 @@ func (e *Key) extract(v reflect.Value) (reflect.Value, bool) {
case reflect.Map:
for _, k := range v.MapKeys() {
k := elem(k)
if k.String() == e.key {
return v.MapIndex(k), true
if e.caseInsensitive {
if strings.ToLower(k.String()) == strings.ToLower(e.key) {
return v.MapIndex(k), true
}
} else {
if k.String() == e.key {
return v.MapIndex(k), true
}
}
}
case reflect.Struct:
inlines := []int{}
for i := 0; i < v.Type().NumField(); i++ {
field := v.Type().FieldByIndex([]int{i})
if e.getFieldName(field) == e.key {
return v.FieldByIndex([]int{i}), true
fieldNames := []string{}
for _, t := range e.structTags {
if s := field.Tag.Get(t); s != "" {
name, opts, _ := strings.Cut(s, ",")
if name != "" {
fieldNames = append(fieldNames, name)
}
for _, o := range strings.Split(opts, ",") {
if o == "inline" {
inlines = append(inlines, i)
}
}
}
}
fieldNames = append(fieldNames, e.getFieldName(field))
for _, name := range fieldNames {
if e.caseInsensitive {
if strings.ToLower(name) == strings.ToLower(e.key) {
return v.FieldByIndex([]int{i}), true
}
} else {
if name == e.key {
return v.FieldByIndex([]int{i}), true
}
}
}
if field.Anonymous {
inlines = append(inlines, i)
}
}
for _, i := range inlines {
val, ok := e.extract(v.FieldByIndex([]int{i}))
if ok {
return val, true
}
}
}
Expand Down
133 changes: 125 additions & 8 deletions key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,24 @@ func (f *keyExtractor) ExtractByKey(_ string) (interface{}, bool) {
return nil, false
}

type testTags struct {
FooBar string `json:"foo_bar" yaml:"fooBar,omitempty"`
AnonymousField
M map[string]string `json:",inline"`
}

type AnonymousField struct {
S string
}

func TestKey_Extract(t *testing.T) {
t.Run("found", func(t *testing.T) {
tests := map[string]struct {
key string
v interface{}
expect interface{}
key string
caseInsensitive bool
structTags []string
v interface{}
expect interface{}
}{
"map[string]string": {
key: "key",
Expand All @@ -33,6 +45,14 @@ func TestKey_Extract(t *testing.T) {
},
expect: "value",
},
"map[string]string (case-insensitive)": {
key: "KEY",
caseInsensitive: true,
v: map[string]string{
"key": "value",
},
expect: "value",
},
"map[interface{}]interface{}": {
key: "key",
v: map[interface{}]interface{}{
Expand All @@ -46,6 +66,60 @@ func TestKey_Extract(t *testing.T) {
v: http.Request{Method: http.MethodGet},
expect: http.MethodGet,
},
"struct (case-insensitive)": {
key: "method",
caseInsensitive: true,
v: http.Request{Method: http.MethodGet},
expect: http.MethodGet,
},
"struct (anonymous field)": {
key: "AnonymousField",
caseInsensitive: true,
v: testTags{
AnonymousField: AnonymousField{
S: "aaa",
},
},
expect: AnonymousField{
S: "aaa",
},
},
"struct (anonymous field's field)": {
key: "S",
caseInsensitive: true,
v: testTags{
AnonymousField: AnonymousField{
S: "aaa",
},
},
expect: "aaa",
},
"struct (strcut tag)": {
key: "foo_bar",
structTags: []string{"json", "yaml"},
v: testTags{
FooBar: "xxx",
},
expect: "xxx",
},
"struct (strcut tag with option)": {
key: "fooBar",
structTags: []string{"json", "yaml"},
v: testTags{
FooBar: "xxx",
},
expect: "xxx",
},
"struct (inline strcut tag option)": {
key: "aaa",
structTags: []string{"json", "yaml"},
v: testTags{
M: map[string]string{
"aaa": "xxx",
},
},
expect: "xxx",
},
"struct pointer": {
key: "Method",
v: &http.Request{Method: http.MethodGet},
Expand All @@ -60,7 +134,11 @@ func TestKey_Extract(t *testing.T) {
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
e := &Key{key: test.key}
e := &Key{
key: test.key,
caseInsensitive: test.caseInsensitive,
structTags: test.structTags,
}
v, ok := e.Extract(reflect.ValueOf(test.v))
if !ok {
t.Fatal("not found")
Expand All @@ -73,16 +151,19 @@ func TestKey_Extract(t *testing.T) {
})
t.Run("not found", func(t *testing.T) {
tests := map[string]struct {
key string
v interface{}
key string
structTags []string
v interface{}
}{
"target is nil": {
key: "key",
v: nil,
},
"key not found": {
key: "key",
v: map[string]string{},
v: map[string]string{
"Key": "case sensitive",
},
},
"field not found": {
key: "Invalid",
Expand All @@ -92,11 +173,47 @@ func TestKey_Extract(t *testing.T) {
key: "key",
v: &keyExtractor{},
},
"strcut tag option": {
key: "FOO_BAR",
structTags: []string{"json", "yaml"},
v: testTags{
FooBar: "xxx",
},
},
"struct (anonymous field's field)": {
key: "s",
v: testTags{
AnonymousField: AnonymousField{
S: "aaa",
},
},
},
"inline": {
key: "AAA",
structTags: []string{"json", "yaml"},
v: testTags{
M: map[string]string{
"aaa": "xxx",
},
},
},
"inline (not contains json tag)": {
key: "aaa",
structTags: []string{"yaml"},
v: testTags{
M: map[string]string{
"aaa": "xxx",
},
},
},
}
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
e := &Key{key: test.key}
e := &Key{
key: test.key,
structTags: test.structTags,
}
v, ok := e.Extract(reflect.ValueOf(test.v))
if ok {
t.Fatalf("unexpected value: %#v", v)
Expand Down
23 changes: 23 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,31 @@ import "reflect"
// Option represents an option for Query.
type Option func(*Query)

// CaseInsensitive returns the Option to match case insensitivity.
func CaseInsensitive() Option {
return func(q *Query) {
q.caseInsensitive = true
}
}

// ExtractByStructTag returns the Option to allow extracting by struct tag.
func ExtractByStructTag(tagNames ...string) Option {
return func(q *Query) {
q.structTags = tagNames
}
}

// CustomExtractFunc returns the Option to customize the behavior of extractors.
func CustomExtractFunc(f func(ExtractFunc) ExtractFunc) Option {
return func(q *Query) {
q.customExtractFuncs = append(q.customExtractFuncs, f)
}
}

// CustomStructFieldNameGetter returns the Option to set f as custom function which gets struct field name.
// f is called by Key.Extract to get struct field name, if the target value is a struct.
//
// Deprecated: Use CustomExtractFunc instead.
func CustomStructFieldNameGetter(f func(f reflect.StructField) string) Option {
return func(q *Query) {
q.customStructFieldNameGetter = f
Expand Down
Loading

0 comments on commit bac45fe

Please sign in to comment.