diff --git a/_examples/README.md b/_examples/README.md
index 261c4972..f988d46d 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -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
@@ -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)
diff --git a/_examples/hero/overview/datamodels/movie.go b/_examples/hero/overview/datamodels/movie.go
new file mode 100644
index 00000000..7649d487
--- /dev/null
+++ b/_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"`
+}
diff --git a/_examples/hero/overview/datasource/movies.go b/_examples/hero/overview/datasource/movies.go
new file mode 100644
index 00000000..dbbe4fdf
--- /dev/null
+++ b/_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",
+ },
+}
diff --git a/_examples/hero/overview/main.go b/_examples/hero/overview/main.go
new file mode 100644
index 00000000..58f0b04e
--- /dev/null
+++ b/_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,
+ )
+}
diff --git a/_examples/hero/overview/repositories/movie_repository.go b/_examples/hero/overview/repositories/movie_repository.go
new file mode 100644
index 00000000..de3275ac
--- /dev/null
+++ b/_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)
+}
diff --git a/_examples/hero/overview/services/movie_service.go b/_examples/hero/overview/services/movie_service.go
new file mode 100644
index 00000000..16ed6412
--- /dev/null
+++ b/_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)
+}
diff --git a/_examples/hero/overview/web/middleware/basicauth.go b/_examples/hero/overview/web/middleware/basicauth.go
new file mode 100644
index 00000000..c9b6eacf
--- /dev/null
+++ b/_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",
+ },
+})
diff --git a/_examples/hero/overview/web/routes/hello.go b/_examples/hero/overview/web/routes/hello.go
new file mode 100644
index 00000000..8feeca14
--- /dev/null
+++ b/_examples/hero/overview/web/routes/hello.go
@@ -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,
+ }
+}
diff --git a/_examples/hero/overview/web/routes/movies.go b/_examples/hero/overview/web/routes/movies.go
new file mode 100644
index 00000000..d3947d1b
--- /dev/null
+++ b/_examples/hero/overview/web/routes/movies.go
@@ -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
+}
diff --git a/_examples/hero/overview/web/views/hello/index.html b/_examples/hero/overview/web/views/hello/index.html
new file mode 100644
index 00000000..9e7b03d6
--- /dev/null
+++ b/_examples/hero/overview/web/views/hello/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ {{.Title}} - My App
+
+
+
+ {{.MyMessage}}
+
+
+
\ No newline at end of file
diff --git a/_examples/hero/overview/web/views/hello/name.html b/_examples/hero/overview/web/views/hello/name.html
new file mode 100644
index 00000000..d6dd5ac6
--- /dev/null
+++ b/_examples/hero/overview/web/views/hello/name.html
@@ -0,0 +1,12 @@
+
+
+
+
+ {{.}}' Portfolio - My App
+
+
+
+ Hello {{.}}
+
+
+
\ No newline at end of file
diff --git a/_examples/http_responsewriter/hero/README.md b/_examples/http_responsewriter/herotemplate/README.md
similarity index 100%
rename from _examples/http_responsewriter/hero/README.md
rename to _examples/http_responsewriter/herotemplate/README.md
diff --git a/_examples/http_responsewriter/hero/app.go b/_examples/http_responsewriter/herotemplate/app.go
similarity index 93%
rename from _examples/http_responsewriter/hero/app.go
rename to _examples/http_responsewriter/herotemplate/app.go
index bf5c1ecc..4f1412ac 100644
--- a/_examples/http_responsewriter/hero/app.go
+++ b/_examples/http_responsewriter/herotemplate/app.go
@@ -4,7 +4,7 @@ import (
"bytes"
"log"
- "github.com/kataras/iris/_examples/http_responsewriter/hero/template"
+ "github.com/kataras/iris/_examples/http_responsewriter/herotemplate/template"
"github.com/kataras/iris"
)
diff --git a/_examples/http_responsewriter/hero/template/index.html b/_examples/http_responsewriter/herotemplate/template/index.html
similarity index 100%
rename from _examples/http_responsewriter/hero/template/index.html
rename to _examples/http_responsewriter/herotemplate/template/index.html
diff --git a/_examples/http_responsewriter/hero/template/index.html.go b/_examples/http_responsewriter/herotemplate/template/index.html.go
similarity index 100%
rename from _examples/http_responsewriter/hero/template/index.html.go
rename to _examples/http_responsewriter/herotemplate/template/index.html.go
diff --git a/_examples/http_responsewriter/hero/template/user.html b/_examples/http_responsewriter/herotemplate/template/user.html
similarity index 100%
rename from _examples/http_responsewriter/hero/template/user.html
rename to _examples/http_responsewriter/herotemplate/template/user.html
diff --git a/_examples/http_responsewriter/hero/template/user.html.go b/_examples/http_responsewriter/herotemplate/template/user.html.go
similarity index 100%
rename from _examples/http_responsewriter/hero/template/user.html.go
rename to _examples/http_responsewriter/herotemplate/template/user.html.go
diff --git a/_examples/http_responsewriter/hero/template/userlist.html b/_examples/http_responsewriter/herotemplate/template/userlist.html
similarity index 100%
rename from _examples/http_responsewriter/hero/template/userlist.html
rename to _examples/http_responsewriter/herotemplate/template/userlist.html
diff --git a/_examples/http_responsewriter/hero/template/userlist.html.go b/_examples/http_responsewriter/herotemplate/template/userlist.html.go
similarity index 100%
rename from _examples/http_responsewriter/hero/template/userlist.html.go
rename to _examples/http_responsewriter/herotemplate/template/userlist.html.go
diff --git a/_examples/http_responsewriter/hero/template/userlistwriter.html b/_examples/http_responsewriter/herotemplate/template/userlistwriter.html
similarity index 100%
rename from _examples/http_responsewriter/hero/template/userlistwriter.html
rename to _examples/http_responsewriter/herotemplate/template/userlistwriter.html
diff --git a/_examples/http_responsewriter/hero/template/userlistwriter.html.go b/_examples/http_responsewriter/herotemplate/template/userlistwriter.html.go
similarity index 100%
rename from _examples/http_responsewriter/hero/template/userlistwriter.html.go
rename to _examples/http_responsewriter/herotemplate/template/userlistwriter.html.go
diff --git a/context/context.go b/context/context.go
index f8d288a0..0f1f5d37 100644
--- a/context/context.go
+++ b/context/context.go
@@ -92,15 +92,27 @@ func (r *RequestParams) Visit(visitor func(key string, value string)) {
})
}
-// GetEntry returns the internal Entry of the memstore, as value
-// if not found then it returns a zero Entry and false.
+var emptyEntry memstore.Entry
+
+// GetEntryAt returns the internal Entry of the memstore based on its index,
+// the stored index by the router.
+// If not found then it returns a zero Entry and false.
+func (r RequestParams) GetEntryAt(index int) (memstore.Entry, bool) {
+ if len(r.store) > index {
+ return r.store[index], true
+ }
+ return emptyEntry, false
+}
+
+// GetEntry returns the internal Entry of the memstore based on its "key".
+// If not found then it returns a zero Entry and false.
func (r RequestParams) GetEntry(key string) (memstore.Entry, bool) {
// we don't return the pointer here, we don't want to give the end-developer
// the strength to change the entry that way.
if e := r.store.GetEntry(key); e != nil {
return *e, true
}
- return memstore.Entry{}, false
+ return emptyEntry, false
}
// Get returns a path parameter's value based on its route's dynamic path key.
diff --git a/hero/AUTHORS b/hero/AUTHORS
new file mode 100644
index 00000000..848245bb
--- /dev/null
+++ b/hero/AUTHORS
@@ -0,0 +1 @@
+Gerasimos Maropoulos
diff --git a/hero/LICENSE b/hero/LICENSE
new file mode 100644
index 00000000..a0b2d92f
--- /dev/null
+++ b/hero/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2018 Gerasimos Maropoulos. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Iris nor the name of Iris Hero, nor the names of its
+contributor, Gerasimos Maropoulos, may be used to endorse or promote products
+derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/hero/di.go b/hero/di.go
new file mode 100644
index 00000000..4244cfa0
--- /dev/null
+++ b/hero/di.go
@@ -0,0 +1,31 @@
+package hero
+
+import (
+ "reflect"
+
+ "github.com/kataras/iris/hero/di"
+)
+
+func init() {
+ di.DefaultHijacker = func(fieldOrFuncInput reflect.Type) (*di.BindObject, bool) {
+ if !IsContext(fieldOrFuncInput) {
+ return nil, false
+ }
+ // this is being used on both func injector and struct injector.
+ // if the func's input argument or the struct's field is a type of Context
+ // then we can do a fast binding using the ctxValue
+ // which is used as slice of reflect.Value, because of the final method's `Call`.
+ return &di.BindObject{
+ Type: contextTyp,
+ BindType: di.Dynamic,
+ ReturnValue: func(ctxValue []reflect.Value) reflect.Value {
+ return ctxValue[0]
+ },
+ }, true
+ }
+
+ di.DefaultTypeChecker = func(fn reflect.Type) bool {
+ // valid if that single input arg is a typeof context.Context.
+ return fn.NumIn() == 1 && IsContext(fn.In(0))
+ }
+}
diff --git a/hero/di/TODO.txt b/hero/di/TODO.txt
new file mode 100644
index 00000000..569cb392
--- /dev/null
+++ b/hero/di/TODO.txt
@@ -0,0 +1,11 @@
+I can do one of the followings to this "di" folder when I finish the cleanup and document it a bit,
+although I'm sick I will try to finish it tomorrow.
+
+End-users don't need this.
+1) So, rename this to "internal".
+
+I don't know if something similar exist in Go,
+it's a dependency injection framework at the end, and a very fast one.
+
+2) So I'm thinking to push it to a different repo,
+ like https://github.com/kataras/di or even to my small common https://github.com/kataras/pkg collection.
\ No newline at end of file
diff --git a/hero/di/di.go b/hero/di/di.go
new file mode 100644
index 00000000..e4144793
--- /dev/null
+++ b/hero/di/di.go
@@ -0,0 +1,129 @@
+package di
+
+import "reflect"
+
+type (
+ // Hijacker is a type which is used to catch fields or function's input argument
+ // to bind a custom object based on their type.
+ Hijacker func(reflect.Type) (*BindObject, bool)
+ // TypeChecker checks if a specific field's or function input argument's
+ // is valid to be binded.
+ TypeChecker func(reflect.Type) bool
+)
+
+var (
+ // DefaultHijacker is the hijacker used on the package-level Struct & Func functions.
+ DefaultHijacker Hijacker
+ // DefaultTypeChecker is the typechecker used on the package-level Struct & Func functions.
+ DefaultTypeChecker TypeChecker
+)
+
+// Struct is being used to return a new injector based on
+// a struct value instance, if it contains fields that the types of those
+// are matching with one or more of the `Values` then they are binded
+// with the injector's `Inject` and `InjectElem` methods.
+func Struct(s interface{}, values ...reflect.Value) *StructInjector {
+ if s == nil {
+ return &StructInjector{Has: false}
+ }
+
+ return MakeStructInjector(
+ ValueOf(s),
+ DefaultHijacker,
+ DefaultTypeChecker,
+ Values(values).CloneWithFieldsOf(s)...,
+ )
+}
+
+// Func is being used to return a new injector based on
+// a function, if it contains input arguments that the types of those
+// are matching with one or more of the `Values` then they are binded
+// to the function's input argument when called
+// with the injector's `Inject` method.
+func Func(fn interface{}, values ...reflect.Value) *FuncInjector {
+ if fn == nil {
+ return &FuncInjector{Has: false}
+ }
+
+ return MakeFuncInjector(
+ ValueOf(fn),
+ DefaultHijacker,
+ DefaultTypeChecker,
+ values...,
+ )
+}
+
+// D is the Dependency Injection container,
+// it contains the Values that can be changed before the injectors.
+// `Struct` and the `Func` methods returns an injector for specific
+// struct instance-value or function.
+type D struct {
+ Values
+
+ hijacker Hijacker
+ goodFunc TypeChecker
+}
+
+// New creates and returns a new Dependency Injection container.
+// See `Values` field and `Func` and `Struct` methods for more.
+func New() *D {
+ return &D{}
+}
+
+// Hijack sets a hijacker function, read the `Hijacker` type for more explanation.
+func (d *D) Hijack(fn Hijacker) *D {
+ d.hijacker = fn
+ return d
+}
+
+// GoodFunc sets a type checker for a valid function that can be binded,
+// read the `TypeChecker` type for more explanation.
+func (d *D) GoodFunc(fn TypeChecker) *D {
+ d.goodFunc = fn
+ return d
+}
+
+// Clone returns a new Dependency Injection container, it adopts the
+// parent's (current "D") hijacker, good func type checker and all dependencies values.
+func (d *D) Clone() *D {
+ return &D{
+ Values: d.Values.Clone(),
+ hijacker: d.hijacker,
+ goodFunc: d.goodFunc,
+ }
+}
+
+// Struct is being used to return a new injector based on
+// a struct value instance, if it contains fields that the types of those
+// are matching with one or more of the `Values` then they are binded
+// with the injector's `Inject` and `InjectElem` methods.
+func (d *D) Struct(s interface{}) *StructInjector {
+ if s == nil {
+ return &StructInjector{Has: false}
+ }
+
+ return MakeStructInjector(
+ ValueOf(s),
+ d.hijacker,
+ d.goodFunc,
+ d.Values.CloneWithFieldsOf(s)...,
+ )
+}
+
+// Func is being used to return a new injector based on
+// a function, if it contains input arguments that the types of those
+// are matching with one or more of the `Values` then they are binded
+// to the function's input argument when called
+// with the injector's `Inject` method.
+func (d *D) Func(fn interface{}) *FuncInjector {
+ if fn == nil {
+ return &FuncInjector{Has: false}
+ }
+
+ return MakeFuncInjector(
+ ValueOf(fn),
+ d.hijacker,
+ d.goodFunc,
+ d.Values...,
+ )
+}
diff --git a/hero/di/func.go b/hero/di/func.go
new file mode 100644
index 00000000..fe69dfb0
--- /dev/null
+++ b/hero/di/func.go
@@ -0,0 +1,215 @@
+package di
+
+import (
+ "fmt"
+ "reflect"
+)
+
+type (
+ targetFuncInput struct {
+ Object *BindObject
+ InputIndex int
+ }
+
+ // FuncInjector keeps the data that are needed in order to do the binding injection
+ // as fast as possible and with the best possible and safest way.
+ FuncInjector struct {
+ // the original function, is being used
+ // only the .Call, which is referring to the same function, always.
+ fn reflect.Value
+ typ reflect.Type
+ goodFunc TypeChecker
+
+ inputs []*targetFuncInput
+ // Length is the number of the valid, final binded input arguments.
+ Length int
+ // Valid is True when `Length` is > 0, it's statically set-ed for
+ // performance reasons.
+ Has bool
+
+ trace string // for debug info.
+
+ lost []*missingInput // Author's note: don't change this to a map.
+ }
+)
+
+type missingInput struct {
+ index int // the function's input argument's index.
+ found bool
+}
+
+func (s *FuncInjector) miss(index int) {
+ s.lost = append(s.lost, &missingInput{
+ index: index,
+ })
+}
+
+// MakeFuncInjector returns a new func injector, which will be the object
+// that the caller should use to bind input arguments of the "fn" function.
+//
+// The hijack and the goodFunc are optional, the "values" is the dependencies collection.
+func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *FuncInjector {
+ typ := IndirectType(fn.Type())
+ s := &FuncInjector{
+ fn: fn,
+ typ: typ,
+ goodFunc: goodFunc,
+ }
+
+ if !IsFunc(typ) {
+ return s
+ }
+
+ defer s.refresh()
+
+ n := typ.NumIn()
+
+ for i := 0; i < n; i++ {
+ inTyp := typ.In(i)
+
+ if hijack != nil {
+ b, ok := hijack(inTyp)
+
+ if ok && b != nil {
+ s.inputs = append(s.inputs, &targetFuncInput{
+ InputIndex: i,
+ Object: b,
+ })
+ continue
+ }
+ }
+
+ matched := false
+
+ for j, v := range values {
+ if s.addValue(i, v) {
+ matched = true
+ // remove this value, so it will not try to get binded
+ // again, a next value even with the same type is able to be
+ // used to other input arg. One value per input argument, order
+ // matters if same type of course.
+ //if len(values) > j+1 {
+ values = append(values[:j], values[j+1:]...)
+ //}
+
+ break
+ }
+ }
+
+ if !matched {
+ // if no binding for this input argument,
+ // this will make the func injector invalid state,
+ // but before this let's make a list of failed
+ // inputs, so they can be used for a re-try
+ // with different set of binding "values".
+ s.miss(i)
+ }
+
+ }
+
+ return s
+}
+
+func (s *FuncInjector) refresh() {
+ s.Length = len(s.inputs)
+ s.Has = s.Length > 0
+}
+
+func (s *FuncInjector) addValue(inputIndex int, value reflect.Value) bool {
+ defer s.refresh()
+
+ if s.typ.NumIn() < inputIndex {
+ return false
+ }
+
+ inTyp := s.typ.In(inputIndex)
+
+ // the binded values to the func's inputs.
+ b, err := MakeBindObject(value, s.goodFunc)
+
+ if err != nil {
+ return false
+ }
+
+ if b.IsAssignable(inTyp) {
+ // println(inTyp.String() + " is assignable to " + val.Type().String())
+ // fmt.Printf("binded input index: %d for type: %s and value: %v with pointer: %v\n",
+ // i, b.Type.String(), value.String(), val.Pointer())
+ s.inputs = append(s.inputs, &targetFuncInput{
+ InputIndex: inputIndex,
+ Object: &b,
+ })
+ return true
+ }
+
+ return false
+}
+
+func (s *FuncInjector) Retry(retryFn func(inIndex int, inTyp reflect.Type) (reflect.Value, bool)) bool {
+ for _, missing := range s.lost {
+ if missing.found {
+ continue
+ }
+
+ invalidIndex := missing.index
+
+ inTyp := s.typ.In(invalidIndex)
+ v, ok := retryFn(invalidIndex, inTyp)
+ if !ok {
+ continue
+ }
+
+ if !s.addValue(invalidIndex, v) {
+ continue
+ }
+
+ // if this value completes an invalid index
+ // then remove this from the invalid input indexes.
+ missing.found = true
+ }
+
+ return s.Length == s.typ.NumIn()
+}
+
+// String returns a debug trace text.
+func (s *FuncInjector) String() (trace string) {
+ for i, in := range s.inputs {
+ bindmethodTyp := bindTypeString(in.Object.BindType)
+ typIn := s.typ.In(in.InputIndex)
+ // remember: on methods that are part of a struct (i.e controller)
+ // the input index = 1 is the begggining instead of the 0,
+ // because the 0 is the controller receiver pointer of the method.
+ trace += fmt.Sprintf("[%d] %s binding: '%s' for input position: %d and type: '%s'\n",
+ i+1, bindmethodTyp, in.Object.Type.String(), in.InputIndex, typIn.String())
+ }
+ return
+}
+
+// Inject accepts an already created slice of input arguments
+// and fills them, the "ctx" is optional and it's used
+// on the dependencies that depends on one or more input arguments, these are the "ctx".
+func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) {
+ args := *in
+ for _, input := range s.inputs {
+ input.Object.Assign(ctx, func(v reflect.Value) {
+ // fmt.Printf("assign input index: %d for value: %v\n",
+ // input.InputIndex, v.String())
+ args[input.InputIndex] = v
+ })
+
+ }
+
+ *in = args
+}
+
+// Call calls the "Inject" with a new slice of input arguments
+// that are computed by the length of the input argument from the MakeFuncInjector's "fn" function.
+//
+// If the function needs a receiver, so
+// the caller should be able to in[0] = receiver before injection,
+// then the `Inject` method should be used instead.
+func (s *FuncInjector) Call(ctx ...reflect.Value) []reflect.Value {
+ in := make([]reflect.Value, s.Length, s.Length)
+ s.Inject(&in, ctx...)
+ return s.fn.Call(in)
+}
diff --git a/hero/di/object.go b/hero/di/object.go
new file mode 100644
index 00000000..392abcc5
--- /dev/null
+++ b/hero/di/object.go
@@ -0,0 +1,123 @@
+package di
+
+import (
+ "errors"
+ "reflect"
+)
+
+// BindType is the type of a binded object/value, it's being used to
+// check if the value is accessible after a function call with a "ctx" when needed ( Dynamic type)
+// or it's just a struct value (a service | Static type).
+type BindType uint32
+
+const (
+ // Static is the simple assignable value, a static value.
+ Static BindType = iota
+ // Dynamic returns a value but it depends on some input arguments from the caller,
+ // on serve time.
+ Dynamic
+)
+
+func bindTypeString(typ BindType) string {
+ switch typ {
+ case Dynamic:
+ return "Dynamic"
+ default:
+ return "Static"
+ }
+}
+
+// BindObject contains the dependency value's read-only information.
+// FuncInjector and StructInjector keeps information about their
+// input arguments/or fields, these properties contain a `BindObject` inside them.
+type BindObject struct {
+ Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' .
+ Value reflect.Value
+
+ BindType BindType
+ ReturnValue func([]reflect.Value) reflect.Value
+}
+
+// MakeBindObject accepts any "v" value, struct, pointer or a function
+// and a type checker that is used to check if the fields (if "v.elem()" is struct)
+// or the input arguments (if "v.elem()" is func)
+// are valid to be included as the final object's dependencies, even if the caller added more
+// the "di" is smart enough to select what each "v" needs and what not before serve time.
+func MakeBindObject(v reflect.Value, goodFunc TypeChecker) (b BindObject, err error) {
+ if IsFunc(v) {
+ b.BindType = Dynamic
+ b.ReturnValue, b.Type, err = MakeReturnValue(v, goodFunc)
+ } else {
+ b.BindType = Static
+ b.Type = v.Type()
+ b.Value = v
+ }
+
+ return
+}
+
+var errBad = errors.New("bad")
+
+// MakeReturnValue takes any function
+// that accept custom values and returns something,
+// it returns a binder function, which accepts a slice of reflect.Value
+// and returns a single one reflect.Value for that.
+// It's being used to resolve the input parameters on a "x" consumer faster.
+//
+// The "fn" can have the following form:
+// `func(myService) MyViewModel`.
+//
+// The return type of the "fn" should be a value instance, not a pointer, for your own protection.
+// The binder function should return only one value.
+func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Value) reflect.Value, reflect.Type, error) {
+ typ := IndirectType(fn.Type())
+
+ // invalid if not a func.
+ if typ.Kind() != reflect.Func {
+ return nil, typ, errBad
+ }
+
+ // invalid if not returns one single value.
+ if typ.NumOut() != 1 {
+ return nil, typ, errBad
+ }
+
+ if goodFunc != nil {
+ if !goodFunc(typ) {
+ return nil, typ, errBad
+ }
+ }
+
+ outTyp := typ.Out(0)
+ zeroOutVal := reflect.New(outTyp).Elem()
+
+ bf := func(ctxValue []reflect.Value) reflect.Value {
+ results := fn.Call(ctxValue)
+ if len(results) == 0 {
+ return zeroOutVal
+ }
+
+ v := results[0]
+ if !v.IsValid() {
+ return zeroOutVal
+ }
+ return v
+ }
+
+ return bf, outTyp, nil
+}
+
+// IsAssignable checks if "to" type can be used as "b.Value/ReturnValue".
+func (b *BindObject) IsAssignable(to reflect.Type) bool {
+ return equalTypes(b.Type, to)
+}
+
+// Assign sets the values to a setter, "toSetter" contains the setter, so the caller
+// can use it for multiple and different structs/functions as well.
+func (b *BindObject) Assign(ctx []reflect.Value, toSetter func(reflect.Value)) {
+ if b.BindType == Dynamic {
+ toSetter(b.ReturnValue(ctx))
+ return
+ }
+ toSetter(b.Value)
+}
diff --git a/hero/di/reflect.go b/hero/di/reflect.go
new file mode 100644
index 00000000..9713ddb8
--- /dev/null
+++ b/hero/di/reflect.go
@@ -0,0 +1,202 @@
+package di
+
+import "reflect"
+
+// EmptyIn is just an empty slice of reflect.Value.
+var EmptyIn = []reflect.Value{}
+
+// IsZero returns true if a value is nil.
+// Remember; fields to be checked should be exported otherwise it returns false.
+// Notes for users:
+// Boolean's zero value is false, even if not set-ed.
+// UintXX are not zero on 0 because they are pointers to.
+func IsZero(v reflect.Value) bool {
+ switch v.Kind() {
+ case reflect.Struct:
+ zero := true
+ for i := 0; i < v.NumField(); i++ {
+ zero = zero && IsZero(v.Field(i))
+ }
+
+ if typ := v.Type(); typ != nil && v.IsValid() {
+ f, ok := typ.MethodByName("IsZero")
+ // if not found
+ // if has input arguments (1 is for the value receiver, so > 1 for the actual input args)
+ // if output argument is not boolean
+ // then skip this IsZero user-defined function.
+ if !ok || f.Type.NumIn() > 1 || f.Type.NumOut() != 1 && f.Type.Out(0).Kind() != reflect.Bool {
+ return zero
+ }
+
+ method := v.Method(f.Index)
+ // no needed check but:
+ if method.IsValid() && !method.IsNil() {
+ // it shouldn't panic here.
+ zero = method.Call(EmptyIn)[0].Interface().(bool)
+ }
+ }
+
+ return zero
+ case reflect.Func, reflect.Map, reflect.Slice:
+ return v.IsNil()
+ case reflect.Array:
+ zero := true
+ for i := 0; i < v.Len(); i++ {
+ zero = zero && IsZero(v.Index(i))
+ }
+ return zero
+ }
+ // if not any special type then use the reflect's .Zero
+ // usually for fields, but remember if it's boolean and it's false
+ // then it's zero, even if set-ed.
+
+ if !v.CanInterface() {
+ // if can't interface, i.e return value from unexported field or method then return false
+ return false
+ }
+ zero := reflect.Zero(v.Type())
+ return v.Interface() == zero.Interface()
+}
+
+func IndirectValue(v reflect.Value) reflect.Value {
+ return reflect.Indirect(v)
+}
+
+func ValueOf(o interface{}) reflect.Value {
+ if v, ok := o.(reflect.Value); ok {
+ return v
+ }
+
+ return reflect.ValueOf(o)
+}
+
+func ValuesOf(valuesAsInterface []interface{}) (values []reflect.Value) {
+ for _, v := range valuesAsInterface {
+ values = append(values, ValueOf(v))
+ }
+ return
+}
+
+func IndirectType(typ reflect.Type) reflect.Type {
+ switch typ.Kind() {
+ case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
+ return typ.Elem()
+ }
+ return typ
+}
+
+func goodVal(v reflect.Value) bool {
+ switch v.Kind() {
+ case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice:
+ if v.IsNil() {
+ return false
+ }
+ }
+
+ return v.IsValid()
+}
+
+// IsFunc returns true if the passed type is function.
+func IsFunc(kindable interface {
+ Kind() reflect.Kind
+}) bool {
+ return kindable.Kind() == reflect.Func
+}
+
+func equalTypes(got reflect.Type, expected reflect.Type) bool {
+ if got == expected {
+ return true
+ }
+ // if accepts an interface, check if the given "got" type does
+ // implement this "expected" user handler's input argument.
+ if expected.Kind() == reflect.Interface {
+ // fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String())
+ return got.Implements(expected)
+ }
+ return false
+}
+
+// for controller's fields only.
+func structFieldIgnored(f reflect.StructField) bool {
+ if !f.Anonymous {
+ return true // if not anonymous(embedded), ignore it.
+ }
+
+ s := f.Tag.Get("ignore")
+ return s == "true" // if has an ignore tag then ignore it.
+}
+
+type field struct {
+ Type reflect.Type
+ Name string // the actual name.
+ Index []int // the index of the field, slice if it's part of a embedded struct
+ CanSet bool // is true if it's exported.
+
+ // this could be empty, but in our cases it's not,
+ // it's filled with the bind object (as service which means as static value)
+ // and it's filled from the lookupFields' caller.
+ AnyValue reflect.Value
+}
+
+// NumFields returns the total number of fields, and the embedded, even if the embedded struct is not exported,
+// it will check for its exported fields.
+func NumFields(elemTyp reflect.Type, skipUnexported bool) int {
+ return len(lookupFields(elemTyp, skipUnexported, nil))
+}
+
+func lookupFields(elemTyp reflect.Type, skipUnexported bool, parentIndex []int) (fields []field) {
+ if elemTyp.Kind() != reflect.Struct {
+ return
+ }
+
+ for i, n := 0, elemTyp.NumField(); i < n; i++ {
+ f := elemTyp.Field(i)
+
+ if IndirectType(f.Type).Kind() == reflect.Struct &&
+ !structFieldIgnored(f) {
+ fields = append(fields, lookupFields(f.Type, skipUnexported, append(parentIndex, i))...)
+ continue
+ }
+
+ // skip unexported fields here,
+ // after the check for embedded structs, these can be binded if their
+ // fields are exported.
+ isExported := f.PkgPath == ""
+ if skipUnexported && !isExported {
+ continue
+ }
+
+ index := []int{i}
+ if len(parentIndex) > 0 {
+ index = append(parentIndex, i)
+ }
+
+ field := field{
+ Type: f.Type,
+ Name: f.Name,
+ Index: index,
+ CanSet: isExported,
+ }
+
+ fields = append(fields, field)
+ }
+
+ return
+}
+
+// LookupNonZeroFieldsValues lookup for filled fields based on the "v" struct value instance.
+// It returns a slice of reflect.Value (same type as `Values`) that can be binded,
+// like the end-developer's custom values.
+func LookupNonZeroFieldsValues(v reflect.Value, skipUnexported bool) (bindValues []reflect.Value) {
+ elem := IndirectValue(v)
+ fields := lookupFields(IndirectType(v.Type()), skipUnexported, nil)
+
+ for _, f := range fields {
+ if fieldVal := elem.FieldByIndex(f.Index); /*f.Type.Kind() == reflect.Ptr &&*/
+ !IsZero(fieldVal) {
+ bindValues = append(bindValues, fieldVal)
+ }
+ }
+
+ return
+}
diff --git a/hero/di/struct.go b/hero/di/struct.go
new file mode 100644
index 00000000..cc48b1e4
--- /dev/null
+++ b/hero/di/struct.go
@@ -0,0 +1,201 @@
+package di
+
+import (
+ "fmt"
+ "reflect"
+)
+
+type Scope uint8
+
+const (
+ Stateless Scope = iota
+ Singleton
+)
+
+type (
+ targetStructField struct {
+ Object *BindObject
+ FieldIndex []int
+ }
+
+ // StructInjector keeps the data that are needed in order to do the binding injection
+ // as fast as possible and with the best possible and safest way.
+ StructInjector struct {
+ initRef reflect.Value
+ initRefAsSlice []reflect.Value // useful when the struct is passed on a func as input args via reflection.
+ elemType reflect.Type
+ //
+ fields []*targetStructField
+ // is true when contains bindable fields and it's a valid target struct,
+ // it maybe 0 but struct may contain unexported fields or exported but no bindable (Stateless)
+ // see `setState`.
+ Has bool
+ CanInject bool // if any bindable fields when the state is NOT singleton.
+ Scope Scope
+ }
+)
+
+func (s *StructInjector) countBindType(typ BindType) (n int) {
+ for _, f := range s.fields {
+ if f.Object.BindType == typ {
+ n++
+ }
+ }
+ return
+}
+
+// MakeStructInjector returns a new struct injector, which will be the object
+// that the caller should use to bind exported fields or
+// embedded unexported fields that contain exported fields
+// of the "v" struct value or pointer.
+//
+// The hijack and the goodFunc are optional, the "values" is the dependencies collection.
+func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *StructInjector {
+ s := &StructInjector{
+ initRef: v,
+ initRefAsSlice: []reflect.Value{v},
+ elemType: IndirectType(v.Type()),
+ }
+
+ fields := lookupFields(s.elemType, true, nil)
+ for _, f := range fields {
+ if hijack != nil {
+ if b, ok := hijack(f.Type); ok && b != nil {
+ s.fields = append(s.fields, &targetStructField{
+ FieldIndex: f.Index,
+ Object: b,
+ })
+
+ continue
+ }
+ }
+
+ for _, val := range values {
+ // the binded values to the struct's fields.
+ b, err := MakeBindObject(val, goodFunc)
+
+ if err != nil {
+ return s // if error stop here.
+ }
+
+ if b.IsAssignable(f.Type) {
+ // fmt.Printf("bind the object to the field: %s at index: %#v and type: %s\n", f.Name, f.Index, f.Type.String())
+ s.fields = append(s.fields, &targetStructField{
+ FieldIndex: f.Index,
+ Object: &b,
+ })
+ break
+ }
+ }
+ }
+
+ s.Has = len(s.fields) > 0
+ // set the overall state of this injector.
+ s.fillStruct()
+ s.setState()
+
+ return s
+}
+
+// set the state, once.
+// Here the "initRef" have already the static bindings and the manually-filled fields.
+func (s *StructInjector) setState() {
+ // note for zero length of struct's fields:
+ // if struct doesn't contain any field
+ // so both of the below variables will be 0,
+ // so it's a singleton.
+ // At the other hand the `s.HasFields` maybe false
+ // but the struct may contain UNEXPORTED fields or non-bindable fields (request-scoped on both cases)
+ // so a new controller/struct at the caller side should be initialized on each request,
+ // we should not depend on the `HasFields` for singleton or no, this is the reason I
+ // added the `.State` now.
+
+ staticBindingsFieldsLength := s.countBindType(Static)
+ allStructFieldsLength := NumFields(s.elemType, false)
+ // check if unexported(and exported) fields are set-ed manually or via binding (at this time we have all fields set-ed inside the "initRef")
+ // i.e &Controller{unexportedField: "my value"}
+ // or dependencies values = "my value" and Controller struct {Field string}
+ // if so then set the temp staticBindingsFieldsLength to that number, so for example:
+ // if static binding length is 0
+ // but an unexported field is set-ed then act that as singleton.
+ if allStructFieldsLength > staticBindingsFieldsLength {
+ structFieldsUnexportedNonZero := LookupNonZeroFieldsValues(s.initRef, false)
+ staticBindingsFieldsLength = len(structFieldsUnexportedNonZero)
+ }
+
+ // println("staticBindingsFieldsLength: ", staticBindingsFieldsLength)
+ // println("allStructFieldsLength: ", allStructFieldsLength)
+
+ // if the number of static values binded is equal to the
+ // total struct's fields(including unexported fields this time) then set as singleton.
+ if staticBindingsFieldsLength == allStructFieldsLength {
+ s.Scope = Singleton
+ // the default is `Stateless`, which means that a new instance should be created
+ // on each inject action by the caller.
+ return
+ }
+
+ s.CanInject = s.Scope == Stateless && s.Has
+}
+
+// fill the static bindings values once.
+func (s *StructInjector) fillStruct() {
+ if !s.Has {
+ return
+ }
+ // if field is Static then set it to the value that passed by the caller,
+ // so will have the static bindings already and we can just use that value instead
+ // of creating new instance.
+ destElem := IndirectValue(s.initRef)
+ for _, f := range s.fields {
+ // if field is Static then set it to the value that passed by the caller,
+ // so will have the static bindings already and we can just use that value instead
+ // of creating new instance.
+ if f.Object.BindType == Static {
+ destElem.FieldByIndex(f.FieldIndex).Set(f.Object.Value)
+ }
+ }
+}
+
+// String returns a debug trace message.
+func (s *StructInjector) String() (trace string) {
+ for i, f := range s.fields {
+ elemField := s.elemType.FieldByIndex(f.FieldIndex)
+ trace += fmt.Sprintf("[%d] %s binding: '%s' for field '%s %s'\n",
+ i+1, bindTypeString(f.Object.BindType), f.Object.Type.String(),
+ elemField.Name, elemField.Type.String())
+ }
+
+ return
+}
+
+func (s *StructInjector) Inject(dest interface{}, ctx ...reflect.Value) {
+ if dest == nil {
+ return
+ }
+
+ v := IndirectValue(ValueOf(dest))
+ s.InjectElem(v, ctx...)
+}
+
+func (s *StructInjector) InjectElem(destElem reflect.Value, ctx ...reflect.Value) {
+ for _, f := range s.fields {
+ f.Object.Assign(ctx, func(v reflect.Value) {
+ destElem.FieldByIndex(f.FieldIndex).Set(v)
+ })
+ }
+}
+
+func (s *StructInjector) Acquire() reflect.Value {
+ if s.Scope == Singleton {
+ return s.initRef
+ }
+ return reflect.New(s.elemType)
+}
+
+func (s *StructInjector) AcquireSlice() []reflect.Value {
+ if s.Scope == Singleton {
+ return s.initRefAsSlice
+ }
+ return []reflect.Value{reflect.New(s.elemType)}
+}
diff --git a/hero/di/values.go b/hero/di/values.go
new file mode 100644
index 00000000..1033b957
--- /dev/null
+++ b/hero/di/values.go
@@ -0,0 +1,126 @@
+package di
+
+import "reflect"
+
+// Values is a shortcut of []reflect.Value,
+// it makes easier to remove and add dependencies.
+type Values []reflect.Value
+
+// NewValues returns new empty (dependencies) values.
+func NewValues() Values {
+ return Values{}
+}
+
+// Clone returns a copy of the current values.
+func (bv Values) Clone() Values {
+ if n := len(bv); n > 0 {
+ values := make(Values, n, n)
+ copy(values, bv)
+ return values
+ }
+
+ return NewValues()
+}
+
+// CloneWithFieldsOf will return a copy of the current values
+// plus the "s" struct's fields that are filled(non-zero) by the caller.
+func (bv Values) CloneWithFieldsOf(s interface{}) Values {
+ values := bv.Clone()
+
+ // add the manual filled fields to the dependencies.
+ filledFieldValues := LookupNonZeroFieldsValues(ValueOf(s), true)
+ values = append(values, filledFieldValues...)
+ return values
+}
+
+// Len returns the length of the current "bv" values slice.
+func (bv Values) Len() int {
+ return len(bv)
+}
+
+// Add adds values as dependencies, if the struct's fields
+// or the function's input arguments needs them, they will be defined as
+// bindings (at build-time) and they will be used (at serve-time).
+func (bv *Values) Add(values ...interface{}) {
+ bv.AddValues(ValuesOf(values)...)
+}
+
+// AddValues same as `Add` but accepts reflect.Value dependencies instead of interface{}
+// and appends them to the list if they pass some checks.
+func (bv *Values) AddValues(values ...reflect.Value) {
+ for _, v := range values {
+ if !goodVal(v) {
+ continue
+ }
+ *bv = append(*bv, v)
+ }
+}
+
+// Remove unbinds a binding value based on the type,
+// it returns true if at least one field is not binded anymore.
+//
+// The "n" indicates the number of elements to remove, if <=0 then it's 1,
+// this is useful because you may have bind more than one value to two or more fields
+// with the same type.
+func (bv *Values) Remove(value interface{}, n int) bool {
+ return bv.remove(reflect.TypeOf(value), n)
+}
+
+func (bv *Values) remove(typ reflect.Type, n int) (ok bool) {
+ input := *bv
+ for i, in := range input {
+ if equalTypes(in.Type(), typ) {
+ ok = true
+ input = input[:i+copy(input[i:], input[i+1:])]
+ if n > 1 {
+ continue
+ }
+ break
+ }
+ }
+
+ *bv = input
+
+ return
+}
+
+// Has returns true if a binder responsible to
+// bind and return a type of "typ" is already registered to this controller.
+func (bv Values) Has(value interface{}) bool {
+ return bv.valueTypeExists(reflect.TypeOf(value))
+}
+
+func (bv Values) valueTypeExists(typ reflect.Type) bool {
+ for _, in := range bv {
+ if equalTypes(in.Type(), typ) {
+ return true
+ }
+ }
+ return false
+}
+
+// AddOnce binds a value to the controller's field with the same type,
+// if it's not binded already.
+//
+// Returns false if binded already or the value is not the proper one for binding,
+// otherwise true.
+func (bv *Values) AddOnce(value interface{}) bool {
+ return bv.addIfNotExists(reflect.ValueOf(value))
+}
+
+func (bv *Values) addIfNotExists(v reflect.Value) bool {
+ var (
+ typ = v.Type() // no element, raw things here.
+ )
+
+ if !goodVal(v) {
+ return false
+ }
+
+ if bv.valueTypeExists(typ) {
+ return false
+ }
+
+ bv.Add(v)
+ return true
+}
diff --git a/hero/func_result.go b/hero/func_result.go
new file mode 100644
index 00000000..86c286c1
--- /dev/null
+++ b/hero/func_result.go
@@ -0,0 +1,474 @@
+package hero
+
+import (
+ "reflect"
+ "strings"
+
+ "github.com/kataras/iris/context"
+ "github.com/kataras/iris/hero/di"
+
+ "github.com/fatih/structs"
+)
+
+// Result is a response dispatcher.
+// 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/hero/overview.
+type Result interface {
+ // Dispatch should sends the response to the context's response writer.
+ Dispatch(ctx context.Context)
+}
+
+var defaultFailureResponse = Response{Code: DefaultErrStatusCode}
+
+// Try will check if "fn" ran without any panics,
+// using recovery,
+// and return its result as the final response
+// otherwise it returns the "failure" response if any,
+// if not then a 400 bad request is being sent.
+//
+// Example usage at: https://github.com/kataras/iris/blob/master/hero/func_result_test.go.
+func Try(fn func() Result, failure ...Result) Result {
+ var failed bool
+ var actionResponse Result
+
+ func() {
+ defer func() {
+ if rec := recover(); rec != nil {
+ failed = true
+ }
+ }()
+ actionResponse = fn()
+ }()
+
+ if failed {
+ if len(failure) > 0 {
+ return failure[0]
+ }
+ return defaultFailureResponse
+ }
+
+ return actionResponse
+}
+
+const slashB byte = '/'
+
+type compatibleErr interface {
+ Error() string
+}
+
+// DefaultErrStatusCode is the default error status code (400)
+// when the response contains an error which is not nil.
+var DefaultErrStatusCode = 400
+
+// DispatchErr writes the error to the response.
+func DispatchErr(ctx context.Context, status int, err error) {
+ if status < 400 {
+ status = DefaultErrStatusCode
+ }
+ ctx.StatusCode(status)
+ if text := err.Error(); text != "" {
+ ctx.WriteString(text)
+ ctx.StopExecution()
+ }
+}
+
+// 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, 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 {
+ status = 200
+ }
+
+ if err != nil {
+ DispatchErr(ctx, status, err)
+ return
+ }
+
+ // write the status code, the rest will need that before any write ofc.
+ ctx.StatusCode(status)
+ if contentType == "" {
+ // to respect any ctx.ContentType(...) call
+ // especially if v is not nil.
+ contentType = ctx.GetContentType()
+ }
+
+ if v != nil {
+ if d, ok := v.(Result); ok {
+ // write the content type now (internal check for empty value)
+ ctx.ContentType(contentType)
+ d.Dispatch(ctx)
+ return
+ }
+
+ if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) {
+ _, err = ctx.JSONP(v)
+ } else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) {
+ _, err = ctx.XML(v, context.XML{Indent: " "})
+ } else {
+ // defaults to json if content type is missing or its application/json.
+ _, err = ctx.JSON(v, context.JSON{Indent: " "})
+ }
+
+ if err != nil {
+ DispatchErr(ctx, status, err)
+ }
+
+ return
+ }
+
+ ctx.ContentType(contentType)
+ // .Write even len(content) == 0 , this should be called in order to call the internal tryWriteHeader,
+ // it will not cost anything.
+ ctx.Write(content)
+}
+
+// DispatchFuncResult is being used internally to resolve
+// and send the method function's output values to the
+// context's response writer using a smart way which
+// respects status code, content type, content, custom struct
+// and an error type.
+// Supports for:
+// 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) and so on...
+//
+// where Get is an HTTP METHOD.
+func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
+ if len(values) == 0 {
+ return
+ }
+
+ var (
+ // 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
+ // 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 {
+
+ // order of these checks matters
+ // for example, first we need to check for status code,
+ // secondly the string (for content type and content)...
+ // if !v.IsValid() || !v.CanInterface() {
+ // continue
+ // }
+ if !v.IsValid() {
+ continue
+ }
+
+ 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 higher in order.
+ break
+ }
+ continue
+ }
+
+ if i, ok := f.(int); ok {
+ statusCode = i
+ continue
+ }
+
+ if s, ok := f.(string); ok {
+ // a string is content type when it contains a slash and
+ // content or custom struct is being calculated already;
+ // (string -> content, string-> content type)
+ // (customStruct, string -> content type)
+ if (len(content) > 0 || custom != nil) && strings.IndexByte(s, slashB) > 0 {
+ contentType = s
+ } else {
+ // otherwise is content
+ content = []byte(s)
+ }
+
+ continue
+ }
+
+ if b, ok := f.([]byte); ok {
+ // it's raw content, get the latest
+ content = b
+ continue
+ }
+
+ if e, ok := f.(compatibleErr); ok {
+ if e != nil { // it's always not nil but keep it here.
+ err = e
+ if statusCode < 400 {
+ statusCode = DefaultErrStatusCode
+ }
+ break // break on first error, error should be in the end but we
+ // need to know break the dispatcher if any error.
+ // at the end; we don't want to write anything to the response if error is not nil.
+ }
+ continue
+ }
+
+ // else it's a custom struct or a dispatcher, we'll decide later
+ // because content type and status code matters
+ // do that check in order to be able to correctly dispatch:
+ // (customStruct, error) -> customStruct filled and error is nil
+ if custom == nil && f != nil {
+ custom = f
+ }
+
+ }
+
+ */
+ switch value := f.(type) {
+ case bool:
+ found = value
+ if !found {
+ // skip everything, skip other values, we don't care about other return values,
+ // this boolean is the higher in order.
+ break
+ }
+ case int:
+ statusCode = value
+ case string:
+ // a string is content type when it contains a slash and
+ // content or custom struct is being calculated already;
+ // (string -> content, string-> content type)
+ // (customStruct, string -> content type)
+ if (len(content) > 0 || custom != nil) && strings.IndexByte(value, slashB) > 0 {
+ contentType = value
+ } else {
+ // otherwise is content
+ content = []byte(value)
+ }
+
+ case []byte:
+ // it's raw content, get the latest
+ content = value
+ case compatibleErr:
+ if value != nil { // it's always not nil but keep it here.
+ err = value
+ if statusCode < 400 {
+ statusCode = DefaultErrStatusCode
+ }
+ break // break on first error, error should be in the end but we
+ // need to know break the dispatcher if any error.
+ // at the end; we don't want to write anything to the response if error is not nil.
+ }
+ default:
+ // else it's a custom struct or a dispatcher, we'll decide later
+ // because content type and status code matters
+ // do that check in order to be able to correctly dispatch:
+ // (customStruct, error) -> customStruct filled and error is nil
+ if custom == nil && f != nil {
+ custom = f
+ }
+ }
+ }
+
+ DispatchCommon(ctx, statusCode, contentType, content, custom, err, found)
+}
+
+// Response completes the `methodfunc.Result` interface.
+// It's being used as an alternative return value which
+// wraps the status code, the content type, a content as bytes or as string
+// and an error, it's smart enough to complete the request and send the correct response to the client.
+type Response struct {
+ Code int
+ ContentType string
+ Content []byte
+
+ // if not empty then content type is the text/plain
+ // and content is the text as []byte.
+ Text string
+ // If not nil then it will fire that as "application/json" or the
+ // "ContentType" if not empty.
+ Object interface{}
+
+ // If Path is not empty then it will redirect
+ // the client to this Path, if Code is >= 300 and < 400
+ // then it will use that Code to do the redirection, otherwise
+ // StatusFound(302) or StatusSeeOther(303) for post methods will be used.
+ // Except when err != nil.
+ Path string
+
+ // if not empty then fire a 400 bad request error
+ // unless the Status is > 200, then fire that error code
+ // with the Err.Error() string as its content.
+ //
+ // if Err.Error() is empty then it fires the custom error handler
+ // if any otherwise the framework sends the default http error text based on the status.
+ Err error
+ Try func() int
+
+ // if true then it skips everything else and it throws a 404 not found error.
+ // Can be named as Failure but NotFound is more precise name in order
+ // to be visible that it's different than the `Err`
+ // because it throws a 404 not found instead of a 400 bad request.
+ // NotFound bool
+ // let's don't add this yet, it has its dangerous of missuse.
+}
+
+var _ Result = Response{}
+
+// Dispatch writes the response result to the context's response writer.
+func (r Response) Dispatch(ctx context.Context) {
+ if r.Path != "" && r.Err == nil {
+ // it's not a redirect valid status
+ if r.Code < 300 || r.Code >= 400 {
+ if ctx.Method() == "POST" {
+ r.Code = 303 // StatusSeeOther
+ }
+ r.Code = 302 // StatusFound
+ }
+ ctx.Redirect(r.Path, r.Code)
+ return
+ }
+
+ if s := r.Text; s != "" {
+ r.Content = []byte(s)
+ }
+
+ DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err, true)
+}
+
+// View completes the `hero.Result` interface.
+// It's being used as an alternative return value which
+// 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/hero/overview/web/controllers/hello_controller.go.
+type View struct {
+ Name string
+ Layout string
+ Data interface{} // map or a custom struct.
+ Code int
+ Err error
+}
+
+var _ Result = View{}
+
+const dotB = byte('.')
+
+// DefaultViewExt is the default extension if `view.Name `is missing,
+// but note that it doesn't care about
+// the app.RegisterView(iris.$VIEW_ENGINE("./$dir", "$ext"))'s $ext.
+// so if you don't use the ".html" as extension for your files
+// you have to append the extension manually into the `view.Name`
+// or change this global variable.
+var DefaultViewExt = ".html"
+
+func ensureExt(s string) string {
+ if len(s) == 0 {
+ return "index" + DefaultViewExt
+ }
+
+ if strings.IndexByte(s, dotB) < 1 {
+ s += DefaultViewExt
+ }
+
+ return s
+}
+
+// Dispatch writes the template filename, template layout and (any) data to the client.
+// Completes the `Result` interface.
+func (r View) Dispatch(ctx context.Context) { // r as Response view.
+ if r.Err != nil {
+ if r.Code < 400 {
+ r.Code = DefaultErrStatusCode
+ }
+ ctx.StatusCode(r.Code)
+ ctx.WriteString(r.Err.Error())
+ ctx.StopExecution()
+ return
+ }
+
+ if r.Code > 0 {
+ ctx.StatusCode(r.Code)
+ }
+
+ if r.Name != "" {
+ r.Name = ensureExt(r.Name)
+
+ if r.Layout != "" {
+ r.Layout = ensureExt(r.Layout)
+ ctx.ViewLayout(r.Layout)
+ }
+
+ if r.Data != nil {
+ // In order to respect any c.Ctx.ViewData that may called manually before;
+ dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey()
+ if ctx.Values().Get(dataKey) == nil {
+ // if no c.Ctx.ViewData set-ed before (the most common scenario) then do a
+ // simple set, it's faster.
+ ctx.Values().Set(dataKey, r.Data)
+ } else {
+ // else check if r.Data is map or struct, if struct convert it to map,
+ // do a range loop and modify the data one by one.
+ // context.Map is actually a map[string]interface{} but we have to make that check:
+ if m, ok := r.Data.(map[string]interface{}); ok {
+ setViewData(ctx, m)
+ } else if m, ok := r.Data.(context.Map); ok {
+ setViewData(ctx, m)
+ } else if di.IndirectValue(reflect.ValueOf(r.Data)).Kind() == reflect.Struct {
+ setViewData(ctx, structs.Map(r))
+ }
+ }
+ }
+
+ ctx.View(r.Name)
+ }
+}
+
+func setViewData(ctx context.Context, data map[string]interface{}) {
+ for k, v := range data {
+ ctx.ViewData(k, v)
+ }
+}
diff --git a/hero/func_result_test.go b/hero/func_result_test.go
new file mode 100644
index 00000000..54a62c62
--- /dev/null
+++ b/hero/func_result_test.go
@@ -0,0 +1,152 @@
+package hero_test
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/kataras/iris"
+ "github.com/kataras/iris/context"
+ "github.com/kataras/iris/httptest"
+
+ . "github.com/kataras/iris/hero"
+)
+
+func GetText() string {
+ return "text"
+}
+
+func GetStatus() int {
+ return iris.StatusBadGateway
+}
+
+func GetTextWithStatusOk() (string, int) {
+ return "OK", iris.StatusOK
+}
+
+// tests should have output arguments mixed
+func GetStatusWithTextNotOkBy(first string, second string) (int, string) {
+ return iris.StatusForbidden, "NOT_OK_" + first + second
+}
+
+func GetTextAndContentType() (string, string) {
+ return "text", "text/html"
+}
+
+type testCustomResult struct {
+ HTML string
+}
+
+// The only one required function to make that a custom Response dispatcher.
+func (r testCustomResult) Dispatch(ctx context.Context) {
+ ctx.HTML(r.HTML)
+}
+
+func GetCustomResponse() testCustomResult {
+ return testCustomResult{"text"}
+}
+
+func GetCustomResponseWithStatusOk() (testCustomResult, int) {
+ return testCustomResult{"OK"}, iris.StatusOK
+}
+
+func GetCustomResponseWithStatusNotOk() (testCustomResult, int) {
+ return testCustomResult{"internal server error"}, iris.StatusInternalServerError
+}
+
+type testCustomStruct struct {
+ Name string `json:"name" xml:"name"`
+ Age int `json:"age" xml:"age"`
+}
+
+func GetCustomStruct() testCustomStruct {
+ return testCustomStruct{"Iris", 2}
+}
+
+func GetCustomStructWithStatusNotOk() (testCustomStruct, int) {
+ return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError
+}
+
+func GetCustomStructWithContentType() (testCustomStruct, string) {
+ return testCustomStruct{"Iris", 2}, "text/xml"
+}
+
+func GetCustomStructWithError(ctx iris.Context) (s testCustomStruct, err error) {
+ s = testCustomStruct{"Iris", 2}
+ if ctx.URLParamExists("err") {
+ err = errors.New("omit return of testCustomStruct and fire error")
+ }
+
+ // it should send the testCustomStruct as JSON if error is nil
+ // otherwise it should fire the default error(BadRequest) with the error's text.
+ return
+}
+
+func TestFuncResult(t *testing.T) {
+ app := iris.New()
+ h := New()
+ // for any 'By', by is not required but we use this suffix here, like controllers
+ // to make it easier for the future to resolve if any bug.
+ // add the binding for path parameters.
+
+ app.Get("/text", h.Handler(GetText))
+ app.Get("/status", h.Handler(GetStatus))
+ app.Get("/text/with/status/ok", h.Handler(GetTextWithStatusOk))
+ app.Get("/status/with/text/not/ok/{first}/{second}", h.Handler(GetStatusWithTextNotOkBy))
+ app.Get("/text/and/content/type", h.Handler(GetTextAndContentType))
+ //
+ app.Get("/custom/response", h.Handler(GetCustomResponse))
+ app.Get("/custom/response/with/status/ok", h.Handler(GetCustomResponseWithStatusOk))
+ app.Get("/custom/response/with/status/not/ok", h.Handler(GetCustomResponseWithStatusNotOk))
+ //
+ app.Get("/custom/struct", h.Handler(GetCustomStruct))
+ app.Get("/custom/struct/with/status/not/ok", h.Handler(GetCustomStructWithStatusNotOk))
+ app.Get("/custom/struct/with/content/type", h.Handler(GetCustomStructWithContentType))
+ app.Get("/custom/struct/with/error", h.Handler(GetCustomStructWithError))
+
+ e := httptest.New(t, app)
+
+ e.GET("/text").Expect().Status(iris.StatusOK).
+ Body().Equal("text")
+
+ e.GET("/status").Expect().Status(iris.StatusBadGateway)
+
+ e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK).
+ Body().Equal("OK")
+
+ e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden).
+ Body().Equal("NOT_OK_firstsecond")
+ // Author's note: <-- if that fails means that the last binder called for both input args,
+ // see path_param_binder.go
+
+ e.GET("/text/and/content/type").Expect().Status(iris.StatusOK).
+ ContentType("text/html", "utf-8").
+ Body().Equal("text")
+
+ e.GET("/custom/response").Expect().Status(iris.StatusOK).
+ ContentType("text/html", "utf-8").
+ Body().Equal("text")
+ e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK).
+ ContentType("text/html", "utf-8").
+ Body().Equal("OK")
+ e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError).
+ ContentType("text/html", "utf-8").
+ Body().Equal("internal server error")
+
+ expectedResultFromCustomStruct := map[string]interface{}{
+ "name": "Iris",
+ "age": 2,
+ }
+ e.GET("/custom/struct").Expect().Status(iris.StatusOK).
+ JSON().Equal(expectedResultFromCustomStruct)
+ e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError).
+ JSON().Equal(expectedResultFromCustomStruct)
+ e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK).
+ ContentType("text/xml", "utf-8")
+ e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK).
+ JSON().Equal(expectedResultFromCustomStruct)
+ e.GET("/custom/struct/with/error").WithQuery("err", true).Expect().
+ Status(iris.StatusBadRequest). // the default status code if error is not nil
+ // the content should be not JSON it should be the status code's text
+ // it will fire the error's text
+ Body().Equal("omit return of testCustomStruct and fire error")
+}
diff --git a/hero/handler.go b/hero/handler.go
new file mode 100644
index 00000000..bb427d77
--- /dev/null
+++ b/hero/handler.go
@@ -0,0 +1,97 @@
+package hero
+
+import (
+ "fmt"
+ "reflect"
+ "runtime"
+
+ "github.com/kataras/iris/hero/di"
+
+ "github.com/kataras/golog"
+ "github.com/kataras/iris/context"
+)
+
+var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem()
+
+// IsContext returns true if the "inTyp" is a type of Context.
+func IsContext(inTyp reflect.Type) bool {
+ return inTyp.Implements(contextTyp)
+}
+
+// checks if "handler" is context.Handler: func(context.Context).
+func isContextHandler(handler interface{}) (context.Handler, bool) {
+ h, is := handler.(context.Handler)
+ if !is {
+ fh, is := handler.(func(context.Context))
+ if is {
+ return fh, is
+ }
+ }
+ return h, is
+}
+
+func validateHandler(handler interface{}) error {
+ if typ := reflect.TypeOf(handler); !di.IsFunc(typ) {
+ return fmt.Errorf("handler expected to be a kind of func but got typeof(%s)", typ.String())
+ }
+ return nil
+}
+
+// makeHandler accepts a "handler" function which can accept any input arguments that match
+// with the "values" types and any output result, that matches the hero types, like string, int (string,int),
+// custom structs, Result(View | Response) and anything that you can imagine,
+// and returns a low-level `context/iris.Handler` which can be used anywhere in the Iris Application,
+// as middleware or as simple route handler or party handler or subdomain handler-router.
+func makeHandler(handler interface{}, values ...reflect.Value) (context.Handler, error) {
+ if err := validateHandler(handler); err != nil {
+ return nil, err
+ }
+
+ if h, is := isContextHandler(handler); is {
+ golog.Warnf("the standard API to register a context handler could be used instead")
+ return h, nil
+ }
+
+ fn := reflect.ValueOf(handler)
+ n := fn.Type().NumIn()
+
+ if n == 0 {
+ h := func(ctx context.Context) {
+ DispatchFuncResult(ctx, fn.Call(di.EmptyIn))
+ }
+
+ return h, nil
+ }
+
+ funcInjector := di.Func(fn, values...)
+ valid := funcInjector.Length == n
+
+ if !valid {
+ // is invalid when input len and values are not match
+ // or their types are not match, we will take look at the
+ // second statement, here we will re-try it
+ // using binders for path parameters: string, int, int64, bool.
+ // We don't have access to the path, so neither to the macros here,
+ // but in mvc. So we have to do it here.
+ if valid = funcInjector.Retry(new(params).resolve); !valid {
+ pc := fn.Pointer()
+ fpc := runtime.FuncForPC(pc)
+ callerFileName, callerLineNumber := fpc.FileLine(pc)
+ callerName := fpc.Name()
+
+ err := fmt.Errorf("input arguments length(%d) and valid binders length(%d) are not equal for typeof '%s' which is defined at %s:%d by %s",
+ n, funcInjector.Length, fn.Type().String(), callerFileName, callerLineNumber, callerName)
+ return nil, err
+ }
+ }
+
+ h := func(ctx context.Context) {
+ // in := make([]reflect.Value, n, n)
+ // funcInjector.Inject(&in, reflect.ValueOf(ctx))
+ // DispatchFuncResult(ctx, fn.Call(in))
+ DispatchFuncResult(ctx, funcInjector.Call(reflect.ValueOf(ctx)))
+ }
+
+ return h, nil
+
+}
diff --git a/hero/handler_test.go b/hero/handler_test.go
new file mode 100644
index 00000000..9766b377
--- /dev/null
+++ b/hero/handler_test.go
@@ -0,0 +1,128 @@
+package hero_test
+
+// black-box
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/kataras/iris"
+ "github.com/kataras/iris/httptest"
+
+ . "github.com/kataras/iris/hero"
+)
+
+// dynamic func
+type testUserStruct struct {
+ ID int64
+ Username string
+}
+
+func testBinderFunc(ctx iris.Context) testUserStruct {
+ id, _ := ctx.Params().GetInt64("id")
+ username := ctx.Params().Get("username")
+ return testUserStruct{
+ ID: id,
+ Username: username,
+ }
+}
+
+// service
+type (
+ // these TestService and TestServiceImpl could be in lowercase, unexported
+ // but the `Say` method should be exported however we have those exported
+ // because of the controller handler test.
+ TestService interface {
+ Say(string) string
+ }
+ TestServiceImpl struct {
+ prefix string
+ }
+)
+
+func (s *TestServiceImpl) Say(message string) string {
+ return s.prefix + " " + message
+}
+
+var (
+ // binders, as user-defined
+ testBinderFuncUserStruct = testBinderFunc
+ testBinderService = &TestServiceImpl{prefix: "say"}
+ testBinderFuncParam = func(ctx iris.Context) string {
+ return ctx.Params().Get("param")
+ }
+
+ // consumers
+ // a context as first input arg, which is not needed to be binded manually,
+ // and a user struct which is binded to the input arg by the #1 func(ctx) any binder.
+ testConsumeUserHandler = func(ctx iris.Context, user testUserStruct) {
+ ctx.JSON(user)
+ }
+
+ // just one input arg, the service which is binded by the #2 service binder.
+ testConsumeServiceHandler = func(service TestService) string {
+ return service.Say("something")
+ }
+ // just one input arg, a standar string which is binded by the #3 func(ctx) any binder.
+ testConsumeParamHandler = func(myParam string) string {
+ return "param is: " + myParam
+ }
+)
+
+func TestHandler(t *testing.T) {
+ Register(testBinderFuncUserStruct, testBinderService, testBinderFuncParam)
+ var (
+ h1 = Handler(testConsumeUserHandler)
+ h2 = Handler(testConsumeServiceHandler)
+ h3 = Handler(testConsumeParamHandler)
+ )
+
+ testAppWithHeroHandlers(t, h1, h2, h3)
+}
+
+func testAppWithHeroHandlers(t *testing.T, h1, h2, h3 iris.Handler) {
+ app := iris.New()
+ app.Get("/{id:long}/{username:string}", h1)
+ app.Get("/service", h2)
+ app.Get("/param/{param:string}", h3)
+
+ expectedUser := testUserStruct{
+ ID: 42,
+ Username: "kataras",
+ }
+
+ e := httptest.New(t, app)
+ // 1
+ e.GET(fmt.Sprintf("/%d/%s", expectedUser.ID, expectedUser.Username)).Expect().Status(httptest.StatusOK).
+ JSON().Equal(expectedUser)
+ // 2
+ e.GET("/service").Expect().Status(httptest.StatusOK).
+ Body().Equal("say something")
+ // 3
+ e.GET("/param/the_param_value").Expect().Status(httptest.StatusOK).
+ Body().Equal("param is: the_param_value")
+}
+
+// TestBindFunctionAsFunctionInputArgument tests to bind
+// a whole dynamic function based on the current context
+// as an input argument in the hero handler's function.
+func TestBindFunctionAsFunctionInputArgument(t *testing.T) {
+ app := iris.New()
+ postsBinder := func(ctx iris.Context) func(string) string {
+ return ctx.PostValue // or FormValue, the same here.
+ }
+
+ h := New().Register(postsBinder).Handler(func(get func(string) string) string {
+ // send the `ctx.PostValue/FormValue("username")` value
+ // to the client.
+ return get("username")
+ })
+
+ app.Post("/", h)
+
+ e := httptest.New(t, app)
+
+ expectedUsername := "kataras"
+ e.POST("/").WithFormField("username", expectedUsername).
+ Expect().Status(iris.StatusOK).Body().Equal(expectedUsername)
+}
diff --git a/hero/hero.go b/hero/hero.go
new file mode 100644
index 00000000..9862816d
--- /dev/null
+++ b/hero/hero.go
@@ -0,0 +1,106 @@
+package hero
+
+import (
+ "github.com/kataras/iris/hero/di"
+
+ "github.com/kataras/golog"
+ "github.com/kataras/iris/context"
+)
+
+// def is the default herp value which can be used for dependencies share.
+var def = New()
+
+// Hero contains the Dependencies which will be binded
+// to the controller(s) or handler(s) that can be created
+// using the Hero's `Handler` and `Controller` methods.
+//
+// This is not exported for being used by everyone, use it only when you want
+// to share heroes between multi mvc.go#Application
+// or make custom hero handlers that can be used on the standard
+// iris' APIBuilder. The last one reason is the most useful here,
+// although end-devs can use the `MakeHandler` as well.
+//
+// For a more high-level structure please take a look at the "mvc.go#Application".
+type Hero struct {
+ values di.Values
+}
+
+// New returns a new Hero, a container for dependencies and a factory
+// for handlers and controllers, this is used internally by the `mvc#Application` structure.
+// Please take a look at the structure's documentation for more information.
+func New() *Hero {
+ return &Hero{
+ values: di.NewValues(),
+ }
+}
+
+// Dependencies returns the dependencies collection if the default hero,
+// those can be modified at any way but before the consumer `Handler`.
+func Dependencies() *di.Values {
+ return def.Dependencies()
+}
+
+// Dependencies returns the dependencies collection of this hero,
+// those can be modified at any way but before the consumer `Handler`.
+func (h *Hero) Dependencies() *di.Values {
+ return &h.values
+}
+
+// Register adds one or more values as dependencies.
+// The value can be a single struct value-instance or a function
+// which has one input and one output, the input should be
+// an `iris.Context` and the output can be any type, that output type
+// will be binded to the handler's input argument, if matching.
+//
+// Example: `.Register(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`.
+func Register(values ...interface{}) *Hero {
+ return def.Register(values...)
+}
+
+// Register adds one or more values as dependencies.
+// The value can be a single struct value-instance or a function
+// which has one input and one output, the input should be
+// an `iris.Context` and the output can be any type, that output type
+// will be binded to the handler's input argument, if matching.
+//
+// Example: `.Register(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`.
+func (h *Hero) Register(values ...interface{}) *Hero {
+ h.values.Add(values...)
+ return h
+}
+
+// Clone creates and returns a new hero with the default Dependencies.
+// It copies the default's dependencies and returns a new hero.
+func Clone() *Hero {
+ return def.Clone()
+}
+
+// Clone creates and returns a new hero with the parent's(current) Dependencies.
+// It copies the current "h" dependencies and returns a new hero.
+func (h *Hero) Clone() *Hero {
+ child := New()
+ child.values = h.values.Clone()
+ return child
+}
+
+// Handler accepts a "handler" function which can accept any input arguments that match
+// with the Hero's `Dependencies` and any output result; like string, int (string,int),
+// custom structs, Result(View | Response) and anything you can imagine.
+// It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application,
+// as middleware or as simple route handler or subdomain's handler.
+func Handler(handler interface{}) context.Handler {
+ return def.Handler(handler)
+}
+
+// Handler accepts a handler "fn" function which can accept any input arguments that match
+// with the Hero's `Dependencies` and any output result; like string, int (string,int),
+// custom structs, Result(View | Response) and anything you can imagine.
+// It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application,
+// as middleware or as simple route handler or subdomain's handler.
+func (h *Hero) Handler(fn interface{}) context.Handler {
+ handler, err := makeHandler(fn, h.values.Clone()...)
+ if err != nil {
+ golog.Errorf("hero handler: %v", err)
+ }
+ return handler
+}
diff --git a/hero/param.go b/hero/param.go
new file mode 100644
index 00000000..9a9f028f
--- /dev/null
+++ b/hero/param.go
@@ -0,0 +1,68 @@
+package hero
+
+import (
+ "reflect"
+
+ "github.com/kataras/iris/context"
+)
+
+// weak because we don't have access to the path, neither
+// the macros, so this is just a guess based on the index of the path parameter,
+// the function's path parameters should be like a chain, in the same order as
+// the caller registers a route's path.
+// A context or any value(s) can be in front or back or even between them.
+type params struct {
+ // the next function input index of where the next path parameter
+ // should be inside the CONTEXT.
+ next int
+}
+
+func (p *params) resolve(index int, typ reflect.Type) (reflect.Value, bool) {
+ currentParamIndex := p.next
+ v, ok := resolveParam(currentParamIndex, typ)
+
+ p.next = p.next + 1
+ return v, ok
+}
+
+func resolveParam(currentParamIndex int, typ reflect.Type) (reflect.Value, bool) {
+ var fn interface{}
+
+ switch typ.Kind() {
+ case reflect.Int:
+ fn = func(ctx context.Context) int {
+ // the second "ok/found" check is not necessary,
+ // because even if the entry didn't found on that "index"
+ // it will return an empty entry which will return the
+ // default value passed from the xDefault(def) because its `ValueRaw` is nil.
+ entry, _ := ctx.Params().GetEntryAt(currentParamIndex)
+ v, _ := entry.IntDefault(0)
+ return v
+ }
+ case reflect.Int64:
+ fn = func(ctx context.Context) int64 {
+ entry, _ := ctx.Params().GetEntryAt(currentParamIndex)
+ v, _ := entry.Int64Default(0)
+
+ return v
+ }
+ case reflect.Bool:
+ fn = func(ctx context.Context) bool {
+ entry, _ := ctx.Params().GetEntryAt(currentParamIndex)
+ v, _ := entry.BoolDefault(false)
+ return v
+ }
+ case reflect.String:
+ fn = func(ctx context.Context) string {
+ entry, _ := ctx.Params().GetEntryAt(currentParamIndex)
+ // print(entry.Key + " with index of: ")
+ // print(currentParamIndex)
+ // println(" and value: " + entry.String())
+ return entry.String()
+ }
+ default:
+ return reflect.Value{}, false
+ }
+
+ return reflect.ValueOf(fn), true
+}
diff --git a/hero/param_test.go b/hero/param_test.go
new file mode 100644
index 00000000..112edd0f
--- /dev/null
+++ b/hero/param_test.go
@@ -0,0 +1,48 @@
+package hero
+
+import (
+ "testing"
+
+ "github.com/kataras/iris/context"
+)
+
+func TestPathParams(t *testing.T) {
+ got := ""
+ h := New()
+ handler := h.Handler(func(firstname string, lastname string) {
+ got = firstname + lastname
+ })
+
+ h.Register(func(ctx context.Context) func() string { return func() string { return "" } })
+ handlerWithOther := h.Handler(func(f func() string, firstname string, lastname string) {
+ got = f() + firstname + lastname
+ })
+
+ handlerWithOtherBetweenThem := h.Handler(func(firstname string, f func() string, lastname string) {
+ got = f() + firstname + lastname
+ })
+
+ ctx := context.NewContext(nil)
+ ctx.Params().Set("firstname", "Gerasimos")
+ ctx.Params().Set("lastname", "Maropoulos")
+ handler(ctx)
+ expected := "GerasimosMaropoulos"
+ if got != expected {
+ t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got)
+ }
+
+ got = ""
+ handlerWithOther(ctx)
+ expected = "GerasimosMaropoulos"
+ if got != expected {
+ t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got)
+ }
+
+ got = ""
+ handlerWithOtherBetweenThem(ctx)
+ expected = "GerasimosMaropoulos"
+ if got != expected {
+ t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got)
+ }
+
+}
diff --git a/hero/session.go b/hero/session.go
new file mode 100644
index 00000000..49887104
--- /dev/null
+++ b/hero/session.go
@@ -0,0 +1,12 @@
+package hero
+
+import (
+ "github.com/kataras/iris/context"
+ "github.com/kataras/iris/sessions"
+)
+
+// Session is a binder that will fill a *sessions.Session function input argument
+// or a Controller struct's field.
+func Session(sess *sessions.Sessions) func(context.Context) *sessions.Session {
+ return sess.Start
+}
diff --git a/mvc/AUTHORS b/mvc/AUTHORS
index 669cf5d3..848245bb 100644
--- a/mvc/AUTHORS
+++ b/mvc/AUTHORS
@@ -1,4 +1 @@
-# This is the official list of Iris MVC authors for copyright
-# purposes.
-
Gerasimos Maropoulos
diff --git a/mvc/func_result_test.go b/mvc/controller_method_result_test.go
similarity index 98%
rename from mvc/func_result_test.go
rename to mvc/controller_method_result_test.go
index 0ef9db37..f7cc4c0b 100644
--- a/mvc/func_result_test.go
+++ b/mvc/controller_method_result_test.go
@@ -11,9 +11,6 @@ import (
. "github.com/kataras/iris/mvc"
)
-// activator/methodfunc/func_caller.go.
-// and activator/methodfunc/func_result_dispatcher.go
-
type testControllerMethodResult struct {
Ctx context.Context
}
diff --git a/mvc/session.go b/mvc/session.go
index c1e00964..9e20cc7d 100644
--- a/mvc/session.go
+++ b/mvc/session.go
@@ -5,13 +5,8 @@ import (
"github.com/kataras/iris/sessions"
)
-// Session -> TODO: think of move all bindings to
-// a different folder like "bindings"
-// so it will be used as .Bind(bindings.Session(manager))
-// or let it here but change the rest of the binding names as well
-// because they are not "binders", their result are binders to be precise.
+// Session is a binder that will fill a *sessions.Session function input argument
+// or a Controller struct's field.
func Session(sess *sessions.Sessions) func(context.Context) *sessions.Session {
- return func(ctx context.Context) *sessions.Session {
- return sess.Start(ctx)
- }
+ return sess.Start
}