diff --git a/HISTORY.md b/HISTORY.md index cde93ef1..f46f3a4b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -18,13 +18,13 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`. -# Su, 09 October 2017 | v8.5.0 +# Mo, 09 October 2017 | v8.5.0 ## MVC Great news for our **MVC** Fans or if you're not you may want to use that powerful feature today, because of the smart coding and decisions the performance is quite the same to the pure handlers, see [_benchmarks](_benchmarks). -Iris now gives you the ability to render a response based on the **output values** returned method functions! +Iris now gives you the ability to render a response based on the **output values** returned from the controller's method functions! You can return any value of any type from a method function and it will be sent to the client as expected. @@ -162,14 +162,10 @@ func (c *MoviesController) DeleteBy(id int) iris.Map { } ``` -Another good example with a typical folder structure, that many developers are used to work, is located at the new [README.md](README.md) under the [Quick MVC Tutorial #3](README.md#quick-mvc-tutorial--3) section. +Another good example with a typical folder structure, that many developers are used to work, is located at the new [README.md](README.md) under the [Quick MVC Tutorial #3](README.md#quick-mvc-tutorial-3) section. ### The complete example source code can be found at [_examples/mvc/using-method-result](_examples/mvc/using-method-result) folder. ----- - -Upgrade with `go get -u -v github.com/kataras/iris` or let the auto-updater to do its job. - # Fr, 06 October 2017 | v8.4.5 - Badger team added support for transactions [yesterday](https://github.com/dgraph-io/badger/commit/06242925c2f2a5e73dc688e9049004029dd7f9f7), therefore the [badger session database](sessions/sessiondb/badger) is updated via https://github.com/kataras/iris/commit/0b48927562a2202809a7674ebedb738dc3da57e8. diff --git a/README.md b/README.md index cf60b392..b7bcb775 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Iris is a fast, simple and efficient micro web framework for Go. It provides a b ### About our User Experience Report -Three days ago, _at 03 October_, we announced the first [Iris User Experience form-based Report](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) to let us learn more about you and any issues that troubles you with Iris (if any). +A week ago, _at 03 October_, we announced the first [Iris User Experience form-based Report](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) to let us learn more about you and any issues that troubles you with Iris (if any). At overall, the results (so far) are very promising, high number of participations and the answers to the questions are near to the green feedback we were receiving over the past months from Gophers worldwide via our [rocket chat](https://chat.iris-go.com) and [author's twitter](https://twitter.com/makismaropoulos). **If you didn't complete the form yet, [please do so](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) as soon as possible!** @@ -67,7 +67,7 @@ $ go get -u github.com/kataras/iris - Iris takes advantage of the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature. You get truly reproducible builds, as this method guards against upstream renames and deletes. -- [Latest changes | v8.5.0](https://github.com/kataras/iris/blob/master/HISTORY.md#su-09-october-2017--v850) +- [Latest changes | v8.5.0](https://github.com/kataras/iris/blob/master/HISTORY.md#mo-09-october-2017--v850) ## Getting Started @@ -449,83 +449,336 @@ like shared layouts, tmpl funcs, reverse routing and more! package models +import "github.com/kataras/iris/context" + // Movie is our sample data structure. type Movie struct { + ID int64 `json:"id"` Name string `json:"name"` Year int `json:"year"` Genre string `json:"genre"` Poster string `json:"poster"` } + +// Dispatch completes the `kataras/iris/mvc#Result` interface. +// Sends a `Movie` as a controlled http response. +// If its ID is zero or less then it returns a 404 not found error +// else it returns its json representation, +// (just like the controller's functions do for custom types by default). +// +// Don't overdo it, the application's logic should not be here. +// It's just one more step of validation before the response, +// simple checks can be added here. +// +// It's just a showcase, +// imagine what possible this opens when you designing a bigger application. +// +// This is called where the return value from a controller's method functions +// is type of `Movie`. +// For example the `controllers/movie_controller.go#GetBy`. +func (m Movie) Dispatch(ctx context.Context) { + if m.ID <= 0 { + ctx.NotFound() + return + } + ctx.JSON(m, context.JSON{Indent: " "}) +} ``` +> For those who wonder `iris.Context`(go 1.9 type alias feature) and `context.Context` is the same [exact thing](faq.md#type-aliases). + +```go +// file: services/movie_service.go + +package services + +import ( + "errors" + "sync" + + "github.com/kataras/iris/_examples/mvc/using-method-result/models" +) + +// MovieService handles CRUID operations of a movie entity/model. +// It's here to decouple the data source from the higher level compoments. +// As a result a different service for a specific datasource (or repository) +// can be used from the main application without any additional changes. +type MovieService interface { + GetSingle(query func(models.Movie) bool) (movie models.Movie, found bool) + GetByID(id int64) (models.Movie, bool) + + InsertOrUpdate(movie models.Movie) (models.Movie, error) + DeleteByID(id int64) bool + + GetMany(query func(models.Movie) bool, limit int) (result []models.Movie) + GetAll() []models.Movie +} + +// NewMovieServiceFromMemory returns a new memory-based movie service. +func NewMovieServiceFromMemory(source map[int64]models.Movie) MovieService { + return &MovieMemoryService{ + source: source, + } +} + +/* +A Movie Service can have different data sources: +func NewMovieServiceFromDB(db datasource.MySQL) { + return &MovieDatabaseService{ + db: db, + } +} + +Another pattern is to initialize the database connection +or any source here based on a "string" name or an "enum". +func NewMovieService(source string) MovieService { + if source == "memory" { + return NewMovieServiceFromMemory(datasource.Movies) + } + if source == "database" { + db = datasource.NewDB("....") + return NewMovieServiceFromDB(db) + } + [...] + return nil +} +*/ + +// MovieMemoryService is a "MovieService" +// which manages the movies using the memory data source (map). +type MovieMemoryService struct { + source map[int64]models.Movie + mu sync.RWMutex +} + +// GetSingle receives a query function +// which is fired for every single movie model inside +// our imaginary data source. +// When that function returns true then it stops the iteration. +// +// It returns the query's return last known boolean value +// and the last known movie model +// to help callers to reduce the LOC. +// +// It's actually a simple but very clever prototype function +// I'm using everywhere since I firstly think of it, +// hope you'll find it very useful as well. +func (s *MovieMemoryService) GetSingle(query func(models.Movie) bool) (movie models.Movie, found bool) { + s.mu.RLock() + for _, movie = range s.source { + found = query(movie) + if found { + break + } + } + s.mu.RUnlock() + + // set an empty models.Movie if not found at all. + if !found { + movie = models.Movie{} + } + + return +} + +// GetByID returns a movie based on its id. +// Returns true if found, otherwise false, the bool should be always checked +// because the models.Movie may be filled with the latest element +// but not the correct one, although it can be used for debugging. +func (s *MovieMemoryService) GetByID(id int64) (models.Movie, bool) { + return s.GetSingle(func(m models.Movie) bool { + return m.ID == id + }) +} + +// InsertOrUpdate adds or updates a movie to the (memory) storage. +// +// Returns the new movie and an error if any. +func (s *MovieMemoryService) InsertOrUpdate(movie models.Movie) (models.Movie, error) { + id := movie.ID + + if id == 0 { // Create new action + var lastID int64 + // find the biggest ID in order to not have duplications + // in productions apps you can use a third-party + // library to generate a UUID as string. + s.mu.RLock() + for _, item := range s.source { + if item.ID > lastID { + lastID = item.ID + } + } + s.mu.RUnlock() + + id = lastID + 1 + movie.ID = id + + // map-specific thing + s.mu.Lock() + s.source[id] = movie + s.mu.Unlock() + + return movie, nil + } + + // Update action based on the movie.ID, + // here we will allow updating the poster and genre if not empty. + // Alternatively we could do pure replace instead: + // s.source[id] = movie + // and comment the code below; + current, exists := s.GetByID(id) + if !exists { // ID is not a real one, return an error. + return models.Movie{}, errors.New("failed to update a nonexistent movie") + } + + // or comment these and s.source[id] = m for pure replace + if movie.Poster != "" { + current.Poster = movie.Poster + } + + if movie.Genre != "" { + current.Genre = movie.Genre + } + + // map-specific thing + s.mu.Lock() + s.source[id] = current + s.mu.Unlock() + + return movie, nil +} + +// DeleteByID deletes a movie by its id. +// +// Returns true if deleted otherwise false. +func (s *MovieMemoryService) DeleteByID(id int64) bool { + if _, exists := s.GetByID(id); !exists { + // we could do _, exists := s.source[id] instead + // but we don't because you should learn + // how you can use that service's functions + // with any other source, i.e database. + return false + } + + // map-specific thing + s.mu.Lock() + delete(s.source, id) + s.mu.Unlock() + + return true +} + +// GetMany same as GetSingle but returns one or more models.Movie as a slice. +// If limit <=0 then it returns everything. +func (s *MovieMemoryService) GetMany(query func(models.Movie) bool, limit int) (result []models.Movie) { + loops := 0 + + s.mu.RLock() + for _, movie := range s.source { + loops++ + + passed := query(movie) + if passed { + result = append(result, movie) + } + // we have to return at least one movie if "passed" was true. + if limit >= loops { + break + } + } + s.mu.RUnlock() + + return +} + +// GetAll returns all movies. +func (s *MovieMemoryService) GetAll() []models.Movie { + movies := s.GetMany(func(m models.Movie) bool { return true }, -1) + return movies +} +``` + + ```go // file: controllers/movies_controller.go package controllers import ( - "github.com/kataras/iris/_examples/mvc/using-method-result/datasource" + "errors" + "github.com/kataras/iris/_examples/mvc/using-method-result/models" + "github.com/kataras/iris/_examples/mvc/using-method-result/services" "github.com/kataras/iris" "github.com/kataras/iris/mvc" ) -// MoviesController is our /movies controller. -type MoviesController struct { +// MovieController is our /movies controller. +type MovieController struct { + // mvc.C is just a lightweight lightweight alternative + // to the "mvc.Controller" controller type, + // use it when you don't need mvc.Controller's fields + // (you don't need those fields when you return values from the method functions). mvc.C + + // Our MovieService, it's an interface which + // is binded from the main application. + Service services.MovieService } // Get returns list of the movies. // Demo: // curl -i http://localhost:8080/movies -func (c *MoviesController) Get() []models.Movie { - return datasource.Movies +func (c *MovieController) Get() []models.Movie { + return c.Service.GetAll() } // GetBy returns a movie. // Demo: // curl -i http://localhost:8080/movies/1 -func (c *MoviesController) GetBy(id int) models.Movie { - return datasource.Movies[id] +func (c *MovieController) GetBy(id int64) models.Movie { + m, _ := c.Service.GetByID(id) + return m } // PutBy updates a movie. // Demo: // curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MoviesController) PutBy(id int) (models.Movie, int) { - // get the movie - m := datasource.Movies[id] - +func (c *MovieController) PutBy(id int64) (models.Movie, error) { // get the request data for poster and genre file, info, err := c.Ctx.FormFile("poster") if err != nil { - return models.Movie{}, iris.StatusInternalServerError + return models.Movie{}, errors.New("failed due form file 'poster' missing") } - // we don't need the file so close it now + // we don't need the file so close it now. file.Close() // imagine that is the url of the uploaded file... poster := info.Filename genre := c.Ctx.FormValue("genre") - // update the poster - m.Poster = poster - m.Genre = genre - datasource.Movies[id] = m - - return m, iris.StatusOK + // update the movie and return it. + return c.Service.InsertOrUpdate(models.Movie{ + ID: id, + Poster: poster, + Genre: genre, + }) } // DeleteBy deletes a movie. // Demo: // curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MoviesController) DeleteBy(id int) iris.Map { - // delete the entry from the movies slice - deleted := datasource.Movies[id].Name - datasource.Movies = append(datasource.Movies[:id], datasource.Movies[id+1:]...) - // and return the deleted movie's name - return iris.Map{"deleted": deleted} +func (c *MovieController) DeleteBy(id int64) interface{} { + // delete the entry from the movies slice. + wasDel := c.Service.DeleteByID(id) + if wasDel { + // and return the deleted movie's ID + return iris.Map{"deleted": id} + } + // 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 iris.StatusBadRequest } ``` @@ -537,32 +790,37 @@ package datasource import "github.com/kataras/iris/_examples/mvc/using-method-result/models" // Movies is our imaginary data source. -var Movies = []models.Movie{ - { +var Movies = map[int64]models.Movie{ + 1: { + ID: 1, Name: "Casablanca", Year: 1942, Genre: "Romance", Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", }, - { + 2: { + ID: 2, Name: "Gone with the Wind", Year: 1939, Genre: "Romance", Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", }, - { + 3: { + ID: 3, Name: "Citizen Kane", Year: 1941, Genre: "Mystery", Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", }, - { + 4: { + ID: 4, Name: "The Wizard of Oz", Year: 1939, Genre: "Fantasy", Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", }, - { + 5: { + ID: 5, Name: "North by Northwest", Year: 1959, Genre: "Thriller", @@ -593,21 +851,31 @@ package main import ( "github.com/kataras/iris/_examples/mvc/using-method-result/controllers" + "github.com/kataras/iris/_examples/mvc/using-method-result/datasource" "github.com/kataras/iris/_examples/mvc/using-method-result/middleware" + "github.com/kataras/iris/_examples/mvc/using-method-result/services" "github.com/kataras/iris" ) func main() { app := iris.New() + // Load the template files. app.RegisterView(iris.HTML("./views", ".html")) // Register our controllers. app.Controller("/hello", new(controllers.HelloController)) - // Add the basic authentication(admin:password) middleware - // for the /movies based requests. - app.Controller("/movies", new(controllers.MoviesController), middleware.BasicAuth) + + // Create our movie service (memory), we will bind it to the movies controller. + service := services.NewMovieServiceFromMemory(datasource.Movies) + + app.Controller("/movies", new(controllers.MovieController), + // Bind the "service" to the MovieController's Service (interface) field. + service, + // Add the basic authentication(admin:password) middleware + // for the /movies based requests. + middleware.BasicAuth) // Start the web server at localhost:8080 // http://localhost:8080/hello diff --git a/README_NEXT.md b/README_NEXT.md index e345a003..1d70cce8 100644 --- a/README_NEXT.md +++ b/README_NEXT.md @@ -86,7 +86,7 @@ _Psst_, we've produced a small video about your feelings regrating to Iris! You ### 📑 Table Of Content * [Installation](#-installation) -* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#su-09-october-2017--v850) +* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#mo-09-october-2017--v850) * [Learn](#-learn) * [Structuring](_examples/#structuring) * [HTTP Listening](_examples/#http-listening) diff --git a/VERSION b/VERSION index 8cf24a05..6d4ab2f0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.5.0:https://github.com/kataras/iris/blob/master/HISTORY.md#su-09-october-2017--v850 \ No newline at end of file +8.5.0:https://github.com/kataras/iris/blob/master/HISTORY.md#mo-09-october-2017--v850 \ No newline at end of file diff --git a/_examples/mvc/login/user/datasource.go b/_examples/mvc/login/user/datasource.go index 2ce11773..ad596e21 100644 --- a/_examples/mvc/login/user/datasource.go +++ b/_examples/mvc/login/user/datasource.go @@ -27,7 +27,7 @@ func NewDataSource() *DataSource { } } -// GetBy returns receives a query function +// GetBy receives a query function // which is fired for every single user model inside // our imaginary database. // When that function returns true then it stops the iteration. diff --git a/_examples/mvc/using-method-result/controllers/movie_controller.go b/_examples/mvc/using-method-result/controllers/movie_controller.go new file mode 100644 index 00000000..4ed5fc6a --- /dev/null +++ b/_examples/mvc/using-method-result/controllers/movie_controller.go @@ -0,0 +1,80 @@ +// file: controllers/movies_controller.go + +package controllers + +import ( + "errors" + + "github.com/kataras/iris/_examples/mvc/using-method-result/models" + "github.com/kataras/iris/_examples/mvc/using-method-result/services" + + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" +) + +// MovieController is our /movies controller. +type MovieController struct { + // mvc.C is just a lightweight lightweight alternative + // to the "mvc.Controller" controller type, + // use it when you don't need mvc.Controller's fields + // (you don't need those fields when you return values from the method functions). + mvc.C + + // Our MovieService, it's an interface which + // is binded from the main application. + Service services.MovieService +} + +// Get returns list of the movies. +// Demo: +// curl -i http://localhost:8080/movies +func (c *MovieController) Get() []models.Movie { + return c.Service.GetAll() +} + +// GetBy returns a movie. +// Demo: +// curl -i http://localhost:8080/movies/1 +func (c *MovieController) GetBy(id int64) models.Movie { + m, _ := c.Service.GetByID(id) + return m +} + +// PutBy updates a movie. +// Demo: +// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 +func (c *MovieController) PutBy(id int64) (models.Movie, error) { + // get the request data for poster and genre + file, info, err := c.Ctx.FormFile("poster") + if err != nil { + return models.Movie{}, errors.New("failed due form file 'poster' missing") + } + // we don't need the file so close it now. + file.Close() + + // imagine that is the url of the uploaded file... + poster := info.Filename + genre := c.Ctx.FormValue("genre") + + // update the movie and return it. + return c.Service.InsertOrUpdate(models.Movie{ + ID: id, + Poster: poster, + Genre: genre, + }) +} + +// DeleteBy deletes a movie. +// Demo: +// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 +func (c *MovieController) DeleteBy(id int64) interface{} { + // delete the entry from the movies slice. + wasDel := c.Service.DeleteByID(id) + if wasDel { + // and return the deleted movie's ID + return iris.Map{"deleted": id} + } + // 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 iris.StatusBadRequest +} diff --git a/_examples/mvc/using-method-result/controllers/movies_controller.go b/_examples/mvc/using-method-result/controllers/movies_controller.go deleted file mode 100644 index 6d4d4d69..00000000 --- a/_examples/mvc/using-method-result/controllers/movies_controller.go +++ /dev/null @@ -1,75 +0,0 @@ -// file: controllers/movies_controller.go -// -// This is just an example of usage, don't use it for production, it even doesn't check for -// index exceed! - -package controllers - -import ( - "github.com/kataras/iris/_examples/mvc/using-method-result/datasource" - "github.com/kataras/iris/_examples/mvc/using-method-result/models" - - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -// MoviesController is our /movies controller. -type MoviesController struct { - // mvc.C is just a lightweight lightweight alternative - // to the "mvc.Controller" controller type, - // use it when you don't need mvc.Controller's fields - // (you don't need those fields when you return values from the method functions). - mvc.C -} - -// Get returns list of the movies. -// Demo: -// curl -i http://localhost:8080/movies -func (c *MoviesController) Get() []models.Movie { - return datasource.Movies -} - -// GetBy returns a movie. -// Demo: -// curl -i http://localhost:8080/movies/1 -func (c *MoviesController) GetBy(id int) models.Movie { - return datasource.Movies[id] -} - -// PutBy updates a movie. -// Demo: -// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 -func (c *MoviesController) PutBy(id int) (models.Movie, int) { - // get the movie - m := datasource.Movies[id] - - // get the request data for poster and genre - file, info, err := c.Ctx.FormFile("poster") - if err != nil { - return models.Movie{}, iris.StatusInternalServerError - } - // we don't need the file so close it now - file.Close() - - // imagine that is the url of the uploaded file... - poster := info.Filename - genre := c.Ctx.FormValue("genre") - - // update the poster - m.Poster = poster - m.Genre = genre - datasource.Movies[id] = m - - return m, iris.StatusOK -} - -// DeleteBy deletes a movie. -// Demo: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MoviesController) DeleteBy(id int) iris.Map { - // delete the entry from the movies slice - deleted := datasource.Movies[id].Name - datasource.Movies = append(datasource.Movies[:id], datasource.Movies[id+1:]...) - // and return the deleted movie's name - return iris.Map{"deleted": deleted} -} diff --git a/_examples/mvc/using-method-result/datasource/movies.go b/_examples/mvc/using-method-result/datasource/movies.go index a1d275b2..b330aad3 100644 --- a/_examples/mvc/using-method-result/datasource/movies.go +++ b/_examples/mvc/using-method-result/datasource/movies.go @@ -5,32 +5,37 @@ package datasource import "github.com/kataras/iris/_examples/mvc/using-method-result/models" // Movies is our imaginary data source. -var Movies = []models.Movie{ - { +var Movies = map[int64]models.Movie{ + 1: { + ID: 1, Name: "Casablanca", Year: 1942, Genre: "Romance", Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", }, - { + 2: { + ID: 2, Name: "Gone with the Wind", Year: 1939, Genre: "Romance", Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", }, - { + 3: { + ID: 3, Name: "Citizen Kane", Year: 1941, Genre: "Mystery", Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", }, - { + 4: { + ID: 4, Name: "The Wizard of Oz", Year: 1939, Genre: "Fantasy", Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", }, - { + 5: { + ID: 5, Name: "North by Northwest", Year: 1959, Genre: "Thriller", diff --git a/_examples/mvc/using-method-result/folder_structure.png b/_examples/mvc/using-method-result/folder_structure.png index d37dd97a..f3d39df1 100644 Binary files a/_examples/mvc/using-method-result/folder_structure.png and b/_examples/mvc/using-method-result/folder_structure.png differ diff --git a/_examples/mvc/using-method-result/main.go b/_examples/mvc/using-method-result/main.go index 29ba0fb9..1675db8a 100644 --- a/_examples/mvc/using-method-result/main.go +++ b/_examples/mvc/using-method-result/main.go @@ -4,21 +4,31 @@ package main import ( "github.com/kataras/iris/_examples/mvc/using-method-result/controllers" + "github.com/kataras/iris/_examples/mvc/using-method-result/datasource" "github.com/kataras/iris/_examples/mvc/using-method-result/middleware" + "github.com/kataras/iris/_examples/mvc/using-method-result/services" "github.com/kataras/iris" ) func main() { app := iris.New() + // Load the template files. app.RegisterView(iris.HTML("./views", ".html")) // Register our controllers. app.Controller("/hello", new(controllers.HelloController)) - // Add the basic authentication(admin:password) middleware - // for the /movies based requests. - app.Controller("/movies", new(controllers.MoviesController), middleware.BasicAuth) + + // Create our movie service (memory), we will bind it to the movies controller. + service := services.NewMovieServiceFromMemory(datasource.Movies) + + app.Controller("/movies", new(controllers.MovieController), + // Bind the "service" to the MovieController's Service (interface) field. + service, + // Add the basic authentication(admin:password) middleware + // for the /movies based requests. + middleware.BasicAuth) // Start the web server at localhost:8080 // http://localhost:8080/hello diff --git a/_examples/mvc/using-method-result/models/movie.go b/_examples/mvc/using-method-result/models/movie.go index 874a679a..86ce3a69 100644 --- a/_examples/mvc/using-method-result/models/movie.go +++ b/_examples/mvc/using-method-result/models/movie.go @@ -2,10 +2,40 @@ package models +import "github.com/kataras/iris/context" + // Movie is our sample data structure. type Movie struct { + ID int64 `json:"id"` Name string `json:"name"` Year int `json:"year"` Genre string `json:"genre"` Poster string `json:"poster"` } + +// Dispatch completes the `kataras/iris/mvc#Result` interface. +// Sends a `Movie` as a controlled http response. +// If its ID is zero or less then it returns a 404 not found error +// else it returns its json representation, +// (just like the controller's functions do for custom types by default). +// +// Don't overdo it, the application's logic should not be here. +// It's just one more step of validation before the response, +// simple checks can be added here. +// +// It's just a showcase, +// imagine what possible this opens when you designing a bigger application. +// +// This is called where the return value from a controller's method functions +// is type of `Movie`. +// For example the `controllers/movie_controller.go#GetBy`. +func (m Movie) Dispatch(ctx context.Context) { + if m.ID <= 0 { + ctx.NotFound() + return + } + ctx.JSON(m, context.JSON{Indent: " "}) +} + +// For those who wonder `iris.Context`(go 1.9 type alias feature) and +// `context.Context` is the same exact thing. diff --git a/_examples/mvc/using-method-result/services/movie_service.go b/_examples/mvc/using-method-result/services/movie_service.go new file mode 100644 index 00000000..cc21fd7f --- /dev/null +++ b/_examples/mvc/using-method-result/services/movie_service.go @@ -0,0 +1,206 @@ +// file: services/movie_service.go + +package services + +import ( + "errors" + "sync" + + "github.com/kataras/iris/_examples/mvc/using-method-result/models" +) + +// MovieService handles CRUID operations of a movie entity/model. +// It's here to decouple the data source from the higher level compoments. +// As a result a different service for a specific datasource (or repository) +// can be used from the main application without any additional changes. +type MovieService interface { + GetSingle(query func(models.Movie) bool) (movie models.Movie, found bool) + GetByID(id int64) (models.Movie, bool) + + InsertOrUpdate(movie models.Movie) (models.Movie, error) + DeleteByID(id int64) bool + + GetMany(query func(models.Movie) bool, limit int) (result []models.Movie) + GetAll() []models.Movie +} + +// NewMovieServiceFromMemory returns a new memory-based movie service. +func NewMovieServiceFromMemory(source map[int64]models.Movie) MovieService { + return &MovieMemoryService{ + source: source, + } +} + +// A Movie Service can have different data sources: +// func NewMovieServiceFromDB(db datasource.MySQL) { +// return &MovieDatabaseService{ +// db: db, +// } +// } + +// Another pattern is to initialize the database connection +// or any source here based on a "string" name or an "enum". +// func NewMovieService(source string) MovieService { +// if source == "memory" { +// return NewMovieServiceFromMemory(datasource.Movies) +// } +// if source == "database" { +// db = datasource.NewDB("....") +// return NewMovieServiceFromDB(db) +// } +// [...] +// return nil +// } + +// MovieMemoryService is a "MovieService" +// which manages the movies using the memory data source (map). +type MovieMemoryService struct { + source map[int64]models.Movie + mu sync.RWMutex +} + +// GetSingle receives a query function +// which is fired for every single movie model inside +// our imaginary data source. +// When that function returns true then it stops the iteration. +// +// It returns the query's return last known boolean value +// and the last known movie model +// to help callers to reduce the LOC. +// +// It's actually a simple but very clever prototype function +// I'm using everywhere since I firstly think of it, +// hope you'll find it very useful as well. +func (s *MovieMemoryService) GetSingle(query func(models.Movie) bool) (movie models.Movie, found bool) { + s.mu.RLock() + for _, movie = range s.source { + found = query(movie) + if found { + break + } + } + s.mu.RUnlock() + + // set an empty models.Movie if not found at all. + if !found { + movie = models.Movie{} + } + + return +} + +// GetByID returns a movie based on its id. +// Returns true if found, otherwise false, the bool should be always checked +// because the models.Movie may be filled with the latest element +// but not the correct one, although it can be used for debugging. +func (s *MovieMemoryService) GetByID(id int64) (models.Movie, bool) { + return s.GetSingle(func(m models.Movie) bool { + return m.ID == id + }) +} + +// InsertOrUpdate adds or updates a movie to the (memory) storage. +// +// Returns the new movie and an error if any. +func (s *MovieMemoryService) InsertOrUpdate(movie models.Movie) (models.Movie, error) { + id := movie.ID + + if id == 0 { // Create new action + var lastID int64 + // find the biggest ID in order to not have duplications + // in productions apps you can use a third-party + // library to generate a UUID as string. + s.mu.RLock() + for _, item := range s.source { + if item.ID > lastID { + lastID = item.ID + } + } + s.mu.RUnlock() + + id = lastID + 1 + movie.ID = id + + // map-specific thing + s.mu.Lock() + s.source[id] = movie + s.mu.Unlock() + + return movie, nil + } + + // Update action based on the movie.ID, + // here we will allow updating the poster and genre if not empty. + // Alternatively we could do pure replace instead: + // s.source[id] = movie + // and comment the code below; + current, exists := s.GetByID(id) + if !exists { // ID is not a real one, return an error. + return models.Movie{}, errors.New("failed to update a nonexistent movie") + } + + // or comment these and s.source[id] = m for pure replace + if movie.Poster != "" { + current.Poster = movie.Poster + } + + if movie.Genre != "" { + current.Genre = movie.Genre + } + + // map-specific thing + s.mu.Lock() + s.source[id] = current + s.mu.Unlock() + + return movie, nil +} + +// DeleteByID deletes a movie by its id. +// +// Returns true if deleted otherwise false. +func (s *MovieMemoryService) DeleteByID(id int64) bool { + if _, exists := s.GetByID(id); !exists { + // we could do _, exists := s.source[id] instead + // but we don't because you should learn + // how you can use that service's functions + // with any other source, i.e database. + return false + } + + // map-specific thing + s.mu.Lock() + delete(s.source, id) + s.mu.Unlock() + + return true +} + +// GetMany same as GetSingle but returns one or more models.Movie as a slice. +// If limit <=0 then it returns everything. +func (s *MovieMemoryService) GetMany(query func(models.Movie) bool, limit int) (result []models.Movie) { + loops := 0 + + s.mu.RLock() + for _, movie := range s.source { + loops++ + + passed := query(movie) + if passed { + result = append(result, movie) + } + // we have to return at least one movie if "passed" was true. + if limit >= loops { + break + } + } + s.mu.RUnlock() + + return +} + +// GetAll returns all movies. +func (s *MovieMemoryService) GetAll() []models.Movie { + movies := s.GetMany(func(m models.Movie) bool { return true }, -1) + return movies +} diff --git a/mvc/activator/binder.go b/mvc/activator/binder.go index edf99d6f..83bfb803 100644 --- a/mvc/activator/binder.go +++ b/mvc/activator/binder.go @@ -69,6 +69,16 @@ func (b *binder) lookup(elem reflect.Type) (fields []field.Field) { } matcher := func(elemField reflect.StructField) bool { + // If the controller's field is interface then check + // if the given binded value implements that interface. + // i.e MovieController { service services.MoviesController /* interface */ } + // app.Controller("/", new(MovieController), + // services.NewMovieMemoryController(...)) + // *MovieMemoryService type + // that implements the MovieService interface. + if elemField.Type.Kind() == reflect.Interface { + return value.Type().Implements(elemField.Type) + } return elemField.Type == value.Type() }