1. Fix index, including both start and end. So Literal[start:end+1] will
be a valid part.

2. Replace any with string, add file param type

3. Start of making the evaluator, starting with regexp for param types
(these expression can be changed or/and overriden by user later on)


Former-commit-id: ab95265f953dadbf84170b543e1ff8840f9c4a14
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-03-27 22:33:19 +03:00
parent 251eeb6bd0
commit 126c4de29b
8 changed files with 158 additions and 55 deletions

View File

@ -7,13 +7,13 @@
- No Breaking Changes. - No Breaking Changes.
- No performance cost if not used. - 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. - 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. - 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. - 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, - 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). 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. - 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 `any`'s, so `any` will contain global-use functions too. - 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 ## Preview
@ -67,7 +67,7 @@ app.String.Set("isVersion", isVersionStrValidator)
app.String.Set("len", lenStrValidator) app.String.Set("len", lenStrValidator)
``` ```
`/uploads/{filepath:tail contains(.) else 403}` `/uploads/{fullpath:path contains(.) else 403}`
```go ```go
[...] [...]

View File

@ -4,25 +4,29 @@ type ParamType uint8
const ( const (
ParamTypeUnExpected ParamType = iota ParamTypeUnExpected ParamType = iota
// /42
ParamTypeInt
// /myparam1 // /myparam1
ParamTypeString ParamTypeString
// /42
ParamTypeInt
// /myparam // /myparam
ParamTypeAlphabetical ParamTypeAlphabetical
// /main.css
ParamTypeFile
// /myparam1/myparam2 // /myparam1/myparam2
ParamTypePath ParamTypePath
) )
var paramTypes = map[string]ParamType{ var paramTypes = map[string]ParamType{
"int": ParamTypeInt,
"string": ParamTypeString, "string": ParamTypeString,
"int": ParamTypeInt,
"alphabetical": ParamTypeAlphabetical, "alphabetical": ParamTypeAlphabetical,
"file": ParamTypeFile,
"path": ParamTypePath, "path": ParamTypePath,
// could be named also: // could be named also:
// "tail": // "tail":
// "wild" // "wild"
// "wildcard" // "wildcard"
} }
func LookupParamType(ident string) ParamType { func LookupParamType(ident string) ParamType {

View File

@ -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
}

View File

@ -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
}

View File

@ -11,9 +11,9 @@ type Lexer struct {
ch byte // current char under examination ch byte // current char under examination
} }
func New(input string) *Lexer { func New(src string) *Lexer {
l := &Lexer{ l := &Lexer{
input: input, input: src,
} }
// step to the first character in order to be ready // step to the first character in order to be ready
l.readChar() l.readChar()
@ -86,9 +86,12 @@ func (l *Lexer) newToken(tokenType token.TokenType, lit string) token.Token {
Start: l.pos, Start: l.pos,
End: 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 { 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 return t

View File

@ -15,19 +15,23 @@ type Parser struct {
errors []string errors []string
} }
func New(lexer *lexer.Lexer) *Parser { func New(src string) *Parser {
p := &Parser{ p := new(Parser)
l: lexer, p.Reset(src)
}
return p return p
} }
func (p *Parser) Reset(src string) {
p.l = lexer.New(src)
p.errors = []string{}
}
func (p *Parser) appendErr(format string, a ...interface{}) { func (p *Parser) appendErr(format string, a ...interface{}) {
p.errors = append(p.errors, fmt.Sprintf(format, a...)) p.errors = append(p.errors, fmt.Sprintf(format, a...))
} }
const DefaultParamErrorCode = 404 const DefaultParamErrorCode = 404
const DefaultParamType = ast.ParamTypeString
func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) { func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) {
if t.Type == token.INT { if t.Type == token.INT {
@ -36,20 +40,35 @@ func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) {
return t.Literal, nil 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) { func (p *Parser) Parse() (*ast.ParamStatement, error) {
stmt := new(ast.ParamStatement) p.errors = []string{}
stmt.ErrorCode = DefaultParamErrorCode
// let's have them nilled stmt.Funcs = make([]ast.ParamFunc, 0) stmt := &ast.ParamStatement{
ErrorCode: DefaultParamErrorCode,
Type: DefaultParamType,
}
lastParamFunc := ast.ParamFunc{} lastParamFunc := ast.ParamFunc{}
for { for {
t := p.l.NextToken() t := p.l.NextToken()
if t.Type == token.EOF { if t.Type == token.EOF {
if stmt.Name == "" {
p.appendErr("[1:] parameter name is missing")
}
break break
} }
switch t.Type { switch t.Type {
case token.LBRACE: case token.LBRACE:
// name // name, alphabetical and _, param names are not allowed to contain any number.
nextTok := p.l.NextToken() nextTok := p.l.NextToken()
stmt.Name = nextTok.Literal stmt.Name = nextTok.Literal
case token.COLON: case token.COLON:
@ -58,7 +77,6 @@ func (p *Parser) Parse() (*ast.ParamStatement, error) {
paramType := ast.LookupParamType(nextTok.Literal) paramType := ast.LookupParamType(nextTok.Literal)
if paramType == ast.ParamTypeUnExpected { if paramType == ast.ParamTypeUnExpected {
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)
continue
} }
stmt.Type = paramType stmt.Type = paramType
// param func // 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) p.appendErr("[%d:%d] illegal token: %s", t.Start, t.End, t.Literal)
default: default:
p.appendErr("[%d:%d] unexpected token type: %q with value %s", t.Start, t.End, t.Type, t.Literal) 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 stmt, p.Error()
return nil, fmt.Errorf(strings.Join(p.errors, "\n"))
}
return stmt, nil
} }

View File

@ -7,7 +7,6 @@ import (
"testing" "testing"
"gopkg.in/kataras/iris.v6/_future/ipel/ast" "gopkg.in/kataras/iris.v6/_future/ipel/ast"
"gopkg.in/kataras/iris.v6/_future/ipel/lexer"
) )
func TestParseError(t *testing.T) { func TestParseError(t *testing.T) {
@ -15,8 +14,7 @@ func TestParseError(t *testing.T) {
illegalChar := '$' illegalChar := '$'
input := "{id" + string(illegalChar) + "int range(1,5) else 404}" input := "{id" + string(illegalChar) + "int range(1,5) else 404}"
l := lexer.New(input) p := New(input)
p := New(l)
_, err := p.Parse() _, err := p.Parse()
@ -36,10 +34,8 @@ func TestParseError(t *testing.T) {
// success // success
input2 := "{id:int range(1,5) else 404}" input2 := "{id:int range(1,5) else 404}"
l2 := lexer.New(input2) p.Reset(input2)
p2 := New(l2) _, err = p.Parse()
_, err = p2.Parse()
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())
@ -89,23 +85,35 @@ func TestParse(t *testing.T) {
}, },
ErrorCode: 404, ErrorCode: 404,
}}, // 2 }}, // 2
{"{username:alphabetical", true, {"{username:alphabetical}", true,
ast.ParamStatement{ ast.ParamStatement{
Name: "username", Name: "username",
Type: ast.ParamTypeAlphabetical, Type: ast.ParamTypeAlphabetical,
ErrorCode: 404, ErrorCode: 404,
}}, // 3 }}, // 3
{"{username:thisianunexpected", false, {"{myparam}", true,
ast.ParamStatement{ ast.ParamStatement{
Name: "username", Name: "myparam",
Type: ast.ParamTypeUnExpected, Type: ast.ParamTypeString,
ErrorCode: 404, ErrorCode: 404,
}}, // 4 }}, // 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 { for i, tt := range tests {
l := lexer.New(tt.input) p.Reset(tt.input)
p := New(l)
resultStmt, err := p.Parse() resultStmt, err := p.Parse()
if tt.valid && err != nil { if tt.valid && err != nil {

View File

@ -5,24 +5,15 @@ type TokenType int
type Token struct { type Token struct {
Type TokenType Type TokenType
Literal string Literal string
Start int // excluding, useful for user Start int // including the first char, Literal[index:]
End int // excluding, useful for user and index End int // including the last char, Literal[start:end+1)
}
func (t Token) StartIndex() int {
if t.Start > 0 {
return t.Start + 1
}
return t.Start
}
func (t Token) EndIndex() int {
return t.End
} }
// /about/{fullname:alphabetical}
// /profile/{anySpecialName:string}
// {id:int range(1,5) else 404} // {id:int range(1,5) else 404}
// /admin/{id:int eq(1) else 402} // /admin/{id:int eq(1) else 402}
// /file/{filepath:tail else 405} // /file/{filepath:file else 405}
const ( const (
EOF = iota // 0 EOF = iota // 0
ILLEGAL ILLEGAL
@ -33,7 +24,7 @@ const (
// PARAM_IDENTIFIER // id // PARAM_IDENTIFIER // id
COLON // : COLON // :
// let's take them in parser // let's take them in parser
// PARAM_TYPE // int, string, alphabetic, tail // PARAM_TYPE // int, string, alphabetical, file, path or unexpected
// PARAM_FUNC // range // PARAM_FUNC // range
LPAREN // ( LPAREN // (
RPAREN // ) RPAREN // )