mirror of
https://github.com/kataras/iris.git
synced 2025-03-14 02:46:28 +01:00
Update to version 8.5.2 | Read HISTORY.md: https://github.com/kataras/iris/blob/master/HISTORY.md#th-12-october-2017--v852
https://github.com/kataras/iris/blob/master/HISTORY.md#th-12-october-2017--v852 Former-commit-id: 2501cf6066812c2aac158d8d6cd4e992a2b538f9
This commit is contained in:
parent
92bb47803f
commit
b0f8329768
20
HISTORY.md
20
HISTORY.md
|
@ -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`.
|
||||
|
||||
# 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
|
||||
|
||||
## MVC
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<!-- #  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!
|
||||
|
||||
|
@ -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#tu-10-october-2017--v851)
|
||||
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#th-12-october-2017--v852)
|
||||
* [Learn](#-learn)
|
||||
* [Structuring](_examples/#structuring)
|
||||
* [HTTP Listening](_examples/#http-listening)
|
||||
|
|
2
VERSION
2
VERSION
|
@ -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
|
|
@ -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 2](structuring/mvc)
|
||||
- [Example 3](structuring/handler-based)
|
||||
- [Example 4](mvc/using-method-result)
|
||||
- [Example 4](mvc/overview)
|
||||
|
||||
### 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,
|
||||
|
||||
- [Overview - Plus Repository and Service layers](mvc/overview) **NEW**
|
||||
|
||||
<!--
|
||||
- [Hello world](mvc/hello-world/main.go)
|
||||
- [Session Controller](mvc/session-controller/main.go)
|
||||
- [A simple but featured Controller with model and views](mvc/controller-with-model-and-view)
|
||||
- [Login showcase](mvc/login/main.go) **NEW**
|
||||
- [Using Method Result (plus Service-oriented design)](mvc/using-method-result)
|
||||
- [Login showcase](mvc/login/main.go)
|
||||
-->
|
||||
|
||||
### Subdomains
|
||||
|
||||
|
|
1
_examples/mvc/overview/datamodels/README.md
Normal file
1
_examples/mvc/overview/datamodels/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Data Model Layer
|
18
_examples/mvc/overview/datamodels/movie.go
Normal file
18
_examples/mvc/overview/datamodels/movie.go
Normal 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"`
|
||||
}
|
1
_examples/mvc/overview/datasource/README.md
Normal file
1
_examples/mvc/overview/datasource/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Data Source / Data Store Layer
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
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.
|
||||
var Movies = map[int64]models.Movie{
|
||||
var Movies = map[int64]datamodels.Movie{
|
||||
1: {
|
||||
ID: 1,
|
||||
Name: "Casablanca",
|
BIN
_examples/mvc/overview/folder_structure.png
Normal file
BIN
_examples/mvc/overview/folder_structure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
|
@ -3,10 +3,11 @@
|
|||
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/_examples/mvc/overview/datasource"
|
||||
"github.com/kataras/iris/_examples/mvc/overview/repositories"
|
||||
"github.com/kataras/iris/_examples/mvc/overview/services"
|
||||
"github.com/kataras/iris/_examples/mvc/overview/web/controllers"
|
||||
"github.com/kataras/iris/_examples/mvc/overview/web/middleware"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
)
|
||||
|
@ -15,17 +16,19 @@ func main() {
|
|||
app := iris.New()
|
||||
|
||||
// Load the template files.
|
||||
app.RegisterView(iris.HTML("./views", ".html"))
|
||||
app.RegisterView(iris.HTML("./web/views", ".html"))
|
||||
|
||||
// Register our controllers.
|
||||
app.Controller("/hello", new(controllers.HelloController))
|
||||
|
||||
// Create our movie service (memory), we will bind it to the movie controller.
|
||||
service := services.NewMovieServiceFromMemory(datasource.Movies)
|
||||
// Create our movie repository with some (memory) data from the datasource.
|
||||
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),
|
||||
// Bind the "service" to the MovieController's Service (interface) field.
|
||||
service,
|
||||
// Bind the "movieService" to the MovieController's Service (interface) field.
|
||||
movieService,
|
||||
// Add the basic authentication(admin:password) middleware
|
||||
// for the /movies based requests.
|
||||
middleware.BasicAuth)
|
||||
|
@ -33,6 +36,7 @@ func main() {
|
|||
// Start the web server at localhost:8080
|
||||
// http://localhost:8080/hello
|
||||
// http://localhost:8080/hello/iris
|
||||
// http://localhost:8080/movies
|
||||
// http://localhost:8080/movies/1
|
||||
app.Run(
|
||||
iris.Addr("localhost:8080"),
|
20
_examples/mvc/overview/models/README.md
Normal file
20
_examples/mvc/overview/models/README.md
Normal 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.
|
3
_examples/mvc/overview/repositories/README.md
Normal file
3
_examples/mvc/overview/repositories/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Repositories
|
||||
|
||||
The package which has direct access to the "datasource" and can manipulate data directly.
|
175
_examples/mvc/overview/repositories/movie_repository.go
Normal file
175
_examples/mvc/overview/repositories/movie_repository.go
Normal 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)
|
||||
}
|
3
_examples/mvc/overview/services/README.md
Normal file
3
_examples/mvc/overview/services/README.md
Normal 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.
|
65
_examples/mvc/overview/services/movie_service.go
Normal file
65
_examples/mvc/overview/services/movie_service.go
Normal 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)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// file: controllers/hello_controller.go
|
||||
// file: web/controllers/hello_controller.go
|
||||
|
||||
package controllers
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
// file: controllers/movie_controller.go
|
||||
// file: web/controllers/movie_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/_examples/mvc/overview/datamodels"
|
||||
"github.com/kataras/iris/_examples/mvc/overview/services"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
"github.com/kataras/iris/mvc"
|
||||
|
@ -28,26 +28,36 @@ type MovieController struct {
|
|||
// Get returns list of the movies.
|
||||
// Demo:
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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
|
||||
func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) {
|
||||
return c.Service.GetByID(id) // it will throw 404 if not found.
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func (c *MovieController) PutBy(id int64) (datamodels.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")
|
||||
return datamodels.Movie{}, errors.New("failed due form file 'poster' missing")
|
||||
}
|
||||
// we don't need the file so close it now.
|
||||
file.Close()
|
||||
|
@ -56,12 +66,7 @@ func (c *MovieController) PutBy(id int64) (models.Movie, error) {
|
|||
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,
|
||||
})
|
||||
return c.Service.UpdatePosterAndGenreByID(id, poster, genre)
|
||||
}
|
||||
|
||||
// DeleteBy deletes a movie.
|
|
@ -1,4 +1,4 @@
|
|||
// file: middleware/basicauth.go
|
||||
// file: web/middleware/basicauth.go
|
||||
|
||||
package middleware
|
||||
|
55
_examples/mvc/overview/web/viewmodels/README.md
Normal file
55
_examples/mvc/overview/web/viewmodels/README.md
Normal 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.
|
|
@ -1,4 +1,4 @@
|
|||
<!-- file: views/hello/index.html -->
|
||||
<!-- file: web/views/hello/index.html -->
|
||||
<html>
|
||||
|
||||
<head>
|
|
@ -1,4 +1,4 @@
|
|||
<!-- file: views/hello/name.html -->
|
||||
<!-- file: web/views/hello/name.html -->
|
||||
<html>
|
||||
|
||||
<head>
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -110,11 +110,11 @@ func (f *uploadedFiles) scan(dir string) {
|
|||
|
||||
// add the file's Name and Size to the uploadedFiles memory list
|
||||
func (f *uploadedFiles) add(name string, size int64) uploadedFile {
|
||||
f.mu.Lock()
|
||||
uf := uploadedFile{
|
||||
Name: name,
|
||||
Size: size,
|
||||
}
|
||||
f.mu.Lock()
|
||||
f.items = append(f.items, uf)
|
||||
f.mu.Unlock()
|
||||
|
||||
|
|
|
@ -66,11 +66,12 @@ func (f *uploadedFiles) scan(dir string) {
|
|||
}
|
||||
|
||||
func (f *uploadedFiles) add(name string, size int64) uploadedFile {
|
||||
f.mu.Lock()
|
||||
uf := uploadedFile{
|
||||
Name: name,
|
||||
Size: size,
|
||||
}
|
||||
|
||||
f.mu.Lock()
|
||||
f.items = append(f.items, uf)
|
||||
f.mu.Unlock()
|
||||
|
||||
|
|
19
doc.go
19
doc.go
|
@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub:
|
|||
|
||||
Current Version
|
||||
|
||||
8.5.1
|
||||
8.5.2
|
||||
|
||||
Installation
|
||||
|
||||
|
@ -860,18 +860,23 @@ Response via output arguments, optionally, i.e
|
|||
func(c *ExampleController) Get() string |
|
||||
(string, string) |
|
||||
(string, int) |
|
||||
int |
|
||||
(int, string |
|
||||
(string, error) |
|
||||
int |
|
||||
(int, string) |
|
||||
(any, int) |
|
||||
error |
|
||||
(int, error) |
|
||||
(customStruct, error) |
|
||||
(any, error) |
|
||||
bool |
|
||||
(any, bool)
|
||||
customStruct |
|
||||
(customStruct, int) |
|
||||
(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...).
|
||||
|
||||
|
||||
|
@ -883,11 +888,13 @@ and it will be sent to the client as expected.
|
|||
* if `string` then it's the body.
|
||||
* if `string` is the second output argument then it's the content type.
|
||||
* 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 `(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 `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;
|
||||
|
||||
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,
|
||||
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
|
||||
|
|
2
iris.go
2
iris.go
|
@ -32,7 +32,7 @@ import (
|
|||
|
||||
const (
|
||||
// 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.
|
||||
|
|
|
@ -46,8 +46,8 @@ type (
|
|||
|
||||
var (
|
||||
// ErrMissingControllerInstance is a static error which fired from `Controller` when
|
||||
// the passed "c" instnace is not a valid type of `Controller`.
|
||||
ErrMissingControllerInstance = errors.New("controller should have a field of Controller type")
|
||||
// the passed "c" instnace is not a valid type of `Controller` or `C`.
|
||||
ErrMissingControllerInstance = errors.New("controller should have a field of mvc.Controller or mvc.C type")
|
||||
// ErrInvalidControllerType fired when the "Controller" field is not
|
||||
// the correct type.
|
||||
ErrInvalidControllerType = errors.New("controller instance is not a valid implementation")
|
||||
|
|
|
@ -40,7 +40,15 @@ func DispatchErr(ctx context.Context, status int, err error) {
|
|||
// DispatchCommon is being used internally to send
|
||||
// commonly used data to the response writer with a smart way.
|
||||
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
|
||||
if status == 0 {
|
||||
|
@ -99,16 +107,25 @@ func DispatchCommon(ctx context.Context,
|
|||
// func(c *ExampleController) Get() string |
|
||||
// (string, string) |
|
||||
// (string, int) |
|
||||
// ...
|
||||
// int |
|
||||
// (int, string |
|
||||
// (string, error) |
|
||||
// ...
|
||||
// error |
|
||||
// (int, error) |
|
||||
// (customStruct, error) |
|
||||
// ...
|
||||
// bool |
|
||||
// (int, bool) |
|
||||
// (string, bool) |
|
||||
// (customStruct, bool) |
|
||||
// ...
|
||||
// customStruct |
|
||||
// (customStruct, int) |
|
||||
// (customStruct, string) |
|
||||
// Result or (Result, error)
|
||||
// Result or (Result, error) and so on...
|
||||
//
|
||||
// where Get is an HTTP METHOD.
|
||||
func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
|
||||
numOut := len(values)
|
||||
|
@ -117,11 +134,27 @@ func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
|
|||
}
|
||||
|
||||
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
|
||||
content []byte
|
||||
custom interface{}
|
||||
err error
|
||||
// if len > 0 then write that to the response writer as raw bytes,
|
||||
// except when found == false or err != nil or custom != nil.
|
||||
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 {
|
||||
|
@ -134,6 +167,16 @@ func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
|
|||
|
||||
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 {
|
||||
statusCode = i
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import (
|
|||
// int |
|
||||
// (int, string |
|
||||
// (string, error) |
|
||||
// bool |
|
||||
// (any, bool) |
|
||||
// error |
|
||||
// (int, error) |
|
||||
// (customStruct, error) |
|
||||
|
@ -32,7 +34,7 @@ import (
|
|||
//
|
||||
// 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.
|
||||
type C struct {
|
||||
// The Name of the `C` controller.
|
||||
|
|
|
@ -19,7 +19,7 @@ import (
|
|||
// All types that complete this interface
|
||||
// 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.
|
||||
// Dispatch should sends the response to the context's response writer.
|
||||
Dispatch(ctx context.Context)
|
||||
|
|
|
@ -39,5 +39,5 @@ func (r Response) Dispatch(ctx context.Context) {
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
// 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.
|
||||
//
|
||||
// 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 {
|
||||
Name string
|
||||
Layout string
|
||||
|
|
Loading…
Reference in New Issue
Block a user