1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-21 11:57:02 +00:00

partial cleanup of the macro pkg and move it from /core/router to the root because it may be used by the end-developers now to ammend the available macros per application

Former-commit-id: 951a5e7a401af25ecaa904ff6463b0def2c87afb
This commit is contained in:
Gerasimos (Makis) Maropoulos
2018-09-29 02:41:51 +03:00
parent bf880033cd
commit 6d9a35ddba
25 changed files with 119 additions and 129 deletions

View File

@@ -0,0 +1,192 @@
package parser
import (
"fmt"
"strconv"
"strings"
"github.com/kataras/iris/macro/interpreter/ast"
"github.com/kataras/iris/macro/interpreter/lexer"
"github.com/kataras/iris/macro/interpreter/token"
)
// Parse takes a route "fullpath"
// and returns its param statements
// or an error if failed.
func Parse(fullpath string, paramTypes []ast.ParamType) ([]*ast.ParamStatement, error) {
if len(paramTypes) == 0 {
return nil, fmt.Errorf("empty parameter types")
}
pathParts := strings.SplitN(fullpath, "/", -1)
p := new(ParamParser)
statements := make([]*ast.ParamStatement, 0)
for i, s := range pathParts {
if s == "" { // if starts with /
continue
}
// if it's not a named path parameter of the new syntax then continue to the next
if s[0] != lexer.Begin || s[len(s)-1] != lexer.End {
continue
}
p.Reset(s)
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 ast.IsTrailing(stmt.Type) && i < len(pathParts)-1 {
return nil, fmt.Errorf("%s: parameter type \"%s\" should be registered to the very last of a path", s, stmt.Type.Indent())
}
statements = append(statements, stmt)
}
return statements, nil
}
// ParamParser is the parser
// which is being used by the Parse function
// to parse path segments one by one
// and return their parsed parameter statements (param name, param type its functions and the inline route's functions).
type ParamParser struct {
src string
errors []string
}
// NewParamParser receives a "src" of a single parameter
// and returns a new ParamParser, ready to Parse.
func NewParamParser(src string) *ParamParser {
p := new(ParamParser)
p.Reset(src)
return p
}
// Reset resets this ParamParser,
// reset the errors and set the source to the input "src".
func (p *ParamParser) Reset(src string) {
p.src = src
p.errors = []string{}
}
func (p *ParamParser) appendErr(format string, a ...interface{}) {
p.errors = append(p.errors, fmt.Sprintf(format, a...))
}
const (
// DefaultParamErrorCode is the default http error code, 404 not found,
// per-parameter. An error code can be setted via
// the "else" keyword inside a route's path.
DefaultParamErrorCode = 404
)
// func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) {
// if t.Type == token.INT {
// return ast.ParamFuncArgToInt(t.Literal)
// }
// // act all as strings here, because of int vs int64 vs uint64 and etc.
// return t.Literal, nil
// }
func parseParamFuncArg(t token.Token) (a string, err error) {
// act all as strings here, because of int vs int64 vs uint64 and etc.
return t.Literal, nil
}
func (p ParamParser) Error() error {
if len(p.errors) > 0 {
return fmt.Errorf(strings.Join(p.errors, "\n"))
}
return nil
}
// 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(paramTypes []ast.ParamType) (*ast.ParamStatement, error) {
l := lexer.New(p.src)
stmt := &ast.ParamStatement{
ErrorCode: DefaultParamErrorCode,
Type: ast.GetMasterParamType(paramTypes...),
Src: p.src,
}
lastParamFunc := ast.ParamFunc{}
for {
t := l.NextToken()
if t.Type == token.EOF {
if stmt.Name == "" {
p.appendErr("[1:] parameter name is missing")
}
break
}
switch t.Type {
case token.LBRACE:
// can accept only letter or number only.
nextTok := l.NextToken()
stmt.Name = nextTok.Literal
case token.COLON:
// type can accept both letters and numbers but not symbols ofc.
nextTok := l.NextToken()
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
// param func
case token.IDENT:
lastParamFunc.Name = t.Literal
case token.LPAREN:
// param function without arguments ()
if l.PeekNextTokenType() == token.RPAREN {
// do nothing, just continue to the RPAREN
continue
}
argValTok := l.NextDynamicToken() // catch anything from "(" and forward, until ")", because we need to
// be able to use regex expression as a macro type's func argument too.
// fmt.Printf("argValTok: %#v\n", argValTok)
// fmt.Printf("argVal: %#v\n", argVal)
lastParamFunc.Args = append(lastParamFunc.Args, argValTok.Literal)
case token.COMMA:
argValTok := l.NextToken()
lastParamFunc.Args = append(lastParamFunc.Args, argValTok.Literal)
case token.RPAREN:
stmt.Funcs = append(stmt.Funcs, lastParamFunc)
lastParamFunc = ast.ParamFunc{} // reset
case token.ELSE:
errCodeTok := l.NextToken()
if errCodeTok.Type != token.INT {
p.appendErr("[%d:%d] expected error code to be an integer but got %s", t.Start, t.End, errCodeTok.Literal)
continue
}
errCode, err := strconv.Atoi(errCodeTok.Literal)
if err != nil {
// this is a bug on lexer if throws because we already check for token.INT
p.appendErr("[%d:%d] unexpected lexer error while trying to convert error code to an integer, %s", t.Start, t.End, err.Error())
continue
}
stmt.ErrorCode = errCode
case token.RBRACE:
// check if } but not {
if stmt.Name == "" {
p.appendErr("[%d:%d] illegal token: }, forgot '{' ?", t.Start, t.End)
}
break
case token.ILLEGAL:
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)
}
}
return stmt, p.Error()
}

View File

@@ -0,0 +1,339 @@
package parser
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/kataras/iris/macro/interpreter/ast"
)
type simpleParamType string
func (pt simpleParamType) Indent() string { return string(pt) }
type masterParamType simpleParamType
func (pt masterParamType) Indent() string { return string(pt) }
func (pt masterParamType) Master() bool { return true }
type wildcardParamType string
func (pt wildcardParamType) Indent() string { return string(pt) }
func (pt wildcardParamType) Trailing() bool { return true }
type aliasedParamType []string
func (pt aliasedParamType) Indent() string { return string(pt[0]) }
func (pt aliasedParamType) Alias() string { return pt[1] }
var (
paramTypeString = masterParamType("string")
paramTypeNumber = aliasedParamType{"number", "int"}
paramTypeInt64 = aliasedParamType{"int64", "long"}
paramTypeUint8 = simpleParamType("uint8")
paramTypeUint64 = simpleParamType("uint64")
paramTypeBool = aliasedParamType{"bool", "boolean"}
paramTypeAlphabetical = simpleParamType("alphabetical")
paramTypeFile = simpleParamType("file")
paramTypePath = wildcardParamType("path")
)
var testParamTypes = []ast.ParamType{
paramTypeString,
paramTypeNumber, paramTypeInt64, paramTypeUint8, paramTypeUint64,
paramTypeBool,
paramTypeAlphabetical, paramTypeFile, paramTypePath,
}
func TestParseParamError(t *testing.T) {
// fail
illegalChar := '$'
input := "{id" + string(illegalChar) + "int range(1,5) else 404}"
p := NewParamParser(input)
_, err := p.Parse(testParamTypes)
if err == nil {
t.Fatalf("expecting not empty error on input '%s'", input)
}
illIdx := strings.IndexRune(input, illegalChar)
expectedErr := fmt.Sprintf("[%d:%d] illegal token: %s", illIdx, illIdx, "$")
if got := err.Error(); got != expectedErr {
t.Fatalf("expecting error to be '%s' but got: %s", expectedErr, got)
}
//
// success
input2 := "{id:uint64 range(1,5) else 404}"
p.Reset(input2)
_, err = p.Parse(testParamTypes)
if err != nil {
t.Fatalf("expecting empty error on input '%s', but got: %s", input2, err.Error())
}
//
}
// 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, testParamTypes...)
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
expectedStatement ast.ParamStatement
}{
{true,
ast.ParamStatement{
Src: "{id:number min(1) max(5) else 404}",
Name: "id",
Type: mustLookupParamType("number"),
Funcs: []ast.ParamFunc{
{
Name: "min",
Args: []string{"1"}},
{
Name: "max",
Args: []string{"5"}},
},
ErrorCode: 404,
}}, // 0
{true,
ast.ParamStatement{
Src: "{id:number range(1,5)}",
Name: "id",
Type: mustLookupParamType("number"),
Funcs: []ast.ParamFunc{
{
Name: "range",
Args: []string{"1", "5"}},
},
ErrorCode: 404,
}}, // 1
{true,
ast.ParamStatement{
Src: "{file:path contains(.)}",
Name: "file",
Type: mustLookupParamType("path"),
Funcs: []ast.ParamFunc{
{
Name: "contains",
Args: []string{"."}},
},
ErrorCode: 404,
}}, // 2
{true,
ast.ParamStatement{
Src: "{username:alphabetical}",
Name: "username",
Type: mustLookupParamType("alphabetical"),
ErrorCode: 404,
}}, // 3
{true,
ast.ParamStatement{
Src: "{myparam}",
Name: "myparam",
Type: mustLookupParamType("string"),
ErrorCode: 404,
}}, // 4
{false,
ast.ParamStatement{
Src: "{myparam_:thisianunexpected}",
Name: "myparam_",
Type: nil,
ErrorCode: 404,
}}, // 5
{true,
ast.ParamStatement{
Src: "{myparam2}",
Name: "myparam2", // we now allow integers to the parameter names.
Type: ast.GetMasterParamType(testParamTypes...),
ErrorCode: 404,
}}, // 6
{true,
ast.ParamStatement{
Src: "{id:number even()}", // test param funcs without any arguments (LPAREN peek for RPAREN)
Name: "id",
Type: mustLookupParamType("number"),
Funcs: []ast.ParamFunc{
{
Name: "even"},
},
ErrorCode: 404,
}}, // 7
{true,
ast.ParamStatement{
Src: "{id:int64 else 404}",
Name: "id",
Type: mustLookupParamType("int64"),
ErrorCode: 404,
}}, // 8
{true,
ast.ParamStatement{
Src: "{id:long else 404}", // backwards-compatible test.
Name: "id",
Type: mustLookupParamType("int64"),
ErrorCode: 404,
}}, // 9
{true,
ast.ParamStatement{
Src: "{id:long else 404}",
Name: "id",
Type: mustLookupParamType("int64"), // backwards-compatible test of LookupParamType.
ErrorCode: 404,
}}, // 10
{true,
ast.ParamStatement{
Src: "{has:bool else 404}",
Name: "has",
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(testParamTypes)
if tt.valid && err != nil {
t.Fatalf("tests[%d] - error %s", i, err.Error())
} else if !tt.valid && err == nil {
t.Fatalf("tests[%d] - expected to be a failure", i)
}
if resultStmt != nil { // is valid here
if !reflect.DeepEqual(tt.expectedStatement, *resultStmt) {
t.Fatalf("tests[%d] - wrong statement, expected and result differs. Details:\n%#v\n%#v", i, tt.expectedStatement, *resultStmt)
}
}
}
}
func TestParse(t *testing.T) {
tests := []struct {
path string
valid bool
expectedStatements []ast.ParamStatement
}{
{"/api/users/{id:number min(1) max(5) else 404}", true,
[]ast.ParamStatement{{
Src: "{id:number min(1) max(5) else 404}",
Name: "id",
Type: paramTypeNumber,
Funcs: []ast.ParamFunc{
{
Name: "min",
Args: []string{"1"}},
{
Name: "max",
Args: []string{"5"}},
},
ErrorCode: 404,
},
}}, // 0
{"/admin/{id:uint64 range(1,5)}", true,
[]ast.ParamStatement{{
Src: "{id:uint64 range(1,5)}",
Name: "id",
Type: paramTypeUint64,
Funcs: []ast.ParamFunc{
{
Name: "range",
Args: []string{"1", "5"}},
},
ErrorCode: 404,
},
}}, // 1
{"/files/{file:path contains(.)}", true,
[]ast.ParamStatement{{
Src: "{file:path contains(.)}",
Name: "file",
Type: paramTypePath,
Funcs: []ast.ParamFunc{
{
Name: "contains",
Args: []string{"."}},
},
ErrorCode: 404,
},
}}, // 2
{"/profile/{username:alphabetical}", true,
[]ast.ParamStatement{{
Src: "{username:alphabetical}",
Name: "username",
Type: paramTypeAlphabetical,
ErrorCode: 404,
},
}}, // 3
{"/something/here/{myparam}", true,
[]ast.ParamStatement{{
Src: "{myparam}",
Name: "myparam",
Type: paramTypeString,
ErrorCode: 404,
},
}}, // 4
{"/unexpected/{myparam_:thisianunexpected}", false,
[]ast.ParamStatement{{
Src: "{myparam_:thisianunexpected}",
Name: "myparam_",
Type: nil,
ErrorCode: 404,
},
}}, // 5
{"/p2/{myparam2}", true,
[]ast.ParamStatement{{
Src: "{myparam2}",
Name: "myparam2", // we now allow integers to the parameter names.
Type: paramTypeString,
ErrorCode: 404,
},
}}, // 6
{"/assets/{file:path}/invalid", false, // path should be in the end segment
[]ast.ParamStatement{{
Src: "{file:path}",
Name: "file",
Type: paramTypePath,
ErrorCode: 404,
},
}}, // 7
}
for i, tt := range tests {
statements, err := Parse(tt.path, testParamTypes)
if tt.valid && err != nil {
t.Fatalf("tests[%d] - error %s", i, err.Error())
} else if !tt.valid && err == nil {
t.Fatalf("tests[%d] - expected to be a failure", i)
}
for j := range statements {
for l := range tt.expectedStatements {
if !reflect.DeepEqual(tt.expectedStatements[l], *statements[j]) {
t.Fatalf("tests[%d] - wrong statements, expected and result differs. Details:\n%#v\n%#v", i, tt.expectedStatements[l], *statements[j])
}
}
}
}
}