2017-03-20 22:54:50 +01:00
// I would be grateful if I had the chance to see the whole work-in-progress in a codebase when I started.
// You have the chance to learn faster nowdays, don't underestimate that, that's the only reason that this "_future" folder exists now.
//
2017-03-21 23:33:38 +01:00
//
2017-03-20 22:54:50 +01:00
// The whole "router" package is a temp place to test my ideas and implementations for future iris' features.
// Young developers can understand and see how ideas can be transform to real implementations on a software like Iris,
// watching the history of a "dirty" code can be useful for some of you.
2017-03-21 00:26:54 +01:00
//
2017-03-20 15:00:42 +01:00
package router
import (
2017-03-21 23:33:38 +01:00
"reflect"
2017-03-20 15:00:42 +01:00
"regexp"
2017-03-21 00:26:54 +01:00
"strconv"
2017-03-22 04:07:30 +01:00
"strings"
2017-03-20 15:00:42 +01:00
"testing"
2017-03-20 22:54:50 +01:00
"gopkg.in/kataras/iris.v6"
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
"gopkg.in/kataras/iris.v6/httptest"
2017-03-20 15:00:42 +01:00
)
2017-03-27 03:09:44 +02:00
// No, better to have simple functions, it will be easier for users to understand
// type ParamEvaluator interface {
// Eval() func(string) bool
// Literal() string
// }
// type IntParam struct {
// }
// func (i IntParam) Literal() string {
// return "int"
// }
// func (i IntParam) Eval() func(string) bool {
// r, err := regexp.Compile("[1-9]+$")
// if err != nil {
// panic(err)
// }
// return r.MatchString
// }
// func (i IntParam) Eq(eqToNumber int) func(int) bool {
// return func(param int) bool {
// return eqToNumber == param
// }
// }
2017-03-20 15:00:42 +01:00
// a helper to return a macro from a simple regexp
// it compiles the regexp and after returns the macro, for obviously performance reasons.
2017-03-21 04:22:01 +01:00
func fromRegexp ( expr string ) _macrofn {
2017-03-20 15:00:42 +01:00
if expr == "" {
panic ( "empty expr on regex" )
}
// 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 {
panic ( err )
}
2017-03-22 04:07:30 +01:00
return r . MatchString
2017-03-20 15:00:42 +01:00
}
// link the path tmpl with macros, at .Boot time, before Listen.
// make it a as middleware from the beginning and prepend that before the main handler.
2017-03-20 22:54:50 +01:00
func link ( path string , mac _macros ) iris . HandlerFunc {
2017-03-20 15:00:42 +01:00
tmpl , err := ParsePath ( path )
if err != nil {
panic ( err )
}
2017-03-23 01:34:45 +01:00
// println(tmpl.Params[0].Param.FailStatusCode)
2017-03-20 15:00:42 +01:00
// link the path, based on its template with a macro
// and return a new compiled macro or a list of iris handlers
// in order to be prepended on the original route or make a different function for that?
// we'll see.
2017-03-20 22:54:50 +01:00
var h iris . HandlerFunc // we could add an empty handler but we wouldn't know where to ctx.Next if this path doesn't uses macros.
2017-03-21 04:22:01 +01:00
createH := func ( paramName string , validator _macrofn , failStatus int , prevH iris . HandlerFunc ) iris . HandlerFunc {
2017-03-20 22:54:50 +01:00
return func ( ctx * iris . Context ) {
if prevH != nil {
prevH ( ctx )
}
paramValue := ctx . Param ( paramName )
if paramValue != "" {
2017-03-22 04:07:30 +01:00
valid := validator ( paramValue )
2017-03-20 22:54:50 +01:00
if ! valid {
2017-03-21 04:22:01 +01:00
// print("not valid for validator on paramValue= '" + paramValue + "' ctx.Pos = ")
// println(ctx.Pos) // it should be always 0.
2017-03-20 22:54:50 +01:00
ctx . EmitError ( failStatus )
return
}
}
// remember: router already matches the path, so here if a path param is missing then it was allowed by the router.
ctx . Next ( )
}
}
for i := range tmpl . Params {
p := tmpl . Params [ i ]
2017-03-21 00:26:54 +01:00
if m , found := mac [ p . Param . Macro . Name ] ; found && m . eval != nil {
2017-03-20 22:54:50 +01:00
prevH := h
2017-03-21 00:26:54 +01:00
eval := m . eval
for _ , fi := range m . funcs {
for _ , mi := range p . Param . Macro . Funcs {
2017-03-21 04:22:01 +01:00
hasFunc := fi . name == mi . Name
if ! hasFunc {
for _ , gb := range mac [ global_macro ] . funcs {
if gb . name == fi . name {
hasFunc = true
break
}
}
}
2017-03-21 00:26:54 +01:00
2017-03-21 04:22:01 +01:00
if hasFunc {
2017-03-21 00:26:54 +01:00
prevEval := eval
macroFuncEval := fi . eval ( mi . Params )
2017-03-22 04:07:30 +01:00
eval = func ( pvalue string ) bool {
if prevEval ( pvalue ) {
return macroFuncEval ( pvalue )
2017-03-21 00:26:54 +01:00
}
return false
}
continue
}
}
}
h = createH ( p . Param . Name , eval , p . Param . FailStatusCode , prevH )
2017-03-20 22:54:50 +01:00
}
}
if h == nil {
// println("h is nil")
return func ( ctx * iris . Context ) {
ctx . Next ( ) // is ok, the route doesn't contains any valid macros
}
}
return h
2017-03-20 15:00:42 +01:00
}
// eval runs while serving paths
// instead of path it can receive the iris.Context and work as middleware
// if the macro passed completely then do ctx.Next() to continue to the main handler and the following,
// otherwise ctx.EmitError(pathTmpl.FailStatusCode) , which defaults to 404 for normal behavior on not found a route,
// but the developer can change that too,
// for example in order to fire the 402 if the compiled macro(I should think the name later) failed to be evaluted
// then the user should add !+statuscode, i.e "{id:int !402}".
// func eval(path string, tmpl *PathTmpl) bool {
// return false
// }
// <--- fun(c)k it, we will do it directly to be iris' middleware or create a new type which will save a macro and tries to eval it with a path
// only for test-cases? and after on iris we can make a middleware from this, I should think it more when I stop the drinking.
func testMacros ( source string ) error {
2017-03-20 22:54:50 +01:00
return nil
}
2017-03-21 04:22:01 +01:00
// let's give the macro's funcs access to context, it will be great experimental to serve templates just with a path signature
2017-03-22 04:07:30 +01:00
type _macrofn func ( pathParamValue string ) bool
2017-03-21 04:22:01 +01:00
2017-03-21 00:26:54 +01:00
type _macrofunc struct {
name string
2017-03-21 04:22:01 +01:00
eval func ( [ ] string ) _macrofn
2017-03-21 00:26:54 +01:00
}
2017-03-21 04:22:01 +01:00
2017-03-21 00:26:54 +01:00
type _macro struct {
funcs [ ] _macrofunc
2017-03-21 04:22:01 +01:00
eval _macrofn
2017-03-21 00:26:54 +01:00
}
// macros should be registered before .Listen
type _macros map [ string ] * _macro
var all_macros = _macros { }
2017-03-21 04:22:01 +01:00
func addMacro ( name string , v _macrofn ) {
2017-03-21 00:26:54 +01:00
all_macros [ name ] = & _macro { eval : v }
}
2017-03-21 04:22:01 +01:00
func addMacroFunc ( macroName string , funcName string , v func ( [ ] string ) _macrofn ) {
2017-03-21 00:26:54 +01:00
if m , found := all_macros [ macroName ] ; found {
m . funcs = append ( m . funcs , _macrofunc { name : funcName , eval : v } )
2017-03-20 15:00:42 +01:00
}
2017-03-21 00:26:54 +01:00
}
2017-03-21 04:22:01 +01:00
const global_macro = "any"
2017-03-22 04:07:30 +01:00
// func(min int, max int) func(paramValue string)bool
2017-03-21 23:33:38 +01:00
func macroFuncFrom ( v interface { } ) func ( params [ ] string ) _macrofn {
// this is executed once on boot time, not at serve time:
vot := reflect . TypeOf ( v )
numFields := vot . NumIn ( )
2017-03-21 04:22:01 +01:00
2017-03-21 23:33:38 +01:00
return func ( params [ ] string ) _macrofn {
if len ( params ) != numFields {
panic ( "should accepts _numFields_ args" )
2017-03-21 00:26:54 +01:00
}
2017-03-21 23:33:38 +01:00
var args [ ] reflect . Value
2017-03-21 00:26:54 +01:00
2017-03-21 23:33:38 +01:00
// check for accepting arguments
for i := 0 ; i < numFields ; i ++ {
field := vot . In ( i )
2017-03-22 04:07:30 +01:00
param := params [ i ]
// if field.IsVariadic() {
// panic("variadic arguments are not supported") // or they will do ?
// }
var val interface { }
var err error
2017-03-21 23:33:38 +01:00
switch field . Kind ( ) {
2017-03-22 04:07:30 +01:00
// these can be transfered to another function with supported type conversions
// the dev can also be able to modify how a string converted to x kind of type,
// even custom type, i.e User{}, (I have to give an easy way to do hard things
// but also extensibility for devs that are experienced,
// like I did with the rest of the features).
2017-03-21 23:33:38 +01:00
case reflect . String :
2017-03-22 04:07:30 +01:00
val = param
case reflect . Int :
val , err = strconv . Atoi ( param )
2017-03-21 23:33:38 +01:00
case reflect . Bool :
2017-03-22 04:07:30 +01:00
val , err = strconv . ParseBool ( param )
2017-03-21 23:33:38 +01:00
default :
panic ( "unsported type!" )
}
2017-03-22 04:07:30 +01:00
if err != nil {
panic ( err )
}
args = append ( args , reflect . ValueOf ( val ) )
2017-03-21 23:33:38 +01:00
}
// check for the return type (only one ofc, which again is a function but it returns a boolean)
// which accepts one argument which is the parameter value.
if vot . NumOut ( ) != 1 {
panic ( "expecting to return only one (func)" )
}
rof := vot . Out ( 0 )
if rof . Kind ( ) != reflect . Func {
panic ( "expecting to return a function!" )
}
returnRof := rof . Out ( 0 )
if rof . NumOut ( ) != 1 {
panic ( "expecting to return only one (bool)" )
}
if returnRof . Kind ( ) != reflect . Bool {
panic ( "expecting this func to return a boolean" )
2017-03-21 00:26:54 +01:00
}
2017-03-21 23:33:38 +01:00
if rof . NumIn ( ) != 1 {
panic ( "expecting this func to receive one arg" )
2017-03-21 00:26:54 +01:00
}
2017-03-22 04:07:30 +01:00
vofi := reflect . ValueOf ( v ) . Call ( args ) [ 0 ] . Interface ( )
var validator _macrofn
// check for typed and not typed
if _v , ok := vofi . ( _macrofn ) ; ok {
validator = _v
} else if _v , ok = vofi . ( func ( string ) bool ) ; ok {
validator = _v
}
2017-03-21 23:33:38 +01:00
//
// this is executed when a route requested:
2017-03-22 04:07:30 +01:00
return func ( paramValue string ) bool {
return validator ( paramValue )
2017-03-21 23:33:38 +01:00
}
//
}
}
func TestMacros ( t * testing . T ) {
addMacro ( "int" , fromRegexp ( "[1-9]+$" ) )
2017-03-22 04:07:30 +01:00
// // {id:int range(42,49)}
// // "hard" manually way(it will not be included on the final feature(;)):
2017-03-21 23:33:38 +01:00
// addMacroFunc("int", "range", func(params []string) _macrofn {
// // start: .Boot time, before .Listen
// allowedParamsLen := 2
// // params: 42,49 (including first and second)
// if len(params) != allowedParamsLen {
// panic("range accepts two parameters")
// }
// min, err := strconv.Atoi(params[0])
// if err != nil {
// panic("invalid first parameter: " + err.Error())
// }
// max, err := strconv.Atoi(params[1])
// if err != nil {
// panic("invalid second parameter: " + err.Error())
// }
// // end
2017-03-22 04:07:30 +01:00
// return func(paramValue string) bool {
2017-03-21 23:33:38 +01:00
// paramValueInt, err := strconv.Atoi(paramValue)
// if err != nil {
// return false
// }
// if paramValueInt >= min && paramValueInt <= max {
// return true
// }
// return false
// }
// })
2017-03-22 04:07:30 +01:00
//
// {id:int range(42,49)}
// easy way, same performance as the hard way, no cost while serving requests.
// ::
2017-03-21 23:33:38 +01:00
// result should be like that in the final feature implementation, using reflection BEFORE .Listen on .Boot time,
// so no performance cost(done) =>
addMacroFunc ( "int" , "range" , macroFuncFrom ( func ( min int , max int ) func ( string ) bool {
return func ( paramValue string ) bool {
2017-03-21 00:26:54 +01:00
paramValueInt , err := strconv . Atoi ( paramValue )
if err != nil {
return false
}
if paramValueInt >= min && paramValueInt <= max {
return true
}
return false
}
2017-03-21 23:33:38 +01:00
} ) )
2017-03-21 00:26:54 +01:00
2017-03-21 04:22:01 +01:00
addMacroFunc ( "int" , "even" , func ( params [ ] string ) _macrofn {
2017-03-22 04:07:30 +01:00
return func ( paramValue string ) bool {
2017-03-21 04:22:01 +01:00
paramValueInt , err := strconv . Atoi ( paramValue )
if err != nil {
return false
}
if paramValueInt % 2 == 0 {
return true
}
return false
}
} )
// "any" will contain macros functions
2017-03-22 04:07:30 +01:00
// which are available to all other, we will need some functions to be 'globally' registered when don't care about.
2017-03-21 04:22:01 +01:00
addMacro ( "any" , fromRegexp ( ".*" ) )
2017-03-22 04:07:30 +01:00
addMacroFunc ( "any" , "contains" , macroFuncFrom ( func ( text string ) _macrofn {
return func ( paramValue string ) bool {
return strings . Contains ( paramValue , text )
}
} ) )
addMacroFunc ( "any" , "suffix" , macroFuncFrom ( func ( text string ) _macrofn {
return func ( paramValue string ) bool {
return strings . HasSuffix ( paramValue , text )
2017-03-21 04:22:01 +01:00
}
2017-03-22 04:07:30 +01:00
} ) )
addMacro ( "string" , fromRegexp ( "[a-zA-Z]+$" ) )
// this will 'override' the "any contains"
// when string macro is used:
addMacroFunc ( "string" , "contains" , macroFuncFrom ( func ( text string ) _macrofn {
return func ( paramValue string ) bool {
2017-03-23 01:34:45 +01:00
// println("from string:contains instead of any:string")
// println("'" + text + "' vs '" + paramValue + "'")
2017-03-21 04:22:01 +01:00
2017-03-22 04:07:30 +01:00
return strings . Contains ( paramValue , text )
2017-03-21 04:22:01 +01:00
}
2017-03-22 04:07:30 +01:00
} ) )
2017-03-21 00:26:54 +01:00
2017-03-21 04:22:01 +01:00
path := "/api/users/{id:int range(42,49) even() !600}/posts"
2017-03-20 22:54:50 +01:00
app := iris . New ( )
app . Adapt ( httprouter . New ( ) )
2017-03-20 15:00:42 +01:00
2017-03-21 00:26:54 +01:00
hv := link ( path , all_macros )
2017-03-21 04:22:01 +01:00
// 600 is a custom virtual error code to handle "int" param invalids
// it sends a custom error message with a 404 (not found) http status code.
app . OnError ( 600 , func ( ctx * iris . Context ) {
ctx . SetStatusCode ( 404 ) // throw a raw 404 not found
ctx . Writef ( "Expecting an integer in range between and 42-49, should be even number too" )
// println("600 -> 404 from " + ctx.Path())
} )
2017-03-20 22:54:50 +01:00
app . Get ( "/api/users/:id/posts" , hv , func ( ctx * iris . Context ) {
ctx . ResponseWriter . WriteString ( ctx . Path ( ) )
} )
2017-03-20 15:00:42 +01:00
2017-03-22 04:07:30 +01:00
path2 := "/markdown/{file:any suffix(.md)}"
2017-03-21 04:22:01 +01:00
hv2 := link ( path2 , all_macros )
2017-03-22 04:07:30 +01:00
app . Get ( "/markdown/*file" , hv2 , func ( ctx * iris . Context ) {
ctx . Markdown ( iris . StatusOK , "**hello**" )
} )
// contains a space(on tests)
path3 := "/hello/{fullname:string contains( )}"
hv3 := link ( path3 , all_macros )
app . Get ( "/hello/:fullname" , hv3 , func ( ctx * iris . Context ) {
ctx . Writef ( "hello %s" , ctx . Param ( "fullname" ) )
} )
2017-03-21 04:22:01 +01:00
2017-03-20 22:54:50 +01:00
e := httptest . New ( app , t )
2017-03-20 15:00:42 +01:00
2017-03-20 22:54:50 +01:00
e . GET ( "/api/users/42/posts" ) . Expect ( ) . Status ( iris . StatusOK ) . Body ( ) . Equal ( "/api/users/42/posts" )
2017-03-21 04:22:01 +01:00
e . GET ( "/api/users/50/posts" ) . Expect ( ) . Status ( iris . StatusNotFound ) . Body ( ) . Equal ( "Expecting an integer in range between and 42-49, should be even number too" ) // remember, it accepts 1-9 not matched if zero.
2017-03-20 22:54:50 +01:00
e . GET ( "/api/users/0/posts" ) . Expect ( ) . Status ( iris . StatusNotFound )
e . GET ( "/api/users/_/posts" ) . Expect ( ) . Status ( iris . StatusNotFound )
e . GET ( "/api/users/s/posts" ) . Expect ( ) . Status ( iris . StatusNotFound )
e . GET ( "/api/users/posts" ) . Expect ( ) . Status ( iris . StatusNotFound )
2017-03-21 00:26:54 +01:00
// macro func invalidate test with a non-zero value between 1-9 but bigger than the max(49)
e . GET ( "/api/users/51/posts" ) . Expect ( ) . Status ( iris . StatusNotFound )
2017-03-21 04:22:01 +01:00
// macro func invalidate "even" with a non-zero value but 49 is not an even number
e . GET ( "/api/users/49/posts" ) . Expect ( ) . Status ( iris . StatusNotFound )
2017-03-21 00:26:54 +01:00
2017-03-21 04:22:01 +01:00
// test any and global
// response with "path language" only no need of handler too.
// As it goes I love the idea and users will embrace and built awesome things on top of it.
// maybe I have to 'rename' the final feature on something like iris expression language and document it as much as I can, people will love that
2017-03-22 04:07:30 +01:00
e . GET ( "/markdown/something.md" ) . Expect ( ) . Status ( iris . StatusOK ) . ContentType ( "text/html" , "utf-8" ) . Body ( ) . Equal ( "<p><strong>hello</strong></p>\n" )
e . GET ( "/hello/Makis Maropoulos" ) . Expect ( ) . Status ( iris . StatusOK ) . Body ( ) . Equal ( "hello Makis Maropoulos" )
e . GET ( "/hello/MakisMaropoulos" ) . Expect ( ) . Status ( iris . StatusNotFound ) // no space -> invalidate -> fail status code
2017-03-20 15:00:42 +01:00
}