Enhance the MVC "using-method-result" example. Prev: update to version 8.5.0

Prev Commit: 49ee8f2d75 [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
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-10-10 04:46:32 +03:00
parent 49ee8f2d75
commit 82b5a1d4ed
13 changed files with 660 additions and 130 deletions

View File

@ -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.

340
README.md
View File

@ -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

View File

@ -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)

View File

@ -1 +1 @@
8.5.0:https://github.com/kataras/iris/blob/master/HISTORY.md#su-09-october-2017--v850
8.5.0:https://github.com/kataras/iris/blob/master/HISTORY.md#mo-09-october-2017--v850

View File

@ -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.

View File

@ -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
}

View File

@ -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}
}

View File

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -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

View File

@ -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.

View File

@ -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
}

View File

@ -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()
}