Skip to content

Commit 7010929

Browse files
Allow renaming struct fields using struct tags (#251)
1 parent cd021ef commit 7010929

File tree

8 files changed

+122
-21
lines changed

8 files changed

+122
-21
lines changed

checker/checker_test.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ func TestVisitor_MethodNode(t *testing.T) {
5151
var err error
5252

5353
env := &mockEnv{}
54-
input := `Var.Set(1, 0.5)
55-
+ Var.Add(2)
56-
+ Var.Any(true)
57-
+ Var.Get()
54+
input := `Var.Set(1, 0.5)
55+
+ Var.Add(2)
56+
+ Var.Any(true)
57+
+ Var.Get()
5858
+ Var.Sub(3)
5959
+ (Duration.String() == "" ? 1 : 0)
6060
+ Interface.Method(0)
@@ -72,7 +72,7 @@ func TestVisitor_MethodNode(t *testing.T) {
7272
}
7373

7474
func TestVisitor_BuiltinNode(t *testing.T) {
75-
var typeTests = []string{
75+
typeTests := []string{
7676
`all(Tickets, {.Price > 0}) && any(map(Tickets, {.Price}), {# < 1000})`,
7777
`filter(map(Tickets, {.Origin}), {len(#) != 3})[0]`,
7878
`none(Any, {#.Any < 1})`,
@@ -102,7 +102,7 @@ func TestVisitor_ConstantNode(t *testing.T) {
102102
}
103103

104104
func TestCheck(t *testing.T) {
105-
var typeTests = []string{
105+
typeTests := []string{
106106
"!Bool",
107107
"!BoolPtr == Bool",
108108
"'a' == 'b' + 'c'",
@@ -551,6 +551,24 @@ func TestCheck_AsBool(t *testing.T) {
551551
assert.Equal(t, "expected bool, but got int", err.Error())
552552
}
553553

554+
func TestCheck_tagged_field_name(t *testing.T) {
555+
input := `foo.bar`
556+
557+
tree, err := parser.Parse(input)
558+
assert.NoError(t, err)
559+
560+
config := &conf.Config{}
561+
expr.Env(struct {
562+
x struct {
563+
y bool `expr:"bar"`
564+
} `expr:"foo"`
565+
}{})
566+
expr.AsBool()(config)
567+
568+
_, err = checker.Check(tree, config)
569+
assert.Error(t, err)
570+
}
571+
554572
//
555573
// Mock types
556574
//

checker/types.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,7 @@ func fieldType(ntype reflect.Type, name string) (reflect.Type, bool) {
233233
case reflect.Struct:
234234
// First check all struct's fields.
235235
for i := 0; i < ntype.NumField(); i++ {
236-
f := ntype.Field(i)
237-
if f.Name == name {
236+
if f := ntype.Field(i); actualFieldName(f) == name {
238237
return f.Type, true
239238
}
240239
}
@@ -372,3 +371,11 @@ func setTypeForIntegers(node ast.Node, t reflect.Type) {
372371
}
373372
}
374373
}
374+
375+
func actualFieldName(f reflect.StructField) string {
376+
if taggedName := f.Tag.Get("expr"); taggedName != "" {
377+
return taggedName
378+
}
379+
380+
return f.Name
381+
}

conf/types_table.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func FieldsFromStruct(t reflect.Type) TypesTable {
8282
}
8383
}
8484

85-
types[f.Name] = Tag{Type: f.Type}
85+
types[actualFieldName(f)] = Tag{Type: f.Type}
8686
}
8787
}
8888

@@ -98,3 +98,11 @@ func dereference(t reflect.Type) reflect.Type {
9898
}
9999
return t
100100
}
101+
102+
func actualFieldName(f reflect.StructField) string {
103+
if taggedName := f.Tag.Get("expr"); taggedName != "" {
104+
return taggedName
105+
}
106+
107+
return f.Name
108+
}

docs/Getting-Started.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func main() {
2929

3030
## Compile
3131

32-
Usually we want to compile the code on save (For example, in [web user interface](https://antonmedv.github.io/expr/)).
32+
Usually we want to compile the code on save (For example, in [web user interface](https://antonmedv.github.io/expr/)).
3333

3434
```go
3535
package main
@@ -65,7 +65,9 @@ func main() {
6565
}
6666
```
6767

68-
You may use existing types. For example, an environment can be a struct.
68+
You may use existing types. For example, an environment can be a struct. The
69+
struct fields can be renamed by adding struct tags such as `expr:"timestamp"` in
70+
the example below:
6971

7072
```go
7173
package main
@@ -86,11 +88,11 @@ func (Env) Format(t time.Time) string { return t.Format(time.RFC822) }
8688

8789
type Tweet struct {
8890
Text string
89-
Date time.Time
91+
Date time.Time `expr:"Timestamp"`
9092
}
9193

9294
func main() {
93-
code := `map(filter(Tweets, {len(.Text) > 0}), {.Text + Format(.Date)})`
95+
code := `map(filter(Tweets, {len(.Text) > 0}), {.Text + Format(.Timestamp)})`
9496

9597
// We can use an empty instance of the struct as an environment.
9698
program, err := expr.Compile(code, expr.Env(Env{}))
@@ -111,5 +113,5 @@ func main() {
111113
}
112114
```
113115

114-
* [Contents](README.md)
115-
* Next: [Custom functions](Custom-Functions.md)
116+
- [Contents](README.md)
117+
- Next: [Custom functions](Custom-Functions.md)

expr_test.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,28 @@ func ExampleEnv() {
114114
// Output: true
115115
}
116116

117+
func ExampleEnv_tagged_field_names() {
118+
env := struct {
119+
FirstWord string
120+
Separator string `expr:"Space"`
121+
SecondWord string `expr:"second_word"`
122+
}{
123+
FirstWord: "Hello",
124+
Separator: " ",
125+
SecondWord: "World",
126+
}
127+
128+
output, err := expr.Eval(`FirstWord + Space + second_word`, env)
129+
if err != nil {
130+
fmt.Printf("%v", err)
131+
return
132+
}
133+
134+
fmt.Printf("%v", output)
135+
136+
// Output : Hello World
137+
}
138+
117139
func ExampleAsBool() {
118140
env := map[string]int{
119141
"foo": 0,
@@ -513,9 +535,10 @@ func TestExpr(t *testing.T) {
513535
}
514536
return ret
515537
},
516-
Inc: func(a int) int { return a + 1 },
517-
Nil: nil,
518-
Tweets: []tweet{{"Oh My God!", date}, {"How you doin?", date}, {"Could I be wearing any more clothes?", date}},
538+
Inc: func(a int) int { return a + 1 },
539+
Nil: nil,
540+
Tweets: []tweet{{"Oh My God!", date}, {"How you doin?", date}, {"Could I be wearing any more clothes?", date}},
541+
Lowercase: "lowercase",
519542
}
520543

521544
tests := []struct {
@@ -934,6 +957,10 @@ func TestExpr(t *testing.T) {
934957
`OneDayDuration + Now`,
935958
tnowPlusOne,
936959
},
960+
{
961+
`lowercase`,
962+
"lowercase",
963+
},
937964
}
938965

939966
for _, tt := range tests {
@@ -1229,6 +1256,7 @@ type divideError struct{ Message string }
12291256
func (e divideError) Error() string {
12301257
return e.Message
12311258
}
1259+
12321260
func TestConstExpr_error_as_error(t *testing.T) {
12331261
env := map[string]interface{}{
12341262
"divide": func(a, b int) (int, error) {
@@ -1415,6 +1443,7 @@ type mockEnv struct {
14151443
NilInt *int
14161444
NilSlice []ticket
14171445
Tweets []tweet
1446+
Lowercase string `expr:"lowercase"`
14181447
}
14191448

14201449
func (e *mockEnv) GetInt() int {

file/source_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ func TestStringSource_SnippetSingleLine(t *testing.T) {
4343
source := NewSource("hello, world")
4444
if str, found := source.Snippet(1); !found {
4545
t.Errorf(snippetNotFound, t.Name(), 1)
46-
4746
} else if str != "hello, world" {
4847
t.Errorf(unexpectedSnippet, t.Name(), str, "hello, world")
4948
}

vm/runtime.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,19 @@ func fetch(from, i interface{}, nilsafe bool) interface{} {
6161
}
6262

6363
case reflect.Struct:
64-
value := v.FieldByName(reflect.ValueOf(i).String())
64+
fieldName := reflect.ValueOf(i).String()
65+
66+
value := v.FieldByNameFunc(func(name string) bool {
67+
switch field, _ := v.Type().FieldByName(name); field.Tag.Get("expr") {
68+
case fieldName:
69+
return true
70+
case "":
71+
return name == fieldName
72+
default:
73+
return false
74+
}
75+
})
76+
6577
if value.IsValid() && value.CanInterface() {
6678
return value.Interface()
6779
}

vm/vm_test.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
)
1818

1919
func TestRun_debug(t *testing.T) {
20-
var input = `[1, 2, 3]`
20+
input := `[1, 2, 3]`
2121

2222
node, err := parser.Parse(input)
2323
require.NoError(t, err)
@@ -256,6 +256,7 @@ func TestRun_method_with_error(t *testing.T) {
256256

257257
require.Equal(t, nil, out)
258258
}
259+
259260
func TestRun_fast_methods(t *testing.T) {
260261
input := `hello() + world()`
261262

@@ -316,3 +317,28 @@ func TestRun_inner_method_with_error(t *testing.T) {
316317

317318
require.Equal(t, nil, out)
318319
}
320+
321+
func TestRun_tagged_field_name(t *testing.T) {
322+
input := `value`
323+
324+
tree, err := parser.Parse(input)
325+
require.NoError(t, err)
326+
327+
env := struct {
328+
V string `expr:"value"`
329+
}{
330+
V: "hello world",
331+
}
332+
333+
funcConf := conf.New(env)
334+
_, err = checker.Check(tree, funcConf)
335+
require.NoError(t, err)
336+
337+
program, err := compiler.Compile(tree, funcConf)
338+
require.NoError(t, err)
339+
340+
out, err := vm.Run(program, env)
341+
require.NoError(t, err)
342+
343+
require.Equal(t, "hello world", out)
344+
}

0 commit comments

Comments
 (0)