diff --git a/_future/ipel/README.md b/_future/ipel/README.md index 88399cd5..c5e2553c 100644 --- a/_future/ipel/README.md +++ b/_future/ipel/README.md @@ -7,13 +7,13 @@ - No Breaking Changes. - No performance cost if not used. - Can convert a path for the existing routers, if no router is being used, then it will use its own, new, router. -- 4+1 basic parameter types: `int`, `string`, `alphabetical`, `path`, (wildcard), `any` based on regexp. +- 4+1 basic parameter types: `string`, `int`, `alphabetical`, `file`, `path` (file with any number of slashes), based on regexp. - Each type has unlimited functions of its own, they should be able to be overriden. - Give devs the ability to parse their function's arguments before use them and return a func which is the validator. - Function will be a generic type(`interface{}`) in order to devs be able to use any type without boilerplate code for conversions, can be done using reflection and reflect.Call, on .Boot time to parse the function automatically, and keep the returning validator function (already tested and worked). -- The `any` will be the default if dev use functions to the named path parameter but missing a type. -- If a type doesnt't contains a function of its own, then it will use the `any`'s, so `any` will contain global-use functions too. +- The `string` will be the default if dev use functions to the named path parameter but missing a type. +- If a type doesnt't contains a function of its own, then it will use the `string`'s, so `string` will contain global-use functions too. ## Preview @@ -67,7 +67,7 @@ app.String.Set("isVersion", isVersionStrValidator) app.String.Set("len", lenStrValidator) ``` -`/uploads/{filepath:tail contains(.) else 403}` +`/uploads/{fullpath:path contains(.) else 403}` ```go [...] diff --git a/_future/ipel/ast/param.go b/_future/ipel/ast/param.go index 3384452b..8b43afe4 100644 --- a/_future/ipel/ast/param.go +++ b/_future/ipel/ast/param.go @@ -4,25 +4,29 @@ type ParamType uint8 const ( ParamTypeUnExpected ParamType = iota - // /42 - ParamTypeInt // /myparam1 ParamTypeString + // /42 + ParamTypeInt // /myparam ParamTypeAlphabetical + // /main.css + ParamTypeFile // /myparam1/myparam2 ParamTypePath ) var paramTypes = map[string]ParamType{ - "int": ParamTypeInt, "string": ParamTypeString, + "int": ParamTypeInt, "alphabetical": ParamTypeAlphabetical, + "file": ParamTypeFile, "path": ParamTypePath, // could be named also: // "tail": // "wild" // "wildcard" + } func LookupParamType(ident string) ParamType { diff --git a/_future/ipel/evaluator/evaluator.go b/_future/ipel/evaluator/evaluator.go new file mode 100644 index 00000000..46c1a07c --- /dev/null +++ b/_future/ipel/evaluator/evaluator.go @@ -0,0 +1,27 @@ +package evaluator + +import ( + "fmt" + "regexp" +) + +// final evaluator signature for both param types and param funcs +type ParamEvaluator func(paramValue string) bool + +func NewParamEvaluatorFromRegexp(expr string) (ParamEvaluator, error) { + if expr == "" { + return nil, fmt.Errorf("empty regex expression") + } + + // add the last $ if missing (and not wildcard(?)) + if i := expr[len(expr)-1]; i != '$' && i != '*' { + expr += "$" + } + + r, err := regexp.Compile(expr) + if err != nil { + return nil, err + } + + return r.MatchString, nil +} diff --git a/_future/ipel/evaluator/param.go b/_future/ipel/evaluator/param.go new file mode 100644 index 00000000..e3e34e87 --- /dev/null +++ b/_future/ipel/evaluator/param.go @@ -0,0 +1,56 @@ +package evaluator + +import ( + "gopkg.in/kataras/iris.v6/_future/ipel/ast" +) + +// exported to be able to change how param types are evaluating +var ParamTypeEvaluator = make(map[ast.ParamType]ParamEvaluator, 0) + +func init() { + + // string type + // anything. + stringRegex, err := NewParamEvaluatorFromRegexp(".*") + if err != nil { + panic(err) + } + ParamTypeEvaluator[ast.ParamTypeString] = stringRegex + + // int type + // only numbers (0-9) + numRegex, err := NewParamEvaluatorFromRegexp("[0-9]+$") + if err != nil { + panic(err) + } + ParamTypeEvaluator[ast.ParamTypeInt] = numRegex + + // alphabetical/letter type + // letters only (upper or lowercase) + alphabeticRegex, err := NewParamEvaluatorFromRegexp("[a-zA-Z]+$") + if err != nil { + panic(err) + } + ParamTypeEvaluator[ast.ParamTypeAlphabetical] = alphabeticRegex + + // file type + // letters (upper or lowercase) + // numbers (0-9) + // underscore (_) + // dash (-) + // point (.) + // no spaces! or other character + fileRegex, err := NewParamEvaluatorFromRegexp("[a-zA-Z0-9_.-]*$") + if err != nil { + panic(err) + } + ParamTypeEvaluator[ast.ParamTypeFile] = fileRegex + + // path type + // file with slashes(anywhere) + pathRegex, err := NewParamEvaluatorFromRegexp("[a-zA-Z0-9_.-/]*$") + if err != nil { + panic(err) + } + ParamTypeEvaluator[ast.ParamTypePath] = pathRegex +} diff --git a/_future/ipel/lexer/lexer.go b/_future/ipel/lexer/lexer.go index 0d11eea5..7b1c73dd 100644 --- a/_future/ipel/lexer/lexer.go +++ b/_future/ipel/lexer/lexer.go @@ -11,9 +11,9 @@ type Lexer struct { ch byte // current char under examination } -func New(input string) *Lexer { +func New(src string) *Lexer { l := &Lexer{ - input: input, + input: src, } // step to the first character in order to be ready l.readChar() @@ -86,9 +86,12 @@ func (l *Lexer) newToken(tokenType token.TokenType, lit string) token.Token { Start: l.pos, End: l.pos, } - + // remember, l.pos is the last char + // and we want to include both start and end + // in order to be easy to the user to see by just marking the expression if l.pos > 1 && len(lit) > 1 { - t.End = t.Start + len(lit) - 1 + t.End = l.pos - 1 + t.Start = t.End - len(lit) + 1 } return t diff --git a/_future/ipel/parser/parser.go b/_future/ipel/parser/parser.go index b5059dcd..67fd1de8 100644 --- a/_future/ipel/parser/parser.go +++ b/_future/ipel/parser/parser.go @@ -15,19 +15,23 @@ type Parser struct { errors []string } -func New(lexer *lexer.Lexer) *Parser { - p := &Parser{ - l: lexer, - } - +func New(src string) *Parser { + p := new(Parser) + p.Reset(src) return p } +func (p *Parser) Reset(src string) { + p.l = lexer.New(src) + p.errors = []string{} +} + func (p *Parser) appendErr(format string, a ...interface{}) { p.errors = append(p.errors, fmt.Sprintf(format, a...)) } const DefaultParamErrorCode = 404 +const DefaultParamType = ast.ParamTypeString func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) { if t.Type == token.INT { @@ -36,20 +40,35 @@ func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) { return t.Literal, nil } +func (p Parser) Error() error { + if len(p.errors) > 0 { + return fmt.Errorf(strings.Join(p.errors, "\n")) + } + return nil +} + func (p *Parser) Parse() (*ast.ParamStatement, error) { - stmt := new(ast.ParamStatement) - stmt.ErrorCode = DefaultParamErrorCode - // let's have them nilled stmt.Funcs = make([]ast.ParamFunc, 0) + p.errors = []string{} + + stmt := &ast.ParamStatement{ + ErrorCode: DefaultParamErrorCode, + Type: DefaultParamType, + } + lastParamFunc := ast.ParamFunc{} + for { t := p.l.NextToken() if t.Type == token.EOF { + if stmt.Name == "" { + p.appendErr("[1:] parameter name is missing") + } break } switch t.Type { case token.LBRACE: - // name + // name, alphabetical and _, param names are not allowed to contain any number. nextTok := p.l.NextToken() stmt.Name = nextTok.Literal case token.COLON: @@ -58,7 +77,6 @@ func (p *Parser) Parse() (*ast.ParamStatement, error) { paramType := ast.LookupParamType(nextTok.Literal) if paramType == ast.ParamTypeUnExpected { p.appendErr("[%d:%d] unexpected parameter type: %s", t.Start, t.End, nextTok.Literal) - continue } stmt.Type = paramType // param func @@ -104,12 +122,8 @@ func (p *Parser) Parse() (*ast.ParamStatement, error) { p.appendErr("[%d:%d] illegal token: %s", t.Start, t.End, t.Literal) default: p.appendErr("[%d:%d] unexpected token type: %q with value %s", t.Start, t.End, t.Type, t.Literal) - } } - if len(p.errors) > 0 { - return nil, fmt.Errorf(strings.Join(p.errors, "\n")) - } - return stmt, nil + return stmt, p.Error() } diff --git a/_future/ipel/parser/parser_test.go b/_future/ipel/parser/parser_test.go index cc94d909..e0c3ca62 100644 --- a/_future/ipel/parser/parser_test.go +++ b/_future/ipel/parser/parser_test.go @@ -7,7 +7,6 @@ import ( "testing" "gopkg.in/kataras/iris.v6/_future/ipel/ast" - "gopkg.in/kataras/iris.v6/_future/ipel/lexer" ) func TestParseError(t *testing.T) { @@ -15,8 +14,7 @@ func TestParseError(t *testing.T) { illegalChar := '$' input := "{id" + string(illegalChar) + "int range(1,5) else 404}" - l := lexer.New(input) - p := New(l) + p := New(input) _, err := p.Parse() @@ -36,10 +34,8 @@ func TestParseError(t *testing.T) { // success input2 := "{id:int range(1,5) else 404}" - l2 := lexer.New(input2) - p2 := New(l2) - - _, err = p2.Parse() + p.Reset(input2) + _, err = p.Parse() if err != nil { t.Fatalf("expecting empty error on input '%s', but got: %s", input2, err.Error()) @@ -89,23 +85,35 @@ func TestParse(t *testing.T) { }, ErrorCode: 404, }}, // 2 - {"{username:alphabetical", true, + {"{username:alphabetical}", true, ast.ParamStatement{ Name: "username", Type: ast.ParamTypeAlphabetical, ErrorCode: 404, }}, // 3 - {"{username:thisianunexpected", false, + {"{myparam}", true, ast.ParamStatement{ - Name: "username", - Type: ast.ParamTypeUnExpected, + Name: "myparam", + Type: ast.ParamTypeString, ErrorCode: 404, }}, // 4 - } + {"{myparam_:thisianunexpected}", false, + ast.ParamStatement{ + Name: "myparam_", + Type: ast.ParamTypeUnExpected, + ErrorCode: 404, + }}, // 5 + {"{myparam2}", false, // false because it will give an error of unexpeced token type with value 2 + ast.ParamStatement{ + Name: "myparam", // expected "myparam" because we don't allow integers to the parameter names. + Type: ast.ParamTypeString, + ErrorCode: 404, + }}, // 6 + } + var p *Parser = new(Parser) for i, tt := range tests { - l := lexer.New(tt.input) - p := New(l) + p.Reset(tt.input) resultStmt, err := p.Parse() if tt.valid && err != nil { diff --git a/_future/ipel/token/token.go b/_future/ipel/token/token.go index fed7c365..d076141f 100644 --- a/_future/ipel/token/token.go +++ b/_future/ipel/token/token.go @@ -5,24 +5,15 @@ type TokenType int type Token struct { Type TokenType Literal string - Start int // excluding, useful for user - End int // excluding, useful for user and index -} - -func (t Token) StartIndex() int { - if t.Start > 0 { - return t.Start + 1 - } - return t.Start -} - -func (t Token) EndIndex() int { - return t.End + Start int // including the first char, Literal[index:] + End int // including the last char, Literal[start:end+1) } +// /about/{fullname:alphabetical} +// /profile/{anySpecialName:string} // {id:int range(1,5) else 404} // /admin/{id:int eq(1) else 402} -// /file/{filepath:tail else 405} +// /file/{filepath:file else 405} const ( EOF = iota // 0 ILLEGAL @@ -33,7 +24,7 @@ const ( // PARAM_IDENTIFIER // id COLON // : // let's take them in parser - // PARAM_TYPE // int, string, alphabetic, tail + // PARAM_TYPE // int, string, alphabetical, file, path or unexpected // PARAM_FUNC // range LPAREN // ( RPAREN // )