From b0f8329768c789fd9c7c020fdc96c8ede3526772 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 12 Oct 2017 03:51:06 +0300 Subject: [PATCH] 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 --- HISTORY.md | 20 + README.md | 932 ++++++++++-------- README_NEXT.md | 6 +- VERSION | 2 +- _examples/README.md | 9 +- _examples/mvc/overview/datamodels/README.md | 1 + _examples/mvc/overview/datamodels/movie.go | 18 + _examples/mvc/overview/datasource/README.md | 1 + .../datasource/movies.go | 4 +- _examples/mvc/overview/folder_structure.png | Bin 0 -> 30336 bytes .../{using-method-result => overview}/main.go | 22 +- _examples/mvc/overview/models/README.md | 20 + _examples/mvc/overview/repositories/README.md | 3 + .../overview/repositories/movie_repository.go | 175 ++++ _examples/mvc/overview/services/README.md | 3 + .../mvc/overview/services/movie_service.go | 65 ++ .../web}/controllers/hello_controller.go | 2 +- .../web}/controllers/movie_controller.go | 35 +- .../web}/middleware/basicauth.go | 2 +- .../mvc/overview/web/viewmodels/README.md | 55 ++ .../web}/views/hello/index.html | 2 +- .../web}/views/hello/name.html | 2 +- .../using-method-result/folder_structure.png | Bin 25547 -> 0 bytes .../mvc/using-method-result/models/movie.go | 41 - .../services/movie_service.go | 206 ---- _examples/tutorial/dropzonejs/README_PART2.md | 2 +- _examples/tutorial/dropzonejs/src/main.go | 3 +- doc.go | 19 +- iris.go | 2 +- mvc/activator/activator.go | 4 +- .../methodfunc/func_result_dispatcher.go | 57 +- mvc/controller.go | 4 +- mvc/method_result.go | 2 +- mvc/method_result_response.go | 2 +- mvc/method_result_view.go | 2 +- 35 files changed, 1007 insertions(+), 716 deletions(-) create mode 100644 _examples/mvc/overview/datamodels/README.md create mode 100644 _examples/mvc/overview/datamodels/movie.go create mode 100644 _examples/mvc/overview/datasource/README.md rename _examples/mvc/{using-method-result => overview}/datasource/movies.go (88%) create mode 100644 _examples/mvc/overview/folder_structure.png rename _examples/mvc/{using-method-result => overview}/main.go (50%) create mode 100644 _examples/mvc/overview/models/README.md create mode 100644 _examples/mvc/overview/repositories/README.md create mode 100644 _examples/mvc/overview/repositories/movie_repository.go create mode 100644 _examples/mvc/overview/services/README.md create mode 100644 _examples/mvc/overview/services/movie_service.go rename _examples/mvc/{using-method-result => overview/web}/controllers/hello_controller.go (97%) rename _examples/mvc/{using-method-result => overview/web}/controllers/movie_controller.go (65%) rename _examples/mvc/{using-method-result => overview/web}/middleware/basicauth.go (85%) create mode 100644 _examples/mvc/overview/web/viewmodels/README.md rename _examples/mvc/{using-method-result => overview/web}/views/hello/index.html (68%) rename _examples/mvc/{using-method-result => overview/web}/views/hello/name.html (69%) delete mode 100644 _examples/mvc/using-method-result/folder_structure.png delete mode 100644 _examples/mvc/using-method-result/models/movie.go delete mode 100644 _examples/mvc/using-method-result/services/movie_service.go 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 0000000000000000000000000000000000000000..0029e8f74cb290572df90924dcabc8939fd39ccf GIT binary patch literal 30336 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 AasU7T literal 0 HcmV?d00001 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 f3d39df1d4a00c5c327146baf61a80563972aeb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25547 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 ON$H8Y+ 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