dynamic param types part 1

Former-commit-id: 5829d53de848c0ea4491b53e4798f6c9cdf8d9a7
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-09-01 18:53:42 +03:00
parent 91fe161e90
commit efa17e8899
4 changed files with 169 additions and 186 deletions

View File

@ -2,154 +2,63 @@ package ast
import ( import (
"reflect" "reflect"
"strings"
) )
// ParamType is a specific uint8 type // ParamType holds the necessary information about a parameter type.
// which holds the parameter types' type. type ParamType struct {
type ParamType uint8 Indent string // the name of the parameter type.
Aliases []string // any aliases, can be empty.
const ( GoType reflect.Kind // the go type useful for "mvc" and "hero" bindings.
// 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
// ParamTypeNumber is the integer, a number type. 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.
// Allows both positive and negative numbers, any number of digits. 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.
// Declaration: /mypath/{myparam:number} or {myparam:int} for backwards-compatibility
ParamTypeNumber
// ParamTypeInt64 is a number type. invalid bool // only true if returned by the parser via `LookupParamType`.
// 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
// ParamTypeBoolean is the bool type. // ParamTypeUnExpected is the unexpected parameter type.
// Allows only "1" or "t" or "T" or "TRUE" or "true" or "True" var ParamTypeUnExpected = ParamType{invalid: 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
)
func (pt ParamType) String() string { func (pt ParamType) String() string {
for k, v := range paramTypes { return pt.Indent
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
}
} }
// Assignable returns true if the "k" standard type // Assignable returns true if the "k" standard type
// is assignabled to this ParamType. // is assignabled to this ParamType.
func (pt ParamType) Assignable(k reflect.Kind) bool { func (pt ParamType) Assignable(k reflect.Kind) bool {
return pt.Kind() == k return pt.GoType == k
} }
var paramTypes = map[string]ParamType{ // GetDefaultParamType accepts a list of ParamType and returns its default.
"string": ParamTypeString, // 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, if len(paramTypes) > 0 {
"int": ParamTypeNumber, // same as number. return paramTypes[0]
"long": ParamTypeInt64, }
"int64": ParamTypeInt64, // same as long.
"uint8": ParamTypeUint8,
"uint64": ParamTypeUint64,
"boolean": ParamTypeBoolean, return ParamType{Indent: "string", GoType: reflect.String, Default: true}
"bool": ParamTypeBoolean, // same as boolean. }
"alphabetical": ParamTypeAlphabetical, // ValidKind will return true if at least one param type is supported
"file": ParamTypeFile, // for this std kind.
"path": ParamTypePath, func ValidKind(k reflect.Kind, paramTypes ...ParamType) bool {
// could be named also: for _, pt := range paramTypes {
// "tail": if pt.GoType == k {
// "wild" return true
// "wildcard" }
}
return false
} }
// LookupParamType accepts the string // LookupParamType accepts the string
@ -164,11 +73,20 @@ var paramTypes = map[string]ParamType{
// "alphabetical" // "alphabetical"
// "file" // "file"
// "path" // "path"
func LookupParamType(ident string) ParamType { func LookupParamType(indent string, paramTypes ...ParamType) (ParamType, bool) {
if typ, ok := paramTypes[ident]; ok { for _, pt := range paramTypes {
return typ 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. // LookupParamTypeFromStd accepts the string representation of a standard go type.
@ -181,23 +99,15 @@ func LookupParamType(ident string) ParamType {
// int64 matches to int64/long // int64 matches to int64/long
// uint64 matches to uint64 // uint64 matches to uint64
// bool matches to bool/boolean // bool matches to bool/boolean
func LookupParamTypeFromStd(goType string) ParamType { func LookupParamTypeFromStd(goType string, paramTypes ...ParamType) (ParamType, bool) {
switch goType { goType = strings.ToLower(goType)
case "string": for _, pt := range paramTypes {
return ParamTypeString if strings.ToLower(pt.GoType.String()) == goType {
case "int": return pt, true
return ParamTypeNumber }
case "int64":
return ParamTypeInt64
case "uint8":
return ParamTypeUint8
case "uint64":
return ParamTypeUint64
case "bool":
return ParamTypeBoolean
default:
return ParamTypeUnExpected
} }
return ParamTypeUnExpected, false
} }
// ParamStatement is a struct // ParamStatement is a struct

View File

@ -2,6 +2,7 @@ package parser
import ( import (
"fmt" "fmt"
"reflect"
"strconv" "strconv"
"strings" "strings"
@ -10,10 +11,69 @@ import (
"github.com/kataras/iris/core/router/macro/interpreter/token" "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" // Parse takes a route "fullpath"
// and returns its param statements // and returns its param statements
// and an error on failure. // 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) pathParts := strings.SplitN(fullpath, "/", -1)
p := new(ParamParser) p := new(ParamParser)
statements := make([]*ast.ParamStatement, 0) statements := make([]*ast.ParamStatement, 0)
@ -28,14 +88,14 @@ func Parse(fullpath string) ([]*ast.ParamStatement, error) {
} }
p.Reset(s) p.Reset(s)
stmt, err := p.Parse() stmt, err := p.Parse(paramTypes)
if err != nil { if err != nil {
// exit on first error // exit on first error
return nil, err return nil, err
} }
// if we have param type path but it's not the last path part // if we have param type path but it's not the last path part
if stmt.Type == ast.ParamTypePath && i < len(pathParts)-1 { if stmt.Type.End && 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) 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) statements = append(statements, stmt)
@ -77,9 +137,6 @@ const (
// per-parameter. An error code can be setted via // per-parameter. An error code can be setted via
// the "else" keyword inside a route's path. // the "else" keyword inside a route's path.
DefaultParamErrorCode = 404 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) { // func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) {
@ -102,14 +159,14 @@ func (p ParamParser) Error() error {
return nil 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. // 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) l := lexer.New(p.src)
stmt := &ast.ParamStatement{ stmt := &ast.ParamStatement{
ErrorCode: DefaultParamErrorCode, ErrorCode: DefaultParamErrorCode,
Type: DefaultParamType, Type: ast.GetDefaultParamType(paramTypes...),
Src: p.src, Src: p.src,
} }
@ -132,8 +189,8 @@ func (p *ParamParser) Parse() (*ast.ParamStatement, error) {
case token.COLON: case token.COLON:
// type can accept both letters and numbers but not symbols ofc. // type can accept both letters and numbers but not symbols ofc.
nextTok := l.NextToken() nextTok := l.NextToken()
paramType := ast.LookupParamType(nextTok.Literal) paramType, found := ast.LookupParamType(nextTok.Literal, paramTypes...)
if paramType == ast.ParamTypeUnExpected { if !found {
p.appendErr("[%d:%d] unexpected parameter type: %s", t.Start, t.End, nextTok.Literal) p.appendErr("[%d:%d] unexpected parameter type: %s", t.Start, t.End, nextTok.Literal)
} }
stmt.Type = paramType stmt.Type = paramType

View File

@ -16,7 +16,7 @@ func TestParseParamError(t *testing.T) {
input := "{id" + string(illegalChar) + "int range(1,5) else 404}" input := "{id" + string(illegalChar) + "int range(1,5) else 404}"
p := NewParamParser(input) p := NewParamParser(input)
_, err := p.Parse() _, err := p.Parse(DefaultParamTypes)
if err == nil { if err == nil {
t.Fatalf("expecting not empty error on input '%s'", input) t.Fatalf("expecting not empty error on input '%s'", input)
@ -32,7 +32,7 @@ func TestParseParamError(t *testing.T) {
// success // success
input2 := "{id:uint64 range(1,5) else 404}" input2 := "{id:uint64 range(1,5) else 404}"
p.Reset(input2) p.Reset(input2)
_, err = p.Parse() _, err = p.Parse(DefaultParamTypes)
if err != nil { if err != nil {
t.Fatalf("expecting empty error on input '%s', but got: %s", input2, err.Error()) 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) { func TestParseParam(t *testing.T) {
tests := []struct { tests := []struct {
valid bool valid bool
@ -49,7 +59,7 @@ func TestParseParam(t *testing.T) {
ast.ParamStatement{ ast.ParamStatement{
Src: "{id:number min(1) max(5) else 404}", Src: "{id:number min(1) max(5) else 404}",
Name: "id", Name: "id",
Type: ast.ParamTypeNumber, Type: mustLookupParamType("number"),
Funcs: []ast.ParamFunc{ Funcs: []ast.ParamFunc{
{ {
Name: "min", Name: "min",
@ -65,7 +75,7 @@ func TestParseParam(t *testing.T) {
ast.ParamStatement{ ast.ParamStatement{
Src: "{id:number range(1,5)}", Src: "{id:number range(1,5)}",
Name: "id", Name: "id",
Type: ast.ParamTypeNumber, Type: mustLookupParamType("number"),
Funcs: []ast.ParamFunc{ Funcs: []ast.ParamFunc{
{ {
Name: "range", Name: "range",
@ -77,7 +87,7 @@ func TestParseParam(t *testing.T) {
ast.ParamStatement{ ast.ParamStatement{
Src: "{file:path contains(.)}", Src: "{file:path contains(.)}",
Name: "file", Name: "file",
Type: ast.ParamTypePath, Type: mustLookupParamType("path"),
Funcs: []ast.ParamFunc{ Funcs: []ast.ParamFunc{
{ {
Name: "contains", Name: "contains",
@ -89,14 +99,14 @@ func TestParseParam(t *testing.T) {
ast.ParamStatement{ ast.ParamStatement{
Src: "{username:alphabetical}", Src: "{username:alphabetical}",
Name: "username", Name: "username",
Type: ast.ParamTypeAlphabetical, Type: mustLookupParamType("alphabetical"),
ErrorCode: 404, ErrorCode: 404,
}}, // 3 }}, // 3
{true, {true,
ast.ParamStatement{ ast.ParamStatement{
Src: "{myparam}", Src: "{myparam}",
Name: "myparam", Name: "myparam",
Type: ast.ParamTypeString, Type: mustLookupParamType("string"),
ErrorCode: 404, ErrorCode: 404,
}}, // 4 }}, // 4
{false, {false,
@ -110,14 +120,14 @@ func TestParseParam(t *testing.T) {
ast.ParamStatement{ ast.ParamStatement{
Src: "{myparam2}", Src: "{myparam2}",
Name: "myparam2", // we now allow integers to the parameter names. Name: "myparam2", // we now allow integers to the parameter names.
Type: ast.ParamTypeString, Type: ast.GetDefaultParamType(DefaultParamTypes...),
ErrorCode: 404, ErrorCode: 404,
}}, // 6 }}, // 6
{true, {true,
ast.ParamStatement{ ast.ParamStatement{
Src: "{id:number even()}", // test param funcs without any arguments (LPAREN peek for RPAREN) Src: "{id:number even()}", // test param funcs without any arguments (LPAREN peek for RPAREN)
Name: "id", Name: "id",
Type: ast.ParamTypeNumber, Type: mustLookupParamType("number"),
Funcs: []ast.ParamFunc{ Funcs: []ast.ParamFunc{
{ {
Name: "even"}, Name: "even"},
@ -128,37 +138,44 @@ func TestParseParam(t *testing.T) {
ast.ParamStatement{ ast.ParamStatement{
Src: "{id:int64 else 404}", Src: "{id:int64 else 404}",
Name: "id", Name: "id",
Type: ast.ParamTypeInt64, Type: mustLookupParamType("int64"),
ErrorCode: 404, ErrorCode: 404,
}}, // 8 }}, // 8
{true, {true,
ast.ParamStatement{ ast.ParamStatement{
Src: "{id:long else 404}", // backwards-compatible test. Src: "{id:long else 404}", // backwards-compatible test.
Name: "id", Name: "id",
Type: ast.ParamTypeInt64, Type: mustLookupParamType("int64"),
ErrorCode: 404, ErrorCode: 404,
}}, // 9 }}, // 9
{true, {true,
ast.ParamStatement{ ast.ParamStatement{
Src: "{has:bool else 404}", Src: "{id:long else 404}",
Name: "has", Name: "id",
Type: ast.ParamTypeBoolean, Type: mustLookupParamType("long"), // backwards-compatible test of LookupParamType.
ErrorCode: 404, ErrorCode: 404,
}}, // 10 }}, // 10
{true, {true,
ast.ParamStatement{ ast.ParamStatement{
Src: "{has:boolean else 404}", // backwards-compatible test. Src: "{has:bool else 404}",
Name: "has", Name: "has",
Type: ast.ParamTypeBoolean, Type: mustLookupParamType("bool"),
ErrorCode: 404, ErrorCode: 404,
}}, // 11 }}, // 11
{true,
ast.ParamStatement{
Src: "{has:boolean else 404}", // backwards-compatible test.
Name: "has",
Type: mustLookupParamType("bool"),
ErrorCode: 404,
}}, // 12
} }
p := new(ParamParser) p := new(ParamParser)
for i, tt := range tests { for i, tt := range tests {
p.Reset(tt.expectedStatement.Src) p.Reset(tt.expectedStatement.Src)
resultStmt, err := p.Parse() resultStmt, err := p.Parse(DefaultParamTypes)
if tt.valid && err != nil { if tt.valid && err != nil {
t.Fatalf("tests[%d] - error %s", i, err.Error()) t.Fatalf("tests[%d] - error %s", i, err.Error())
@ -185,7 +202,7 @@ func TestParse(t *testing.T) {
[]ast.ParamStatement{{ []ast.ParamStatement{{
Src: "{id:number min(1) max(5) else 404}", Src: "{id:number min(1) max(5) else 404}",
Name: "id", Name: "id",
Type: ast.ParamTypeNumber, Type: paramTypeNumber,
Funcs: []ast.ParamFunc{ Funcs: []ast.ParamFunc{
{ {
Name: "min", Name: "min",
@ -201,7 +218,7 @@ func TestParse(t *testing.T) {
[]ast.ParamStatement{{ []ast.ParamStatement{{
Src: "{id:uint64 range(1,5)}", // test alternative (backwards-compatibility) "int" Src: "{id:uint64 range(1,5)}", // test alternative (backwards-compatibility) "int"
Name: "id", Name: "id",
Type: ast.ParamTypeUint64, Type: paramTypeUint64,
Funcs: []ast.ParamFunc{ Funcs: []ast.ParamFunc{
{ {
Name: "range", Name: "range",
@ -214,7 +231,7 @@ func TestParse(t *testing.T) {
[]ast.ParamStatement{{ []ast.ParamStatement{{
Src: "{file:path contains(.)}", Src: "{file:path contains(.)}",
Name: "file", Name: "file",
Type: ast.ParamTypePath, Type: paramTypePath,
Funcs: []ast.ParamFunc{ Funcs: []ast.ParamFunc{
{ {
Name: "contains", Name: "contains",
@ -227,7 +244,7 @@ func TestParse(t *testing.T) {
[]ast.ParamStatement{{ []ast.ParamStatement{{
Src: "{username:alphabetical}", Src: "{username:alphabetical}",
Name: "username", Name: "username",
Type: ast.ParamTypeAlphabetical, Type: paramTypeAlphabetical,
ErrorCode: 404, ErrorCode: 404,
}, },
}}, // 3 }}, // 3
@ -235,7 +252,7 @@ func TestParse(t *testing.T) {
[]ast.ParamStatement{{ []ast.ParamStatement{{
Src: "{myparam}", Src: "{myparam}",
Name: "myparam", Name: "myparam",
Type: ast.ParamTypeString, Type: paramTypeString,
ErrorCode: 404, ErrorCode: 404,
}, },
}}, // 4 }}, // 4
@ -251,7 +268,7 @@ func TestParse(t *testing.T) {
[]ast.ParamStatement{{ []ast.ParamStatement{{
Src: "{myparam2}", Src: "{myparam2}",
Name: "myparam2", // we now allow integers to the parameter names. Name: "myparam2", // we now allow integers to the parameter names.
Type: ast.ParamTypeString, Type: paramTypeString,
ErrorCode: 404, ErrorCode: 404,
}, },
}}, // 6 }}, // 6
@ -259,7 +276,7 @@ func TestParse(t *testing.T) {
[]ast.ParamStatement{{ []ast.ParamStatement{{
Src: "{file:path}", Src: "{file:path}",
Name: "file", Name: "file",
Type: ast.ParamTypePath, Type: paramTypePath,
ErrorCode: 404, ErrorCode: 404,
}, },
}}, // 7 }}, // 7

View File

@ -1 +0,0 @@
package iris