mirror of
https://github.com/kataras/iris.git
synced 2026-01-06 19:47:05 +00:00
create a new package, name it as hero, I was thinking super or superb but hero is better name for what it does - the goal is to split the new 'mvc handlers' from the mvc system because they are not the same, users should know that they can use these type of rich binded handlers without controllers as well, like a normal handler and that I implemented here, the old files exist on the mvc package but will be removed at the next commit, I have to decide if we want type aliases for Result or no
Former-commit-id: cb775edc72bedc88aeab4c5a6de6bfc6bd56fae2
This commit is contained in:
18
_examples/hero/overview/datamodels/movie.go
Normal file
18
_examples/hero/overview/datamodels/movie.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// file: datamodels/movie.go
|
||||
|
||||
package datamodels
|
||||
|
||||
// Movie is our sample data structure.
|
||||
// Keep note that the tags for public-use (for our web app)
|
||||
// should be kept in other file like "web/viewmodels/movie.go"
|
||||
// which could wrap by embedding the datamodels.Movie or
|
||||
// declare new fields instead butwe will use this datamodel
|
||||
// as the only one Movie model in our application,
|
||||
// for the shake of simplicty.
|
||||
type Movie struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Year int `json:"year"`
|
||||
Genre string `json:"genre"`
|
||||
Poster string `json:"poster"`
|
||||
}
|
||||
44
_examples/hero/overview/datasource/movies.go
Normal file
44
_examples/hero/overview/datasource/movies.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// file: datasource/movies.go
|
||||
|
||||
package datasource
|
||||
|
||||
import "github.com/kataras/iris/_examples/hero/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",
|
||||
},
|
||||
}
|
||||
60
_examples/hero/overview/main.go
Normal file
60
_examples/hero/overview/main.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// file: main.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/_examples/hero/overview/datasource"
|
||||
"github.com/kataras/iris/_examples/hero/overview/repositories"
|
||||
"github.com/kataras/iris/_examples/hero/overview/services"
|
||||
"github.com/kataras/iris/_examples/hero/overview/web/middleware"
|
||||
"github.com/kataras/iris/_examples/hero/overview/web/routes"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
"github.com/kataras/iris/hero"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.Logger().SetLevel("debug")
|
||||
|
||||
// Load the template files.
|
||||
app.RegisterView(iris.HTML("./web/views", ".html"))
|
||||
|
||||
// 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 app's dependencies.
|
||||
movieService := services.NewMovieService(repo)
|
||||
hero.Register(movieService)
|
||||
|
||||
// Register our routes with hero handlers.
|
||||
app.PartyFunc("/hello", func(r iris.Party) {
|
||||
r.Get("/", hero.Handler(routes.Hello))
|
||||
r.Get("/{name}", hero.Handler(routes.HelloName))
|
||||
})
|
||||
|
||||
app.PartyFunc("/movies", func(r iris.Party) {
|
||||
// Add the basic authentication(admin:password) middleware
|
||||
// for the /movies based requests.
|
||||
r.Use(middleware.BasicAuth)
|
||||
|
||||
r.Get("/", hero.Handler(routes.Movies))
|
||||
r.Get("/{id:long}", hero.Handler(routes.MovieByID))
|
||||
r.Put("/{id:long}", hero.Handler(routes.UpdateMovieByID))
|
||||
r.Delete("/{id:long}", hero.Handler(routes.DeleteMovieByID))
|
||||
})
|
||||
|
||||
// http://localhost:8080/hello
|
||||
// http://localhost:8080/hello/iris
|
||||
// http://localhost:8080/movies
|
||||
// http://localhost:8080/movies/1
|
||||
app.Run(
|
||||
// Start the web server at localhost:8080
|
||||
iris.Addr("localhost:8080"),
|
||||
// disables updates:
|
||||
iris.WithoutVersionChecker,
|
||||
// skip err server closed when CTRL/CMD+C pressed:
|
||||
iris.WithoutServerError(iris.ErrServerClosed),
|
||||
// enables faster json serialization and more:
|
||||
iris.WithOptimizations,
|
||||
)
|
||||
}
|
||||
176
_examples/hero/overview/repositories/movie_repository.go
Normal file
176
_examples/hero/overview/repositories/movie_repository.go
Normal file
@@ -0,0 +1,176 @@
|
||||
// file: repositories/movie_repository.go
|
||||
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/kataras/iris/_examples/hero/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) {
|
||||
loops++
|
||||
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)
|
||||
}
|
||||
65
_examples/hero/overview/services/movie_service.go
Normal file
65
_examples/hero/overview/services/movie_service.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// file: services/movie_service.go
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/_examples/hero/overview/datamodels"
|
||||
"github.com/kataras/iris/_examples/hero/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)
|
||||
}
|
||||
12
_examples/hero/overview/web/middleware/basicauth.go
Normal file
12
_examples/hero/overview/web/middleware/basicauth.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// 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",
|
||||
},
|
||||
})
|
||||
50
_examples/hero/overview/web/routes/hello.go
Normal file
50
_examples/hero/overview/web/routes/hello.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// file: web/routes/hello.go
|
||||
|
||||
package routes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/kataras/iris/hero"
|
||||
)
|
||||
|
||||
var helloView = hero.View{
|
||||
Name: "hello/index.html",
|
||||
Data: map[string]interface{}{
|
||||
"Title": "Hello Page",
|
||||
"MyMessage": "Welcome to my awesome website",
|
||||
},
|
||||
}
|
||||
|
||||
// Hello will return a predefined view with bind data.
|
||||
//
|
||||
// `hero.Result` is just an interface with a `Dispatch` function.
|
||||
// `hero.Response` and `hero.View` are the built'n result type dispatchers
|
||||
// you can even create custom response dispatchers by
|
||||
// implementing the `github.com/kataras/iris/hero#Result` interface.
|
||||
func Hello() hero.Result {
|
||||
return helloView
|
||||
}
|
||||
|
||||
// you can define a standard error in order to re-use anywhere in your app.
|
||||
var errBadName = errors.New("bad name")
|
||||
|
||||
// you can just return it as error or even better
|
||||
// wrap this error with an hero.Response to make it an hero.Result compatible type.
|
||||
var badName = hero.Response{Err: errBadName, Code: 400}
|
||||
|
||||
// HelloName returns a "Hello {name}" response.
|
||||
// Demos:
|
||||
// curl -i http://localhost:8080/hello/iris
|
||||
// curl -i http://localhost:8080/hello/anything
|
||||
func HelloName(name string) hero.Result {
|
||||
if name != "iris" {
|
||||
return badName
|
||||
}
|
||||
|
||||
// return hero.Response{Text: "Hello " + name} OR:
|
||||
return hero.View{
|
||||
Name: "hello/name.html",
|
||||
Data: name,
|
||||
}
|
||||
}
|
||||
59
_examples/hero/overview/web/routes/movies.go
Normal file
59
_examples/hero/overview/web/routes/movies.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// file: web/routes/movie.go
|
||||
|
||||
package routes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/kataras/iris/_examples/hero/overview/datamodels"
|
||||
"github.com/kataras/iris/_examples/hero/overview/services"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
)
|
||||
|
||||
// Movies returns list of the movies.
|
||||
// Demo:
|
||||
// curl -i http://localhost:8080/movies
|
||||
func Movies(service services.MovieService) (results []datamodels.Movie) {
|
||||
return service.GetAll()
|
||||
}
|
||||
|
||||
// MovieByID returns a movie.
|
||||
// Demo:
|
||||
// curl -i http://localhost:8080/movies/1
|
||||
func MovieByID(service services.MovieService, id int64) (movie datamodels.Movie, found bool) {
|
||||
return service.GetByID(id) // it will throw 404 if not found.
|
||||
}
|
||||
|
||||
// UpdateMovieByID updates a movie.
|
||||
// Demo:
|
||||
// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1
|
||||
func UpdateMovieByID(ctx iris.Context, service services.MovieService, id int64) (datamodels.Movie, error) {
|
||||
// get the request data for poster and genre
|
||||
file, info, err := 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 := ctx.FormValue("genre")
|
||||
|
||||
return service.UpdatePosterAndGenreByID(id, poster, genre)
|
||||
}
|
||||
|
||||
// DeleteMovieByID deletes a movie.
|
||||
// Demo:
|
||||
// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1
|
||||
func DeleteMovieByID(service services.MovieService, id int64) interface{} {
|
||||
wasDel := 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
|
||||
}
|
||||
12
_examples/hero/overview/web/views/hello/index.html
Normal file
12
_examples/hero/overview/web/views/hello/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!-- file: web/views/hello/index.html -->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>{{.Title}} - My App</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>{{.MyMessage}}</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
12
_examples/hero/overview/web/views/hello/name.html
Normal file
12
_examples/hero/overview/web/views/hello/name.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!-- file: web/views/hello/name.html -->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>{{.}}' Portfolio - My App</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Hello {{.}}</h1>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user