From 55e1e79816acf5ed271db80c1a21e19f0506fe07 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 19 Mar 2017 03:49:17 +0200 Subject: [PATCH] Build a better web, together! Former-commit-id: 3cfe87da405d0ff749e1a1010660c556b047f333 --- HISTORY.md | 16 +++- _future/README.md | 38 ++++++++ _future/macros.go | 22 +++++ _future/param_parser.go | 163 +++++++++++++++++++++++++++++++++++ _future/param_parser_test.go | 115 ++++++++++++++++++++++++ _future/path_parser.go | 73 ++++++++++++++++ _future/path_parser_test.go | 88 +++++++++++++++++++ 7 files changed, 514 insertions(+), 1 deletion(-) create mode 100644 _future/README.md create mode 100644 _future/macros.go create mode 100644 _future/param_parser.go create mode 100644 _future/param_parser_test.go create mode 100644 _future/path_parser.go create mode 100644 _future/path_parser_test.go diff --git a/HISTORY.md b/HISTORY.md index c5bdcf37..74af6f46 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -47,7 +47,21 @@ to adapt the new changes to your application, it contains an overview of the new - Add `.Regex` middleware which does path validation using the `regexp` package, i.e `.Regex("param", "[0-9]+$")`. Useful for routers that don't support regex route path validation out-of-the-box. -- Websocket additions: `c.Context() *iris.Context`, `ws.GetConnectionsByRoom("room name") []websocket.Connection`, `c.OnLeave(func(roomName string){})`, `c.Values().Set(key,value)/.Get(key).Reset()` (where ws:websocket.Server insance, where c:websocket.Connection instance) +- Websocket additions: `c.Context() *iris.Context`, `ws.GetConnectionsByRoom("room name") []websocket.Connection`, `c.OnLeave(func(roomName string){})`, +```go + // SetValue sets a key-value pair on the connection's mem store. + c.SetValue(key string, value interface{}) + // GetValue gets a value by its key from the connection's mem store. + c.GetValue(key string) interface{} + // GetValueArrString gets a value as []string by its key from the connection's mem store. + c.GetValueArrString(key string) []string + // GetValueString gets a value as string by its key from the connection's mem store. + c.GetValueString(key string) string + // GetValueInt gets a value as integer by its key from the connection's mem store. + c.GetValueInt(key string) int + +``` +[examples here](https://github.com/kataras/iris/blob/v6/adaptors/websocket/_examples). Fixes: diff --git a/_future/README.md b/_future/README.md new file mode 100644 index 00000000..7083985f --- /dev/null +++ b/_future/README.md @@ -0,0 +1,38 @@ +

+ +Simplicity Equals Productivity + +
+ +Simplicity Equals Productivity + +

+ + + + +# Future | Inspiration for new developers. + + + + +This folder contains features that are marked as 'work-in-progress', they can take a long to be fully implemented and adopted to the Iris framework itself, even months. + +Some people may find that bad idea, for many and different reasons, to upload them on that public repository so soon. + +But I think that it is a good place for new developers to view and track how a feature is being implemented step-by-step. How I develop Iris step-by-step. + + +I have collected some tips for you! + +- Do What They Think You Can't Do. +- It's Not How Good You Are, It's How Good You Want To Be. +- Genius is 1% Inspiration, 99% Perspiration. +- You Are Your Only Limit. +- Do Something Today That Your Future Self Will Thank You. +- Don't Call It A Dream - Call It A Plan. + + +And never forget, **If I can do it, so can you!** + + diff --git a/_future/macros.go b/_future/macros.go new file mode 100644 index 00000000..48a5be96 --- /dev/null +++ b/_future/macros.go @@ -0,0 +1,22 @@ +package router + +/* +TODO: + +Here I should think a way to link the framework and user-defined macros +with their one-by-one(?) custom function(s) and all these with one or more PathTmpls or visa-versa + +These should be linked at .Boot time, so before the server starts. +Tthe work I have done so far it should be resulted in a single middleware +which will be prepended to the zero position, so no performance cost when no new features are used. +The performance should be the same as now if the path doesn't contains +any macros: +macro = /api/users/{id:int} or /api/users/{id:int range(1,100) !404} +no macro = /api/users/id}). + +I should add a good detailed examples on how the user can override or add his/her +own macros and optional functions can be followed (i.e, func = range(1,5)). + +Of course no breaking-changes to the user's workflow(I should not and not need to touch the existing router adaptors). + +*/ diff --git a/_future/param_parser.go b/_future/param_parser.go new file mode 100644 index 00000000..49ed72c8 --- /dev/null +++ b/_future/param_parser.go @@ -0,0 +1,163 @@ +package router + +import ( + "fmt" + "strconv" + "strings" +) + +type ParamTmpl struct { + // id + Name string + // int range(1,5)!fail # fail fails on int parse or on range, it will work reclusive + Expression string + // fail + FailStatusCode int + + Macro MacroTmpl +} + +type MacroTmpl struct { + // int + Name string + // Macro will allow more than one funcs. + // []*MacroFuncs{ {Name: range, Params: []string{1,5}}} + Funcs []MacroFuncTmpl +} + +type MacroFuncTmpl struct { + // range + Name string + // 1,5 + Params []string +} + +const ( + ParamNameSeperator = ':' + FuncSeperator = ' ' + FuncStart = '(' + FuncEnd = ')' + FuncParamSeperator = ',' + FailSeparator = '!' + ORSymbol = '|' + ANDSymbol = '&' +) + +const DefaultFailStatusCode = 0 + +// Parse i.e: +// id:int range(1,5) otherFunc(3) !404 +// +// id = param name | can front-end end here but the parser should add :any +// int = marco | can end here +// +// range = marco's funcs(range) +// 1,5 = range's func.params | can end here +// + +// otherFunc = marco's funcs(otherFunc) +// 3 = otherFunc's func.params | can end here +// +// 404 = fail http custom error status code -> handler , will fail rescuslive +func ParseParam(source string) (*ParamTmpl, error) { + // first, do a simple check if that's 'valid' + sepIndex := strings.IndexRune(source, ParamNameSeperator) + + if sepIndex <= 0 { + // if not found or + // if starts with : + return nil, fmt.Errorf("invalid source '%s', separator should be after the parameter name", source) + } + + t := new(ParamTmpl) + // id:int range(1,5) + // id:int min(1) max(5) + // id:int range(1,5)!404 or !404, space doesn't matters on fail error code. + cursor := 0 + for i := 0; i < len(source); i++ { + if source[i] == ParamNameSeperator { + if i+1 >= len(source) { + return nil, fmt.Errorf("missing marco or raw expression after seperator, on source '%s'", source) + } + + // id: , take the left, skip the : and continue + t.Name = source[0:i] + // set the expression, after the i, i.e: + // int range(1,5) + t.Expression = source[i+1:] + // set the macro's name to the full expression + // because we don't know if the user has put functions + // and we follow the < left 'pattern' + // (I don't know if that's valid but that is what + // I think to do and is working). + t.Macro = MacroTmpl{Name: t.Expression} + + // cursor knows the last known(parsed) char position. + cursor = i + 1 + continue + } + // int ... + if source[i] == FuncSeperator { + // take the left part: int if it's the first + // space after the param name + if t.Macro.Name == t.Expression { + t.Macro.Name = source[cursor:i] + } // else we have one or more functions, skip. + + cursor = i + 1 + continue + } + + if source[i] == FuncStart { + // take the left part: range + funcName := source[cursor:i] + t.Macro.Funcs = append(t.Macro.Funcs, MacroFuncTmpl{Name: funcName}) + + cursor = i + 1 + continue + } + // 1,5) + if source[i] == FuncEnd { + // check if we have end parenthesis but not start + if len(t.Macro.Funcs) == 0 { + return nil, fmt.Errorf("missing start macro's '%s' function, on source '%s'", t.Macro.Name, source) + } + + // take the left part, between Start and End: 1,5 + funcParamsStr := source[cursor:i] + + funcParams := strings.SplitN(funcParamsStr, string(FuncParamSeperator), -1) + t.Macro.Funcs[len(t.Macro.Funcs)-1].Params = funcParams + + cursor = i + 1 + continue + } + + if source[i] == FailSeparator { + // it should be the last element + // so no problem if we set the cursor here and work with that + // we will not need that later. + cursor = i + 1 + + if cursor >= len(source) { + return nil, fmt.Errorf("missing fail status code after '%q', on source '%s'", FailSeparator, source) + } + + failCodeStr := source[cursor:] // should be the last + failCode, err := strconv.Atoi(failCodeStr) + if err != nil { + return nil, fmt.Errorf("fail status code should be integer but got '%s', on source '%s'", failCodeStr, source) + } + + t.FailStatusCode = failCode + + continue + } + + } + + if t.FailStatusCode == 0 { + t.FailStatusCode = DefaultFailStatusCode + } + + return t, nil +} diff --git a/_future/param_parser_test.go b/_future/param_parser_test.go new file mode 100644 index 00000000..486e0820 --- /dev/null +++ b/_future/param_parser_test.go @@ -0,0 +1,115 @@ +package router + +import ( + "fmt" + "reflect" + "testing" +) + +func testParamParser(source string, t *ParamTmpl) error { + result, err := ParseParam(source) + if err != nil { + return err + } + + // first check of param name + if expected, got := t.Name, result.Name; expected != got { + return fmt.Errorf("Expecting Name to be '%s' but got '%s'", expected, got) + } + + // first check on macro name + if expected, got := t.Macro.Name, result.Macro.Name; expected != got { + return fmt.Errorf("Expecting Macro.Name to be '%s' but got '%s'", expected, got) + } + + // first check of length of the macro's funcs + if expected, got := len(t.Macro.Funcs), len(result.Macro.Funcs); expected != got { + return fmt.Errorf("Expecting Macro.Funs Len to be '%d' but got '%d'", expected, got) + } + + // first check of the functions + if len(t.Macro.Funcs) > 0 { + if expected, got := t.Macro.Funcs[0].Name, result.Macro.Funcs[0].Name; expected != got { + return fmt.Errorf("Expecting Macro.Funcs[0].Name to be '%s' but got '%s'", expected, got) + } + + if expected, got := t.Macro.Funcs[0].Params, result.Macro.Funcs[0].Params; expected[0] != got[0] { + return fmt.Errorf("Expecting Macro.Funcs[0].Params to be '%s' but got '%s'", expected, got) + } + } + + // and the final test for all, to be sure + // here the details are more + if !reflect.DeepEqual(*t, *result) { + return fmt.Errorf("Expected and Result don't match. Details:\n%#v\n%#v", *t, *result) + } + return nil + +} + +func TestParamParser(t *testing.T) { + + // id:int + expected := &ParamTmpl{ + Name: "id", + Expression: "int", + Macro: MacroTmpl{Name: "int"}, + } + source := expected.Name + string(ParamNameSeperator) + expected.Expression + if err := testParamParser(expected.Name+":"+expected.Expression, expected); err != nil { + t.Fatal(err) + return + } + + // id:int range(1,5) + expected = &ParamTmpl{ + Name: "id", + Expression: "int range(1,5)", + Macro: MacroTmpl{Name: "int", + Funcs: []MacroFuncTmpl{ + MacroFuncTmpl{Name: "range", Params: []string{"1", "5"}}, + }, + }, + } + source = expected.Name + string(ParamNameSeperator) + expected.Expression + if err := testParamParser(expected.Name+":"+expected.Expression, expected); err != nil { + t.Fatal(err) + return + } + + // id:int min(1) max(5) + expected = &ParamTmpl{ + Name: "id", + Expression: "int min(1) max(5)", + Macro: MacroTmpl{Name: "int", + Funcs: []MacroFuncTmpl{ + MacroFuncTmpl{Name: "min", Params: []string{"1"}}, + MacroFuncTmpl{Name: "max", Params: []string{"5"}}, + }, + }, + } + source = expected.Name + string(ParamNameSeperator) + expected.Expression + if err := testParamParser(expected.Name+":"+expected.Expression, expected); err != nil { + t.Fatal(err) + return + } + + // username:string contains('blabla') max(20) !402 + expected = &ParamTmpl{ + Name: "username", + Expression: "string contains(blabla) max(20) !402", + FailStatusCode: 402, + Macro: MacroTmpl{Name: "string", + Funcs: []MacroFuncTmpl{ + MacroFuncTmpl{Name: "contains", Params: []string{"blabla"}}, + MacroFuncTmpl{Name: "max", Params: []string{"20"}}, + }, + }, + } + source = expected.Name + string(ParamNameSeperator) + expected.Expression + if err := testParamParser(source, expected); err != nil { + t.Fatal(err) + return + } + +} diff --git a/_future/path_parser.go b/_future/path_parser.go new file mode 100644 index 00000000..5c6a54ff --- /dev/null +++ b/_future/path_parser.go @@ -0,0 +1,73 @@ +package router + +import ( + "fmt" +) + +type PathTmpl struct { + Params []PathParamTmpl + SegmentsLength int +} + +type PathParamTmpl struct { + SegmentIndex int + Param ParamTmpl +} + +const ( + PathSeparator = '/' + // Out means that it doesn't being included in param. + ParamStartOut = '{' + ParamEndOut = '}' +) + +// /users/{id:int range(1,5)}/profile +// parses only the contents inside {} +// but it gives back the position so it will be '1' +func ParsePath(source string) (*PathTmpl, error) { + t := new(PathTmpl) + cursor := 0 + segmentIndex := -1 + + // error if path is empty + if len(source) < 1 { + return nil, fmt.Errorf("source cannot be empty ") + } + // error if not starts with '/' + if source[0] != PathSeparator { + return nil, fmt.Errorf("source '%s' should start with a path separator(%q)", source, PathSeparator) + } + // if path ends with '/' remove the last '/' + if source[len(source)-1] == PathSeparator { + source = source[0 : len(source)-1] + } + + for i := range source { + if source[i] == PathSeparator { + segmentIndex++ + t.SegmentsLength++ + continue + } + + if source[i] == ParamStartOut { + cursor = i + 1 + continue + } + + if source[i] == ParamEndOut { + // take the left part id:int range(1,5) + paramSource := source[cursor:i] + paramTmpl, err := ParseParam(paramSource) + if err != nil { + return nil, err + } + + t.Params = append(t.Params, PathParamTmpl{SegmentIndex: segmentIndex, Param: *paramTmpl}) + + cursor = i + 1 + continue + } + } + + return t, nil +} diff --git a/_future/path_parser_test.go b/_future/path_parser_test.go new file mode 100644 index 00000000..39c160f7 --- /dev/null +++ b/_future/path_parser_test.go @@ -0,0 +1,88 @@ +package router + +import ( + "fmt" + "reflect" + "testing" +) + +func testPathParser(source string, t *PathTmpl) error { + result, err := ParsePath(source) + if err != nil { + return err + } + + if expected, got := t.SegmentsLength, result.SegmentsLength; expected != got { + return fmt.Errorf("expecting SegmentsLength to be %d but got %d", expected, got) + } + + if expected, got := t.Params, result.Params; len(expected) != len(got) { + return fmt.Errorf("expecting Params length to be %d but got %d", expected, got) + } + + if !reflect.DeepEqual(*t, *result) { + return fmt.Errorf("Expected and Result don't match. Details:\n%#v\nvs\n%#v\n", *t, *result) + } + + return nil +} + +func TestPathParser(t *testing.T) { + // /users/{id:int} + expected := &PathTmpl{ + SegmentsLength: 2, + Params: []PathParamTmpl{ + PathParamTmpl{ + SegmentIndex: 1, + Param: ParamTmpl{ + Name: "id", + Expression: "int", + Macro: MacroTmpl{Name: "int"}, + }, + }, + }, + } + + if err := testPathParser("/users/{id:int}", expected); err != nil { + t.Fatal(err) + return + } + + // /api/users/{id:int range(1,5) !404}/other/{username:string contains(s) min(10) !402} + expected = &PathTmpl{ + SegmentsLength: 5, + Params: []PathParamTmpl{ + PathParamTmpl{ + SegmentIndex: 2, + Param: ParamTmpl{ + Name: "id", + Expression: "int range(1,5) !404", + FailStatusCode: 404, + Macro: MacroTmpl{Name: "int", + Funcs: []MacroFuncTmpl{ + MacroFuncTmpl{Name: "range", Params: []string{"1", "5"}}, + }, + }, + }, + }, + PathParamTmpl{ + SegmentIndex: 4, + Param: ParamTmpl{ + Name: "username", + Expression: "string contains(s) min(10) !402", + FailStatusCode: 402, + Macro: MacroTmpl{Name: "string", + Funcs: []MacroFuncTmpl{ + MacroFuncTmpl{Name: "contains", Params: []string{"s"}}, + MacroFuncTmpl{Name: "min", Params: []string{"10"}}, + }, + }, + }, + }, + }, + } + if err := testPathParser("/api/users/{id:int range(1,5) !404}/other/{username:string contains(s) min(10) !402}", expected); err != nil { + t.Fatal(err) + return + } +}