diff --git a/HISTORY.md b/HISTORY.md index 72496cec..74f6b150 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,7 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements -- Make `Context.Domain()` customizable by letting developers to set the custom `Context.GetDomain` package-level function. +- Add new `iris.NewGuide` which helps you build a simple and nice JSON API with services as dependencies and better design pattern. +- Make `Context.Domain()` customizable by letting developers to modify the `Context.GetDomain` package-level function. - Remove Request Context-based Transaction feature as its usage can be replaced with just the Iris Context (as of go1.7+) and better [project](_examples/project) structure. - Fix [#1882](https://github.com/kataras/iris/issues/1882) - Fix [#1877](https://github.com/kataras/iris/issues/1877) diff --git a/README.md b/README.md index cf7aa513..4de86b2c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Iris is a fast, simple yet fully featured and very efficient web framework for Go. -It provides a beautifully expressive and easy to use foundation for your next website or API. +It provides a [beautifully](iris_guide.go#L31-L44) expressive and easy to use foundation for your next website or API. ```go package main @@ -38,7 +38,37 @@ func main() { } ``` -
Simple Handler +
API Guide + +```go +package main + +import ( + // [other packages...] + + "github.com/kataras/iris/v12" +) + +func main() { + app := iris.NewGuide(). + AllowOrigin("*"). + Compression(true). + Health(true, "development", "kataras"). + Timeout(0, 20*time.Second, 20*time.Second). + Middlewares(basicauth.New(...)). + Services( + // NewDatabase(), + // NewPostgresRepositoryRegistry, + // NewUserService, + ). + API("/users", new(UsersAPI)). + Listen(":80") +} +``` + +
+ +
More with simple Handler ```go package main diff --git a/_examples/routing/subdomains/www/main.go b/_examples/routing/subdomains/www/main.go index 2f095585..0215f6fa 100644 --- a/_examples/routing/subdomains/www/main.go +++ b/_examples/routing/subdomains/www/main.go @@ -32,7 +32,7 @@ func newApp() *iris.Application { // party or subdomain: // Get all routes that are registered so far, including all "Parties" and subdomains: currentRoutes := app.GetRoutes() - // Register them to the www subdomain/vhost as well: + // Register them to the www subdomain/VHost as well: for _, r := range currentRoutes { www.Handle(r.Method, r.Tmpl().Src, r.Handlers...) } diff --git a/configuration.go b/configuration.go index 43dab318..49cab717 100644 --- a/configuration.go +++ b/configuration.go @@ -613,9 +613,10 @@ type ( // app.Configure(iris.WithConfiguration(conf)) OR // app.Run/Listen(..., iris.WithConfiguration(conf)). type Configuration struct { - // vhost is private and set only with .Run/Listen methods, it cannot be changed after the first set. + // VHost lets you customize the trusted domain this server should run on. + // Its value will be used as the return value of Context.Domain() too. // It can be retrieved by the context if needed (i.e router for subdomains) - vhost string + VHost string `ini:"v_host" json:"vHost" yaml:"VHost" toml:"VHost" env:"V_HOST"` // LogLevel is the log level the application should use to output messages. // Logger, by default, is mostly used on Build state but it is also possible @@ -941,7 +942,7 @@ var _ context.ConfigurationReadOnly = (*Configuration)(nil) // GetVHost returns the non-exported vhost config field. func (c *Configuration) GetVHost() string { - return c.vhost + return c.VHost } // GetLogLevel returns the LogLevel field. diff --git a/hero/dependency.go b/hero/dependency.go index 48527832..d6f4f6d4 100644 --- a/hero/dependency.go +++ b/hero/dependency.go @@ -157,7 +157,16 @@ func fromFunc(v reflect.Value, dest *Dependency) bool { numOut := typ.NumOut() if numIn == 0 { - panic("bad value: function has zero inputs") + // it's an empty function, that must return a structure. + if numOut != 1 { + firstOutType := indirectType(typ.Out(0)) + if firstOutType.Kind() != reflect.Struct && firstOutType.Kind() != reflect.Interface { + panic(fmt.Sprintf("bad value: function has zero inputs: empty input function must output a single value but got: length=%v, type[0]=%s", numOut, firstOutType.String())) + } + } + + // fallback to structure. + return fromStructValue(v.Call(nil)[0], dest) } if numOut == 0 { diff --git a/iris.go b/iris.go index 20318c3f..217d13d8 100644 --- a/iris.go +++ b/iris.go @@ -545,17 +545,21 @@ func (app *Application) NewHost(srv *http.Server) *host.Supervisor { // bind the constructed server and return it su := host.New(srv) - if app.config.vhost == "" { // vhost now is useful for router subdomain on wildcard subdomains, + if app.config.VHost == "" { // vhost now is useful for router subdomain on wildcard subdomains, // in order to correct decide what to do on: // mydomain.com -> invalid // localhost -> invalid // sub.mydomain.com -> valid // sub.localhost -> valid // we need the host (without port if 80 or 443) in order to validate these, so: - app.config.vhost = netutil.ResolveVHost(srv.Addr) + app.config.VHost = netutil.ResolveVHost(srv.Addr) + } else { + context.GetDomain = func(_ string) string { // #1886 + return app.config.VHost + } } - // app.logger.Debugf("Host: virtual host is %s", app.config.vhost) + // app.logger.Debugf("Host: virtual host is %s", app.config.VHost) // the below schedules some tasks that will run among the server @@ -604,6 +608,15 @@ func (app *Application) NewHost(srv *http.Server) *host.Supervisor { return su } +// func (app *Application) OnShutdown(closers ...func()) { +// for _,cb := range closers { +// if cb == nil { +// continue +// } +// RegisterOnInterrupt(cb) +// } +// } + // Shutdown gracefully terminates all the application's server hosts and any tunnels. // Returns an error on the first failure, otherwise nil. func (app *Application) Shutdown(ctx stdContext.Context) error { @@ -795,7 +808,7 @@ type Runner func(*Application) error // See `Run` for more. func Listener(l net.Listener, hostConfigs ...host.Configurator) Runner { return func(app *Application) error { - app.config.vhost = netutil.ResolveVHost(l.Addr().String()) + app.config.VHost = netutil.ResolveVHost(l.Addr().String()) return app.NewHost(&http.Server{Addr: l.Addr().String()}). Configure(hostConfigs...). Serve(l) @@ -1059,7 +1072,7 @@ func (app *Application) tryStartTunneling() { publicAddr := publicAddrs[0] // to make subdomains resolution still based on this new remote, public addresses. - app.config.vhost = publicAddr[strings.Index(publicAddr, "://")+3:] + app.config.VHost = publicAddr[strings.Index(publicAddr, "://")+3:] directLog := []byte(fmt.Sprintf("• Public Address: %s\n", publicAddr)) app.logger.Printer.Write(directLog) // nolint:errcheck diff --git a/iris_guide.go b/iris_guide.go new file mode 100644 index 00000000..cccce2df --- /dev/null +++ b/iris_guide.go @@ -0,0 +1,530 @@ +package iris + +import ( + "time" + + "github.com/kataras/iris/v12/core/router" + + "github.com/kataras/iris/v12/middleware/cors" + "github.com/kataras/iris/v12/middleware/modrevision" + "github.com/kataras/iris/v12/middleware/recover" + + "github.com/kataras/iris/v12/x/errors" +) + +// NewGuide returns a simple Iris API builder. +// +// Example Code: +/* + package main + + import ( + "context" + "database/sql" + "time" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/x/errors" + ) + + func main() { + iris.NewGuide(). + AllowOrigin("*"). + Compression(true). + Health(true, "development", "kataras"). + Timeout(0, 20*time.Second, 20*time.Second). + Middlewares(). + Services( + // openDatabase(), + // NewSQLRepoRegistry, + NewMemRepoRegistry, + NewTestService, + ). + API("/tests", new(TestAPI)). + Listen(":80") + } + + // Recommendation: move it to /api/tests/api.go file. + type TestAPI struct { + TestService *TestService + } + + func (api *TestAPI) Configure(r iris.Party) { + r.Get("/", api.listTests) + } + + func (api *TestAPI) listTests(ctx iris.Context) { + tests, err := api.TestService.ListTests(ctx) + if err != nil { + errors.Internal.LogErr(ctx, err) + return + } + + ctx.JSON(tests) + } + + // Recommendation: move it to /pkg/storage/sql/db.go file. + type DB struct { + *sql.DB + } + + func openDatabase( your database configuration... ) *DB { + conn, err := sql.Open(...) + // handle error. + return &DB{DB: conn} + } + + func (db *DB) Close() error { + return nil + } + + // Recommendation: move it to /pkg/repository/registry.go file. + type RepoRegistry interface { + Tests() TestRepository + + InTransaction(ctx context.Context, fn func(RepoRegistry) error) error + } + + // Recommendation: move it to /pkg/repository/registry/memory.go file. + type repoRegistryMem struct { + tests TestRepository + } + + func NewMemRepoRegistry() RepoRegistry { + return &repoRegistryMem{ + tests: NewMemTestRepository(), + } + } + + func (r *repoRegistryMem) Tests() TestRepository { + return r.tests + } + + func (r *repoRegistryMem) InTransaction(ctx context.Context, fn func(RepoRegistry) error) error { + return nil + } + + // Recommendation: move it to /pkg/repository/registry/sql.go file. + type repoRegistrySQL struct { + db *DB + + tests TestRepository + } + + func NewSQLRepoRegistry(db *DB) RepoRegistry { + return &repoRegistrySQL{ + db: db, + tests: NewSQLTestRepository(db), + } + } + + func (r *repoRegistrySQL) Tests() TestRepository { + return r.tests + } + + func (r *repoRegistrySQL) InTransaction(ctx context.Context, fn func(RepoRegistry) error) error { + return nil + + // your own database transaction code, may look something like that: + // tx, err := r.db.BeginTx(ctx, nil) + // if err != nil { + // return err + // } + // defer tx.Rollback() + // newRegistry := NewSQLRepoRegistry(tx) + // if err := fn(newRegistry);err!=nil{ + // return err + // } + // return tx.Commit() + } + + // Recommendation: move it to /pkg/test/test.go + type Test struct { + Name string `db:"name"` + } + + // Recommendation: move it to /pkg/test/repository.go + type TestRepository interface { + ListTests(ctx context.Context) ([]Test, error) + } + + type testRepositoryMem struct { + tests []Test + } + + func NewMemTestRepository() TestRepository { + list := []Test{ + {Name: "test1"}, + {Name: "test2"}, + {Name: "test3"}, + } + + return &testRepositoryMem{ + tests: list, + } + } + + func (r *testRepositoryMem) ListTests(ctx context.Context) ([]Test, error) { + return r.tests, nil + } + + type testRepositorySQL struct { + db *DB + } + + func NewSQLTestRepository(db *DB) TestRepository { + return &testRepositorySQL{db: db} + } + + func (r *testRepositorySQL) ListTests(ctx context.Context) ([]Test, error) { + query := `SELECT * FROM tests ORDER BY created_at;` + + rows, err := r.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + tests := make([]Test, 0) + for rows.Next() { + var t Test + if err := rows.Scan(&t.Name); err != nil { + return nil, err + } + tests = append(tests, t) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return tests, nil + } + + // Recommendation: move it to /pkg/service/test_service.go file. + type TestService struct { + repos RepoRegistry + } + + func NewTestService(registry RepoRegistry) *TestService { + return &TestService{ + repos: registry, + } + } + + func (s *TestService) ListTests(ctx context.Context) ([]Test, error) { + return s.repos.Tests().ListTests(ctx) + } +*/ +func NewGuide() Step1 { + return &step1{} +} + +type ( + Step1 interface { + // AllowOrigin defines the CORS allowed domains. + // Many can be splitted by comma. + // If "*" is provided then all origins are accepted (use it for public APIs). + AllowOrigin(originLine string) Step2 + } + + Step2 interface { + // Compression enables or disables the gzip (or any other client-preferred) compression algorithm + // for response writes. + Compression(b bool) Step3 + } + + Step3 interface { + // Health enables the /health route. + // If "env" and "developer" are given, these fields will be populated to the client + // through headers and environemnt on health route. + Health(b bool, env, developer string) Step4 + } + + Step4 interface { + // Timeout defines the http timeout, server read & write timeouts. + Timeout(requestResponseLife, read time.Duration, write time.Duration) Step5 + } + + Step5 interface { + // Middlewares registers one or more handlers to run before the requested route's handler. + Middlewares(handlers ...Handler) Step6 + } + + Step6 interface { + // Services registers one or more dependencies that APIs can use. + Services(deps ...interface{}) Step7 + } + + Step7 interface { + // Handle registers a simple route on specific method and (dynamic) path. + // It simply calls the Iris Application's Handle method. + // Use the "API" method instead to keep the app organized. + Handle(method, path string, handlers ...Handler) Step7 + // API registers a router which is responsible to serve the /api group. + API(pathPrefix string, c ...router.PartyConfigurator) Step7 + // Build builds the application with the prior configuration and returns the + // Iris Application instance for further customizations. + // + // Use "Build" before "Listen" or "Run" to apply further modifications + // to the framework before starting the server. Calling "Build" is optional. + Build() *Application // optional call. + // Listen calls the Application's Listen method. + // Use "Run" instead if you need to customize the HTTP/2 server itself. + Listen(hostPort string, configurators ...Configurator) error // Listen OR Run. + // Run calls the Application's Run method. + Run(runner Runner, configurators ...Configurator) error + } +) + +type step1 struct { + originLine string +} + +func (s *step1) AllowOrigin(originLine string) Step2 { + s.originLine = originLine + return &step2{ + step1: s, + } +} + +type step2 struct { + step1 *step1 + + enableCompression bool +} + +func (s *step2) Compression(b bool) Step3 { + s.enableCompression = b + return &step3{ + step2: s, + } +} + +type step3 struct { + step2 *step2 + + enableHealth bool + env, developer string +} + +func (s *step3) Health(b bool, env, developer string) Step4 { + s.enableHealth = b + s.env, s.developer = env, developer + return &step4{ + step3: s, + } +} + +type step4 struct { + step3 *step3 + + handlerTimeout time.Duration + + serverTimeoutRead time.Duration + serverTimeoutWrite time.Duration +} + +func (s *step4) Timeout(requestResponseLife, read, write time.Duration) Step5 { + s.handlerTimeout = requestResponseLife + + s.serverTimeoutRead = read + s.serverTimeoutWrite = write + return &step5{ + step4: s, + } +} + +type step5 struct { + step4 *step4 + + middlewares []Handler +} + +func (s *step5) Middlewares(handlers ...Handler) Step6 { + s.middlewares = handlers + + return &step6{ + step5: s, + } +} + +type step6 struct { + step5 *step5 + + deps []interface{} + // derives from "deps". + closers []func() + // derives from "deps". + configuratorsAsDeps []Configurator +} + +func (s *step6) Services(deps ...interface{}) Step7 { + s.deps = deps + for _, d := range deps { + if d == nil { + continue + } + + switch cb := d.(type) { + case func(): + s.closers = append(s.closers, cb) + case func() error: + s.closers = append(s.closers, func() { cb() }) + case interface{ Close() }: + s.closers = append(s.closers, cb.Close) + case interface{ Close() error }: + s.closers = append(s.closers, func() { + cb.Close() + }) + case Configurator: + s.configuratorsAsDeps = append(s.configuratorsAsDeps, cb) + } + } + + return &step7{ + step6: s, + } +} + +type step7 struct { + step6 *step6 + + app *Application + + m map[string][]router.PartyConfigurator + handlers []step7SimpleRoute +} + +type step7SimpleRoute struct { + method, path string + handlers []Handler +} + +func (s *step7) Handle(method, path string, handlers ...Handler) Step7 { + s.handlers = append(s.handlers, step7SimpleRoute{method: method, path: path, handlers: handlers}) + return s +} + +func (s *step7) API(prefix string, c ...router.PartyConfigurator) Step7 { + if s.m == nil { + s.m = make(map[string][]router.PartyConfigurator) + } + + s.m[prefix] = append(s.m[prefix], c...) + return s +} + +func (s *step7) Build() *Application { + if s.app != nil { + return s.app + } + + app := New() + app.SetContextErrorHandler(errors.DefaultContextErrorHandler) + app.Macros().SetErrorHandler(errors.DefaultPathParameterTypeErrorHandler) + + app.UseRouter(recover.New()) + + app.UseRouter(func(ctx Context) { + ctx.Header("Server", "Iris") + if dev := s.step6.step5.step4.step3.developer; dev != "" { + ctx.Header("X-Developer", dev) + } + + ctx.Next() + }) + + if allowOrigin := s.step6.step5.step4.step3.step2.step1.originLine; allowOrigin != "" && allowOrigin != "none" { + app.UseRouter(cors.New().AllowOrigin(allowOrigin).Handler()) + } + + if s.step6.step5.step4.step3.step2.enableCompression { + app.Use(Compression) + } + + for _, middleware := range s.step6.step5.middlewares { + if middleware == nil { + continue + } + + app.Use(middleware) + } + + if configAsDeps := s.step6.configuratorsAsDeps; len(configAsDeps) > 0 { + app.Configure(configAsDeps...) + } + + if s.step6.step5.step4.step3.enableHealth { + app.Get("/health", modrevision.New(modrevision.Options{ + ServerName: "Iris Server", + Env: s.step6.step5.step4.step3.env, + Developer: s.step6.step5.step4.step3.developer, + })) + } + + if deps := s.step6.deps; len(deps) > 0 { + app.EnsureStaticBindings().RegisterDependency(deps...) + } + + for prefix, c := range s.m { + app.PartyConfigure("/api"+prefix, c...) + } + + for _, route := range s.handlers { + app.Handle(route.method, route.path, route.handlers...) + } + + if readTimeout := s.step6.step5.step4.serverTimeoutRead; readTimeout > 0 { + app.ConfigureHost(func(su *Supervisor) { + su.Server.ReadTimeout = readTimeout + su.Server.IdleTimeout = readTimeout + if v, recommended := readTimeout/4, 5*time.Second; v > recommended { + su.Server.ReadHeaderTimeout = v + } else { + su.Server.ReadHeaderTimeout = recommended + } + }) + } + + if writeTimeout := s.step6.step5.step4.serverTimeoutWrite; writeTimeout > 0 { + app.ConfigureHost(func(su *Supervisor) { + su.Server.WriteTimeout = writeTimeout + }) + } + + var defaultConfigurators = []Configurator{ + WithoutServerError(ErrServerClosed, ErrURLQuerySemicolon), + WithOptimizations, + WithRemoteAddrHeader( + "X-Real-Ip", + "X-Forwarded-For", + "CF-Connecting-IP", + "True-Client-Ip", + "X-Appengine-Remote-Addr", + ), + WithTimeout(s.step6.step5.step4.handlerTimeout), + } + app.Configure(defaultConfigurators...) + + s.app = app + return app +} + +func (s *step7) Listen(hostPort string, configurators ...Configurator) error { + return s.Run(Addr(hostPort), configurators...) +} + +func (s *step7) Run(runner Runner, configurators ...Configurator) error { + app := s.Build() + + defer func() { + // they will be called on interrupt signals too, + // because Iris has a builtin mechanism to call server's shutdown on interrupt. + for _, cb := range s.step6.closers { + cb() + } + }() + + return app.Run(runner, configurators...) +}