diff --git a/_examples/mvc/login/web/controllers/users_controller.go b/_examples/mvc/login/web/controllers/users_controller.go index 5e7b9187..ed111706 100644 --- a/_examples/mvc/login/web/controllers/users_controller.go +++ b/_examples/mvc/login/web/controllers/users_controller.go @@ -14,7 +14,7 @@ import ( // DELETE /users/{id:long} | delete by id // Requires basic authentication. type UsersController struct { - // context is auto-binded by Iris on each request, + // Optionally: context is auto-binded by Iris on each request, // remember that on each incoming request iris creates a new UserController each time, // so all fields are request-scoped by-default, only dependency injection is able to set // custom fields like the Service which is the same for all requests (static binding). @@ -84,5 +84,5 @@ func (c *UsersController) DeleteBy(id int64) interface{} { // right here we can see that a method function // can return any of those two types(map or int), // we don't have to specify the return type to a specific type. - return 400 // same as `iris.StatusBadRequest`. + return iris.StatusBadRequest // same as 400. } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go index 744880c4..eb464be2 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go @@ -1,24 +1,8 @@ package todo -type State uint32 - -const ( - StateActive State = iota - StateCompleted -) - -func ParseState(s string) State { - switch s { - case "completed": - return StateCompleted - default: - return StateActive - } -} - type Item struct { - OwnerID string - ID int64 - Body string - CurrentState State + SessionID string `json:"-"` + ID int64 `json:"id,omitempty"` + Title string `json:"title"` + Completed bool `json:"completed"` } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go index 32e53a2c..50f3df03 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go @@ -1,53 +1,31 @@ package todo type Service interface { - GetByID(id int64) (Item, bool) - GetByOwner(owner string) []Item - Complete(item Item) bool - Save(newItem Item) error + Get(owner string) []Item + Save(owner string, newItems []Item) error } type MemoryService struct { - items map[int64]Item + items map[string][]Item } -func (s *MemoryService) getLatestID() (id int64) { - for k := range s.items { - if k > id { - id = k +func NewMemoryService() *MemoryService { + return &MemoryService{make(map[string][]Item, 0)} +} + +func (s *MemoryService) Get(sessionOwner string) (items []Item) { + return s.items[sessionOwner] +} + +func (s *MemoryService) Save(sessionOwner string, newItems []Item) error { + var prevID int64 + for i := range newItems { + if newItems[i].ID == 0 { + newItems[i].ID = prevID + prevID++ } } - return -} - -func (s *MemoryService) GetByID(id int64) (Item, bool) { - item, found := s.items[id] - return item, found -} - -func (s *MemoryService) GetByOwner(owner string) (items []Item) { - for _, item := range s.items { - if item.OwnerID != owner { - continue - } - items = append(items, item) - } - return -} - -func (s *MemoryService) Complete(item Item) bool { - item.CurrentState = StateCompleted - return s.Save(item) == nil -} - -func (s *MemoryService) Save(newItem Item) error { - if newItem.ID == 0 { - // create - newItem.ID = s.getLatestID() + 1 - } - - // full replace here for the shake of simplicity) - s.items[newItem.ID] = newItem + s.items[sessionOwner] = newItems return nil } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go index cb671bad..5688ed0a 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -4,9 +4,9 @@ import ( "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo" "github.com/kataras/iris" - "github.com/kataras/iris/sessions" - "github.com/kataras/iris/mvc" + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" ) // TodoController is our TODO app's web controller. @@ -23,55 +23,39 @@ type TodoController struct { func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { // this could be binded to a controller's function input argument // if any, or struct field if any: - b.Dependencies().Add(func(ctx iris.Context) todo.Item { - // ctx.ReadForm(&item) - var ( - owner = ctx.PostValue("owner") - body = ctx.PostValue("body") - state = ctx.PostValue("state") - ) + b.Dependencies().Add(func(ctx iris.Context) (items []todo.Item) { + ctx.ReadJSON(&items) + return + }) +} - return todo.Item{ - OwnerID: owner, - Body: body, - CurrentState: todo.ParseState(state), - } +// Get handles the GET: /todos route. +func (c *TodoController) Get() []todo.Item { + return c.Service.Get(c.Session.ID()) +} + +// PostItemResponse the response data that will be returned as json +// after a post save action of all todo items. +type PostItemResponse struct { + Success bool `json:"success"` +} + +var emptyResponse = PostItemResponse{Success: false} + +// Post handles the POST: /todos route. +func (c *TodoController) Post(newItems []todo.Item) PostItemResponse { + if err := c.Service.Save(c.Session.ID(), newItems); err != nil { + return emptyResponse + } + + return PostItemResponse{Success: true} +} + +func (c *TodoController) GetSync(conn websocket.Connection) { + conn.Join(c.Session.ID()) + conn.On("save", func() { // "save" event from client. + conn.To(c.Session.ID()).Emit("saved", nil) // fire a "save" event to the rest of the clients. }) - // ca.Router.Use(...).Done(...).Layout(...) -} - -// Get handles the GET: /todo route. -func (c *TodoController) Get() []todo.Item { - return c.Service.GetByOwner(c.Session.ID()) -} - -// PutCompleteBy handles the PUT: /todo/complete/{id:long} route. -func (c *TodoController) PutCompleteBy(id int64) int { - item, found := c.Service.GetByID(id) - if !found { - return iris.StatusNotFound - } - - if item.OwnerID != c.Session.ID() { - return iris.StatusForbidden - } - - if !c.Service.Complete(item) { - return iris.StatusBadRequest - } - - return iris.StatusOK -} - -// Post handles the POST: /todo route. -func (c *TodoController) Post(newItem todo.Item) int { - if newItem.OwnerID != c.Session.ID() { - return iris.StatusForbidden - } - - if err := c.Service.Save(newItem); err != nil { - return iris.StatusBadRequest - } - return iris.StatusOK + conn.Wait() } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go index dbfaa446..0136edfa 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go @@ -6,12 +6,14 @@ import ( "github.com/kataras/iris" "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" "github.com/kataras/iris/mvc" ) func main() { app := iris.New() + // serve our app in public, public folder // contains the client-side vue.js application, // no need for any server-side template here, @@ -20,20 +22,28 @@ func main() { app.StaticWeb("/", "./public") sess := sessions.New(sessions.Config{ - Cookie: "_iris_session", + Cookie: "iris_session", }) - m := mvc.New(app.Party("/todo")) + ws := websocket.New(websocket.Config{}) + // create our mvc application targeted to /todos relative sub path. + m := mvc.New(app.Party("/todos")) // any dependencies bindings here... m.AddDependencies( + todo.NewMemoryService(), mvc.Session(sess), - new(todo.MemoryService), + ws.Upgrade, ) + // http://localhost:8080/iris-ws.js + // serve the javascript client library to communicate with + // the iris high level websocket event system. + m.Router.Any("/iris-ws.js", websocket.ClientHandler()) + // controllers registration here... m.Register(new(controllers.TodoController)) // start the web server at http://localhost:8080 - app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker, iris.WithOptimizations) + app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker) } diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html index f282a65d..3763042c 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html @@ -1,63 +1,71 @@ - - - Iris + Vue.js • TodoMVC - - - - - - - -
-
-

todos

- -
-
- - -
- -
- - - + + + Iris + Vue.js • TodoMVC + + + + + + + + + + + + + + +
+
+

todos

+ +
+
+ + +
+ +
+ + + + + \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js index 248c5333..b06ba921 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js @@ -1,19 +1,42 @@ -// Full spec-compliant TodoMVC with localStorage persistence +// Full spec-compliant TodoMVC with Iris // and hash-based routing in ~120 effective lines of JavaScript. -// localStorage persistence -var STORAGE_KEY = 'todos-vuejs-2.0' +// var socket = new Ws("ws://localhost:8080/todos/sync"); + +// socket.On("saved", function () { +// console.log("receive: on saved"); +// todoStorage.fetch(); +// }); + +var todos = []; + var todoStorage = { fetch: function () { - var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') - todos.forEach(function (todo, index) { - todo.id = index - }) - todoStorage.uid = todos.length + axios.get("/todos").then(response => { + if (response.data == null) { + return; + } + for (var i = 0; i < response.data.length; i++) { + // if (todos.length <=i || todos[i] === null) { + // todos.push(response.data[i]); + // } else { + // todos[i] = response.data[i]; + // } + todos.push(response.data[i]); + } + }); + return todos }, save: function (todos) { - localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)) + axios.post("/todos", JSON.stringify(todos)).then(response => { + if (!response.data.success) { + window.alert("saving had a failure"); + return; + } + // console.log("send: save"); + // socket.Emit("save") + }); } } @@ -44,11 +67,18 @@ var app = new Vue({ visibility: 'all' }, - // watch todos change for localStorage persistence + // watch todos change for persistence watch: { todos: { handler: function (todos) { - todoStorage.save(todos) + // // saved by this client. + // if (todos[todos.length - 1].id === 0) { + // todoStorage.save(todos); + // } else { + // console.log("item cannot be saved, already exists."); + // console.log(todos[todos.length - 1]); + // } + todoStorage.save(todos); }, deep: true } @@ -90,7 +120,7 @@ var app = new Vue({ return } this.todos.push({ - id: todoStorage.uid++, + id: 0, // just for the client-side. title: value, completed: false }) @@ -140,7 +170,7 @@ var app = new Vue({ }) // handle routing -function onHashChange () { +function onHashChange() { var visibility = window.location.hash.replace(/#\/?/, '') if (filters[visibility]) { app.visibility = visibility diff --git a/mvc/di/func.go b/mvc/di/func.go index 36424b06..e49a55c0 100644 --- a/mvc/di/func.go +++ b/mvc/di/func.go @@ -11,6 +11,8 @@ type ( InputIndex int } + // FuncInjector keeps the data that are needed in order to do the binding injection + // as fast as possible and with the best possible and safest way. FuncInjector struct { // the original function, is being used // only the .Call, which is referring to the same function, always. @@ -27,6 +29,10 @@ type ( } ) +// MakeFuncInjector returns a new func injector, which will be the object +// that the caller should use to bind input arguments of the "fn" function. +// +// The hijack and the goodFunc are optional, the "values" is the dependencies values. func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *FuncInjector { typ := IndirectType(fn.Type()) s := &FuncInjector{ @@ -100,10 +106,14 @@ func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, v return s } +// String returns a debug trace text. func (s *FuncInjector) String() string { return s.trace } +// Inject accepts an already created slice of input arguments +// and fills them, the "ctx" is optional and it's used +// on the dependencies that depends on one or more input arguments, these are the "ctx". func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) { args := *in for _, input := range s.inputs { @@ -118,6 +128,12 @@ func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) { *in = args } +// Call calls the "Inject" with a new slice of input arguments +// that are computed by the length of the input argument from the MakeFuncInjector's "fn" function. +// +// If the function needs a receiver, so +// the caller should be able to in[0] = receiver before injection, +// then the `Inject` method should be used instead. func (s *FuncInjector) Call(ctx ...reflect.Value) []reflect.Value { in := make([]reflect.Value, s.Length, s.Length) s.Inject(&in, ctx...) diff --git a/mvc/di/object.go b/mvc/di/object.go index f33e7036..392abcc5 100644 --- a/mvc/di/object.go +++ b/mvc/di/object.go @@ -5,11 +5,17 @@ import ( "reflect" ) +// BindType is the type of a binded object/value, it's being used to +// check if the value is accessible after a function call with a "ctx" when needed ( Dynamic type) +// or it's just a struct value (a service | Static type). type BindType uint32 const ( - Static BindType = iota // simple assignable value, a static value. - Dynamic // dynamic value, depends on some input arguments from the caller. + // Static is the simple assignable value, a static value. + Static BindType = iota + // Dynamic returns a value but it depends on some input arguments from the caller, + // on serve time. + Dynamic ) func bindTypeString(typ BindType) string { @@ -21,6 +27,9 @@ func bindTypeString(typ BindType) string { } } +// BindObject contains the dependency value's read-only information. +// FuncInjector and StructInjector keeps information about their +// input arguments/or fields, these properties contain a `BindObject` inside them. type BindObject struct { Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' . Value reflect.Value @@ -29,6 +38,11 @@ type BindObject struct { ReturnValue func([]reflect.Value) reflect.Value } +// MakeBindObject accepts any "v" value, struct, pointer or a function +// and a type checker that is used to check if the fields (if "v.elem()" is struct) +// or the input arguments (if "v.elem()" is func) +// are valid to be included as the final object's dependencies, even if the caller added more +// the "di" is smart enough to select what each "v" needs and what not before serve time. func MakeBindObject(v reflect.Value, goodFunc TypeChecker) (b BindObject, err error) { if IsFunc(v) { b.BindType = Dynamic @@ -93,10 +107,13 @@ func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Val return bf, outTyp, nil } +// IsAssignable checks if "to" type can be used as "b.Value/ReturnValue". func (b *BindObject) IsAssignable(to reflect.Type) bool { return equalTypes(b.Type, to) } +// Assign sets the values to a setter, "toSetter" contains the setter, so the caller +// can use it for multiple and different structs/functions as well. func (b *BindObject) Assign(ctx []reflect.Value, toSetter func(reflect.Value)) { if b.BindType == Dynamic { toSetter(b.ReturnValue(ctx)) diff --git a/mvc/di/struct.go b/mvc/di/struct.go index 091cd77f..df20d7c8 100644 --- a/mvc/di/struct.go +++ b/mvc/di/struct.go @@ -18,6 +18,8 @@ type ( FieldIndex []int } + // StructInjector keeps the data that are needed in order to do the binding injection + // as fast as possible and with the best possible and safest way. StructInjector struct { initRef reflect.Value initRefAsSlice []reflect.Value // useful when the struct is passed on a func as input args via reflection. @@ -42,6 +44,12 @@ func (s *StructInjector) countBindType(typ BindType) (n int) { return } +// MakeStructInjector returns a new struct injector, which will be the object +// that the caller should use to bind exported fields or +// embedded unexported fields that contain exported fields +// of the "v" struct value or pointer. +// +// The hijack and the goodFunc are optional, the "values" is the dependencies values. func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *StructInjector { s := &StructInjector{ initRef: v, @@ -149,6 +157,7 @@ func (s *StructInjector) fillStruct() { } } +// String returns a debug trace message. func (s *StructInjector) String() (trace string) { for i, f := range s.fields { elemField := s.elemType.FieldByIndex(f.FieldIndex) diff --git a/mvc/di/values.go b/mvc/di/values.go index d7aacb18..1033b957 100644 --- a/mvc/di/values.go +++ b/mvc/di/values.go @@ -2,8 +2,11 @@ package di import "reflect" +// Values is a shortcut of []reflect.Value, +// it makes easier to remove and add dependencies. type Values []reflect.Value +// NewValues returns new empty (dependencies) values. func NewValues() Values { return Values{} } @@ -30,6 +33,7 @@ func (bv Values) CloneWithFieldsOf(s interface{}) Values { return values } +// Len returns the length of the current "bv" values slice. func (bv Values) Len() int { return len(bv) } @@ -41,6 +45,8 @@ func (bv *Values) Add(values ...interface{}) { bv.AddValues(ValuesOf(values)...) } +// AddValues same as `Add` but accepts reflect.Value dependencies instead of interface{} +// and appends them to the list if they pass some checks. func (bv *Values) AddValues(values ...reflect.Value) { for _, v := range values { if !goodVal(v) {