From efa17e889922d847297ba3af4c59fcc376bb5a3a Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 1 Sep 2018 18:53:42 +0300 Subject: [PATCH] dynamic param types part 1 Former-commit-id: 5829d53de848c0ea4491b53e4798f6c9cdf8d9a7 --- core/router/macro/interpreter/ast/ast.go | 208 +++++------------- .../router/macro/interpreter/parser/parser.go | 81 ++++++- .../macro/interpreter/parser/parser_test.go | 65 ++++-- deprecated.go | 1 - 4 files changed, 169 insertions(+), 186 deletions(-) delete mode 100644 deprecated.go diff --git a/core/router/macro/interpreter/ast/ast.go b/core/router/macro/interpreter/ast/ast.go index 42995690..fa695226 100644 --- a/core/router/macro/interpreter/ast/ast.go +++ b/core/router/macro/interpreter/ast/ast.go @@ -2,154 +2,63 @@ package ast import ( "reflect" + "strings" ) -// ParamType is a specific uint8 type -// which holds the parameter types' type. -type ParamType uint8 +// ParamType holds the necessary information about a parameter type. +type ParamType struct { + Indent string // the name of the parameter type. + Aliases []string // any aliases, can be empty. -const ( - // ParamTypeUnExpected is an unexpected parameter type. - ParamTypeUnExpected ParamType = iota - // ParamTypeString is the string type. - // If parameter type is missing then it defaults to String type. - // Allows anything - // Declaration: /mypath/{myparam:string} or {myparam} - ParamTypeString + GoType reflect.Kind // the go type useful for "mvc" and "hero" bindings. - // ParamTypeNumber is the integer, a number type. - // Allows both positive and negative numbers, any number of digits. - // Declaration: /mypath/{myparam:number} or {myparam:int} for backwards-compatibility - ParamTypeNumber + Default bool // if true then empty type param will target this and its functions will be available to the rest of the param type's funcs. + End bool // if true then it should be declared at the end of a route path and can accept any trailing path segment as one parameter. - // ParamTypeInt64 is a number type. - // Allows only -9223372036854775808 to 9223372036854775807. - // Declaration: /mypath/{myparam:int64} or {myparam:long} - ParamTypeInt64 - // ParamTypeUint8 a number type. - // Allows only 0 to 255. - // Declaration: /mypath/{myparam:uint8} - ParamTypeUint8 - // ParamTypeUint64 a number type. - // Allows only 0 to 18446744073709551615. - // Declaration: /mypath/{myparam:uint64} - ParamTypeUint64 + invalid bool // only true if returned by the parser via `LookupParamType`. +} - // ParamTypeBoolean is the bool type. - // Allows only "1" or "t" or "T" or "TRUE" or "true" or "True" - // or "0" or "f" or "F" or "FALSE" or "false" or "False". - // Declaration: /mypath/{myparam:bool} or {myparam:boolean} - ParamTypeBoolean - // ParamTypeAlphabetical is the alphabetical/letter type type. - // Allows letters only (upper or lowercase) - // Declaration: /mypath/{myparam:alphabetical} - ParamTypeAlphabetical - // ParamTypeFile is the file single path type. - // Allows: - // letters (upper or lowercase) - // numbers (0-9) - // underscore (_) - // dash (-) - // point (.) - // no spaces! or other character - // Declaration: /mypath/{myparam:file} - ParamTypeFile - // ParamTypePath is the multi path (or wildcard) type. - // Allows anything, should be the last part - // Declaration: /mypath/{myparam:path} - ParamTypePath -) +// ParamTypeUnExpected is the unexpected parameter type. +var ParamTypeUnExpected = ParamType{invalid: true} func (pt ParamType) String() string { - for k, v := range paramTypes { - if v == pt { - return k - } - } - - return "unexpected" -} - -// Not because for a single reason -// a string may be a -// ParamTypeString or a ParamTypeFile -// or a ParamTypePath or ParamTypeAlphabetical. -// -// func ParamTypeFromStd(k reflect.Kind) ParamType { - -// Kind returns the std kind of this param type. -func (pt ParamType) Kind() reflect.Kind { - switch pt { - case ParamTypeAlphabetical: - fallthrough - case ParamTypeFile: - fallthrough - case ParamTypePath: - fallthrough - case ParamTypeString: - return reflect.String - case ParamTypeNumber: - return reflect.Int - case ParamTypeInt64: - return reflect.Int64 - case ParamTypeUint8: - return reflect.Uint8 - case ParamTypeUint64: - return reflect.Uint64 - case ParamTypeBoolean: - return reflect.Bool - } - return reflect.Invalid // 0 -} - -// ValidKind will return true if at least one param type is supported -// for this std kind. -func ValidKind(k reflect.Kind) bool { - switch k { - case reflect.String: - fallthrough - case reflect.Int: - fallthrough - case reflect.Int64: - fallthrough - case reflect.Uint8: - fallthrough - case reflect.Uint64: - fallthrough - case reflect.Bool: - return true - default: - return false - } + return pt.Indent } // Assignable returns true if the "k" standard type // is assignabled to this ParamType. func (pt ParamType) Assignable(k reflect.Kind) bool { - return pt.Kind() == k + return pt.GoType == k } -var paramTypes = map[string]ParamType{ - "string": ParamTypeString, +// GetDefaultParamType accepts a list of ParamType and returns its default. +// If no `Default` specified: +// and len(paramTypes) > 0 then it will return the first one, +// otherwise it returns a "string" parameter type. +func GetDefaultParamType(paramTypes ...ParamType) ParamType { + for _, pt := range paramTypes { + if pt.Default == true { + return pt + } + } - "number": ParamTypeNumber, - "int": ParamTypeNumber, // same as number. - "long": ParamTypeInt64, - "int64": ParamTypeInt64, // same as long. - "uint8": ParamTypeUint8, - "uint64": ParamTypeUint64, + if len(paramTypes) > 0 { + return paramTypes[0] + } - "boolean": ParamTypeBoolean, - "bool": ParamTypeBoolean, // same as boolean. + return ParamType{Indent: "string", GoType: reflect.String, Default: true} +} - "alphabetical": ParamTypeAlphabetical, - "file": ParamTypeFile, - "path": ParamTypePath, - // could be named also: - // "tail": - // "wild" - // "wildcard" +// ValidKind will return true if at least one param type is supported +// for this std kind. +func ValidKind(k reflect.Kind, paramTypes ...ParamType) bool { + for _, pt := range paramTypes { + if pt.GoType == k { + return true + } + } + return false } // LookupParamType accepts the string @@ -164,11 +73,20 @@ var paramTypes = map[string]ParamType{ // "alphabetical" // "file" // "path" -func LookupParamType(ident string) ParamType { - if typ, ok := paramTypes[ident]; ok { - return typ +func LookupParamType(indent string, paramTypes ...ParamType) (ParamType, bool) { + for _, pt := range paramTypes { + if pt.Indent == indent { + return pt, true + } + + for _, alias := range pt.Aliases { + if alias == indent { + return pt, true + } + } } - return ParamTypeUnExpected + + return ParamTypeUnExpected, false } // LookupParamTypeFromStd accepts the string representation of a standard go type. @@ -181,23 +99,15 @@ func LookupParamType(ident string) ParamType { // int64 matches to int64/long // uint64 matches to uint64 // bool matches to bool/boolean -func LookupParamTypeFromStd(goType string) ParamType { - switch goType { - case "string": - return ParamTypeString - case "int": - return ParamTypeNumber - case "int64": - return ParamTypeInt64 - case "uint8": - return ParamTypeUint8 - case "uint64": - return ParamTypeUint64 - case "bool": - return ParamTypeBoolean - default: - return ParamTypeUnExpected +func LookupParamTypeFromStd(goType string, paramTypes ...ParamType) (ParamType, bool) { + goType = strings.ToLower(goType) + for _, pt := range paramTypes { + if strings.ToLower(pt.GoType.String()) == goType { + return pt, true + } } + + return ParamTypeUnExpected, false } // ParamStatement is a struct diff --git a/core/router/macro/interpreter/parser/parser.go b/core/router/macro/interpreter/parser/parser.go index 97920d5b..9ce125c9 100644 --- a/core/router/macro/interpreter/parser/parser.go +++ b/core/router/macro/interpreter/parser/parser.go @@ -2,6 +2,7 @@ package parser import ( "fmt" + "reflect" "strconv" "strings" @@ -10,10 +11,69 @@ import ( "github.com/kataras/iris/core/router/macro/interpreter/token" ) +var ( + // paramTypeString is the string type. + // If parameter type is missing then it defaults to String type. + // Allows anything + // Declaration: /mypath/{myparam:string} or {myparam} + paramTypeString = ast.ParamType{Indent: "string", GoType: reflect.String, Default: true} + // ParamTypeNumber is the integer, a number type. + // Allows both positive and negative numbers, any number of digits. + // Declaration: /mypath/{myparam:number} or {myparam:int} for backwards-compatibility + paramTypeNumber = ast.ParamType{Indent: "number", Aliases: []string{"int"}, GoType: reflect.Int} + // ParamTypeInt64 is a number type. + // Allows only -9223372036854775808 to 9223372036854775807. + // Declaration: /mypath/{myparam:int64} or {myparam:long} + paramTypeInt64 = ast.ParamType{Indent: "int64", Aliases: []string{"long"}, GoType: reflect.Int64} + // ParamTypeUint8 a number type. + // Allows only 0 to 255. + // Declaration: /mypath/{myparam:uint8} + paramTypeUint8 = ast.ParamType{Indent: "uint8", GoType: reflect.Uint8} + // ParamTypeUint64 a number type. + // Allows only 0 to 18446744073709551615. + // Declaration: /mypath/{myparam:uint64} + paramTypeUint64 = ast.ParamType{Indent: "uint64", GoType: reflect.Uint64} + // ParamTypeBool is the bool type. + // Allows only "1" or "t" or "T" or "TRUE" or "true" or "True" + // or "0" or "f" or "F" or "FALSE" or "false" or "False". + // Declaration: /mypath/{myparam:bool} or {myparam:boolean} + paramTypeBool = ast.ParamType{Indent: "bool", Aliases: []string{"boolean"}, GoType: reflect.Bool} + // ParamTypeAlphabetical is the alphabetical/letter type type. + // Allows letters only (upper or lowercase) + // Declaration: /mypath/{myparam:alphabetical} + paramTypeAlphabetical = ast.ParamType{Indent: "alphabetical", GoType: reflect.String} + // ParamTypeFile is the file single path type. + // Allows: + // letters (upper or lowercase) + // numbers (0-9) + // underscore (_) + // dash (-) + // point (.) + // no spaces! or other character + // Declaration: /mypath/{myparam:file} + paramTypeFile = ast.ParamType{Indent: "file", GoType: reflect.String} + // ParamTypePath is the multi path (or wildcard) type. + // Allows anything, should be the last part + // Declaration: /mypath/{myparam:path} + paramTypePath = ast.ParamType{Indent: "path", GoType: reflect.String, End: true} +) + +// DefaultParamTypes are the built'n parameter types. +var DefaultParamTypes = []ast.ParamType{ + paramTypeString, + paramTypeNumber, paramTypeInt64, paramTypeUint8, paramTypeUint64, + paramTypeBool, + paramTypeAlphabetical, paramTypeFile, paramTypePath, +} + // Parse takes a route "fullpath" // and returns its param statements // and an error on failure. -func Parse(fullpath string) ([]*ast.ParamStatement, error) { +func Parse(fullpath string, paramTypes ...ast.ParamType) ([]*ast.ParamStatement, error) { + if len(paramTypes) == 0 { + paramTypes = DefaultParamTypes + } + pathParts := strings.SplitN(fullpath, "/", -1) p := new(ParamParser) statements := make([]*ast.ParamStatement, 0) @@ -28,14 +88,14 @@ func Parse(fullpath string) ([]*ast.ParamStatement, error) { } p.Reset(s) - stmt, err := p.Parse() + stmt, err := p.Parse(paramTypes) if err != nil { // exit on first error return nil, err } // if we have param type path but it's not the last path part - if stmt.Type == ast.ParamTypePath && i < len(pathParts)-1 { - return nil, fmt.Errorf("param type 'path' should be lived only inside the last path segment, but was inside: %s", s) + if stmt.Type.End && i < len(pathParts)-1 { + return nil, fmt.Errorf("param type '%s' should be lived only inside the last path segment, but was inside: %s", stmt.Type, s) } statements = append(statements, stmt) @@ -77,9 +137,6 @@ const ( // per-parameter. An error code can be setted via // the "else" keyword inside a route's path. DefaultParamErrorCode = 404 - // DefaultParamType when parameter type is missing use this param type, defaults to string - // and it should be remains unless earth split in two. - DefaultParamType = ast.ParamTypeString ) // func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) { @@ -102,14 +159,14 @@ func (p ParamParser) Error() error { return nil } -// Parse parses the p.src and returns its param statement +// Parse parses the p.src based on the given param types and returns its param statement // and an error on failure. -func (p *ParamParser) Parse() (*ast.ParamStatement, error) { +func (p *ParamParser) Parse(paramTypes []ast.ParamType) (*ast.ParamStatement, error) { l := lexer.New(p.src) stmt := &ast.ParamStatement{ ErrorCode: DefaultParamErrorCode, - Type: DefaultParamType, + Type: ast.GetDefaultParamType(paramTypes...), Src: p.src, } @@ -132,8 +189,8 @@ func (p *ParamParser) Parse() (*ast.ParamStatement, error) { case token.COLON: // type can accept both letters and numbers but not symbols ofc. nextTok := l.NextToken() - paramType := ast.LookupParamType(nextTok.Literal) - if paramType == ast.ParamTypeUnExpected { + paramType, found := ast.LookupParamType(nextTok.Literal, paramTypes...) + if !found { p.appendErr("[%d:%d] unexpected parameter type: %s", t.Start, t.End, nextTok.Literal) } stmt.Type = paramType diff --git a/core/router/macro/interpreter/parser/parser_test.go b/core/router/macro/interpreter/parser/parser_test.go index 25fb3400..ffdd5335 100644 --- a/core/router/macro/interpreter/parser/parser_test.go +++ b/core/router/macro/interpreter/parser/parser_test.go @@ -16,7 +16,7 @@ func TestParseParamError(t *testing.T) { input := "{id" + string(illegalChar) + "int range(1,5) else 404}" p := NewParamParser(input) - _, err := p.Parse() + _, err := p.Parse(DefaultParamTypes) if err == nil { t.Fatalf("expecting not empty error on input '%s'", input) @@ -32,7 +32,7 @@ func TestParseParamError(t *testing.T) { // success input2 := "{id:uint64 range(1,5) else 404}" p.Reset(input2) - _, err = p.Parse() + _, err = p.Parse(DefaultParamTypes) if err != nil { t.Fatalf("expecting empty error on input '%s', but got: %s", input2, err.Error()) @@ -40,6 +40,16 @@ func TestParseParamError(t *testing.T) { // } +// mustLookupParamType same as `ast.LookupParamType` but it panics if "indent" does not match with a valid Param Type. +func mustLookupParamType(indent string) ast.ParamType { + pt, found := ast.LookupParamType(indent, DefaultParamTypes...) + if !found { + panic("param type '" + indent + "' is not part of the provided param types") + } + + return pt +} + func TestParseParam(t *testing.T) { tests := []struct { valid bool @@ -49,7 +59,7 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{id:number min(1) max(5) else 404}", Name: "id", - Type: ast.ParamTypeNumber, + Type: mustLookupParamType("number"), Funcs: []ast.ParamFunc{ { Name: "min", @@ -65,7 +75,7 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{id:number range(1,5)}", Name: "id", - Type: ast.ParamTypeNumber, + Type: mustLookupParamType("number"), Funcs: []ast.ParamFunc{ { Name: "range", @@ -77,7 +87,7 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{file:path contains(.)}", Name: "file", - Type: ast.ParamTypePath, + Type: mustLookupParamType("path"), Funcs: []ast.ParamFunc{ { Name: "contains", @@ -89,14 +99,14 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{username:alphabetical}", Name: "username", - Type: ast.ParamTypeAlphabetical, + Type: mustLookupParamType("alphabetical"), ErrorCode: 404, }}, // 3 {true, ast.ParamStatement{ Src: "{myparam}", Name: "myparam", - Type: ast.ParamTypeString, + Type: mustLookupParamType("string"), ErrorCode: 404, }}, // 4 {false, @@ -110,14 +120,14 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{myparam2}", Name: "myparam2", // we now allow integers to the parameter names. - Type: ast.ParamTypeString, + Type: ast.GetDefaultParamType(DefaultParamTypes...), ErrorCode: 404, }}, // 6 {true, ast.ParamStatement{ Src: "{id:number even()}", // test param funcs without any arguments (LPAREN peek for RPAREN) Name: "id", - Type: ast.ParamTypeNumber, + Type: mustLookupParamType("number"), Funcs: []ast.ParamFunc{ { Name: "even"}, @@ -128,37 +138,44 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{id:int64 else 404}", Name: "id", - Type: ast.ParamTypeInt64, + Type: mustLookupParamType("int64"), ErrorCode: 404, }}, // 8 {true, ast.ParamStatement{ Src: "{id:long else 404}", // backwards-compatible test. Name: "id", - Type: ast.ParamTypeInt64, + Type: mustLookupParamType("int64"), ErrorCode: 404, }}, // 9 {true, ast.ParamStatement{ - Src: "{has:bool else 404}", - Name: "has", - Type: ast.ParamTypeBoolean, + Src: "{id:long else 404}", + Name: "id", + Type: mustLookupParamType("long"), // backwards-compatible test of LookupParamType. ErrorCode: 404, }}, // 10 {true, ast.ParamStatement{ - Src: "{has:boolean else 404}", // backwards-compatible test. + Src: "{has:bool else 404}", Name: "has", - Type: ast.ParamTypeBoolean, + Type: mustLookupParamType("bool"), ErrorCode: 404, }}, // 11 + {true, + ast.ParamStatement{ + Src: "{has:boolean else 404}", // backwards-compatible test. + Name: "has", + Type: mustLookupParamType("bool"), + ErrorCode: 404, + }}, // 12 } p := new(ParamParser) for i, tt := range tests { p.Reset(tt.expectedStatement.Src) - resultStmt, err := p.Parse() + resultStmt, err := p.Parse(DefaultParamTypes) if tt.valid && err != nil { t.Fatalf("tests[%d] - error %s", i, err.Error()) @@ -185,7 +202,7 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{id:number min(1) max(5) else 404}", Name: "id", - Type: ast.ParamTypeNumber, + Type: paramTypeNumber, Funcs: []ast.ParamFunc{ { Name: "min", @@ -201,7 +218,7 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{id:uint64 range(1,5)}", // test alternative (backwards-compatibility) "int" Name: "id", - Type: ast.ParamTypeUint64, + Type: paramTypeUint64, Funcs: []ast.ParamFunc{ { Name: "range", @@ -214,7 +231,7 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{file:path contains(.)}", Name: "file", - Type: ast.ParamTypePath, + Type: paramTypePath, Funcs: []ast.ParamFunc{ { Name: "contains", @@ -227,7 +244,7 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{username:alphabetical}", Name: "username", - Type: ast.ParamTypeAlphabetical, + Type: paramTypeAlphabetical, ErrorCode: 404, }, }}, // 3 @@ -235,7 +252,7 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{myparam}", Name: "myparam", - Type: ast.ParamTypeString, + Type: paramTypeString, ErrorCode: 404, }, }}, // 4 @@ -251,7 +268,7 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{myparam2}", Name: "myparam2", // we now allow integers to the parameter names. - Type: ast.ParamTypeString, + Type: paramTypeString, ErrorCode: 404, }, }}, // 6 @@ -259,7 +276,7 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{file:path}", Name: "file", - Type: ast.ParamTypePath, + Type: paramTypePath, ErrorCode: 404, }, }}, // 7 diff --git a/deprecated.go b/deprecated.go deleted file mode 100644 index d44707b2..00000000 --- a/deprecated.go +++ /dev/null @@ -1 +0,0 @@ -package iris