https://github.com/kataras/iris/blob/master/HISTORY.md#th-12-october-2017--v852

Former-commit-id: 2501cf6066812c2aac158d8d6cd4e992a2b538f9
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-10-12 03:51:06 +03:00
parent 92bb47803f
commit b0f8329768
35 changed files with 1007 additions and 716 deletions

View File

@ -18,6 +18,26 @@ 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`. **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`.
# Th, 12 October 2017 | v8.5.2
This version is part of the [releases](https://github.com/kataras/iris/releases).
## MVC
Add `bool` as a supported return value, if false then skips everything else and fires 404 not found.
New example which covers the Service and Repository layers side-by-side with the MVC Architectural pattern, clean and simple: [_examples/mvc/overview](_examples/mvc/overview).
## Websocket
Fix(?) https://github.com/kataras/iris/issues/782 by @jerson with PR: https://github.com/kataras/iris/pull/783.
## Minor
Add some minor comments for the view/django's origin type getters-- as pushed at PR: [#765](https://github.com/kataras/iris/pull/765).
[sessions/sessiondb/badger](sessions/sessiondb/badger) vendored with: https://github.com/kataras/iris/commit/e7517ec79b45673e7cad353e52023ebd7237cf38.
# Tu, 10 October 2017 | v8.5.1 # Tu, 10 October 2017 | v8.5.1
## MVC ## MVC

932
README.md

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
<!-- # ![Logo created by @santoshanand](logo_white_35_24.png) Iris --> <!-- # ![Logo created by @santoshanand](logo_white_35_24.png) Iris -->
## 03, October 2017 | Iris User Experience Report # 03, October 2017 | Iris User Experience Report
Be part of the **first** Iris User Experience Report by submitting a simple form, it won't take more than **5 minutes**. Be part of the **first** Iris User Experience Report by submitting a simple form, it won't take more than **2 minutes**.
The form contains some questions that you may need to answer in order to learn more about you; learning more about you helps us to serve you with the best possible way! The form contains some questions that you may need to answer in order to learn more about you; learning more about you helps us to serve you with the best possible way!
@ -86,7 +86,7 @@ _Psst_, we've produced a small video about your feelings regrating to Iris! You
### 📑 Table Of Content ### 📑 Table Of Content
* [Installation](#-installation) * [Installation](#-installation)
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-10-october-2017--v851) * [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#th-12-october-2017--v852)
* [Learn](#-learn) * [Learn](#-learn)
* [Structuring](_examples/#structuring) * [Structuring](_examples/#structuring)
* [HTTP Listening](_examples/#http-listening) * [HTTP Listening](_examples/#http-listening)

View File

@ -1 +1 @@
8.5.1:https://github.com/kataras/iris/blob/master/HISTORY.md#tu-10-october-2017--v851 8.5.2:https://github.com/kataras/iris/blob/master/HISTORY.md#th-12-october-2017--v852

View File

@ -26,7 +26,7 @@ Structuring depends on your own needs. We can't tell you how to design your own
- [Example 1](mvc/login) - [Example 1](mvc/login)
- [Example 2](structuring/mvc) - [Example 2](structuring/mvc)
- [Example 3](structuring/handler-based) - [Example 3](structuring/handler-based)
- [Example 4](mvc/using-method-result) - [Example 4](mvc/overview)
### HTTP Listening ### HTTP Listening
@ -204,11 +204,14 @@ If you're new to back-end web development read about the MVC architectural patte
Follow the examples below, Follow the examples below,
- [Overview - Plus Repository and Service layers](mvc/overview) **NEW**
<!--
- [Hello world](mvc/hello-world/main.go) - [Hello world](mvc/hello-world/main.go)
- [Session Controller](mvc/session-controller/main.go) - [Session Controller](mvc/session-controller/main.go)
- [A simple but featured Controller with model and views](mvc/controller-with-model-and-view) - [A simple but featured Controller with model and views](mvc/controller-with-model-and-view)
- [Login showcase](mvc/login/main.go) **NEW** - [Login showcase](mvc/login/main.go)
- [Using Method Result (plus Service-oriented design)](mvc/using-method-result) -->
### Subdomains ### Subdomains

View File

@ -0,0 +1 @@
# Data Model Layer

View File

@ -0,0 +1,18 @@
// file: datamodels/movie.go
package datamodels
// Movie is our sample data structure.
// Keep note that the tags for public-use (for our web app)
// should be kept in other file like "web/viewmodels/movie.go"
// which could wrap by embedding the datamodels.Movie or
// declare new fields instead butwe will use this datamodel
// as the only one Movie model in our application,
// for the shake of simplicty.
type Movie struct {
ID int64 `json:"id"`
Name string `json:"name"`
Year int `json:"year"`
Genre string `json:"genre"`
Poster string `json:"poster"`
}

View File

@ -0,0 +1 @@
# Data Source / Data Store Layer

View File

@ -2,10 +2,10 @@
package datasource package datasource
import "github.com/kataras/iris/_examples/mvc/using-method-result/models" import "github.com/kataras/iris/_examples/mvc/overview/datamodels"
// Movies is our imaginary data source. // Movies is our imaginary data source.
var Movies = map[int64]models.Movie{ var Movies = map[int64]datamodels.Movie{
1: { 1: {
ID: 1, ID: 1,
Name: "Casablanca", Name: "Casablanca",

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -3,10 +3,11 @@
package main package main
import ( import (
"github.com/kataras/iris/_examples/mvc/using-method-result/controllers" "github.com/kataras/iris/_examples/mvc/overview/datasource"
"github.com/kataras/iris/_examples/mvc/using-method-result/datasource" "github.com/kataras/iris/_examples/mvc/overview/repositories"
"github.com/kataras/iris/_examples/mvc/using-method-result/middleware" "github.com/kataras/iris/_examples/mvc/overview/services"
"github.com/kataras/iris/_examples/mvc/using-method-result/services" "github.com/kataras/iris/_examples/mvc/overview/web/controllers"
"github.com/kataras/iris/_examples/mvc/overview/web/middleware"
"github.com/kataras/iris" "github.com/kataras/iris"
) )
@ -15,17 +16,19 @@ func main() {
app := iris.New() app := iris.New()
// Load the template files. // Load the template files.
app.RegisterView(iris.HTML("./views", ".html")) app.RegisterView(iris.HTML("./web/views", ".html"))
// Register our controllers. // Register our controllers.
app.Controller("/hello", new(controllers.HelloController)) app.Controller("/hello", new(controllers.HelloController))
// Create our movie service (memory), we will bind it to the movie controller. // Create our movie repository with some (memory) data from the datasource.
service := services.NewMovieServiceFromMemory(datasource.Movies) repo := repositories.NewMovieRepository(datasource.Movies)
// Create our movie service, we will bind it to the movie controller.
movieService := services.NewMovieService(repo)
app.Controller("/movies", new(controllers.MovieController), app.Controller("/movies", new(controllers.MovieController),
// Bind the "service" to the MovieController's Service (interface) field. // Bind the "movieService" to the MovieController's Service (interface) field.
service, movieService,
// Add the basic authentication(admin:password) middleware // Add the basic authentication(admin:password) middleware
// for the /movies based requests. // for the /movies based requests.
middleware.BasicAuth) middleware.BasicAuth)
@ -33,6 +36,7 @@ func main() {
// Start the web server at localhost:8080 // Start the web server at localhost:8080
// http://localhost:8080/hello // http://localhost:8080/hello
// http://localhost:8080/hello/iris // http://localhost:8080/hello/iris
// http://localhost:8080/movies
// http://localhost:8080/movies/1 // http://localhost:8080/movies/1
app.Run( app.Run(
iris.Addr("localhost:8080"), iris.Addr("localhost:8080"),

View File

@ -0,0 +1,20 @@
# Domain Models
There should be the domain/business-level models.
Example:
```go
import "github.com/kataras/iris/_examples/mvc/overview/datamodels"
type Movie struct {
datamodels.Movie
}
func (m Movie) Validate() (Movie, error) {
/* do some checks and return an error if that Movie is not valid */
}
```
However, we will use the "datamodels" as the only one models package because
Movie structure we don't need any extra functionality or validation inside it.

View File

@ -0,0 +1,3 @@
# Repositories
The package which has direct access to the "datasource" and can manipulate data directly.

View File

@ -0,0 +1,175 @@
// file: repositories/movie_repository.go
package repositories
import (
"errors"
"sync"
"github.com/kataras/iris/_examples/mvc/overview/datamodels"
)
// Query represents the visitor and action queries.
type Query func(datamodels.Movie) bool
// MovieRepository handles the basic operations of a movie entity/model.
// It's an interface in order to be testable, i.e a memory movie repository or
// a connected to an sql database.
type MovieRepository interface {
Exec(query Query, action Query, limit int, mode int) (ok bool)
Select(query Query) (movie datamodels.Movie, found bool)
SelectMany(query Query, limit int) (results []datamodels.Movie)
InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error)
Delete(query Query, limit int) (deleted bool)
}
// NewMovieRepository returns a new movie memory-based repository,
// the one and only repository type in our example.
func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository {
return &movieMemoryRepository{source: source}
}
// movieMemoryRepository is a "MovieRepository"
// which manages the movies using the memory data source (map).
type movieMemoryRepository struct {
source map[int64]datamodels.Movie
mu sync.RWMutex
}
const (
// ReadOnlyMode will RLock(read) the data .
ReadOnlyMode = iota
// ReadWriteMode will Lock(read/write) the data.
ReadWriteMode
)
func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) {
loops := 0
if mode == ReadOnlyMode {
r.mu.RLock()
defer r.mu.RUnlock()
} else {
r.mu.Lock()
defer r.mu.Unlock()
}
for _, movie := range r.source {
ok = query(movie)
if ok {
if action(movie) {
if actionLimit >= loops {
break // break
}
}
}
}
return
}
// Select 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 "found" 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 (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) {
found = r.Exec(query, func(m datamodels.Movie) bool {
movie = m
return true
}, 1, ReadOnlyMode)
// set an empty datamodels.Movie if not found at all.
if !found {
movie = datamodels.Movie{}
}
return
}
// SelectMany same as Select but returns one or more datamodels.Movie as a slice.
// If limit <=0 then it returns everything.
func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) {
r.Exec(query, func(m datamodels.Movie) bool {
results = append(results, m)
return true
}, limit, ReadOnlyMode)
return
}
// InsertOrUpdate adds or updates a movie to the (memory) storage.
//
// Returns the new movie and an error if any.
func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.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.
r.mu.RLock()
for _, item := range r.source {
if item.ID > lastID {
lastID = item.ID
}
}
r.mu.RUnlock()
id = lastID + 1
movie.ID = id
// map-specific thing
r.mu.Lock()
r.source[id] = movie
r.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:
// r.source[id] = movie
// and comment the code below;
current, exists := r.Select(func(m datamodels.Movie) bool {
return m.ID == id
})
if !exists { // ID is not a real one, return an error.
return datamodels.Movie{}, errors.New("failed to update a nonexistent movie")
}
// or comment these and r.source[id] = m for pure replace
if movie.Poster != "" {
current.Poster = movie.Poster
}
if movie.Genre != "" {
current.Genre = movie.Genre
}
// map-specific thing
r.mu.Lock()
r.source[id] = current
r.mu.Unlock()
return movie, nil
}
func (r *movieMemoryRepository) Delete(query Query, limit int) bool {
return r.Exec(query, func(m datamodels.Movie) bool {
delete(r.source, m.ID)
return true
}, limit, ReadWriteMode)
}

View File

@ -0,0 +1,3 @@
# Service Layer
The package which has access to call functions from the "repositories" and "models" ("datamodels" only in that simple example). It should contain the domain logic.

View File

@ -0,0 +1,65 @@
// file: services/movie_service.go
package services
import (
"github.com/kataras/iris/_examples/mvc/overview/datamodels"
"github.com/kataras/iris/_examples/mvc/overview/repositories"
)
// MovieService handles some of the CRUID operations of the movie datamodel.
// It depends on a movie repository for its actions.
// It's here to decouple the data source from the higher level compoments.
// As a result a different repository type can be used with the same logic without any aditional changes.
// It's an interface and it's used as interface everywhere
// because we may need to change or try an experimental different domain logic at the future.
type MovieService interface {
GetAll() []datamodels.Movie
GetByID(id int64) (datamodels.Movie, bool)
DeleteByID(id int64) bool
UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error)
}
// NewMovieService returns the default movie service.
func NewMovieService(repo repositories.MovieRepository) MovieService {
return &movieService{
repo: repo,
}
}
type movieService struct {
repo repositories.MovieRepository
}
// GetAll returns all movies.
func (s *movieService) GetAll() []datamodels.Movie {
return s.repo.SelectMany(func(_ datamodels.Movie) bool {
return true
}, -1)
}
// GetByID returns a movie based on its id.
func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) {
return s.repo.Select(func(m datamodels.Movie) bool {
return m.ID == id
})
}
// UpdatePosterAndGenreByID updates a movie's poster and genre.
func (s *movieService) UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) {
// update the movie and return it.
return s.repo.InsertOrUpdate(datamodels.Movie{
ID: id,
Poster: poster,
Genre: genre,
})
}
// DeleteByID deletes a movie by its id.
//
// Returns true if deleted otherwise false.
func (s *movieService) DeleteByID(id int64) bool {
return s.repo.Delete(func(m datamodels.Movie) bool {
return m.ID == id
}, 1)
}

View File

@ -1,4 +1,4 @@
// file: controllers/hello_controller.go // file: web/controllers/hello_controller.go
package controllers package controllers

View File

@ -1,12 +1,12 @@
// file: controllers/movie_controller.go // file: web/controllers/movie_controller.go
package controllers package controllers
import ( import (
"errors" "errors"
"github.com/kataras/iris/_examples/mvc/using-method-result/models" "github.com/kataras/iris/_examples/mvc/overview/datamodels"
"github.com/kataras/iris/_examples/mvc/using-method-result/services" "github.com/kataras/iris/_examples/mvc/overview/services"
"github.com/kataras/iris" "github.com/kataras/iris"
"github.com/kataras/iris/mvc" "github.com/kataras/iris/mvc"
@ -28,26 +28,36 @@ type MovieController struct {
// Get returns list of the movies. // Get returns list of the movies.
// Demo: // Demo:
// curl -i http://localhost:8080/movies // curl -i http://localhost:8080/movies
func (c *MovieController) Get() []models.Movie { //
// The correct way if you have sensitive data:
// func (c *MovieController) Get() (results []viewmodels.Movie) {
// data := c.Service.GetAll()
//
// for _, movie := range data {
// results = append(results, viewmodels.Movie{movie})
// }
// return
// }
// otherwise just return the datamodels.
func (c *MovieController) Get() (results []datamodels.Movie) {
return c.Service.GetAll() return c.Service.GetAll()
} }
// GetBy returns a movie. // GetBy returns a movie.
// Demo: // Demo:
// curl -i http://localhost:8080/movies/1 // curl -i http://localhost:8080/movies/1
func (c *MovieController) GetBy(id int64) models.Movie { func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) {
m, _ := c.Service.GetByID(id) return c.Service.GetByID(id) // it will throw 404 if not found.
return m
} }
// PutBy updates a movie. // PutBy updates a movie.
// Demo: // Demo:
// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 // 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) { func (c *MovieController) PutBy(id int64) (datamodels.Movie, error) {
// get the request data for poster and genre // get the request data for poster and genre
file, info, err := c.Ctx.FormFile("poster") file, info, err := c.Ctx.FormFile("poster")
if err != nil { if err != nil {
return models.Movie{}, errors.New("failed due form file 'poster' missing") return datamodels.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() file.Close()
@ -56,12 +66,7 @@ func (c *MovieController) PutBy(id int64) (models.Movie, error) {
poster := info.Filename poster := info.Filename
genre := c.Ctx.FormValue("genre") genre := c.Ctx.FormValue("genre")
// update the movie and return it. return c.Service.UpdatePosterAndGenreByID(id, poster, genre)
return c.Service.InsertOrUpdate(models.Movie{
ID: id,
Poster: poster,
Genre: genre,
})
} }
// DeleteBy deletes a movie. // DeleteBy deletes a movie.

View File

@ -1,4 +1,4 @@
// file: middleware/basicauth.go // file: web/middleware/basicauth.go
package middleware package middleware

View File

@ -0,0 +1,55 @@
# View Models
There should be the view models, the structure that the client will be able to see.
Example:
```go
import (
"github.com/kataras/iris/_examples/mvc/overview/datamodels"
"github.com/kataras/iris/context"
)
type Movie struct {
datamodels.Movie
}
func (m Movie) IsValid() bool {
/* do some checks and return true if it's valid... */
return m.ID > 0
}
```
Iris is able to convert any custom data Structure into an HTTP Response Dispatcher,
so theoritically, something like the following is permitted if it's really necessary;
```go
// 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 the potentials this feature gives when 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.IsValid() {
ctx.NotFound()
return
}
ctx.JSON(m, context.JSON{Indent: " "})
}
```
However, we will use the "datamodels" as the only one models package because
Movie structure doesn't contain any sensitive data, clients are able to see all of its fields
and we don't need any extra functionality or validation inside it.

View File

@ -1,4 +1,4 @@
<!-- file: views/hello/index.html --> <!-- file: web/views/hello/index.html -->
<html> <html>
<head> <head>

View File

@ -1,4 +1,4 @@
<!-- file: views/hello/name.html --> <!-- file: web/views/hello/name.html -->
<html> <html>
<head> <head>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,41 +0,0 @@
// file: models/movie.go
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 the potentials this feature gives when 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

@ -1,206 +0,0 @@
// 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

@ -110,11 +110,11 @@ func (f *uploadedFiles) scan(dir string) {
// add the file's Name and Size to the uploadedFiles memory list // add the file's Name and Size to the uploadedFiles memory list
func (f *uploadedFiles) add(name string, size int64) uploadedFile { func (f *uploadedFiles) add(name string, size int64) uploadedFile {
f.mu.Lock()
uf := uploadedFile{ uf := uploadedFile{
Name: name, Name: name,
Size: size, Size: size,
} }
f.mu.Lock()
f.items = append(f.items, uf) f.items = append(f.items, uf)
f.mu.Unlock() f.mu.Unlock()

View File

@ -66,11 +66,12 @@ func (f *uploadedFiles) scan(dir string) {
} }
func (f *uploadedFiles) add(name string, size int64) uploadedFile { func (f *uploadedFiles) add(name string, size int64) uploadedFile {
f.mu.Lock()
uf := uploadedFile{ uf := uploadedFile{
Name: name, Name: name,
Size: size, Size: size,
} }
f.mu.Lock()
f.items = append(f.items, uf) f.items = append(f.items, uf)
f.mu.Unlock() f.mu.Unlock()

19
doc.go
View File

@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub:
Current Version Current Version
8.5.1 8.5.2
Installation Installation
@ -860,18 +860,23 @@ Response via output arguments, optionally, i.e
func(c *ExampleController) Get() string | func(c *ExampleController) Get() string |
(string, string) | (string, string) |
(string, int) | (string, int) |
int |
(int, string |
(string, error) | (string, error) |
int |
(int, string) |
(any, int) |
error | error |
(int, error) | (int, error) |
(customStruct, error) | (customStruct, error) |
(any, error) |
bool |
(any, bool)
customStruct | customStruct |
(customStruct, int) | (customStruct, int) |
(customStruct, string) | (customStruct, string) |
Result or (Result, error) `Result` or (`Result`, error)
where Result is an interface which contains only that function: Dispatch(ctx iris.Context) Where `any` means everything, from custom structs to standard language's types-.
`Result` is an interface which contains only that function: Dispatch(ctx iris.Context)
and Get where HTTP Method function(Post, Put, Delete...). and Get where HTTP Method function(Post, Put, Delete...).
@ -883,11 +888,13 @@ and it will be sent to the client as expected.
* if `string` then it's the body. * if `string` then it's the body.
* if `string` is the second output argument then it's the content type. * if `string` is the second output argument then it's the content type.
* if `int` then it's the status code. * if `int` then it's the status code.
* if `bool` is false then it throws 404 not found http error by skipping everything else.
* if `error` and not nil then (any type) response will be omitted and error's text with a 400 bad request will be rendered instead. * if `error` and not nil then (any type) response will be omitted and error's text with a 400 bad request will be rendered instead.
* if `(int, error)` and error is not nil then the response result will be the error's text with the status code as `int`. * if `(int, error)` and error is not nil then the response result will be the error's text with the status code as `int`.
* if `custom struct` or `interface{}` or `slice` or `map` then it will be rendered as json, unless a `string` content type is following. * if `custom struct` or `interface{}` or `slice` or `map` then it will be rendered as json, unless a `string` content type is following.
* if `mvc.Result` then it executes its `Dispatch` function, so good design patters can be used to split the model's logic where needed. * if `mvc.Result` then it executes its `Dispatch` function, so good design patters can be used to split the model's logic where needed.
The example below is not intended to be used in production but it's a good showcase of some of the return types we saw before; The example below is not intended to be used in production but it's a good showcase of some of the return types we saw before;
package main package main
@ -1014,7 +1021,7 @@ The example below is not intended to be used in production but it's a good showc
Another good example with a typical folder structure, Another good example with a typical folder structure,
that many developers are used to work, can be found at: that many developers are used to work, can be found at:
https://github.com/kataras/iris/tree/master/_examples/mvc/using-method-result. https://github.com/kataras/iris/tree/master/_examples/mvc/overview.
Using Iris MVC for code reuse Using Iris MVC for code reuse

View File

@ -32,7 +32,7 @@ import (
const ( const (
// Version is the current version number of the Iris Web Framework. // Version is the current version number of the Iris Web Framework.
Version = "8.5.1" Version = "8.5.2"
) )
// HTTP status codes as registered with IANA. // HTTP status codes as registered with IANA.

View File

@ -46,8 +46,8 @@ type (
var ( var (
// ErrMissingControllerInstance is a static error which fired from `Controller` when // ErrMissingControllerInstance is a static error which fired from `Controller` when
// the passed "c" instnace is not a valid type of `Controller`. // the passed "c" instnace is not a valid type of `Controller` or `C`.
ErrMissingControllerInstance = errors.New("controller should have a field of Controller type") ErrMissingControllerInstance = errors.New("controller should have a field of mvc.Controller or mvc.C type")
// ErrInvalidControllerType fired when the "Controller" field is not // ErrInvalidControllerType fired when the "Controller" field is not
// the correct type. // the correct type.
ErrInvalidControllerType = errors.New("controller instance is not a valid implementation") ErrInvalidControllerType = errors.New("controller instance is not a valid implementation")

View File

@ -40,7 +40,15 @@ func DispatchErr(ctx context.Context, status int, err error) {
// DispatchCommon is being used internally to send // DispatchCommon is being used internally to send
// commonly used data to the response writer with a smart way. // commonly used data to the response writer with a smart way.
func DispatchCommon(ctx context.Context, func DispatchCommon(ctx context.Context,
statusCode int, contentType string, content []byte, v interface{}, err error) { statusCode int, contentType string, content []byte, v interface{}, err error, found bool) {
// if we have a false boolean as a return value
// then skip everything and fire a not found,
// we even don't care about the given status code or the object or the content.
if !found {
ctx.NotFound()
return
}
status := statusCode status := statusCode
if status == 0 { if status == 0 {
@ -99,16 +107,25 @@ func DispatchCommon(ctx context.Context,
// func(c *ExampleController) Get() string | // func(c *ExampleController) Get() string |
// (string, string) | // (string, string) |
// (string, int) | // (string, int) |
// ...
// int | // int |
// (int, string | // (int, string |
// (string, error) | // (string, error) |
// ...
// error | // error |
// (int, error) | // (int, error) |
// (customStruct, error) | // (customStruct, error) |
// ...
// bool |
// (int, bool) |
// (string, bool) |
// (customStruct, bool) |
// ...
// customStruct | // customStruct |
// (customStruct, int) | // (customStruct, int) |
// (customStruct, string) | // (customStruct, string) |
// Result or (Result, error) // Result or (Result, error) and so on...
//
// where Get is an HTTP METHOD. // where Get is an HTTP METHOD.
func DispatchFuncResult(ctx context.Context, values []reflect.Value) { func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
numOut := len(values) numOut := len(values)
@ -117,11 +134,27 @@ func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
} }
var ( var (
statusCode int // if statusCode > 0 then send this status code.
// Except when err != nil then check if status code is < 400 and
// if it's set it as DefaultErrStatusCode.
// Except when found == false, then the status code is 404.
statusCode int
// if not empty then use that as content type,
// if empty and custom != nil then set it to application/json.
contentType string contentType string
content []byte // if len > 0 then write that to the response writer as raw bytes,
custom interface{} // except when found == false or err != nil or custom != nil.
err error content []byte
// if not nil then check
// for content type (or json default) and send the custom data object
// except when found == false or err != nil.
custom interface{}
// if not nil then check for its status code,
// if not status code or < 400 then set it as DefaultErrStatusCode
// and fire the error's text.
err error
// if false then skip everything and fire 404.
found = true // defaults to true of course, otherwise will break :)
) )
for _, v := range values { for _, v := range values {
@ -134,6 +167,16 @@ func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
f := v.Interface() f := v.Interface()
if b, ok := f.(bool); ok {
found = b
if !found {
// skip everything, we don't care about other return values,
// this boolean is the heighest in order.
break
}
continue
}
if i, ok := f.(int); ok { if i, ok := f.(int); ok {
statusCode = i statusCode = i
continue continue
@ -183,5 +226,5 @@ func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
} }
DispatchCommon(ctx, statusCode, contentType, content, custom, err) DispatchCommon(ctx, statusCode, contentType, content, custom, err, found)
} }

View File

@ -19,6 +19,8 @@ import (
// int | // int |
// (int, string | // (int, string |
// (string, error) | // (string, error) |
// bool |
// (any, bool) |
// error | // error |
// (int, error) | // (int, error) |
// (customStruct, error) | // (customStruct, error) |
@ -32,7 +34,7 @@ import (
// //
// It completes the `activator.BaseController` interface. // It completes the `activator.BaseController` interface.
// //
// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/using-method-result/controllers. // Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview/web/controllers.
// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go#L17. // Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go#L17.
type C struct { type C struct {
// The Name of the `C` controller. // The Name of the `C` controller.

View File

@ -19,7 +19,7 @@ import (
// All types that complete this interface // All types that complete this interface
// can be returned as values from the method functions. // can be returned as values from the method functions.
// //
// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/using-method-result. // Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview.
type Result interface { // NOTE: Should be always compatible with the methodfunc.Result. type Result interface { // NOTE: Should be always compatible with the methodfunc.Result.
// Dispatch should sends the response to the context's response writer. // Dispatch should sends the response to the context's response writer.
Dispatch(ctx context.Context) Dispatch(ctx context.Context)

View File

@ -39,5 +39,5 @@ func (r Response) Dispatch(ctx context.Context) {
r.Content = []byte(s) r.Content = []byte(s)
} }
methodfunc.DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err) methodfunc.DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err, true)
} }

View File

@ -14,7 +14,7 @@ import (
// wraps the template file name, layout, (any) view data, status code and error. // wraps the template file name, layout, (any) view data, status code and error.
// It's smart enough to complete the request and send the correct response to the client. // It's smart enough to complete the request and send the correct response to the client.
// //
// Example at: https://github.com/kataras/iris/blob/master/_examples/mvc/using-method-result/controllers/hello_controller.go. // Example at: https://github.com/kataras/iris/blob/master/_examples/mvc/overview/web/controllers/hello_controller.go.
type View struct { type View struct {
Name string Name string
Layout string Layout string