Skip to content

Commit aef99b2

Browse files
committed
support maps in marker marker parsing
Maps in markers always have string keys, and may have any value type. "Guessed" maps are always non-uniform (i.e. key of AnyType), since they're frequently used to represent objects in JSON form.
1 parent 76a25b6 commit aef99b2

File tree

2 files changed

+201
-30
lines changed

2 files changed

+201
-30
lines changed

pkg/markers/parse.go

Lines changed: 184 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package markers
1818

1919
import (
2020
"bytes"
21-
"errors"
2221
"fmt"
2322
"reflect"
2423
"strconv"
@@ -40,6 +39,15 @@ func expect(scanner *sc.Scanner, expected rune, errDesc string) bool {
4039
return true
4140
}
4241

42+
// peekNoSpace is equivalent to scanner.Peek, except that it will consume intervening whitespace.
43+
func peekNoSpace(scanner *sc.Scanner) rune {
44+
hint := scanner.Peek()
45+
for ; hint <= rune(' ') && ((1<<uint64(hint))&scanner.Whitespace) != 0; hint = scanner.Peek() {
46+
scanner.Next() // skip the whitespace
47+
}
48+
return hint
49+
}
50+
4351
var (
4452
// interfaceType is a pre-computed reflect.Type representing the empty interface.
4553
interfaceType = reflect.TypeOf((*interface{})(nil)).Elem()
@@ -84,17 +92,27 @@ const (
8492
AnyType
8593
// SliceType is any slice constructed of the ArgumentTypes
8694
SliceType
95+
// MapType is any map constructed of string keys, and ArgumentType values.
96+
// Keys are strings, and it's common to see AnyType (non-uniform) values.
97+
MapType
8798
// RawType represents content that gets passed directly to the marker
8899
// without any parsing. It should *only* be used with anonymous markers.
89100
RawType
90101
)
91102

92103
// Argument is the type of a marker argument.
93104
type Argument struct {
94-
Type ArgumentType
105+
// Type is the type of this argument For non-scalar types (map and slice),
106+
// further information is specified in ItemType.
107+
Type ArgumentType
108+
// Optional indicates if this argument is optional.
95109
Optional bool
96-
Pointer bool
110+
// Pointer indicates if this argument was a pointer (this is really only
111+
// needed for deserialization, and should alway imply optional)
112+
Pointer bool
97113

114+
// ItemType is the type of the slice item for slices, and the value type
115+
// for maps.
98116
ItemType *Argument
99117
}
100118

@@ -119,6 +137,9 @@ func (a Argument) typeString(out *strings.Builder) {
119137
out.WriteString("[]")
120138
// arguments can't be non-pointer optional, so just call into typeString again.
121139
a.ItemType.typeString(out)
140+
case MapType:
141+
out.WriteString("map[string]")
142+
a.ItemType.typeString(out)
122143
case RawType:
123144
out.WriteString("<raw>")
124145
}
@@ -169,6 +190,13 @@ func makeSliceType(itemType Argument) (reflect.Type, error) {
169190
return nil, err
170191
}
171192
itemReflectedType = subItemType
193+
case MapType:
194+
subItemType, err := makeMapType(*itemType.ItemType)
195+
if err != nil {
196+
return nil, err
197+
}
198+
itemReflectedType = subItemType
199+
// TODO(directxman12): support non-uniform slices? (probably not)
172200
default:
173201
return nil, fmt.Errorf("invalid type when constructing guessed slice out: %v", itemType.Type)
174202
}
@@ -180,10 +208,50 @@ func makeSliceType(itemType Argument) (reflect.Type, error) {
180208
return reflect.SliceOf(itemReflectedType), nil
181209
}
182210

211+
// makeMapType makes a reflect.Type for a map of the given item type.
212+
// Useful for constructing the out value for when AnyType's guess returns a map.
213+
func makeMapType(itemType Argument) (reflect.Type, error) {
214+
var itemReflectedType reflect.Type
215+
switch itemType.Type {
216+
case IntType:
217+
itemReflectedType = reflect.TypeOf(int(0))
218+
case StringType:
219+
itemReflectedType = reflect.TypeOf("")
220+
case BoolType:
221+
itemReflectedType = reflect.TypeOf(false)
222+
case SliceType:
223+
subItemType, err := makeSliceType(*itemType.ItemType)
224+
if err != nil {
225+
return nil, err
226+
}
227+
itemReflectedType = subItemType
228+
// TODO(directxman12): support non-uniform slices? (probably not)
229+
case MapType:
230+
subItemType, err := makeMapType(*itemType.ItemType)
231+
if err != nil {
232+
return nil, err
233+
}
234+
itemReflectedType = subItemType
235+
case AnyType:
236+
// NB(directxman12): maps explicitly allow non-uniform item types, unlike slices at the moment
237+
itemReflectedType = interfaceType
238+
default:
239+
return nil, fmt.Errorf("invalid type when constructing guessed slice out: %v", itemType.Type)
240+
}
241+
242+
if itemType.Pointer {
243+
itemReflectedType = reflect.PtrTo(itemReflectedType)
244+
}
245+
246+
return reflect.MapOf(reflect.TypeOf(""), itemReflectedType), nil
247+
}
248+
183249
// guessType takes an educated guess about the type of the next field. If allowSlice
184250
// is false, it will not guess slices. It's less efficient than parsing with actual
185251
// type information, since we need to allocate to peek ahead full tokens, and the scanner
186252
// only allows peeking ahead one character.
253+
// Maps are *always* non-uniform (i.e. type the AnyType item type), since they're frequently
254+
// used to represent things like defaults for an object in JSON.
187255
func guessType(scanner *sc.Scanner, raw string, allowSlice bool) *Argument {
188256
if allowSlice {
189257
maybeItem := guessType(scanner, raw, false)
@@ -207,25 +275,49 @@ func guessType(scanner *sc.Scanner, raw string, allowSlice bool) *Argument {
207275
return maybeItem
208276
}
209277

210-
// first, try the easy case -- quoted strings strings
211-
hint := scanner.Peek()
212-
switch hint {
213-
case '"', '\'', '`':
214-
return &Argument{Type: StringType}
215-
}
216-
217278
// everything else needs a duplicate scanner to scan properly
218279
// (so we don't consume our scanner tokens until we actually
219280
// go to use this -- Go doesn't like scanners that can be rewound).
220281
subRaw := raw[scanner.Pos().Offset:]
221282
subScanner := parserScanner(subRaw, scanner.Error)
222283

223-
// next, check for slices
284+
// skip whitespace
285+
hint := peekNoSpace(subScanner)
286+
287+
// first, try the easy case -- quoted strings strings
288+
switch hint {
289+
case '"', '\'', '`':
290+
return &Argument{Type: StringType}
291+
}
292+
293+
// next, check for slices or maps
224294
if hint == '{' {
225295
subScanner.Scan()
296+
297+
// TODO(directxman12): this can't guess at empty objects, but that's generally ok.
298+
// We'll cross that bridge when we get there.
299+
300+
// look ahead till we can figure out if this is a map or a slice
301+
firstElemType := guessType(subScanner, subRaw, false)
302+
if firstElemType.Type == StringType {
303+
// might be a map or slice, parse the string and check for colon
304+
// (blech, basically arbitrary look-ahead due to raw strings).
305+
var keyVal string // just ignore this
306+
(&Argument{Type: StringType}).parseString(subScanner, raw, reflect.Indirect(reflect.ValueOf(&keyVal)))
307+
308+
if subScanner.Scan() == ':' {
309+
// it's got a string followed by a colon -- it's a map
310+
return &Argument{
311+
Type: MapType,
312+
ItemType: &Argument{Type: AnyType},
313+
}
314+
}
315+
}
316+
317+
// definitely a slice -- maps have to have string keys and have a value followed by a colon
226318
return &Argument{
227319
Type: SliceType,
228-
ItemType: guessType(subScanner, subRaw, false),
320+
ItemType: firstElemType,
229321
}
230322
}
231323

@@ -276,10 +368,10 @@ func (a *Argument) parseString(scanner *sc.Scanner, raw string, out reflect.Valu
276368
}
277369

278370
// the "hard" case -- bare tokens not including ',' (the argument
279-
// separator), ';' (the slice separator), or '}' (delimitted slice
280-
// ender)
371+
// separator), ';' (the slice separator), ':' (the map separator), or '}'
372+
// (delimitted slice ender)
281373
startPos := scanner.Position.Offset
282-
for hint := scanner.Peek(); hint != ',' && hint != ';' && hint != '}' && hint != sc.EOF; hint = scanner.Peek() {
374+
for hint := peekNoSpace(scanner); hint != ',' && hint != ';' && hint != ':' && hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) {
283375
// skip this token
284376
scanner.Scan()
285377
}
@@ -296,15 +388,15 @@ func (a *Argument) parseSlice(scanner *sc.Scanner, raw string, out reflect.Value
296388
elem := reflect.Indirect(reflect.New(out.Type().Elem()))
297389

298390
// preferred case
299-
if scanner.Peek() == '{' {
391+
if peekNoSpace(scanner) == '{' {
300392
// NB(directxman12): supporting delimitted slices in bare slices
301393
// would require an extra look-ahead here :-/
302394

303395
scanner.Scan() // skip '{'
304-
for hint := scanner.Peek(); hint != '}' && hint != sc.EOF; hint = scanner.Peek() {
305-
a.ItemType.parse(scanner, raw, elem, true)
396+
for hint := peekNoSpace(scanner); hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) {
397+
a.ItemType.parse(scanner, raw, elem, true /* parsing a slice */)
306398
resSlice = reflect.Append(resSlice, elem)
307-
tok := scanner.Peek()
399+
tok := peekNoSpace(scanner)
308400
if tok == '}' {
309401
break
310402
}
@@ -320,10 +412,10 @@ func (a *Argument) parseSlice(scanner *sc.Scanner, raw string, out reflect.Value
320412
}
321413

322414
// legacy case
323-
for hint := scanner.Peek(); hint != ',' && hint != '}' && hint != sc.EOF; hint = scanner.Peek() {
324-
a.ItemType.parse(scanner, raw, elem, true)
415+
for hint := peekNoSpace(scanner); hint != ',' && hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) {
416+
a.ItemType.parse(scanner, raw, elem, true /* parsing a slice */)
325417
resSlice = reflect.Append(resSlice, elem)
326-
tok := scanner.Peek()
418+
tok := peekNoSpace(scanner)
327419
if tok == ',' || tok == '}' || tok == sc.EOF {
328420
break
329421
}
@@ -336,6 +428,39 @@ func (a *Argument) parseSlice(scanner *sc.Scanner, raw string, out reflect.Value
336428
castAndSet(out, resSlice)
337429
}
338430

431+
// parseMap parses a map of the form {string: val, string: val, string: val}
432+
func (a *Argument) parseMap(scanner *sc.Scanner, raw string, out reflect.Value) {
433+
resMap := reflect.MakeMap(out.Type())
434+
elem := reflect.Indirect(reflect.New(out.Type().Elem()))
435+
key := reflect.Indirect(reflect.New(out.Type().Key()))
436+
437+
if !expect(scanner, '{', "open curly brace") {
438+
return
439+
}
440+
441+
for hint := peekNoSpace(scanner); hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) {
442+
a.parseString(scanner, raw, key)
443+
if !expect(scanner, ':', "colon") {
444+
return
445+
}
446+
a.ItemType.parse(scanner, raw, elem, false /* not in a slice */)
447+
resMap.SetMapIndex(key, elem)
448+
449+
if peekNoSpace(scanner) == '}' {
450+
break
451+
}
452+
if !expect(scanner, ',', "comma") {
453+
return
454+
}
455+
}
456+
457+
if !expect(scanner, '}', "close curly brace") {
458+
return
459+
}
460+
461+
castAndSet(out, resMap)
462+
}
463+
339464
// parse functions like Parse, except that it allows passing down whether or not we're
340465
// already in a slice, to avoid duplicate legacy slice detection for AnyType
341466
func (a *Argument) parse(scanner *sc.Scanner, raw string, out reflect.Value, inSlice bool) {
@@ -387,18 +512,27 @@ func (a *Argument) parse(scanner *sc.Scanner, raw string, out reflect.Value, inS
387512
case AnyType:
388513
guessedType := guessType(scanner, raw, !inSlice)
389514
newOut := out
390-
if guessedType.Type == SliceType {
391-
// we need to be able to construct the right element types, below
392-
// in parse, so construct a concretely-typed value to use as "out"
515+
516+
// we need to be able to construct the right element types, below
517+
// in parse, so construct a concretely-typed value to use as "out"
518+
switch guessedType.Type {
519+
case SliceType:
393520
newType, err := makeSliceType(*guessedType.ItemType)
394521
if err != nil {
395522
scanner.Error(scanner, err.Error())
396523
return
397524
}
398525
newOut = reflect.Indirect(reflect.New(newType))
526+
case MapType:
527+
newType, err := makeMapType(*guessedType.ItemType)
528+
if err != nil {
529+
scanner.Error(scanner, err.Error())
530+
return
531+
}
532+
newOut = reflect.Indirect(reflect.New(newType))
399533
}
400534
if !newOut.CanSet() {
401-
panic("at the disco")
535+
panic("at the disco") // TODO(directxman12): this is left over from debugging -- it might need to be an error
402536
}
403537
guessedType.Parse(scanner, raw, newOut)
404538
castAndSet(out, newOut)
@@ -407,6 +541,9 @@ func (a *Argument) parse(scanner *sc.Scanner, raw string, out reflect.Value, inS
407541
// - `{val, val, val}` (preferred)
408542
// - `val;val;val` (legacy)
409543
a.parseSlice(scanner, raw, out)
544+
case MapType:
545+
// maps are {string: val, string: val, string: val}
546+
a.parseMap(scanner, raw, out)
410547
}
411548
}
412549

@@ -454,6 +591,16 @@ func ArgumentFromType(rawType reflect.Type) (Argument, error) {
454591
return Argument{}, fmt.Errorf("bad slice item type: %v", err)
455592
}
456593
arg.ItemType = &itemType
594+
case reflect.Map:
595+
arg.Type = MapType
596+
if rawType.Key().Kind() != reflect.String {
597+
return Argument{}, fmt.Errorf("bad map key type: map keys must be strings")
598+
}
599+
itemType, err := ArgumentFromType(rawType.Elem())
600+
if err != nil {
601+
return Argument{}, fmt.Errorf("bad slice item type: %v", err)
602+
}
603+
arg.ItemType = &itemType
457604
default:
458605
return Argument{}, fmt.Errorf("type has unsupported kind %s", rawType.Kind())
459606
}
@@ -614,8 +761,8 @@ func (d *Definition) Parse(rawMarker string) (interface{}, error) {
614761
}
615762

616763
var errs []error
617-
scanner := parserScanner(fields, func(_ *sc.Scanner, msg string) {
618-
errs = append(errs, errors.New(msg))
764+
scanner := parserScanner(fields, func(scanner *sc.Scanner, msg string) {
765+
errs = append(errs, &ScannerError{Msg: msg, Pos: scanner.Position})
619766
})
620767

621768
// TODO(directxman12): strict parsing where we error out if certain fields aren't optional
@@ -725,3 +872,12 @@ func splitMarker(raw string) (name string, anonymousName string, restFields stri
725872
}
726873
return name, anonymousName, restFields
727874
}
875+
876+
type ScannerError struct {
877+
Msg string
878+
Pos sc.Position
879+
}
880+
881+
func (e *ScannerError) Error() string {
882+
return fmt.Sprintf("%s (at %s)", e.Msg, e.Pos)
883+
}

0 commit comments

Comments
 (0)