1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-02 09:47:17 +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

52
macro/handler/handler.go Normal file
View File

@@ -0,0 +1,52 @@
// Package handler is the highest level module of the macro package which makes use the rest of the macro package,
// it is mainly used, internally, by the router package.
package handler
import (
"github.com/kataras/iris/context"
"github.com/kataras/iris/macro"
)
// MakeHandler creates and returns a handler from a macro template, the handler evaluates each of the parameters if necessary at all.
// If the template does not contain any dynamic attributes and a special handler is NOT required
// then it returns a nil handler and false as its second output value,
// the caller should check those two values before any further action.
func MakeHandler(tmpl *macro.Template) (context.Handler, bool) {
needsMacroHandler := len(tmpl.Params) > 0
if !needsMacroHandler {
return nil, false
}
// check if we have params like: {name:string} or {name} or {anything:path} without else keyword or any functions used inside these params.
// 1. if we don't have, then we don't need to add a handler before the main route's handler (as I said, no performance if macro is not really used)
// 2. if we don't have any named params then we don't need a handler too.
for _, p := range tmpl.Params {
if p.CanEval() {
// if at least one needs it, then create the handler.
needsMacroHandler = true
break
}
}
if !needsMacroHandler {
return nil, false
}
handler := func(ctx context.Context) {
for _, p := range tmpl.Params {
if !p.CanEval() {
continue // allow.
}
if !p.Eval(ctx.Params().Get(p.Name), ctx.Params()) {
ctx.StatusCode(p.ErrCode)
ctx.StopExecution()
return
}
}
// if all passed, just continue.
ctx.Next()
}
return handler, true
}

View File

@@ -0,0 +1,132 @@
package ast
type (
// ParamType holds the necessary information about a parameter type for the parser to lookup for.
ParamType interface {
// The name of the parameter type.
// Indent should contain the characters for the parser.
Indent() string
}
// MasterParamType if implemented and its `Master()` returns true then empty type param will be translated to this param type.
// Also its functions will be available to the rest of the macro param type's funcs.
//
// Only one Master is allowed.
MasterParamType interface {
ParamType
Master() bool
}
// TrailingParamType if implemented and its `Trailing()` returns true
// then it should be declared at the end of a route path and can accept any trailing path segment as one parameter.
TrailingParamType interface {
ParamType
Trailing() bool
}
// AliasParamType if implemeneted nad its `Alias()` returns a non-empty string
// then the param type can be written with that string literal too.
AliasParamType interface {
ParamType
Alias() string
}
)
// IsMaster returns true if the "pt" param type is a master one.
func IsMaster(pt ParamType) bool {
p, ok := pt.(MasterParamType)
return ok && p.Master()
}
// IsTrailing returns true if the "pt" param type is a marked as trailing,
// which should accept more than one path segment when in the end.
func IsTrailing(pt ParamType) bool {
p, ok := pt.(TrailingParamType)
return ok && p.Trailing()
}
// HasAlias returns any alias of the "pt" param type.
// If alias is empty or not found then it returns false as its second output argument.
func HasAlias(pt ParamType) (string, bool) {
if p, ok := pt.(AliasParamType); ok {
alias := p.Alias()
return alias, len(alias) > 0
}
return "", false
}
// GetMasterParamType accepts a list of ParamType and returns its master.
// If no `Master` specified:
// and len(paramTypes) > 0 then it will return the first one,
// otherwise it returns nil.
func GetMasterParamType(paramTypes ...ParamType) ParamType {
for _, pt := range paramTypes {
if IsMaster(pt) {
return pt
}
}
if len(paramTypes) > 0 {
return paramTypes[0]
}
return nil
}
// LookupParamType accepts the string
// representation of a parameter type.
// Example:
// "string"
// "number" or "int"
// "long" or "int64"
// "uint8"
// "uint64"
// "boolean" or "bool"
// "alphabetical"
// "file"
// "path"
func LookupParamType(indentOrAlias string, paramTypes ...ParamType) (ParamType, bool) {
for _, pt := range paramTypes {
if pt.Indent() == indentOrAlias {
return pt, true
}
if alias, has := HasAlias(pt); has {
if alias == indentOrAlias {
return pt, true
}
}
}
return nil, false
}
// ParamStatement is a struct
// which holds all the necessary information about a macro parameter.
// It holds its type (string, int, alphabetical, file, path),
// its source ({param:type}),
// its name ("param"),
// its attached functions by the user (min, max...)
// and the http error code if that parameter
// failed to be evaluated.
type ParamStatement struct {
Src string // the original unparsed source, i.e: {id:int range(1,5) else 404}
Name string // id
Type ParamType // int
Funcs []ParamFunc // range
ErrorCode int // 404
}
// ParamFunc holds the name of a parameter's function
// and its arguments (values)
// A param func is declared with:
// {param:int range(1,5)},
// the range is the
// param function name
// the 1 and 5 are the two param function arguments
// range(1,5)
type ParamFunc struct {
Name string // range
Args []string // ["1","5"]
}

View File

@@ -0,0 +1,202 @@
package lexer
import (
"github.com/kataras/iris/macro/interpreter/token"
)
// Lexer helps us to read/scan characters of a source and resolve their token types.
type Lexer struct {
input string
pos int // current pos in input, current char
readPos int // current reading pos in input, after current char
ch byte // current char under examination
}
// New takes a source, series of chars, and returns
// a new, ready to read from the first letter, lexer.
func New(src string) *Lexer {
l := &Lexer{
input: src,
}
// step to the first character in order to be ready
l.readChar()
return l
}
func (l *Lexer) readChar() {
if l.readPos >= len(l.input) {
l.ch = 0
} else {
l.ch = l.input[l.readPos]
}
l.pos = l.readPos
l.readPos++
}
const (
// Begin is the symbol which lexer should scan forward to.
Begin = '{' // token.LBRACE
// End is the symbol which lexer should stop scanning.
End = '}' // token.RBRACE
)
func resolveTokenType(ch byte) token.Type {
switch ch {
case Begin:
return token.LBRACE
case End:
return token.RBRACE
// Let's keep it simple, no evaluation for logical operators, we are not making a new programming language, keep it simple makis.
// ||
// case '|':
// if l.peekChar() == '|' {
// ch := l.ch
// l.readChar()
// t = token.Token{Type: token.OR, Literal: string(ch) + string(l.ch)}
// }
// ==
case ':':
return token.COLON
case '(':
return token.LPAREN
case ')':
return token.RPAREN
case ',':
return token.COMMA
// literals
case 0:
return token.EOF
default:
return token.IDENT //
}
}
// NextToken returns the next token in the series of characters.
// It can be a single symbol, a token type or a literal.
// It's able to return an EOF token too.
//
// It moves the cursor forward.
func (l *Lexer) NextToken() (t token.Token) {
l.skipWhitespace()
typ := resolveTokenType(l.ch)
t.Type = typ
switch typ {
case token.EOF:
t.Literal = ""
case token.IDENT:
if isLetter(l.ch) {
// letters
lit := l.readIdentifier()
typ := token.LookupIdent(lit)
t = l.newToken(typ, lit)
return
}
if isDigit(l.ch) {
// numbers
lit := l.readNumber()
t = l.newToken(token.INT, lit)
return
}
t = l.newTokenRune(token.ILLEGAL, l.ch)
default:
t = l.newTokenRune(typ, l.ch)
}
l.readChar() // set the pos to the next
return
}
// NextDynamicToken doesn't cares about the grammar.
// It reads numbers or any unknown symbol,
// it's being used by parser to skip all characters
// between parameter function's arguments inside parenthesis,
// in order to allow custom regexp on the end-language too.
//
// It moves the cursor forward.
func (l *Lexer) NextDynamicToken() (t token.Token) {
// calculate anything, even spaces.
// numbers
lit := l.readNumber()
if lit != "" {
return l.newToken(token.INT, lit)
}
lit = l.readIdentifierFuncArgument()
return l.newToken(token.IDENT, lit)
}
// used to skip any illegal token if inside parenthesis, used to be able to set custom regexp inside a func.
func (l *Lexer) readIdentifierFuncArgument() string {
pos := l.pos
for resolveTokenType(l.ch) != token.RPAREN {
l.readChar()
}
return l.input[pos:l.pos]
}
// PeekNextTokenType returns only the token type
// of the next character and it does not move forward the cursor.
// It's being used by parser to recognise empty functions, i.e `even()`
// as valid functions with zero input arguments.
func (l *Lexer) PeekNextTokenType() token.Type {
if len(l.input)-1 > l.pos {
ch := l.input[l.pos]
return resolveTokenType(ch)
}
return resolveTokenType(0) // EOF
}
func (l *Lexer) newToken(tokenType token.Type, lit string) token.Token {
t := token.Token{
Type: tokenType,
Literal: lit,
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 = l.pos - 1
t.Start = t.End - len(lit) + 1
}
return t
}
func (l *Lexer) newTokenRune(tokenType token.Type, ch byte) token.Token {
return l.newToken(tokenType, string(ch))
}
func (l *Lexer) skipWhitespace() {
for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
l.readChar()
}
}
func (l *Lexer) readIdentifier() string {
pos := l.pos
for isLetter(l.ch) || isDigit(l.ch) {
l.readChar()
}
return l.input[pos:l.pos]
}
func isLetter(ch byte) bool {
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_'
}
func (l *Lexer) readNumber() string {
pos := l.pos
for isDigit(l.ch) {
l.readChar()
}
return l.input[pos:l.pos]
}
func isDigit(ch byte) bool {
return '0' <= ch && ch <= '9'
}

View File

@@ -0,0 +1,54 @@
package lexer
import (
"testing"
"github.com/kataras/iris/macro/interpreter/token"
)
func TestNextToken(t *testing.T) {
input := `{id:number min(1) max(5) else 404}`
tests := []struct {
expectedType token.Type
expectedLiteral string
}{
{token.LBRACE, "{"}, // 0
{token.IDENT, "id"}, // 1
{token.COLON, ":"}, // 2
{token.IDENT, "number"}, // 3
{token.IDENT, "min"}, // 4
{token.LPAREN, "("}, // 5
{token.INT, "1"}, // 6
{token.RPAREN, ")"}, // 7
{token.IDENT, "max"}, // 8
{token.LPAREN, "("}, // 9
{token.INT, "5"}, // 10
{token.RPAREN, ")"}, // 11
{token.ELSE, "else"}, // 12
{token.INT, "404"}, // 13
{token.RBRACE, "}"}, // 14
}
l := New(input)
for i, tt := range tests {
tok := l.NextToken()
if tok.Type != tt.expectedType {
t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q",
i, tt.expectedType, tok.Type)
}
if tok.Literal != tt.expectedLiteral {
t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q",
i, tt.expectedLiteral, tok.Literal)
}
}
}
// EMEINA STO:
// 30/232 selida apto making a interpeter in Go.
// den ekana to skipWhitespaces giati skeftomai
// an borei na to xreiastw 9a dw aurio.

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

View File

@@ -0,0 +1,53 @@
package token
// Type is a specific type of int which describes the symbols.
type Type int
// Token describes the letter(s) or symbol, is a result of the lexer.
type Token struct {
Type Type
Literal string
Start int // including the first char
End int // including the last char
}
// /about/{fullname:alphabetical}
// /profile/{anySpecialName:string}
// {id:uint64 range(1,5) else 404}
// /admin/{id:number eq(1) else 402}
// /file/{filepath:file else 405}
const (
EOF = iota // 0
ILLEGAL
// Identifiers + literals
LBRACE // {
RBRACE // }
// PARAM_IDENTIFIER // id
COLON // :
LPAREN // (
RPAREN // )
// PARAM_FUNC_ARG // 1
COMMA
IDENT // string or keyword
// Keywords
// keywords_start
ELSE // else
// keywords_end
INT // 42
)
const eof rune = 0
var keywords = map[string]Type{
"else": ELSE,
}
// LookupIdent receives a series of chars
// and tries to resolves the token type.
func LookupIdent(ident string) Type {
if tok, ok := keywords[ident]; ok {
return tok
}
return IDENT
}

340
macro/macro.go Normal file
View File

@@ -0,0 +1,340 @@
package macro
import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"unicode"
)
// EvaluatorFunc is the signature for both param types and param funcs.
// It should accepts the param's value as string
// and return true if validated otherwise false.
// type EvaluatorFunc func(paramValue string) bool
// type BinderFunc func(paramValue string) interface{}
type (
ParamEvaluator func(paramValue string) (interface{}, bool)
// FuncEvaluator interface{} // i.e func(paramValue int) bool
)
var goodEvaluatorFuncs = []reflect.Type{
reflect.TypeOf(func(string) (interface{}, bool) { return nil, false }),
reflect.TypeOf(ParamEvaluator(func(string) (interface{}, bool) { return nil, false })),
}
func goodParamFunc(typ reflect.Type) bool {
if typ.Kind() == reflect.Func { // it should be a func which returns a func (see below check).
if typ.NumOut() == 1 {
typOut := typ.Out(0)
if typOut.Kind() != reflect.Func {
return false
}
if typOut.NumOut() == 2 { // if it's a type of EvaluatorFunc, used for param evaluator.
for _, fType := range goodEvaluatorFuncs {
if typOut == fType {
return true
}
}
return false
}
if typOut.NumIn() == 1 && typOut.NumOut() == 1 { // if it's a type of func(paramValue [int,string...]) bool, used for param funcs.
return typOut.Out(0).Kind() == reflect.Bool
}
}
}
return false
}
// Regexp accepts a regexp "expr" expression
// and returns its MatchString.
// The regexp is compiled before return.
//
// Returns a not-nil error on regexp compile failure.
func Regexp(expr string) (func(string) bool, 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
}
// MustRegexp same as Regexp
// but it panics on the "expr" parse failure.
func MustRegexp(expr string) func(string) bool {
r, err := Regexp(expr)
if err != nil {
panic(err)
}
return r
}
// goodParamFuncName reports whether the function name is a valid identifier.
func goodParamFuncName(name string) bool {
if name == "" {
return false
}
// valid names are only letters and _
for _, r := range name {
switch {
case r == '_':
case !unicode.IsLetter(r):
return false
}
}
return true
}
// the convertBuilderFunc return value is generating at boot time.
// convertFunc converts an interface to a valid full param function.
func convertBuilderFunc(fn interface{}) ParamFuncBuilder {
typFn := reflect.TypeOf(fn)
if !goodParamFunc(typFn) {
return nil
}
numFields := typFn.NumIn()
return func(args []string) reflect.Value {
if len(args) != numFields {
// no variadics support, for now.
panic("args should be the same len as numFields")
}
var argValues []reflect.Value
for i := 0; i < numFields; i++ {
field := typFn.In(i)
arg := args[i]
// try to convert the string literal as we get it from the parser.
var (
val interface{}
panicIfErr = func(err error) {
if err != nil {
panic(fmt.Sprintf("on field index: %d: %v", i, err))
}
}
)
// try to get the value based on the expected type.
switch field.Kind() {
case reflect.Int:
v, err := strconv.Atoi(arg)
panicIfErr(err)
val = v
case reflect.Int8:
v, err := strconv.ParseInt(arg, 10, 8)
panicIfErr(err)
val = int8(v)
case reflect.Int16:
v, err := strconv.ParseInt(arg, 10, 16)
panicIfErr(err)
val = int16(v)
case reflect.Int32:
v, err := strconv.ParseInt(arg, 10, 32)
panicIfErr(err)
val = int32(v)
case reflect.Int64:
v, err := strconv.ParseInt(arg, 10, 64)
panicIfErr(err)
val = v
case reflect.Uint:
v, err := strconv.ParseUint(arg, 10, strconv.IntSize)
panicIfErr(err)
val = uint(v)
case reflect.Uint8:
v, err := strconv.ParseUint(arg, 10, 8)
panicIfErr(err)
val = uint8(v)
case reflect.Uint16:
v, err := strconv.ParseUint(arg, 10, 16)
panicIfErr(err)
val = uint16(v)
case reflect.Uint32:
v, err := strconv.ParseUint(arg, 10, 32)
panicIfErr(err)
val = uint32(v)
case reflect.Uint64:
v, err := strconv.ParseUint(arg, 10, 64)
panicIfErr(err)
val = v
case reflect.Float32:
v, err := strconv.ParseFloat(arg, 32)
panicIfErr(err)
val = float32(v)
case reflect.Float64:
v, err := strconv.ParseFloat(arg, 64)
panicIfErr(err)
val = v
case reflect.Bool:
v, err := strconv.ParseBool(arg)
panicIfErr(err)
val = v
case reflect.Slice:
if len(arg) > 1 {
if arg[0] == '[' && arg[len(arg)-1] == ']' {
// it is a single argument but as slice.
val = strings.Split(arg[1:len(arg)-1], ",") // only string slices.
}
}
default:
val = arg
}
argValue := reflect.ValueOf(val)
if expected, got := field.Kind(), argValue.Kind(); expected != got {
panic(fmt.Sprintf("func's input arguments should have the same type: [%d] expected %s but got %s", i, expected, got))
}
argValues = append(argValues, argValue)
}
evalFn := reflect.ValueOf(fn).Call(argValues)[0]
// var evaluator EvaluatorFunc
// // check for typed and not typed
// if _v, ok := evalFn.(EvaluatorFunc); ok {
// evaluator = _v
// } else if _v, ok = evalFn.(func(string) bool); ok {
// evaluator = _v
// }
// return func(paramValue interface{}) bool {
// return evaluator(paramValue)
// }
return evalFn
}
}
type (
// Macro represents the parsed macro,
// which holds
// the evaluator (param type's evaluator + param functions evaluators)
// and its param functions.
//
// Any type contains its own macro
// instance, so an String type
// contains its type evaluator
// which is the "Evaluator" field
// and it can register param functions
// to that macro which maps to a parameter type.
Macro struct {
indent string
alias string
master bool
trailing bool
Evaluator ParamEvaluator
funcs []ParamFunc
}
// ParamFuncBuilder is a func
// which accepts a param function's arguments (values)
// and returns a function as value, its job
// is to make the macros to be registered
// by user at the most generic possible way.
ParamFuncBuilder func([]string) reflect.Value // the func(<T>) bool
// ParamFunc represents the parsed
// parameter function, it holds
// the parameter's name
// and the function which will build
// the evaluator func.
ParamFunc struct {
Name string
Func ParamFuncBuilder
}
)
// NewMacro creates and returns a Macro that can be used as a registry for
// a new customized parameter type and its functions.
func NewMacro(indent, alias string, master, trailing bool, evaluator ParamEvaluator) *Macro {
return &Macro{
indent: indent,
alias: alias,
master: master,
trailing: trailing,
Evaluator: evaluator,
}
}
func (m *Macro) Indent() string {
return m.indent
}
func (m *Macro) Alias() string {
return m.alias
}
func (m *Macro) Master() bool {
return m.master
}
func (m *Macro) Trailing() bool {
return m.trailing
}
// func (m *Macro) SetParamResolver(fn func(memstore.Entry) interface{}) *Macro {
// m.ParamResolver = fn
// return m
// }
// RegisterFunc registers a parameter function
// to that macro.
// Accepts the func name ("range")
// and the function body, which should return an EvaluatorFunc
// a bool (it will be converted to EvaluatorFunc later on),
// i.e RegisterFunc("min", func(minValue int) func(paramValue string) bool){})
func (m *Macro) RegisterFunc(funcName string, fn interface{}) *Macro {
fullFn := convertBuilderFunc(fn)
m.registerFunc(funcName, fullFn)
return m
}
func (m *Macro) registerFunc(funcName string, fullFn ParamFuncBuilder) {
if !goodParamFuncName(funcName) {
return
}
for _, fn := range m.funcs {
if fn.Name == funcName {
fn.Func = fullFn
return
}
}
m.funcs = append(m.funcs, ParamFunc{
Name: funcName,
Func: fullFn,
})
}
func (m *Macro) getFunc(funcName string) ParamFuncBuilder {
for _, fn := range m.funcs {
if fn.Name == funcName {
if fn.Func == nil {
continue
}
return fn.Func
}
}
return nil
}

295
macro/macro_test.go Normal file
View File

@@ -0,0 +1,295 @@
package macro
import (
"reflect"
"strconv"
"testing"
)
// Most important tests to look:
// ../parser/parser_test.go
// ../lexer/lexer_test.go
func TestGoodParamFunc(t *testing.T) {
good1 := func(min int, max int) func(string) bool {
return func(paramValue string) bool {
return true
}
}
good2 := func(min uint64, max uint64) func(string) bool {
return func(paramValue string) bool {
return true
}
}
notgood1 := func(min int, max int) bool {
return false
}
if !goodParamFunc(reflect.TypeOf(good1)) {
t.Fatalf("expected good1 func to be good but it's not")
}
if !goodParamFunc(reflect.TypeOf(good2)) {
t.Fatalf("expected good2 func to be good but it's not")
}
if goodParamFunc(reflect.TypeOf(notgood1)) {
t.Fatalf("expected notgood1 func to be the worst")
}
}
func TestGoodParamFuncName(t *testing.T) {
tests := []struct {
name string
good bool
}{
{"range", true},
{"_range", true},
{"range_", true},
{"r_ange", true},
// numbers or other symbols are invalid.
{"range1", false},
{"2range", false},
{"r@nge", false},
{"rang3", false},
}
for i, tt := range tests {
isGood := goodParamFuncName(tt.name)
if tt.good && !isGood {
t.Fatalf("tests[%d] - expecting valid name but got invalid for name %s", i, tt.name)
} else if !tt.good && isGood {
t.Fatalf("tests[%d] - expecting invalid name but got valid for name %s", i, tt.name)
}
}
}
func testEvaluatorRaw(t *testing.T, macroEvaluator *Macro, input string, expectedType reflect.Kind, pass bool, i int) {
if macroEvaluator.Evaluator == nil && pass {
return // if not evaluator defined then it should allow everything.
}
value, passed := macroEvaluator.Evaluator(input)
if pass != passed {
t.Fatalf("%s - tests[%d] - expecting[pass] %v but got %v", t.Name(), i, pass, passed)
}
if !passed {
return
}
if value == nil && expectedType != reflect.Invalid {
t.Fatalf("%s - tests[%d] - expecting[value] to not be nil", t.Name(), i)
}
if v := reflect.ValueOf(value); v.Kind() != expectedType {
t.Fatalf("%s - tests[%d] - expecting[value.Kind] %v but got %v", t.Name(), i, expectedType, v.Kind())
}
}
func TestStringEvaluatorRaw(t *testing.T) {
tests := []struct {
pass bool
input string
}{
{true, "astring"}, // 0
{true, "astringwith_numb3rS_and_symbol$"}, // 1
{true, "32321"}, // 2
{true, "main.css"}, // 3
{true, "/assets/main.css"}, // 4
// false never
} // 0
for i, tt := range tests {
testEvaluatorRaw(t, String, tt.input, reflect.String, tt.pass, i)
}
}
func TestIntEvaluatorRaw(t *testing.T) {
x64 := strconv.IntSize == 64
tests := []struct {
pass bool
input string
}{
{false, "astring"}, // 0
{false, "astringwith_numb3rS_and_symbol$"}, // 1
{true, "32321"}, // 2
{x64, "9223372036854775807" /*max int64*/}, // 3
{x64, "-9223372036854775808" /*min int64 */}, // 4
{false, "-18446744073709553213213213213213121615"}, // 5
{false, "42 18446744073709551615"}, // 6
{false, "--42"}, // 7
{false, "+42"}, // 8
{false, "main.css"}, // 9
{false, "/assets/main.css"}, // 10
}
for i, tt := range tests {
testEvaluatorRaw(t, Int, tt.input, reflect.Int, tt.pass, i)
}
}
func TestInt64EvaluatorRaw(t *testing.T) {
tests := []struct {
pass bool
input string
}{
{false, "astring"}, // 0
{false, "astringwith_numb3rS_and_symbol$"}, // 1
{false, "18446744073709551615"}, // 2
{false, "92233720368547758079223372036854775807"}, // 3
{false, "9223372036854775808 9223372036854775808"}, // 4
{false, "main.css"}, // 5
{false, "/assets/main.css"}, // 6
{true, "9223372036854775807"}, // 7
{true, "-9223372036854775808"}, // 8
{true, "-0"}, // 9
{true, "1"}, // 10
{true, "-042"}, // 11
{true, "142"}, // 12
}
for i, tt := range tests {
testEvaluatorRaw(t, Int64, tt.input, reflect.Int64, tt.pass, i)
}
}
func TestUint8EvaluatorRaw(t *testing.T) {
tests := []struct {
pass bool
input string
}{
{false, "astring"}, // 0
{false, "astringwith_numb3rS_and_symbol$"}, // 1
{false, "-9223372036854775808"}, // 2
{false, "main.css"}, // 3
{false, "/assets/main.css"}, // 4
{false, "92233720368547758079223372036854775807"}, // 5
{false, "9223372036854775808 9223372036854775808"}, // 6
{false, "-1"}, // 7
{false, "-0"}, // 8
{false, "+1"}, // 9
{false, "18446744073709551615"}, // 10
{false, "9223372036854775807"}, // 11
{false, "021"}, // 12 - no leading zeroes are allowed.
{false, "300"}, // 13
{true, "0"}, // 14
{true, "255"}, // 15
{true, "21"}, // 16
}
for i, tt := range tests {
testEvaluatorRaw(t, Uint8, tt.input, reflect.Uint8, tt.pass, i)
}
}
func TestUint64EvaluatorRaw(t *testing.T) {
tests := []struct {
pass bool
input string
}{
{false, "astring"}, // 0
{false, "astringwith_numb3rS_and_symbol$"}, // 1
{false, "-9223372036854775808"}, // 2
{false, "main.css"}, // 3
{false, "/assets/main.css"}, // 4
{false, "92233720368547758079223372036854775807"}, // 5
{false, "9223372036854775808 9223372036854775808"}, // 6
{false, "-1"}, // 7
{false, "-0"}, // 8
{false, "+1"}, // 9
{true, "18446744073709551615"}, // 10
{true, "9223372036854775807"}, // 11
{true, "0"}, // 12
}
for i, tt := range tests {
testEvaluatorRaw(t, Uint64, tt.input, reflect.Uint64, tt.pass, i)
}
}
func TestAlphabeticalEvaluatorRaw(t *testing.T) {
tests := []struct {
pass bool
input string
}{
{true, "astring"}, // 0
{false, "astringwith_numb3rS_and_symbol$"}, // 1
{false, "32321"}, // 2
{false, "main.css"}, // 3
{false, "/assets/main.css"}, // 4
}
for i, tt := range tests {
testEvaluatorRaw(t, Alphabetical, tt.input, reflect.String, tt.pass, i)
}
}
func TestFileEvaluatorRaw(t *testing.T) {
tests := []struct {
pass bool
input string
}{
{true, "astring"}, // 0
{false, "astringwith_numb3rS_and_symbol$"}, // 1
{true, "32321"}, // 2
{true, "main.css"}, // 3
{false, "/assets/main.css"}, // 4
}
for i, tt := range tests {
testEvaluatorRaw(t, File, tt.input, reflect.String, tt.pass, i)
}
}
func TestPathEvaluatorRaw(t *testing.T) {
pathTests := []struct {
pass bool
input string
}{
{true, "astring"}, // 0
{true, "astringwith_numb3rS_and_symbol$"}, // 1
{true, "32321"}, // 2
{true, "main.css"}, // 3
{true, "/assets/main.css"}, // 4
{true, "disk/assets/main.css"}, // 5
}
for i, tt := range pathTests {
testEvaluatorRaw(t, Path, tt.input, reflect.String, tt.pass, i)
}
}
func TestConvertBuilderFunc(t *testing.T) {
fn := func(min uint64, slice []string) func(string) bool {
return func(paramValue string) bool {
if expected, got := "ok", paramValue; expected != got {
t.Fatalf("paramValue is not the expected one: %s vs %s", expected, got)
}
if expected, got := uint64(1), min; expected != got {
t.Fatalf("min argument is not the expected one: %d vs %d", expected, got)
}
if expected, got := []string{"name1", "name2"}, slice; len(expected) == len(got) {
if expected, got := "name1", slice[0]; expected != got {
t.Fatalf("slice argument[%d] does not contain the expected value: %s vs %s", 0, expected, got)
}
if expected, got := "name2", slice[1]; expected != got {
t.Fatalf("slice argument[%d] does not contain the expected value: %s vs %s", 1, expected, got)
}
} else {
t.Fatalf("slice argument is not the expected one, the length is difference: %d vs %d", len(expected), len(got))
}
return true
}
}
evalFunc := convertBuilderFunc(fn)
if !evalFunc([]string{"1", "[name1,name2]"}).Call([]reflect.Value{reflect.ValueOf("ok")})[0].Interface().(bool) {
t.Fatalf("failed, it should fail already")
}
}

527
macro/macros.go Normal file
View File

@@ -0,0 +1,527 @@
package macro
import (
"strconv"
"strings"
"github.com/kataras/iris/macro/interpreter/ast"
)
var (
// String type
// Allows anything (single path segment, as everything except the `Path`).
String = NewMacro("string", "", true, false, nil).
RegisterFunc("regexp", func(expr string) func(string) bool {
return MustRegexp(expr)
}).
// checks if param value starts with the 'prefix' arg
RegisterFunc("prefix", func(prefix string) func(string) bool {
return func(paramValue string) bool {
return strings.HasPrefix(paramValue, prefix)
}
}).
// checks if param value ends with the 'suffix' arg
RegisterFunc("suffix", func(suffix string) func(string) bool {
return func(paramValue string) bool {
return strings.HasSuffix(paramValue, suffix)
}
}).
// checks if param value contains the 's' arg
RegisterFunc("contains", func(s string) func(string) bool {
return func(paramValue string) bool {
return strings.Contains(paramValue, s)
}
}).
// checks if param value's length is at least 'min'
RegisterFunc("min", func(min int) func(string) bool {
return func(paramValue string) bool {
return len(paramValue) >= min
}
}).
// checks if param value's length is not bigger than 'max'
RegisterFunc("max", func(max int) func(string) bool {
return func(paramValue string) bool {
return max >= len(paramValue)
}
})
simpleNumberEval = MustRegexp("^-?[0-9]+$")
// Int or number type
// both positive and negative numbers, actual value can be min-max int64 or min-max int32 depends on the arch.
Int = NewMacro("int", "number", false, false, func(paramValue string) (interface{}, bool) {
if !simpleNumberEval(paramValue) {
return nil, false
}
v, err := strconv.Atoi(paramValue)
if err != nil {
return nil, false
}
return v, true
}).
// checks if the param value's int representation is
// bigger or equal than 'min'
RegisterFunc("min", func(min int) func(int) bool {
return func(paramValue int) bool {
return paramValue >= min
}
}).
// checks if the param value's int representation is
// smaller or equal than 'max'.
RegisterFunc("max", func(max int) func(int) bool {
return func(paramValue int) bool {
return paramValue <= max
}
}).
// checks if the param value's int representation is
// between min and max, including 'min' and 'max'.
RegisterFunc("range", func(min, max int) func(int) bool {
return func(paramValue int) bool {
return !(paramValue < min || paramValue > max)
}
})
// Int8 type
// -128 to 127.
Int8 = NewMacro("int8", "", false, false, func(paramValue string) (interface{}, bool) {
if !simpleNumberEval(paramValue) {
return nil, false
}
v, err := strconv.ParseInt(paramValue, 10, 8)
if err != nil {
return nil, false
}
return int8(v), true
}).
RegisterFunc("min", func(min int8) func(int8) bool {
return func(paramValue int8) bool {
return paramValue >= min
}
}).
RegisterFunc("max", func(max int8) func(int8) bool {
return func(paramValue int8) bool {
return paramValue <= max
}
}).
RegisterFunc("range", func(min, max int8) func(int8) bool {
return func(paramValue int8) bool {
return !(paramValue < min || paramValue > max)
}
})
// Int16 type
// -32768 to 32767.
Int16 = NewMacro("int16", "", false, false, func(paramValue string) (interface{}, bool) {
if !simpleNumberEval(paramValue) {
return nil, false
}
v, err := strconv.ParseInt(paramValue, 10, 16)
if err != nil {
return nil, false
}
return int16(v), true
}).
RegisterFunc("min", func(min int16) func(int16) bool {
return func(paramValue int16) bool {
return paramValue >= min
}
}).
RegisterFunc("max", func(max int16) func(int16) bool {
return func(paramValue int16) bool {
return paramValue <= max
}
}).
RegisterFunc("range", func(min, max int16) func(int16) bool {
return func(paramValue int16) bool {
return !(paramValue < min || paramValue > max)
}
})
// Int32 type
// -2147483648 to 2147483647.
Int32 = NewMacro("int32", "", false, false, func(paramValue string) (interface{}, bool) {
if !simpleNumberEval(paramValue) {
return nil, false
}
v, err := strconv.ParseInt(paramValue, 10, 32)
if err != nil {
return nil, false
}
return int32(v), true
}).
RegisterFunc("min", func(min int32) func(int32) bool {
return func(paramValue int32) bool {
return paramValue >= min
}
}).
RegisterFunc("max", func(max int32) func(int32) bool {
return func(paramValue int32) bool {
return paramValue <= max
}
}).
RegisterFunc("range", func(min, max int32) func(int32) bool {
return func(paramValue int32) bool {
return !(paramValue < min || paramValue > max)
}
})
// Int64 as int64 type
// -9223372036854775808 to 9223372036854775807.
Int64 = NewMacro("int64", "long", false, false, func(paramValue string) (interface{}, bool) {
if !simpleNumberEval(paramValue) {
return nil, false
}
v, err := strconv.ParseInt(paramValue, 10, 64)
if err != nil { // if err == strconv.ErrRange...
return nil, false
}
return v, true
}).
// checks if the param value's int64 representation is
// bigger or equal than 'min'.
RegisterFunc("min", func(min int64) func(int64) bool {
return func(paramValue int64) bool {
return paramValue >= min
}
}).
// checks if the param value's int64 representation is
// smaller or equal than 'max'.
RegisterFunc("max", func(max int64) func(int64) bool {
return func(paramValue int64) bool {
return paramValue <= max
}
}).
// checks if the param value's int64 representation is
// between min and max, including 'min' and 'max'.
RegisterFunc("range", func(min, max int64) func(int64) bool {
return func(paramValue int64) bool {
return !(paramValue < min || paramValue > max)
}
})
// Uint as uint type
// actual value can be min-max uint64 or min-max uint32 depends on the arch.
// if x64: 0 to 18446744073709551615
// if x32: 0 to 4294967295 and etc.
Uint = NewMacro("uint", "", false, false, func(paramValue string) (interface{}, bool) {
v, err := strconv.ParseUint(paramValue, 10, strconv.IntSize) // 32,64...
if err != nil {
return nil, false
}
return uint(v), true
}).
// checks if the param value's int representation is
// bigger or equal than 'min'
RegisterFunc("min", func(min uint) func(uint) bool {
return func(paramValue uint) bool {
return paramValue >= min
}
}).
// checks if the param value's int representation is
// smaller or equal than 'max'.
RegisterFunc("max", func(max uint) func(uint) bool {
return func(paramValue uint) bool {
return paramValue <= max
}
}).
// checks if the param value's int representation is
// between min and max, including 'min' and 'max'.
RegisterFunc("range", func(min, max uint) func(uint) bool {
return func(paramValue uint) bool {
return !(paramValue < min || paramValue > max)
}
})
uint8Eval = MustRegexp("^([0-9]|[1-8][0-9]|9[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
// Uint8 as uint8 type
// 0 to 255.
Uint8 = NewMacro("uint8", "", false, false, func(paramValue string) (interface{}, bool) {
if !uint8Eval(paramValue) {
return nil, false
}
v, err := strconv.ParseUint(paramValue, 10, 8)
if err != nil {
return nil, false
}
return uint8(v), true
}).
// checks if the param value's uint8 representation is
// bigger or equal than 'min'.
RegisterFunc("min", func(min uint8) func(uint8) bool {
return func(paramValue uint8) bool {
return paramValue >= min
}
}).
// checks if the param value's uint8 representation is
// smaller or equal than 'max'.
RegisterFunc("max", func(max uint8) func(uint8) bool {
return func(paramValue uint8) bool {
return paramValue <= max
}
}).
// checks if the param value's uint8 representation is
// between min and max, including 'min' and 'max'.
RegisterFunc("range", func(min, max uint8) func(uint8) bool {
return func(paramValue uint8) bool {
return !(paramValue < min || paramValue > max)
}
})
// Uint16 as uint16 type
// 0 to 65535.
Uint16 = NewMacro("uint16", "", false, false, func(paramValue string) (interface{}, bool) {
v, err := strconv.ParseUint(paramValue, 10, 16)
if err != nil {
return nil, false
}
return uint16(v), true
}).
RegisterFunc("min", func(min uint16) func(uint16) bool {
return func(paramValue uint16) bool {
return paramValue >= min
}
}).
RegisterFunc("max", func(max uint16) func(uint16) bool {
return func(paramValue uint16) bool {
return paramValue <= max
}
}).
RegisterFunc("range", func(min, max uint16) func(uint16) bool {
return func(paramValue uint16) bool {
return !(paramValue < min || paramValue > max)
}
})
// Uint32 as uint32 type
// 0 to 4294967295.
Uint32 = NewMacro("uint32", "", false, false, func(paramValue string) (interface{}, bool) {
v, err := strconv.ParseUint(paramValue, 10, 32)
if err != nil {
return nil, false
}
return uint32(v), true
}).
RegisterFunc("min", func(min uint32) func(uint32) bool {
return func(paramValue uint32) bool {
return paramValue >= min
}
}).
RegisterFunc("max", func(max uint32) func(uint32) bool {
return func(paramValue uint32) bool {
return paramValue <= max
}
}).
RegisterFunc("range", func(min, max uint32) func(uint32) bool {
return func(paramValue uint32) bool {
return !(paramValue < min || paramValue > max)
}
})
// Uint64 as uint64 type
// 0 to 18446744073709551615.
Uint64 = NewMacro("uint64", "", false, false, func(paramValue string) (interface{}, bool) {
v, err := strconv.ParseUint(paramValue, 10, 64)
if err != nil {
return nil, false
}
return v, true
}).
// checks if the param value's uint64 representation is
// bigger or equal than 'min'.
RegisterFunc("min", func(min uint64) func(uint64) bool {
return func(paramValue uint64) bool {
return paramValue >= min
}
}).
// checks if the param value's uint64 representation is
// smaller or equal than 'max'.
RegisterFunc("max", func(max uint64) func(uint64) bool {
return func(paramValue uint64) bool {
return paramValue <= max
}
}).
// checks if the param value's uint64 representation is
// between min and max, including 'min' and 'max'.
RegisterFunc("range", func(min, max uint64) func(uint64) bool {
return func(paramValue uint64) bool {
return !(paramValue < min || paramValue > max)
}
})
// Bool or boolean as bool type
// a string which is "1" or "t" or "T" or "TRUE" or "true" or "True"
// or "0" or "f" or "F" or "FALSE" or "false" or "False".
Bool = NewMacro("bool", "boolean", false, false, func(paramValue string) (interface{}, bool) {
// a simple if statement is faster than regex ^(true|false|True|False|t|0|f|FALSE|TRUE)$
// in this case.
v, err := strconv.ParseBool(paramValue)
if err != nil {
return nil, false
}
return v, true
})
alphabeticalEval = MustRegexp("^[a-zA-Z ]+$")
// Alphabetical letter type
// letters only (upper or lowercase)
Alphabetical = NewMacro("alphabetical", "", false, false, func(paramValue string) (interface{}, bool) {
if !alphabeticalEval(paramValue) {
return nil, false
}
return paramValue, true
})
fileEval = MustRegexp("^[a-zA-Z0-9_.-]*$")
// File type
// letters (upper or lowercase)
// numbers (0-9)
// underscore (_)
// dash (-)
// point (.)
// no spaces! or other character
File = NewMacro("file", "", false, false, func(paramValue string) (interface{}, bool) {
if !fileEval(paramValue) {
return nil, false
}
return paramValue, true
})
// Path type
// anything, should be the last part
//
// It allows everything, we have String and Path as different
// types because I want to give the opportunity to the user
// to organise the macro functions based on wildcard or single dynamic named path parameter.
// Should be living in the latest path segment of a route path.
Path = NewMacro("path", "", false, true, nil)
Defaults = &Macros{
String,
Int,
Int8,
Int16,
Int32,
Int64,
Uint,
Uint8,
Uint16,
Uint32,
Uint64,
Bool,
Alphabetical,
Path,
}
)
type Macros []*Macro
func (ms *Macros) Register(indent, alias string, isMaster, isTrailing bool, evaluator ParamEvaluator) *Macro {
macro := NewMacro(indent, alias, isMaster, isTrailing, evaluator)
if ms.register(macro) {
return macro
}
return nil
}
func (ms *Macros) register(macro *Macro) bool {
if macro.Indent() == "" || macro.Evaluator == nil {
return false
}
cp := *ms
for _, m := range cp {
// can't add more than one with the same ast characteristics.
if macro.Indent() == m.Indent() {
return false
}
if macro.Alias() == m.Alias() || macro.Alias() == m.Indent() {
return false
}
if macro.Master() && m.Master() {
return false
}
}
cp = append(cp, macro)
*ms = cp
return true
}
func (ms *Macros) Unregister(indent string) bool {
cp := *ms
for i, m := range cp {
if m.Indent() == indent {
copy(cp[i:], cp[i+1:])
cp[len(cp)-1] = nil
cp = cp[:len(cp)-1]
*ms = cp
return true
}
}
return false
}
func (ms *Macros) Lookup(pt ast.ParamType) *Macro {
if m := ms.Get(pt.Indent()); m != nil {
return m
}
if alias, has := ast.HasAlias(pt); has {
if m := ms.Get(alias); m != nil {
return m
}
}
return nil
}
func (ms *Macros) Get(indentOrAlias string) *Macro {
if indentOrAlias == "" {
return nil
}
for _, m := range *ms {
if m.Indent() == indentOrAlias {
return m
}
if m.Alias() == indentOrAlias {
return m
}
}
return nil
}
func (ms *Macros) GetMaster() *Macro {
for _, m := range *ms {
if m.Master() {
return m
}
}
return nil
}
func (ms *Macros) GetTrailings() (macros []*Macro) {
for _, m := range *ms {
if m.Trailing() {
macros = append(macros, m)
}
}
return
}

147
macro/template.go Normal file
View File

@@ -0,0 +1,147 @@
package macro
import (
"reflect"
"github.com/kataras/iris/core/memstore"
"github.com/kataras/iris/macro/interpreter/ast"
"github.com/kataras/iris/macro/interpreter/parser"
)
// Template contains a route's path full parsed template.
//
// Fields:
// Src is the raw source of the path, i.e /users/{id:int min(1)}
// Params is the list of the Params that are being used to the
// path, i.e the min as param name and 1 as the param argument.
type Template struct {
// Src is the original template given by the client
Src string `json:"src"`
Params []TemplateParam `json:"params"`
}
// TemplateParam is the parsed macro parameter's template
// they are being used to describe the param's syntax result.
type TemplateParam struct {
Src string `json:"src"` // the unparsed param'false source
// Type is not useful anywhere here but maybe
// it's useful on host to decide how to convert the path template to specific router's syntax
Type ast.ParamType `json:"type"`
Name string `json:"name"`
Index int `json:"index"`
ErrCode int `json:"errCode"`
TypeEvaluator ParamEvaluator `json:"-"`
Funcs []reflect.Value `json:"-"`
stringInFuncs []func(string) bool
canEval bool
}
func (p TemplateParam) preComputed() TemplateParam {
for _, pfn := range p.Funcs {
if fn, ok := pfn.Interface().(func(string) bool); ok {
p.stringInFuncs = append(p.stringInFuncs, fn)
}
}
// if true then it should be execute the type parameter or its functions
// else it can be ignored,
// i.e {myparam} or {myparam:string} or {myparam:path} ->
// their type evaluator is nil because they don't do any checks and they don't change
// the default parameter value's type (string) so no need for any work).
p.canEval = p.TypeEvaluator != nil || len(p.Funcs) > 0 || p.ErrCode != parser.DefaultParamErrorCode
return p
}
func (p *TemplateParam) CanEval() bool {
return p.canEval
}
// paramChanger is the same form of context's Params().Set
// we could accept a memstore.Store or even context.RequestParams
// but this form has been chosed in order to test easier and fully decoupled from a request when necessary.
func (p *TemplateParam) Eval(paramValue string, paramChanger memstore.ValueSetter) bool {
if p.TypeEvaluator == nil {
for _, fn := range p.stringInFuncs {
if !fn(paramValue) {
return false
}
}
return true
}
newValue, passed := p.TypeEvaluator(paramValue)
if !passed {
return false
}
if len(p.Funcs) > 0 {
paramIn := []reflect.Value{reflect.ValueOf(newValue)}
for _, evalFunc := range p.Funcs {
// or make it as func(interface{}) bool and pass directly the "newValue"
// but that would not be as easy for end-developer, so keep that "slower":
if !evalFunc.Call(paramIn)[0].Interface().(bool) { // i.e func(paramValue int) bool
return false
}
}
}
paramChanger.Set(p.Name, newValue)
return true
}
// Parse takes a full route path and a macro map (macro map contains the macro types with their registered param functions)
// and returns a new Template.
// It builds all the parameter functions for that template
// and their evaluators, it's the api call that makes use the interpeter's parser -> lexer.
func Parse(src string, macros Macros) (*Template, error) {
types := make([]ast.ParamType, len(macros))
for i, m := range macros {
types[i] = m
}
params, err := parser.Parse(src, types)
if err != nil {
return nil, err
}
t := new(Template)
t.Src = src
for idx, p := range params {
m := macros.Lookup(p.Type)
typEval := m.Evaluator
tmplParam := TemplateParam{
Src: p.Src,
Type: p.Type,
Name: p.Name,
Index: idx,
ErrCode: p.ErrorCode,
TypeEvaluator: typEval,
}
for _, paramfn := range p.Funcs {
tmplFn := m.getFunc(paramfn.Name)
if tmplFn == nil { // if not find on this type, check for Master's which is for global funcs too.
if m := macros.GetMaster(); m != nil {
tmplFn = m.getFunc(paramfn.Name)
}
if tmplFn == nil { // if not found then just skip this param.
continue
}
}
evalFn := tmplFn(paramfn.Args)
if evalFn.IsNil() || !evalFn.IsValid() || evalFn.Kind() != reflect.Func {
continue
}
tmplParam.Funcs = append(tmplParam.Funcs, evalFn)
}
t.Params = append(t.Params, tmplParam.preComputed())
}
return t, nil
}