diff --git a/_examples/README.md b/_examples/README.md index 261c4972ec..f988d46d7b 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 0000000000..7649d487e7 --- /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 0000000000..dbbe4fdfe9 --- /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 0000000000..58f0b04e95 --- /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 0000000000..de3275acce --- /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 0000000000..16ed641230 --- /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 0000000000..c9b6eacfdd --- /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 0000000000..8feeca14fc --- /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 0000000000..d3947d1bae --- /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 0000000000..9e7b03d6d0 --- /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 0000000000..d6dd5ac66a --- /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 bf5c1ecc59..4f1412ac89 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 f8d288a0c5..0f1f5d378c 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 0000000000..848245bb12 --- /dev/null +++ b/hero/AUTHORS @@ -0,0 +1 @@ +Gerasimos Maropoulos diff --git a/hero/LICENSE b/hero/LICENSE new file mode 100644 index 0000000000..a0b2d92fe0 --- /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 0000000000..4244cfa0a9 --- /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 0000000000..569cb3922f --- /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 0000000000..e41447934c --- /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 0000000000..fe69dfb0f0 --- /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 0000000000..392abcc587 --- /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 0000000000..9713ddb8ce --- /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 0000000000..cc48b1e45e --- /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 0000000000..1033b95752 --- /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 0000000000..86c286c1bc --- /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 0000000000..54a62c6270 --- /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 0000000000..bb427d772c --- /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 0000000000..9766b37783 --- /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 0000000000..9862816d12 --- /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 0000000000..9a9f028f33 --- /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 0000000000..112edd0f2e --- /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 0000000000..49887104cd --- /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 669cf5d3af..848245bb12 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 0ef9db37c5..f7cc4c0b88 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 c1e009649a..9e20cc7d81 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 }