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 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
[...]

View File

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

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

View File

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

View File

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

View File

@ -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 // )