Skip to content

Commit c395cd4

Browse files
authoredOct 15, 2020
add support for pulling values from arrays and slices. (#50)
* add support for pulling values from arrays and slices. Switch to stackerr for errors. Rename proteus.ProteusError to proteus.Error and add Unwrap() support. * add readme info on nested access. Add test to validate that it works.
1 parent 7c9520b commit c395cd4

14 files changed

+339
-100
lines changed
 

‎README.md

+31
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,37 @@ For insert/updates, return types can be:
7171
- an int64 that indicates the number of rows affected
7272
- an int64 that indicates the number of rows affected and an error
7373

74+
The `proq` struct tag stores the query. You place variable substitutions between `:` s. Proteus allows you
75+
to refer to fields in maps and structs, as well as elements in arrays or slices using `.` as a path separator. If you have a
76+
struct like this:
77+
78+
```go
79+
type Person struct {
80+
Name string
81+
Address Address
82+
Pets []Pet
83+
}
84+
85+
type Pet struct {
86+
Name string
87+
Species string
88+
}
89+
90+
type Address struct {
91+
Street string
92+
City string
93+
State string
94+
}
95+
```
96+
You can write a query like this:
97+
98+
```
99+
insert into person(name, city, pet1_name, pet2_name) values (:p.Name:, :p.Address.City:, :p.Pets.0.Name:, :p.Pets.1.Name:)
100+
```
101+
102+
Note that the index for an array or slice must be an int literal and the key for a map must be a string.
103+
104+
74105
2\. If you want to map response fields to a struct, define a struct with struct tags to indicate the mapping:
75106

76107
```go

‎builder.go

+26-18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/jonbodner/proteus/logger"
1616
"github.com/jonbodner/proteus/mapper"
17+
"github.com/jonbodner/stackerr"
1718
)
1819

1920
func buildNameOrderMap(paramOrder string, startPos int) map[string]int {
@@ -87,7 +88,7 @@ func buildFixedQueryAndParamOrder(c context.Context, query string, nameOrderMap
8788
if inVar {
8889
if len(curVar) == 0 {
8990
//error! must have a something
90-
return nil, nil, fmt.Errorf("empty variable declaration at position %d", k)
91+
return nil, nil, stackerr.Errorf("empty variable declaration at position %d", k)
9192
}
9293
curVarS := string(curVar)
9394
id, err := validIdentifier(c, curVarS)
@@ -103,7 +104,7 @@ func buildFixedQueryAndParamOrder(c context.Context, query string, nameOrderMap
103104
//if it's a slice, then we put in the slice template syntax instead.
104105

105106
//get just the first part of the name, before any .
106-
path := strings.SplitN(id, ".", 2)
107+
path := strings.Split(id, ".")
107108
paramName := path[0]
108109
if paramPos, ok := nameOrderMap[paramName]; ok {
109110
//if the path has more than one part, make sure that the type of the function parameter is map or struct
@@ -113,7 +114,7 @@ func buildFixedQueryAndParamOrder(c context.Context, query string, nameOrderMap
113114
case reflect.Map, reflect.Struct:
114115
//do nothing
115116
default:
116-
return nil, nil, fmt.Errorf("query Parameter %s has a path, but the incoming parameter is not a map or a struct", paramName)
117+
return nil, nil, stackerr.Errorf("query Parameter %s has a path, but the incoming parameter is not a map or a struct", paramName)
117118
}
118119
}
119120
pathType, err := mapper.ExtractType(c, paramType, path)
@@ -129,7 +130,7 @@ func buildFixedQueryAndParamOrder(c context.Context, query string, nameOrderMap
129130
}
130131
paramOrder = append(paramOrder, paramInfo{id, paramPos, isSlice})
131132
} else {
132-
return nil, nil, fmt.Errorf("query Parameter %s cannot be found in the incoming parameters", paramName)
133+
return nil, nil, stackerr.Errorf("query Parameter %s cannot be found in the incoming parameters", paramName)
133134
}
134135

135136
inVar = false
@@ -146,7 +147,7 @@ func buildFixedQueryAndParamOrder(c context.Context, query string, nameOrderMap
146147
}
147148
}
148149
if inVar {
149-
return nil, nil, fmt.Errorf("missing a closing : somewhere: %s", query)
150+
return nil, nil, stackerr.Errorf("missing a closing : somewhere: %s", query)
150151
}
151152

152153
queryString := out.String()
@@ -230,7 +231,7 @@ func addSlice(sliceName string) string {
230231

231232
func validIdentifier(c context.Context, curVar string) (string, error) {
232233
if strings.Contains(curVar, ";") {
233-
return "", fmt.Errorf("; is not allowed in an identifier: %s", curVar)
234+
return "", stackerr.Errorf("; is not allowed in an identifier: %s", curVar)
234235
}
235236
curVarB := []byte(curVar)
236237

@@ -251,7 +252,7 @@ loop:
251252
switch tok {
252253
case token.EOF:
253254
if first || lastPeriod {
254-
return "", fmt.Errorf("identifiers cannot be empty or end with a .: %s", curVar)
255+
return "", stackerr.Errorf("identifiers cannot be empty or end with a .: %s", curVar)
255256
}
256257
break loop
257258
case token.SEMICOLON:
@@ -260,15 +261,15 @@ loop:
260261
continue
261262
case token.IDENT:
262263
if !first && !lastPeriod && !lastFloat {
263-
return "", fmt.Errorf(". missing between parts of an identifier: %s", curVar)
264+
return "", stackerr.Errorf(". missing between parts of an identifier: %s", curVar)
264265
}
265266
first = false
266267
lastPeriod = false
267268
lastFloat = false
268269
identifier += lit
269270
case token.PERIOD:
270271
if first || lastPeriod {
271-
return "", fmt.Errorf("identifier cannot start with . or have two . in a row: %s", curVar)
272+
return "", stackerr.Errorf("identifier cannot start with . or have two . in a row: %s", curVar)
272273
}
273274
lastPeriod = true
274275
identifier += "."
@@ -280,28 +281,35 @@ loop:
280281
first = false
281282
continue
282283
}
283-
return "", fmt.Errorf("invalid character found in identifier: %s", curVar)
284+
return "", stackerr.Errorf("invalid character found in identifier: %s", curVar)
284285
case token.INT:
285-
if !dollar {
286-
return "", fmt.Errorf("invalid character found in identifier: %s", curVar)
286+
if !dollar || first {
287+
return "", stackerr.Errorf("invalid character found in identifier: %s", curVar)
287288
}
288289
identifier += lit
289-
dollar = false
290+
if dollar {
291+
dollar = false
292+
}
290293
case token.FLOAT:
291294
//this is weird. If we have $1.NAME, it will think that there's a FLOAT token with value 1.
292295
//due to float support for exponents, if we have an E after the decimal point, the FLOAT token
293-
//will include the E and any subsequent digits. Obviously, only valid for $ notation
294-
if !dollar {
295-
return "", fmt.Errorf("invalid character found in identifier: %s", curVar)
296+
//will include the E and any subsequent digits.
297+
// also a problem when walking array or slice references (values.0 is the 0th element in array values). This
298+
// returns .0 as the lit value
299+
//Only valid for $ notation and array/slice references.
300+
if first {
301+
return "", stackerr.Errorf("invalid character found in identifier: %s", curVar)
296302
}
297303
identifier += lit
298-
dollar = false
304+
if dollar {
305+
dollar = false
306+
}
299307
lastFloat = true
300308
if lit[len(lit)-1] == '.' {
301309
lastPeriod = true
302310
}
303311
default:
304-
return "", fmt.Errorf("invalid character found in identifier: %s", curVar)
312+
return "", stackerr.Errorf("invalid character found in identifier: %s", curVar)
305313
}
306314
}
307315
return identifier, nil

‎go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ go 1.15
44

55
require (
66
github.com/go-sql-driver/mysql v1.5.0
7-
github.com/google/go-cmp v0.3.1
7+
github.com/google/go-cmp v0.4.0
88
github.com/jonbodner/dbtimer v0.0.0-20170410163237-7002f3758ae1
99
github.com/jonbodner/multierr v0.0.0-20200223210354-ace728439446
10+
github.com/jonbodner/stackerr v1.0.0
1011
github.com/lib/pq v1.8.0
1112
github.com/pkg/profile v1.5.0
1213
github.com/rickar/props v0.0.0-20170718221555-0b06aeb2f037

‎go.sum

+6-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
33
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
44
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
55
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
6-
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
7-
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
6+
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
7+
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
88
github.com/jonbodner/dbtimer v0.0.0-20170410163237-7002f3758ae1 h1:mgFL7UFb88FOlSVgVoIRGJ4yKlkfp8KcXHqy7no+lEU=
99
github.com/jonbodner/dbtimer v0.0.0-20170410163237-7002f3758ae1/go.mod h1:PjOlFbeJKs+4b2CvuN9FFF8Ed8cZ6FHWPb5tLK2QKOM=
1010
github.com/jonbodner/multierr v0.0.0-20200223210354-ace728439446 h1:3JlpjXILq02AhL5iNC/T6M+UwkKvcKq7/h0LPYP+R44=
1111
github.com/jonbodner/multierr v0.0.0-20200223210354-ace728439446/go.mod h1:BN5BmT8XsFrnRDB/uErUiXlda9UQs2YhrlFFtf74JaM=
12+
github.com/jonbodner/stackerr v1.0.0 h1:rAe+Fh13cfC9IGuKE4YWiVCzwt9zce9Saldpc8fYEIM=
13+
github.com/jonbodner/stackerr v1.0.0/go.mod h1:In1ShJr570PDuDHbYfymEQle+H7PgY9KpT+alyk0nEM=
1214
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
1315
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
1416
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
@@ -29,3 +31,5 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/p
2931
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
3032
golang.org/x/sys v0.0.0-20200828161417-c663848e9a16 h1:54u1berWyLujz9htI1BHtZpcCEYaYNUSDFLXMNDd7To=
3133
golang.org/x/sys v0.0.0-20200828161417-c663848e9a16/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
34+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
35+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

‎mapper/extract.go

+38-21
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,52 @@ package mapper
22

33
import (
44
"context"
5-
"errors"
5+
"database/sql/driver"
66
"fmt"
77
"reflect"
8-
9-
"database/sql/driver"
8+
"strconv"
109

1110
"github.com/jonbodner/proteus/logger"
11+
"github.com/jonbodner/stackerr"
1212
)
1313

1414
func ExtractType(c context.Context, curType reflect.Type, path []string) (reflect.Type, error) {
1515
// error case path length == 0
1616
if len(path) == 0 {
17-
return nil, errors.New("cannot extract type; no path remaining")
17+
return nil, stackerr.New("cannot extract type; no path remaining")
1818
}
1919
ss := fromPtrType(curType)
2020
// base case path length == 1
2121
if len(path) == 1 {
2222
return ss, nil
2323
}
2424
// length > 1, find a match for path[1], and recurse
25-
if ss.Kind() == reflect.Map {
25+
switch ss.Kind() {
26+
case reflect.Map:
2627
//give up -- we can't figure out what's in the map, so just return the type of the value
2728
return ss.Elem(), nil
29+
case reflect.Struct:
30+
//make sure the field exists
31+
if f, exists := ss.FieldByName(path[1]); exists {
32+
return ExtractType(c, f.Type, path[1:])
33+
}
34+
return nil, stackerr.New("cannot find the type; no such field " + path[1])
35+
case reflect.Array, reflect.Slice:
36+
// handle slices and arrays
37+
_, err := strconv.Atoi(path[1])
38+
if err != nil {
39+
return nil, stackerr.Errorf("invalid index: %s :%w", path[1], err)
40+
}
41+
return ExtractType(c, ss.Elem(), path[1:])
42+
default:
43+
return nil, stackerr.New("cannot find the type for the subfield of anything other than a map, struct, slice, or array")
2844
}
29-
if ss.Kind() != reflect.Struct {
30-
return nil, errors.New("Cannot find the type for the subfield of anything other than a struct.")
31-
}
32-
//make sure the field exists
33-
if f, exists := ss.FieldByName(path[1]); exists {
34-
return ExtractType(c, f.Type, path[1:])
35-
}
36-
return nil, errors.New("cannot find the type; no such field " + path[1])
3745
}
3846

3947
func Extract(c context.Context, s interface{}, path []string) (interface{}, error) {
4048
// error case path length == 0
4149
if len(path) == 0 {
42-
return nil, errors.New("cannot extract value; no path remaining")
50+
return nil, stackerr.New("cannot extract value; no path remaining")
4351
}
4452
// base case path length == 1
4553
if len(path) == 1 {
@@ -52,29 +60,38 @@ func Extract(c context.Context, s interface{}, path []string) (interface{}, erro
5260
// length > 1, find a match for path[1], and recurse
5361
ss := fromPtr(s)
5462
sv := reflect.ValueOf(ss)
55-
if sv.Kind() == reflect.Map {
63+
switch sv.Kind() {
64+
case reflect.Map:
5665
if sv.Type().Key().Kind() != reflect.String {
57-
return nil, errors.New("cannot extract value; map does not have a string key")
66+
return nil, stackerr.New("cannot extract value; map does not have a string key")
5867
}
5968
logger.Log(c, logger.DEBUG, fmt.Sprintln(path[1]))
6069
logger.Log(c, logger.DEBUG, fmt.Sprintln(sv.MapKeys()))
6170
v := sv.MapIndex(reflect.ValueOf(path[1]))
6271
logger.Log(c, logger.DEBUG, fmt.Sprintln(v))
6372
if !v.IsValid() {
64-
return nil, errors.New("cannot extract value; no such map key " + path[1])
73+
return nil, stackerr.New("cannot extract value; no such map key " + path[1])
6574
}
6675
return Extract(c, v.Interface(), path[1:])
67-
}
68-
if sv.Kind() == reflect.Struct {
76+
case reflect.Struct:
6977
//make sure the field exists
7078
if _, exists := sv.Type().FieldByName(path[1]); !exists {
71-
return nil, errors.New("cannot extract value; no such field " + path[1])
79+
return nil, stackerr.New("cannot extract value; no such field " + path[1])
7280
}
7381

7482
v := sv.FieldByName(path[1])
7583
return Extract(c, v.Interface(), path[1:])
84+
case reflect.Array, reflect.Slice:
85+
// handle slices and arrays
86+
pos, err := strconv.Atoi(path[1])
87+
if err != nil {
88+
return nil, stackerr.Errorf("invalid index: %s :%w", path[1], err)
89+
}
90+
v := sv.Index(pos)
91+
return Extract(c, v.Interface(), path[1:])
92+
default:
93+
return nil, stackerr.New("cannot extract value; only maps and structs can have contained values")
7694
}
77-
return nil, errors.New("cannot extract value; only maps and structs can have contained values")
7895
}
7996

8097
func fromPtr(s interface{}) interface{} {

‎mapper/extract_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package mapper
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76
"reflect"
87
"testing"
98

109
"github.com/jonbodner/proteus/cmp"
1110
"github.com/jonbodner/proteus/logger"
11+
"github.com/jonbodner/stackerr"
1212
)
1313

1414
func TestExtractPointer(t *testing.T) {
@@ -124,7 +124,7 @@ func TestExtractFail(t *testing.T) {
124124
if err == nil {
125125
t.Errorf("Expected an error %s, got none", msg)
126126
}
127-
eExp := errors.New(msg)
127+
eExp := stackerr.New(msg)
128128
if !cmp.Errors(err, eExp) {
129129
t.Errorf("Expected error %s, got %s", eExp, err)
130130
}

0 commit comments

Comments
 (0)
Please sign in to comment.