diff --git a/HISTORY.md b/HISTORY.md index a3d7cfe1..9ef562a7 100644 --- a/HISTORY.md +++ b/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 diff --git a/README.md b/README.md index 29e12881..47112136 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,18 @@ +# 03, October 2017 | Iris User Experience Report + +Be part of the Iris evolution! + +Complete 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! + +https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link + # ![Logo created by @santoshanand](logo_white_35_24.png) Iris Iris is a fast, simple and efficient micro web framework for Go. It provides a beautifully expressive and easy to use foundation for your next website, API, or distributed app. + _Psst_, we've produced a small video about your feelings regrating to Iris! You can watch the whole video at https://www.youtube.com/watch?v=jGx0LkuUs4A. @@ -52,6 +64,7 @@ _Psst_, we've produced a small video about your feelings regrating to Iris! You [![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) +[![CLA assistant](https://cla-assistant.io/readme/badge/kataras/iris?style=flat-square)](https://cla-assistant.io/kataras/iris) [![Iris vs .NET Core(C#) vs Node.js (Express)](https://iris-go.com/images/benchmark-new-gray.png)](_benchmarks) @@ -67,7 +80,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.1](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-10-october-2017--v851) +- [Latest changes | v8.5.2](https://github.com/kataras/iris/blob/master/HISTORY.md#th-12-october-2017--v852) ## Getting Started @@ -212,6 +225,7 @@ and it will be sent to the client as expected. * if `int` then it's the status code. * 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 `bool` is false then it throws 404 not found http error by skipping everything else. * 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. @@ -344,12 +358,485 @@ Nothing stops you from using your favorite **folder structure**. Iris is a low l Structuring depends on your own needs. We can't tell you how to design your own application for sure but you're free to take a closer look to one typical example below; -[![folder structure example](_examples/mvc/using-method-result/folder_structure.png)](_examples/mvc/using-method-result) +[![folder structure example](_examples/mvc/overview/folder_structure.png)](_examples/mvc/overview) Shhh, let's spread the code itself. +#### Data Model Layer + ```go -// file: controllers/hello_controller.go +// 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"` +} +``` + +#### Data Source / Data Store Layer + +```go +// file: datasource/movies.go + +package datasource + +import "github.com/kataras/iris/_examples/mvc/overview/datamodels" + +// Movies is our imaginary data source. +var Movies = map[int64]datamodels.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", + Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg", + }, +} +``` + +#### Repositories + +The layer which has direct access to the "datasource" and can manipulate data directly. + +```go +// 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) +} +``` + +#### Services + +The layer which has access to call functions from the "repositories" and "models" (or even "datamodels" if simple application). It should contain the most of the domain logic. + +```go +// 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 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. + +#### Controllers + +Handles web requests, bridge between the services and the client. + +```go +// file: web/controllers/movie_controller.go + +package controllers + +import ( + "errors" + + "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" +) + +// 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 +// +// 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) (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) (datamodels.Movie, error) { + // get the request data for poster and genre + file, info, err := c.Ctx.FormFile("poster") + if err != nil { + return datamodels.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") + + return c.Service.UpdatePosterAndGenreByID(id, poster, 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{} { + wasDel := c.Service.DeleteByID(id) + if wasDel { + // return the deleted movie's ID + return iris.Map{"deleted": id} + } + // right 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 +} +``` + +```go +// file: web/controllers/hello_controller.go package controllers @@ -411,8 +898,23 @@ func (c *HelloController) GetBy(name string) mvc.Result { } ``` +```go +// file: web/middleware/basicauth.go + +package middleware + +import "github.com/kataras/iris/middleware/basicauth" + +// BasicAuth middleware sample. +var BasicAuth = basicauth.New(basicauth.Config{ + Users: map[string]string{ + "admin": "password", + }, +}) +``` + ```html - + @@ -427,7 +929,7 @@ func (c *HelloController) GetBy(name string) mvc.Result { ``` ```html - + @@ -444,403 +946,9 @@ func (c *HelloController) GetBy(name string) mvc.Result { > Navigate to the [_examples/view](_examples/#view) for more examples like shared layouts, tmpl funcs, reverse routing and more! -```go -// file: models/movie.go +#### Main -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](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/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" - "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{} { - 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 -} -``` - -```go -// file: datasource/movies.go - -package datasource - -import "github.com/kataras/iris/_examples/mvc/using-method-result/models" - -// Movies is our imaginary data source. -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", - Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg", - }, -} -``` - -```go -// file: middleware/basicauth.go - -package middleware - -import "github.com/kataras/iris/middleware/basicauth" - -// BasicAuth middleware sample. -var BasicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) -``` +This file creates any necessary component and links them together. ```go // file: main.go @@ -848,10 +956,11 @@ var BasicAuth = basicauth.New(basicauth.Config{ 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" ) @@ -860,17 +969,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 movies 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) @@ -878,6 +989,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"), diff --git a/README_NEXT.md b/README_NEXT.md index d4279d32..01f29cf7 100644 --- a/README_NEXT.md +++ b/README_NEXT.md @@ -1,8 +1,8 @@ -## 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) diff --git a/VERSION b/VERSION index 0ea7649d..7abd73d1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.5.1:https://github.com/kataras/iris/blob/master/HISTORY.md#tu-10-october-2017--v851 \ No newline at end of file +8.5.2:https://github.com/kataras/iris/blob/master/HISTORY.md#th-12-october-2017--v852 \ No newline at end of file diff --git a/_examples/README.md b/_examples/README.md index 58adea73..5045ba06 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -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** + + ### Subdomains diff --git a/_examples/mvc/overview/datamodels/README.md b/_examples/mvc/overview/datamodels/README.md new file mode 100644 index 00000000..75a1b75c --- /dev/null +++ b/_examples/mvc/overview/datamodels/README.md @@ -0,0 +1 @@ +# Data Model Layer \ No newline at end of file diff --git a/_examples/mvc/overview/datamodels/movie.go b/_examples/mvc/overview/datamodels/movie.go new file mode 100644 index 00000000..7649d487 --- /dev/null +++ b/_examples/mvc/overview/datamodels/movie.go @@ -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"` +} diff --git a/_examples/mvc/overview/datasource/README.md b/_examples/mvc/overview/datasource/README.md new file mode 100644 index 00000000..b5633a54 --- /dev/null +++ b/_examples/mvc/overview/datasource/README.md @@ -0,0 +1 @@ +# Data Source / Data Store Layer \ No newline at end of file diff --git a/_examples/mvc/using-method-result/datasource/movies.go b/_examples/mvc/overview/datasource/movies.go similarity index 88% rename from _examples/mvc/using-method-result/datasource/movies.go rename to _examples/mvc/overview/datasource/movies.go index b330aad3..9851829d 100644 --- a/_examples/mvc/using-method-result/datasource/movies.go +++ b/_examples/mvc/overview/datasource/movies.go @@ -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", diff --git a/_examples/mvc/overview/folder_structure.png b/_examples/mvc/overview/folder_structure.png new file mode 100644 index 00000000..0029e8f7 Binary files /dev/null and b/_examples/mvc/overview/folder_structure.png differ diff --git a/_examples/mvc/using-method-result/main.go b/_examples/mvc/overview/main.go similarity index 50% rename from _examples/mvc/using-method-result/main.go rename to _examples/mvc/overview/main.go index f571fbed..2d0befc5 100644 --- a/_examples/mvc/using-method-result/main.go +++ b/_examples/mvc/overview/main.go @@ -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"), diff --git a/_examples/mvc/overview/models/README.md b/_examples/mvc/overview/models/README.md new file mode 100644 index 00000000..edceda53 --- /dev/null +++ b/_examples/mvc/overview/models/README.md @@ -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. \ No newline at end of file diff --git a/_examples/mvc/overview/repositories/README.md b/_examples/mvc/overview/repositories/README.md new file mode 100644 index 00000000..6de39915 --- /dev/null +++ b/_examples/mvc/overview/repositories/README.md @@ -0,0 +1,3 @@ +# Repositories + +The package which has direct access to the "datasource" and can manipulate data directly. \ No newline at end of file diff --git a/_examples/mvc/overview/repositories/movie_repository.go b/_examples/mvc/overview/repositories/movie_repository.go new file mode 100644 index 00000000..c53ec425 --- /dev/null +++ b/_examples/mvc/overview/repositories/movie_repository.go @@ -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) +} diff --git a/_examples/mvc/overview/services/README.md b/_examples/mvc/overview/services/README.md new file mode 100644 index 00000000..520875c9 --- /dev/null +++ b/_examples/mvc/overview/services/README.md @@ -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. \ No newline at end of file diff --git a/_examples/mvc/overview/services/movie_service.go b/_examples/mvc/overview/services/movie_service.go new file mode 100644 index 00000000..a89d1410 --- /dev/null +++ b/_examples/mvc/overview/services/movie_service.go @@ -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) +} diff --git a/_examples/mvc/using-method-result/controllers/hello_controller.go b/_examples/mvc/overview/web/controllers/hello_controller.go similarity index 97% rename from _examples/mvc/using-method-result/controllers/hello_controller.go rename to _examples/mvc/overview/web/controllers/hello_controller.go index 3c8a5f9e..56518d5e 100644 --- a/_examples/mvc/using-method-result/controllers/hello_controller.go +++ b/_examples/mvc/overview/web/controllers/hello_controller.go @@ -1,4 +1,4 @@ -// file: controllers/hello_controller.go +// file: web/controllers/hello_controller.go package controllers diff --git a/_examples/mvc/using-method-result/controllers/movie_controller.go b/_examples/mvc/overview/web/controllers/movie_controller.go similarity index 65% rename from _examples/mvc/using-method-result/controllers/movie_controller.go rename to _examples/mvc/overview/web/controllers/movie_controller.go index 7f3265ed..10542d1a 100644 --- a/_examples/mvc/using-method-result/controllers/movie_controller.go +++ b/_examples/mvc/overview/web/controllers/movie_controller.go @@ -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. diff --git a/_examples/mvc/using-method-result/middleware/basicauth.go b/_examples/mvc/overview/web/middleware/basicauth.go similarity index 85% rename from _examples/mvc/using-method-result/middleware/basicauth.go rename to _examples/mvc/overview/web/middleware/basicauth.go index b8a5355b..c9b6eacf 100644 --- a/_examples/mvc/using-method-result/middleware/basicauth.go +++ b/_examples/mvc/overview/web/middleware/basicauth.go @@ -1,4 +1,4 @@ -// file: middleware/basicauth.go +// file: web/middleware/basicauth.go package middleware diff --git a/_examples/mvc/overview/web/viewmodels/README.md b/_examples/mvc/overview/web/viewmodels/README.md new file mode 100644 index 00000000..909e83de --- /dev/null +++ b/_examples/mvc/overview/web/viewmodels/README.md @@ -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. \ No newline at end of file diff --git a/_examples/mvc/using-method-result/views/hello/index.html b/_examples/mvc/overview/web/views/hello/index.html similarity index 68% rename from _examples/mvc/using-method-result/views/hello/index.html rename to _examples/mvc/overview/web/views/hello/index.html index 79cbe278..9e7b03d6 100644 --- a/_examples/mvc/using-method-result/views/hello/index.html +++ b/_examples/mvc/overview/web/views/hello/index.html @@ -1,4 +1,4 @@ - + diff --git a/_examples/mvc/using-method-result/views/hello/name.html b/_examples/mvc/overview/web/views/hello/name.html similarity index 69% rename from _examples/mvc/using-method-result/views/hello/name.html rename to _examples/mvc/overview/web/views/hello/name.html index 083c5594..d6dd5ac6 100644 --- a/_examples/mvc/using-method-result/views/hello/name.html +++ b/_examples/mvc/overview/web/views/hello/name.html @@ -1,4 +1,4 @@ - + diff --git a/_examples/mvc/using-method-result/folder_structure.png b/_examples/mvc/using-method-result/folder_structure.png deleted file mode 100644 index f3d39df1..00000000 Binary files a/_examples/mvc/using-method-result/folder_structure.png and /dev/null differ diff --git a/_examples/mvc/using-method-result/models/movie.go b/_examples/mvc/using-method-result/models/movie.go deleted file mode 100644 index 8f0fb678..00000000 --- a/_examples/mvc/using-method-result/models/movie.go +++ /dev/null @@ -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. diff --git a/_examples/mvc/using-method-result/services/movie_service.go b/_examples/mvc/using-method-result/services/movie_service.go deleted file mode 100644 index cc21fd7f..00000000 --- a/_examples/mvc/using-method-result/services/movie_service.go +++ /dev/null @@ -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 -} diff --git a/_examples/tutorial/dropzonejs/README_PART2.md b/_examples/tutorial/dropzonejs/README_PART2.md index 24da5fef..db2bfd0a 100644 --- a/_examples/tutorial/dropzonejs/README_PART2.md +++ b/_examples/tutorial/dropzonejs/README_PART2.md @@ -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() diff --git a/_examples/tutorial/dropzonejs/src/main.go b/_examples/tutorial/dropzonejs/src/main.go index c5522808..86e54982 100644 --- a/_examples/tutorial/dropzonejs/src/main.go +++ b/_examples/tutorial/dropzonejs/src/main.go @@ -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() diff --git a/doc.go b/doc.go index 52def811..03abdee4 100644 --- a/doc.go +++ b/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 diff --git a/iris.go b/iris.go index 851130f6..660477e2 100644 --- a/iris.go +++ b/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. diff --git a/mvc/activator/activator.go b/mvc/activator/activator.go index 719108c8..76a0dd9e 100644 --- a/mvc/activator/activator.go +++ b/mvc/activator/activator.go @@ -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") diff --git a/mvc/activator/methodfunc/func_result_dispatcher.go b/mvc/activator/methodfunc/func_result_dispatcher.go index 5b368e65..6a5a7b87 100644 --- a/mvc/activator/methodfunc/func_result_dispatcher.go +++ b/mvc/activator/methodfunc/func_result_dispatcher.go @@ -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) } diff --git a/mvc/controller.go b/mvc/controller.go index 00a119cd..fe506638 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -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. diff --git a/mvc/method_result.go b/mvc/method_result.go index 9e04230b..7512ff59 100644 --- a/mvc/method_result.go +++ b/mvc/method_result.go @@ -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) diff --git a/mvc/method_result_response.go b/mvc/method_result_response.go index a4db57db..4669409f 100644 --- a/mvc/method_result_response.go +++ b/mvc/method_result_response.go @@ -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) } diff --git a/mvc/method_result_view.go b/mvc/method_result_view.go index cb201b7d..36a898db 100644 --- a/mvc/method_result_view.go +++ b/mvc/method_result_view.go @@ -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