Skip to content

Commit

Permalink
create a new package, name it as hero, I was thinking super or superb…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
kataras committed Dec 25, 2017
1 parent 61cdb9b commit 751a45f
Show file tree
Hide file tree
Showing 43 changed files with 2,680 additions and 20 deletions.
4 changes: 2 additions & 2 deletions _examples/README.md
Expand Up @@ -299,7 +299,7 @@ convert any custom type into a response dispatcher by implementing the `mvc.Resu
- [Embedding Templates Into App Executable File](view/embedding-templates-into-app/main.go)


You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [hero](https://github.com/shiyanhui/hero/hero) files too, simply by using the `context#ResponseWriter`, take a look at the [http_responsewriter/quicktemplate](http_responsewriter/quicktemplate) and [http_responsewriter/hero] examples.
You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [hero templates](https://github.com/shiyanhui/hero/hero) files too, simply by using the `context#ResponseWriter`, take a look at the [http_responsewriter/quicktemplate](http_responsewriter/quicktemplate) and [http_responsewriter/herotemplate](http_responsewriter/herotemplate) examples.

### Authentication

Expand Down Expand Up @@ -330,7 +330,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
### How to Write to `context.ResponseWriter() http.ResponseWriter`

- [Write `valyala/quicktemplate` templates](http_responsewriter/quicktemplate)
- [Write `shiyanhui/hero` templates](http_responsewriter/hero)
- [Write `shiyanhui/hero` templates](http_responsewriter/herotemplate)
- [Text, Markdown, HTML, JSON, JSONP, XML, Binary](http_responsewriter/write-rest/main.go)
- [Write Gzip](http_responsewriter/write-gzip/main.go)
- [Stream Writer](http_responsewriter/stream-writer/main.go)
Expand Down
18 changes: 18 additions & 0 deletions _examples/hero/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"`
}
44 changes: 44 additions & 0 deletions _examples/hero/overview/datasource/movies.go
@@ -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 changes: 60 additions & 0 deletions _examples/hero/overview/main.go
@@ -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 changes: 176 additions & 0 deletions _examples/hero/overview/repositories/movie_repository.go
@@ -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 changes: 65 additions & 0 deletions _examples/hero/overview/services/movie_service.go
@@ -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 changes: 12 additions & 0 deletions _examples/hero/overview/web/middleware/basicauth.go
@@ -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",
},
})

0 comments on commit 751a45f

Please sign in to comment.