From 82b5a1d4ed3ee7b51a48120465caef3010c7893a Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 10 Oct 2017 04:46:32 +0300 Subject: [PATCH] Enhance the MVC "using-method-result" example. Prev: update to version 8.5.0 Prev Commit: https://github.com/kataras/iris/commit/49ee8f2d75764c51cbbfebbd4145e02e97f9b831 [formerly 6a3579f2500fc715d7dc606478960946dcade61d] Changelog: https://github.com/kataras/iris/blob/master/HISTORY.md#mo-09-october-2017--v850 This example is updated with the current commit: https://github.com/kataras/iris/tree/master/_examples/mvc/using-output-result Former-commit-id: 29486ef014b3667fa1c7c66e11c8e95c76a37e57 --- HISTORY.md | 10 +- README.md | 340 ++++++++++++++++-- README_NEXT.md | 2 +- VERSION | 2 +- _examples/mvc/login/user/datasource.go | 2 +- .../controllers/movie_controller.go | 80 +++++ .../controllers/movies_controller.go | 75 ---- .../using-method-result/datasource/movies.go | 17 +- .../using-method-result/folder_structure.png | Bin 15595 -> 25547 bytes _examples/mvc/using-method-result/main.go | 16 +- .../mvc/using-method-result/models/movie.go | 30 ++ .../services/movie_service.go | 206 +++++++++++ mvc/activator/binder.go | 10 + 13 files changed, 660 insertions(+), 130 deletions(-) create mode 100644 _examples/mvc/using-method-result/controllers/movie_controller.go delete mode 100644 _examples/mvc/using-method-result/controllers/movies_controller.go create mode 100644 _examples/mvc/using-method-result/services/movie_service.go 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 d37dd97a626cd25c941b88451eb2073b89efb79f..f3d39df1d4a00c5c327146baf61a80563972aeb3 100644 GIT binary patch literal 25547 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 ON$H8Y+=h|@-zYs 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 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() }