mirror of https://github.com/kubernetes/kops.git
909 lines
28 KiB
Go
909 lines
28 KiB
Go
/*
|
|
Copyright 2019 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package markers
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
sc "text/scanner"
|
|
"unicode"
|
|
|
|
"sigs.k8s.io/controller-tools/pkg/loader"
|
|
)
|
|
|
|
// expect checks that the next token of the scanner is the given token, adding an error
|
|
// to the scanner if not. It returns whether the token was as expected.
|
|
func expect(scanner *sc.Scanner, expected rune, errDesc string) bool {
|
|
tok := scanner.Scan()
|
|
if tok != expected {
|
|
scanner.Error(scanner, fmt.Sprintf("expected %s, got %q", errDesc, scanner.TokenText()))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// peekNoSpace is equivalent to scanner.Peek, except that it will consume intervening whitespace.
|
|
func peekNoSpace(scanner *sc.Scanner) rune {
|
|
hint := scanner.Peek()
|
|
for ; hint <= rune(' ') && ((1<<uint64(hint))&scanner.Whitespace) != 0; hint = scanner.Peek() {
|
|
scanner.Next() // skip the whitespace
|
|
}
|
|
return hint
|
|
}
|
|
|
|
var (
|
|
// interfaceType is a pre-computed reflect.Type representing the empty interface.
|
|
interfaceType = reflect.TypeOf((*interface{})(nil)).Elem()
|
|
rawArgsType = reflect.TypeOf((*RawArguments)(nil)).Elem()
|
|
)
|
|
|
|
// lowerCamelCase converts PascalCase string to
|
|
// a camelCase string (by lowering the first rune).
|
|
func lowerCamelCase(in string) string {
|
|
isFirst := true
|
|
return strings.Map(func(inRune rune) rune {
|
|
if isFirst {
|
|
isFirst = false
|
|
return unicode.ToLower(inRune)
|
|
}
|
|
return inRune
|
|
}, in)
|
|
}
|
|
|
|
// RawArguments is a special type that can be used for a marker
|
|
// to receive *all* raw, underparsed argument data for a marker.
|
|
// You probably want to use `interface{}` to match any type instead.
|
|
// Use *only* for legacy markers that don't follow Definition's normal
|
|
// parsing logic. It should *not* be used as a field in a marker struct.
|
|
type RawArguments []byte
|
|
|
|
// ArgumentType is the kind of a marker argument type.
|
|
// It's roughly analogous to a subset of reflect.Kind, with
|
|
// an extra "AnyType" to represent the empty interface.
|
|
type ArgumentType int
|
|
|
|
const (
|
|
// Invalid represents a type that can't be parsed, and should never be used.
|
|
InvalidType ArgumentType = iota
|
|
// IntType is an int
|
|
IntType
|
|
// StringType is a string
|
|
StringType
|
|
// BoolType is a bool
|
|
BoolType
|
|
// AnyType is the empty interface, and matches the rest of the content
|
|
AnyType
|
|
// SliceType is any slice constructed of the ArgumentTypes
|
|
SliceType
|
|
// MapType is any map constructed of string keys, and ArgumentType values.
|
|
// Keys are strings, and it's common to see AnyType (non-uniform) values.
|
|
MapType
|
|
// RawType represents content that gets passed directly to the marker
|
|
// without any parsing. It should *only* be used with anonymous markers.
|
|
RawType
|
|
)
|
|
|
|
// Argument is the type of a marker argument.
|
|
type Argument struct {
|
|
// Type is the type of this argument For non-scalar types (map and slice),
|
|
// further information is specified in ItemType.
|
|
Type ArgumentType
|
|
// Optional indicates if this argument is optional.
|
|
Optional bool
|
|
// Pointer indicates if this argument was a pointer (this is really only
|
|
// needed for deserialization, and should alway imply optional)
|
|
Pointer bool
|
|
|
|
// ItemType is the type of the slice item for slices, and the value type
|
|
// for maps.
|
|
ItemType *Argument
|
|
}
|
|
|
|
// typeString contains the internals of TypeString.
|
|
func (a Argument) typeString(out *strings.Builder) {
|
|
if a.Pointer {
|
|
out.WriteRune('*')
|
|
}
|
|
|
|
switch a.Type {
|
|
case InvalidType:
|
|
out.WriteString("<invalid>")
|
|
case IntType:
|
|
out.WriteString("int")
|
|
case StringType:
|
|
out.WriteString("string")
|
|
case BoolType:
|
|
out.WriteString("bool")
|
|
case AnyType:
|
|
out.WriteString("<any>")
|
|
case SliceType:
|
|
out.WriteString("[]")
|
|
// arguments can't be non-pointer optional, so just call into typeString again.
|
|
a.ItemType.typeString(out)
|
|
case MapType:
|
|
out.WriteString("map[string]")
|
|
a.ItemType.typeString(out)
|
|
case RawType:
|
|
out.WriteString("<raw>")
|
|
}
|
|
}
|
|
|
|
// TypeString returns a string roughly equivalent
|
|
// (but not identical) to the underlying Go type that
|
|
// this argument would parse to. It's mainly useful
|
|
// for user-friendly formatting of this argument (e.g.
|
|
// help strings).
|
|
func (a Argument) TypeString() string {
|
|
out := &strings.Builder{}
|
|
a.typeString(out)
|
|
return out.String()
|
|
}
|
|
|
|
func (a Argument) String() string {
|
|
if a.Optional {
|
|
return fmt.Sprintf("<optional arg %s>", a.TypeString())
|
|
}
|
|
return fmt.Sprintf("<arg %s>", a.TypeString())
|
|
}
|
|
|
|
// castAndSet casts val to out's type if needed,
|
|
// then sets out to val.
|
|
func castAndSet(out, val reflect.Value) {
|
|
outType := out.Type()
|
|
if outType != val.Type() {
|
|
val = val.Convert(outType)
|
|
}
|
|
out.Set(val)
|
|
}
|
|
|
|
// makeSliceType makes a reflect.Type for a slice of the given type.
|
|
// Useful for constructing the out value for when AnyType's guess returns a slice.
|
|
func makeSliceType(itemType Argument) (reflect.Type, error) {
|
|
var itemReflectedType reflect.Type
|
|
switch itemType.Type {
|
|
case IntType:
|
|
itemReflectedType = reflect.TypeOf(int(0))
|
|
case StringType:
|
|
itemReflectedType = reflect.TypeOf("")
|
|
case BoolType:
|
|
itemReflectedType = reflect.TypeOf(false)
|
|
case SliceType:
|
|
subItemType, err := makeSliceType(*itemType.ItemType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
itemReflectedType = subItemType
|
|
case MapType:
|
|
subItemType, err := makeMapType(*itemType.ItemType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
itemReflectedType = subItemType
|
|
// TODO(directxman12): support non-uniform slices? (probably not)
|
|
default:
|
|
return nil, fmt.Errorf("invalid type when constructing guessed slice out: %v", itemType.Type)
|
|
}
|
|
|
|
if itemType.Pointer {
|
|
itemReflectedType = reflect.PtrTo(itemReflectedType)
|
|
}
|
|
|
|
return reflect.SliceOf(itemReflectedType), nil
|
|
}
|
|
|
|
// makeMapType makes a reflect.Type for a map of the given item type.
|
|
// Useful for constructing the out value for when AnyType's guess returns a map.
|
|
func makeMapType(itemType Argument) (reflect.Type, error) {
|
|
var itemReflectedType reflect.Type
|
|
switch itemType.Type {
|
|
case IntType:
|
|
itemReflectedType = reflect.TypeOf(int(0))
|
|
case StringType:
|
|
itemReflectedType = reflect.TypeOf("")
|
|
case BoolType:
|
|
itemReflectedType = reflect.TypeOf(false)
|
|
case SliceType:
|
|
subItemType, err := makeSliceType(*itemType.ItemType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
itemReflectedType = subItemType
|
|
// TODO(directxman12): support non-uniform slices? (probably not)
|
|
case MapType:
|
|
subItemType, err := makeMapType(*itemType.ItemType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
itemReflectedType = subItemType
|
|
case AnyType:
|
|
// NB(directxman12): maps explicitly allow non-uniform item types, unlike slices at the moment
|
|
itemReflectedType = interfaceType
|
|
default:
|
|
return nil, fmt.Errorf("invalid type when constructing guessed slice out: %v", itemType.Type)
|
|
}
|
|
|
|
if itemType.Pointer {
|
|
itemReflectedType = reflect.PtrTo(itemReflectedType)
|
|
}
|
|
|
|
return reflect.MapOf(reflect.TypeOf(""), itemReflectedType), nil
|
|
}
|
|
|
|
// guessType takes an educated guess about the type of the next field. If allowSlice
|
|
// is false, it will not guess slices. It's less efficient than parsing with actual
|
|
// type information, since we need to allocate to peek ahead full tokens, and the scanner
|
|
// only allows peeking ahead one character.
|
|
// Maps are *always* non-uniform (i.e. type the AnyType item type), since they're frequently
|
|
// used to represent things like defaults for an object in JSON.
|
|
func guessType(scanner *sc.Scanner, raw string, allowSlice bool) *Argument {
|
|
if allowSlice {
|
|
maybeItem := guessType(scanner, raw, false)
|
|
|
|
subRaw := raw[scanner.Pos().Offset:]
|
|
subScanner := parserScanner(subRaw, scanner.Error)
|
|
|
|
var tok rune
|
|
for tok = subScanner.Scan(); tok != ',' && tok != sc.EOF && tok != ';'; tok = subScanner.Scan() {
|
|
// wait till we get something interesting
|
|
}
|
|
|
|
// semicolon means it's a legacy slice
|
|
if tok == ';' {
|
|
return &Argument{
|
|
Type: SliceType,
|
|
ItemType: maybeItem,
|
|
}
|
|
}
|
|
|
|
return maybeItem
|
|
}
|
|
|
|
// everything else needs a duplicate scanner to scan properly
|
|
// (so we don't consume our scanner tokens until we actually
|
|
// go to use this -- Go doesn't like scanners that can be rewound).
|
|
subRaw := raw[scanner.Pos().Offset:]
|
|
subScanner := parserScanner(subRaw, scanner.Error)
|
|
|
|
// skip whitespace
|
|
hint := peekNoSpace(subScanner)
|
|
|
|
// first, try the easy case -- quoted strings strings
|
|
switch hint {
|
|
case '"', '\'', '`':
|
|
return &Argument{Type: StringType}
|
|
}
|
|
|
|
// next, check for slices or maps
|
|
if hint == '{' {
|
|
subScanner.Scan()
|
|
|
|
// TODO(directxman12): this can't guess at empty objects, but that's generally ok.
|
|
// We'll cross that bridge when we get there.
|
|
|
|
// look ahead till we can figure out if this is a map or a slice
|
|
firstElemType := guessType(subScanner, subRaw, false)
|
|
if firstElemType.Type == StringType {
|
|
// might be a map or slice, parse the string and check for colon
|
|
// (blech, basically arbitrary look-ahead due to raw strings).
|
|
var keyVal string // just ignore this
|
|
(&Argument{Type: StringType}).parseString(subScanner, raw, reflect.Indirect(reflect.ValueOf(&keyVal)))
|
|
|
|
if subScanner.Scan() == ':' {
|
|
// it's got a string followed by a colon -- it's a map
|
|
return &Argument{
|
|
Type: MapType,
|
|
ItemType: &Argument{Type: AnyType},
|
|
}
|
|
}
|
|
}
|
|
|
|
// definitely a slice -- maps have to have string keys and have a value followed by a colon
|
|
return &Argument{
|
|
Type: SliceType,
|
|
ItemType: firstElemType,
|
|
}
|
|
}
|
|
|
|
// then, bools...
|
|
probablyString := false
|
|
if hint == 't' || hint == 'f' {
|
|
// maybe a bool
|
|
if nextTok := subScanner.Scan(); nextTok == sc.Ident {
|
|
switch subScanner.TokenText() {
|
|
case "true", "false":
|
|
// definitely a bool
|
|
return &Argument{Type: BoolType}
|
|
}
|
|
// probably a string
|
|
probablyString = true
|
|
} else {
|
|
// we shouldn't ever get here
|
|
scanner.Error(scanner, fmt.Sprintf("got a token (%q) that looked like an ident, but was not", scanner.TokenText()))
|
|
return &Argument{Type: InvalidType}
|
|
}
|
|
}
|
|
|
|
if !probablyString {
|
|
if nextTok := subScanner.Scan(); nextTok == sc.Int {
|
|
return &Argument{Type: IntType}
|
|
}
|
|
}
|
|
|
|
// otherwise assume bare strings
|
|
return &Argument{Type: StringType}
|
|
}
|
|
|
|
// parseString parses either of the two accepted string forms (quoted, or bare tokens).
|
|
func (a *Argument) parseString(scanner *sc.Scanner, raw string, out reflect.Value) {
|
|
// strings are a bit weird -- the "easy" case is quoted strings (tokenized as strings),
|
|
// the "hard" case (present for backwards compat) is a bare sequence of tokens that aren't
|
|
// a comma.
|
|
tok := scanner.Scan()
|
|
if tok == sc.String || tok == sc.RawString {
|
|
// the easy case
|
|
val, err := strconv.Unquote(scanner.TokenText())
|
|
if err != nil {
|
|
scanner.Error(scanner, fmt.Sprintf("unable to parse string: %v", err))
|
|
return
|
|
}
|
|
castAndSet(out, reflect.ValueOf(val))
|
|
return
|
|
}
|
|
|
|
// the "hard" case -- bare tokens not including ',' (the argument
|
|
// separator), ';' (the slice separator), ':' (the map separator), or '}'
|
|
// (delimitted slice ender)
|
|
startPos := scanner.Position.Offset
|
|
for hint := peekNoSpace(scanner); hint != ',' && hint != ';' && hint != ':' && hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) {
|
|
// skip this token
|
|
scanner.Scan()
|
|
}
|
|
endPos := scanner.Position.Offset + len(scanner.TokenText())
|
|
castAndSet(out, reflect.ValueOf(raw[startPos:endPos]))
|
|
}
|
|
|
|
// parseSlice parses either of the two slice forms (curly-brace-delimitted and semicolon-separated).
|
|
func (a *Argument) parseSlice(scanner *sc.Scanner, raw string, out reflect.Value) {
|
|
// slices have two supported formats, like string:
|
|
// - `{val, val, val}` (preferred)
|
|
// - `val;val;val` (legacy)
|
|
resSlice := reflect.Zero(out.Type())
|
|
elem := reflect.Indirect(reflect.New(out.Type().Elem()))
|
|
|
|
// preferred case
|
|
if peekNoSpace(scanner) == '{' {
|
|
// NB(directxman12): supporting delimitted slices in bare slices
|
|
// would require an extra look-ahead here :-/
|
|
|
|
scanner.Scan() // skip '{'
|
|
for hint := peekNoSpace(scanner); hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) {
|
|
a.ItemType.parse(scanner, raw, elem, true /* parsing a slice */)
|
|
resSlice = reflect.Append(resSlice, elem)
|
|
tok := peekNoSpace(scanner)
|
|
if tok == '}' {
|
|
break
|
|
}
|
|
if !expect(scanner, ',', "comma") {
|
|
return
|
|
}
|
|
}
|
|
if !expect(scanner, '}', "close curly brace") {
|
|
return
|
|
}
|
|
castAndSet(out, resSlice)
|
|
return
|
|
}
|
|
|
|
// legacy case
|
|
for hint := peekNoSpace(scanner); hint != ',' && hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) {
|
|
a.ItemType.parse(scanner, raw, elem, true /* parsing a slice */)
|
|
resSlice = reflect.Append(resSlice, elem)
|
|
tok := peekNoSpace(scanner)
|
|
if tok == ',' || tok == '}' || tok == sc.EOF {
|
|
break
|
|
}
|
|
scanner.Scan()
|
|
if tok != ';' {
|
|
scanner.Error(scanner, fmt.Sprintf("expected comma, got %q", scanner.TokenText()))
|
|
return
|
|
}
|
|
}
|
|
castAndSet(out, resSlice)
|
|
}
|
|
|
|
// parseMap parses a map of the form {string: val, string: val, string: val}
|
|
func (a *Argument) parseMap(scanner *sc.Scanner, raw string, out reflect.Value) {
|
|
resMap := reflect.MakeMap(out.Type())
|
|
elem := reflect.Indirect(reflect.New(out.Type().Elem()))
|
|
key := reflect.Indirect(reflect.New(out.Type().Key()))
|
|
|
|
if !expect(scanner, '{', "open curly brace") {
|
|
return
|
|
}
|
|
|
|
for hint := peekNoSpace(scanner); hint != '}' && hint != sc.EOF; hint = peekNoSpace(scanner) {
|
|
a.parseString(scanner, raw, key)
|
|
if !expect(scanner, ':', "colon") {
|
|
return
|
|
}
|
|
a.ItemType.parse(scanner, raw, elem, false /* not in a slice */)
|
|
resMap.SetMapIndex(key, elem)
|
|
|
|
if peekNoSpace(scanner) == '}' {
|
|
break
|
|
}
|
|
if !expect(scanner, ',', "comma") {
|
|
return
|
|
}
|
|
}
|
|
|
|
if !expect(scanner, '}', "close curly brace") {
|
|
return
|
|
}
|
|
|
|
castAndSet(out, resMap)
|
|
}
|
|
|
|
// parse functions like Parse, except that it allows passing down whether or not we're
|
|
// already in a slice, to avoid duplicate legacy slice detection for AnyType
|
|
func (a *Argument) parse(scanner *sc.Scanner, raw string, out reflect.Value, inSlice bool) {
|
|
// nolint:gocyclo
|
|
if a.Type == InvalidType {
|
|
scanner.Error(scanner, fmt.Sprintf("cannot parse invalid type"))
|
|
return
|
|
}
|
|
if a.Pointer {
|
|
out.Set(reflect.New(out.Type().Elem()))
|
|
out = reflect.Indirect(out)
|
|
}
|
|
switch a.Type {
|
|
case RawType:
|
|
// raw consumes everything else
|
|
castAndSet(out, reflect.ValueOf(raw[scanner.Pos().Offset:]))
|
|
// consume everything else
|
|
for tok := scanner.Scan(); tok != sc.EOF; tok = scanner.Scan() {
|
|
}
|
|
case IntType:
|
|
if !expect(scanner, sc.Int, "integer") {
|
|
return
|
|
}
|
|
// TODO(directxman12): respect the size when parsing
|
|
val, err := strconv.Atoi(scanner.TokenText())
|
|
if err != nil {
|
|
scanner.Error(scanner, fmt.Sprintf("unable to parse integer: %v", err))
|
|
return
|
|
}
|
|
castAndSet(out, reflect.ValueOf(val))
|
|
case StringType:
|
|
// strings are a bit weird -- the "easy" case is quoted strings (tokenized as strings),
|
|
// the "hard" case (present for backwards compat) is a bare sequence of tokens that aren't
|
|
// a comma.
|
|
a.parseString(scanner, raw, out)
|
|
case BoolType:
|
|
if !expect(scanner, sc.Ident, "true or false") {
|
|
return
|
|
}
|
|
switch scanner.TokenText() {
|
|
case "true":
|
|
castAndSet(out, reflect.ValueOf(true))
|
|
case "false":
|
|
castAndSet(out, reflect.ValueOf(false))
|
|
default:
|
|
scanner.Error(scanner, fmt.Sprintf("expected true or false, got %q", scanner.TokenText()))
|
|
return
|
|
}
|
|
case AnyType:
|
|
guessedType := guessType(scanner, raw, !inSlice)
|
|
newOut := out
|
|
|
|
// we need to be able to construct the right element types, below
|
|
// in parse, so construct a concretely-typed value to use as "out"
|
|
switch guessedType.Type {
|
|
case SliceType:
|
|
newType, err := makeSliceType(*guessedType.ItemType)
|
|
if err != nil {
|
|
scanner.Error(scanner, err.Error())
|
|
return
|
|
}
|
|
newOut = reflect.Indirect(reflect.New(newType))
|
|
case MapType:
|
|
newType, err := makeMapType(*guessedType.ItemType)
|
|
if err != nil {
|
|
scanner.Error(scanner, err.Error())
|
|
return
|
|
}
|
|
newOut = reflect.Indirect(reflect.New(newType))
|
|
}
|
|
if !newOut.CanSet() {
|
|
panic("at the disco") // TODO(directxman12): this is left over from debugging -- it might need to be an error
|
|
}
|
|
guessedType.Parse(scanner, raw, newOut)
|
|
castAndSet(out, newOut)
|
|
case SliceType:
|
|
// slices have two supported formats, like string:
|
|
// - `{val, val, val}` (preferred)
|
|
// - `val;val;val` (legacy)
|
|
a.parseSlice(scanner, raw, out)
|
|
case MapType:
|
|
// maps are {string: val, string: val, string: val}
|
|
a.parseMap(scanner, raw, out)
|
|
}
|
|
}
|
|
|
|
// Parse attempts to consume the argument from the given scanner (based on the given
|
|
// raw input as well for collecting ranges of content), and places the output value
|
|
// in the given reflect.Value. Errors are reported via the given scanner.
|
|
func (a *Argument) Parse(scanner *sc.Scanner, raw string, out reflect.Value) {
|
|
a.parse(scanner, raw, out, false)
|
|
}
|
|
|
|
// ArgumentFromType constructs an Argument by examining the given
|
|
// raw reflect.Type. It can construct arguments from the Go types
|
|
// corresponding to any of the types listed in ArgumentType.
|
|
func ArgumentFromType(rawType reflect.Type) (Argument, error) {
|
|
if rawType == rawArgsType {
|
|
return Argument{
|
|
Type: RawType,
|
|
}, nil
|
|
}
|
|
|
|
if rawType == interfaceType {
|
|
return Argument{
|
|
Type: AnyType,
|
|
}, nil
|
|
}
|
|
|
|
arg := Argument{}
|
|
if rawType.Kind() == reflect.Ptr {
|
|
rawType = rawType.Elem()
|
|
arg.Pointer = true
|
|
arg.Optional = true
|
|
}
|
|
|
|
switch rawType.Kind() {
|
|
case reflect.String:
|
|
arg.Type = StringType
|
|
case reflect.Int, reflect.Int32: // NB(directxman12): all ints in kubernetes are int32, so explicitly support that
|
|
arg.Type = IntType
|
|
case reflect.Bool:
|
|
arg.Type = BoolType
|
|
case reflect.Slice:
|
|
arg.Type = SliceType
|
|
itemType, err := ArgumentFromType(rawType.Elem())
|
|
if err != nil {
|
|
return Argument{}, fmt.Errorf("bad slice item type: %w", err)
|
|
}
|
|
arg.ItemType = &itemType
|
|
case reflect.Map:
|
|
arg.Type = MapType
|
|
if rawType.Key().Kind() != reflect.String {
|
|
return Argument{}, fmt.Errorf("bad map key type: map keys must be strings")
|
|
}
|
|
itemType, err := ArgumentFromType(rawType.Elem())
|
|
if err != nil {
|
|
return Argument{}, fmt.Errorf("bad slice item type: %w", err)
|
|
}
|
|
arg.ItemType = &itemType
|
|
default:
|
|
return Argument{}, fmt.Errorf("type has unsupported kind %s", rawType.Kind())
|
|
}
|
|
|
|
return arg, nil
|
|
}
|
|
|
|
// TargetType describes which kind of node a given marker is associated with.
|
|
type TargetType int
|
|
|
|
const (
|
|
// DescribesPackage indicates that a marker is associated with a package.
|
|
DescribesPackage TargetType = iota
|
|
// DescribesType indicates that a marker is associated with a type declaration.
|
|
DescribesType
|
|
// DescribesField indicates that a marker is associated with a struct field.
|
|
DescribesField
|
|
)
|
|
|
|
func (t TargetType) String() string {
|
|
switch t {
|
|
case DescribesPackage:
|
|
return "package"
|
|
case DescribesType:
|
|
return "type"
|
|
case DescribesField:
|
|
return "field"
|
|
default:
|
|
return "(unknown)"
|
|
}
|
|
}
|
|
|
|
// Definition is a parsed definition of a marker.
|
|
type Definition struct {
|
|
// Output is the deserialized Go type of the marker.
|
|
Output reflect.Type
|
|
// Name is the marker's name.
|
|
Name string
|
|
// Target indicates which kind of node this marker can be associated with.
|
|
Target TargetType
|
|
// Fields lists out the types of each field that this marker has, by
|
|
// argument name as used in the marker (if the output type isn't a struct,
|
|
// it'll have a single, blank field name). This only lists exported fields,
|
|
// (as per reflection rules).
|
|
Fields map[string]Argument
|
|
// FieldNames maps argument names (as used in the marker) to struct field name
|
|
// in the output type.
|
|
FieldNames map[string]string
|
|
// Strict indicates that this definition should error out when parsing if
|
|
// not all non-optional fields were seen.
|
|
Strict bool
|
|
}
|
|
|
|
// AnonymousField indicates that the definition has one field,
|
|
// (actually the original object), and thus the field
|
|
// doesn't get named as part of the name.
|
|
func (d *Definition) AnonymousField() bool {
|
|
if len(d.Fields) != 1 {
|
|
return false
|
|
}
|
|
_, hasAnonField := d.Fields[""]
|
|
return hasAnonField
|
|
}
|
|
|
|
// Empty indicates that this definition has no fields.
|
|
func (d *Definition) Empty() bool {
|
|
return len(d.Fields) == 0
|
|
}
|
|
|
|
// argumentInfo returns information about an argument field as the marker parser's field loader
|
|
// would see it. This can be useful if you have to interact with marker definition structs
|
|
// externally (e.g. at compile time).
|
|
func argumentInfo(fieldName string, tag reflect.StructTag) (argName string, optionalOpt bool) {
|
|
argName = lowerCamelCase(fieldName)
|
|
markerTag, tagSpecified := tag.Lookup("marker")
|
|
markerTagParts := strings.Split(markerTag, ",")
|
|
if tagSpecified && markerTagParts[0] != "" {
|
|
// allow overriding to support legacy cases where we don't follow camelCase conventions
|
|
argName = markerTagParts[0]
|
|
}
|
|
optionalOpt = false
|
|
for _, tagOption := range markerTagParts[1:] {
|
|
switch tagOption {
|
|
case "optional":
|
|
optionalOpt = true
|
|
}
|
|
}
|
|
|
|
return argName, optionalOpt
|
|
}
|
|
|
|
// loadFields uses reflection to populate argument information from the Output type.
|
|
func (d *Definition) loadFields() error {
|
|
if d.Fields == nil {
|
|
d.Fields = make(map[string]Argument)
|
|
d.FieldNames = make(map[string]string)
|
|
}
|
|
if d.Output.Kind() != reflect.Struct {
|
|
// anonymous field type
|
|
argType, err := ArgumentFromType(d.Output)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.Fields[""] = argType
|
|
d.FieldNames[""] = ""
|
|
return nil
|
|
}
|
|
|
|
for i := 0; i < d.Output.NumField(); i++ {
|
|
field := d.Output.Field(i)
|
|
if field.PkgPath != "" {
|
|
// as per the reflect package docs, pkgpath is empty for exported fields,
|
|
// so non-empty package path means a private field, which we should skip
|
|
continue
|
|
}
|
|
argName, optionalOpt := argumentInfo(field.Name, field.Tag)
|
|
|
|
argType, err := ArgumentFromType(field.Type)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to extract type information for field %q: %w", field.Name, err)
|
|
}
|
|
|
|
if argType.Type == RawType {
|
|
return fmt.Errorf("RawArguments must be the direct type of a marker, and not a field")
|
|
}
|
|
|
|
argType.Optional = optionalOpt || argType.Optional
|
|
|
|
d.Fields[argName] = argType
|
|
d.FieldNames[argName] = field.Name
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parserScanner makes a new scanner appropriate for use in parsing definitions and arguments.
|
|
func parserScanner(raw string, err func(*sc.Scanner, string)) *sc.Scanner {
|
|
scanner := &sc.Scanner{}
|
|
scanner.Init(bytes.NewBufferString(raw))
|
|
scanner.Mode = sc.ScanIdents | sc.ScanInts | sc.ScanStrings | sc.ScanRawStrings | sc.SkipComments
|
|
scanner.Error = err
|
|
|
|
return scanner
|
|
}
|
|
|
|
// Parse uses the type information in this Definition to parse the given
|
|
// raw marker in the form `+a:b:c=arg,d=arg` into an output object of the
|
|
// type specified in the definition.
|
|
func (d *Definition) Parse(rawMarker string) (interface{}, error) {
|
|
name, anonName, fields := splitMarker(rawMarker)
|
|
|
|
out := reflect.Indirect(reflect.New(d.Output))
|
|
|
|
// if we're a not a struct or have no arguments, treat the full `a:b:c` as the name,
|
|
// otherwise, treat `c` as a field name, and `a:b` as the marker name.
|
|
if !d.AnonymousField() && !d.Empty() && len(anonName) >= len(name)+1 {
|
|
fields = anonName[len(name)+1:] + "=" + fields
|
|
}
|
|
|
|
var errs []error
|
|
scanner := parserScanner(fields, func(scanner *sc.Scanner, msg string) {
|
|
errs = append(errs, &ScannerError{Msg: msg, Pos: scanner.Position})
|
|
})
|
|
|
|
// TODO(directxman12): strict parsing where we error out if certain fields aren't optional
|
|
seen := make(map[string]struct{}, len(d.Fields))
|
|
if d.AnonymousField() && scanner.Peek() != sc.EOF {
|
|
// might still be a struct that something fiddled with, so double check
|
|
structFieldName := d.FieldNames[""]
|
|
outTarget := out
|
|
if structFieldName != "" {
|
|
// it's a struct field mapped to an anonymous marker
|
|
outTarget = out.FieldByName(structFieldName)
|
|
if !outTarget.CanSet() {
|
|
scanner.Error(scanner, fmt.Sprintf("cannot set field %q (might not exist)", structFieldName))
|
|
return out.Interface(), loader.MaybeErrList(errs)
|
|
}
|
|
}
|
|
|
|
// no need for trying to parse field names if we're not a struct
|
|
field := d.Fields[""]
|
|
field.Parse(scanner, fields, outTarget)
|
|
seen[""] = struct{}{} // mark as seen for strict definitions
|
|
} else if !d.Empty() && scanner.Peek() != sc.EOF {
|
|
// if we expect *and* actually have arguments passed
|
|
for {
|
|
// parse the argument name
|
|
if !expect(scanner, sc.Ident, "argument name") {
|
|
break
|
|
}
|
|
argName := scanner.TokenText()
|
|
if !expect(scanner, '=', "equals") {
|
|
break
|
|
}
|
|
|
|
// make sure we know the field
|
|
fieldName, known := d.FieldNames[argName]
|
|
if !known {
|
|
scanner.Error(scanner, fmt.Sprintf("unknown argument %q", argName))
|
|
break
|
|
}
|
|
fieldType, known := d.Fields[argName]
|
|
if !known {
|
|
scanner.Error(scanner, fmt.Sprintf("unknown argument %q", argName))
|
|
break
|
|
}
|
|
seen[argName] = struct{}{} // mark as seen for strict definitions
|
|
|
|
// parse the field value
|
|
fieldVal := out.FieldByName(fieldName)
|
|
if !fieldVal.CanSet() {
|
|
scanner.Error(scanner, fmt.Sprintf("cannot set field %q (might not exist)", fieldName))
|
|
break
|
|
}
|
|
fieldType.Parse(scanner, fields, fieldVal)
|
|
|
|
if len(errs) > 0 {
|
|
break
|
|
}
|
|
|
|
if scanner.Peek() == sc.EOF {
|
|
break
|
|
}
|
|
if !expect(scanner, ',', "comma") {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if tok := scanner.Scan(); tok != sc.EOF {
|
|
scanner.Error(scanner, fmt.Sprintf("extra arguments provided: %q", fields[scanner.Position.Offset:]))
|
|
}
|
|
|
|
if d.Strict {
|
|
for argName, arg := range d.Fields {
|
|
if _, wasSeen := seen[argName]; !wasSeen && !arg.Optional {
|
|
scanner.Error(scanner, fmt.Sprintf("missing argument %q", argName))
|
|
}
|
|
}
|
|
}
|
|
|
|
return out.Interface(), loader.MaybeErrList(errs)
|
|
}
|
|
|
|
// MakeDefinition constructs a definition from a name, type, and the output type.
|
|
// All such definitions are strict by default. If a struct is passed as the output
|
|
// type, its public fields will automatically be populated into Fields (and similar
|
|
// fields in Definition). Other values will have a single, empty-string-named Fields
|
|
// entry.
|
|
func MakeDefinition(name string, target TargetType, output interface{}) (*Definition, error) {
|
|
def := &Definition{
|
|
Name: name,
|
|
Target: target,
|
|
Output: reflect.TypeOf(output),
|
|
Strict: true,
|
|
}
|
|
|
|
if err := def.loadFields(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return def, nil
|
|
}
|
|
|
|
// MakeAnyTypeDefinition constructs a definition for an output struct with a
|
|
// field named `Value` of type `interface{}`. The argument to the marker will
|
|
// be parsed as AnyType and assigned to the field named `Value`.
|
|
func MakeAnyTypeDefinition(name string, target TargetType, output interface{}) (*Definition, error) {
|
|
defn, err := MakeDefinition(name, target, output)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defn.FieldNames = map[string]string{"": "Value"}
|
|
defn.Fields = map[string]Argument{"": defn.Fields["value"]}
|
|
return defn, nil
|
|
}
|
|
|
|
// splitMarker takes a marker in the form of `+a:b:c=arg,d=arg` and splits it
|
|
// into the name (`a:b`), the name if it's not a struct (`a:b:c`), and the parts
|
|
// that are definitely fields (`arg,d=arg`).
|
|
func splitMarker(raw string) (name string, anonymousName string, restFields string) {
|
|
raw = raw[1:] // get rid of the leading '+'
|
|
nameFieldParts := strings.SplitN(raw, "=", 2)
|
|
if len(nameFieldParts) == 1 {
|
|
return nameFieldParts[0], nameFieldParts[0], ""
|
|
}
|
|
anonymousName = nameFieldParts[0]
|
|
name = anonymousName
|
|
restFields = nameFieldParts[1]
|
|
|
|
nameParts := strings.Split(name, ":")
|
|
if len(nameParts) > 1 {
|
|
name = strings.Join(nameParts[:len(nameParts)-1], ":")
|
|
}
|
|
return name, anonymousName, restFields
|
|
}
|
|
|
|
type ScannerError struct {
|
|
Msg string
|
|
Pos sc.Position
|
|
}
|
|
|
|
func (e *ScannerError) Error() string {
|
|
return fmt.Sprintf("%s (at %s)", e.Msg, e.Pos)
|
|
}
|