Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ jobs:
strategy:
matrix:
go-version:
- 1.18
- 1.19
- 1.23
- 1.24

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ if [[ -f coverage.txt ]]; then
fi

for d in $(go list ./... | grep -v ditest); do
go test -coverprofile=profile.out -coverpkg=./... -covermode=atomic "$d"
go test -race -coverprofile=profile.out -coverpkg=./... -covermode=atomic "$d"
if [[ -f profile.out ]]; then
cat profile.out >>coverage.txt
rm profile.out
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,28 @@ go get github.com/gozix/echo/v4
}
```

Without server names the `http-server` command runs all servers from the `echo` config section:

```golang
gzEcho.NewBundle()
```

With server names the command runs only these servers and fails if any of them is not configured:

```golang
gzEcho.NewBundle("admin", "public", "private")
```

To get a server echo instance outside the `http-server` command, use the factory:

```golang
func NewRoutesCommand(f *gzEcho.Factory) *cobra.Command {
// ...
var e, err = f.Echo("admin")
// ...
}
```

## Documentation

You can find documentation on [pkg.go.dev][documentation-url] and read source code if needed.
Expand Down
80 changes: 10 additions & 70 deletions di.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,107 +4,47 @@ import (
"strconv"

"github.com/gozix/di"
)

const (
// tagServerName is tag to mark a server name value.
tagServerName = "echo.server_name"

// tagEcho is tag to mark a echo instance.
tagEcho = "echo.echo"

// tagConfigurator is tag to mark configurators.
tagConfigurator = "echo.configurator"

// tagController is tag to mark controllers.
tagController = "echo.controller"

// tagMiddleware is tag to mark middlewares.
tagMiddleware = "echo.middleware"

// argMiddlewarePriority is name of priority argument.
argMiddlewarePriority = "priority"
"github.com/gozix/echo/v4/internal/tag"
)

// AsConfigurator is syntax sugar for the di container.
func AsConfigurator(srvName string) di.ProvideOption {
return di.Tags{{
Name: tagConfigurator + "." + srvName,
Name: tag.Configurator + "." + srvName,
}}
}

// AsController is syntax sugar for the di container.
func AsController(srvName string) di.ProvideOption {
return di.ProvideOptions(di.Tags{{
Name: tagController + "." + srvName,
Name: tag.Controller + "." + srvName,
}}, di.As(new(Controller)))
}

// AsMiddleware is syntax sugar for the di container.
func AsMiddleware(srvName string, priority int64) di.ProvideOption {
return di.Tags{{
Name: tagMiddleware + "." + srvName,
Name: tag.Middleware + "." + srvName,
Args: di.Args{{
Key: argMiddlewarePriority,
Key: tag.ArgMiddlewarePriority,
Value: strconv.FormatInt(priority, 10),
}},
}}
}

func asServerName(srvName string) di.AddOption {
func asServerName() di.AddOption {
return di.Tags{{
Name: tagServerName,
Args: di.Args{{
Key: "name",
Value: srvName,
}},
}, {
Name: tagServerName + "." + srvName,
Name: tag.ServerName,
}}
}

func asEcho(srvName string) di.ProvideOption {
return di.Tags{{
Name: tagEcho + "." + srvName,
Name: tag.Echo + "." + srvName,
}}
}

func sortByPriority() di.Modifier {
return di.Sort(func(x, y di.Definition) bool {
var xp int64
for _, tag := range x.Tags() {
for _, arg := range tag.Args {
if arg.Key == argMiddlewarePriority {
xp, _ = strconv.ParseInt(arg.Value, 10, 64)
}
}
}

var yp int64
for _, tag := range y.Tags() {
for _, arg := range tag.Args {
if arg.Key == argMiddlewarePriority {
yp, _ = strconv.ParseInt(arg.Value, 10, 64)
}
}
}

return xp > yp
})
}

func withServerName(srvName string) di.Modifier {
return di.WithTags(tagServerName + "." + srvName)
}

func withConfigurator(srvName string) di.Modifier {
return di.WithTags(tagConfigurator + "." + srvName)
}

func withController(srvName string) di.Modifier {
return di.WithTags(tagController + "." + srvName)
}

func withMiddleware(srvName string) di.Modifier {
return di.WithTags(tagMiddleware + "." + srvName)
func withServerName() di.Modifier {
return di.WithTags(tag.ServerName)
}
31 changes: 11 additions & 20 deletions echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import (
gzValidator "github.com/gozix/validator/v3"
gzViper "github.com/gozix/viper/v3"
gzZap "github.com/gozix/zap/v3"
"github.com/labstack/echo/v4"

"github.com/gozix/echo/v4/internal/command"
"github.com/gozix/echo/v4/internal/configurator"
"github.com/gozix/echo/v4/internal/echo"
"github.com/gozix/echo/v4/internal/factory"
)

type (
Expand All @@ -27,6 +28,9 @@ type (

// Controller is type alias of controller.Controller.
Controller = configurator.Controller

// Factory is type alias of factory.Factory.
Factory = factory.Factory
)

// BundleName is default definition name.
Expand All @@ -50,34 +54,21 @@ func (b *Bundle) Name() string {
func (b *Bundle) Build(builder di.Builder) error {
var opt = []di.BuilderOption{
di.Provide(command.NewHTTPServer, glue.AsCliCommand()),
di.Provide(factory.New, di.Constraint(4, withServerName(), di.Optional(true))),
}

for _, srvName := range b.svrNames {
opt = append(opt, di.BuilderOptions(
// server name
di.Add(srvName, asServerName(srvName)),
di.Add(srvName, asServerName()),

// echo
di.Provide(
echo.New, asEcho(srvName),
di.Constraint(0, withConfigurator(srvName)),
),

// configurators
di.Provide(
configurator.NewController, AsConfigurator(srvName),
di.Constraint(0, withController(srvName), di.Optional(true)),
),
di.Provide(
configurator.NewMiddleware, AsConfigurator(srvName),
di.Constraint(0, withMiddleware(srvName), di.Optional(true), sortByPriority()),
),
di.Provide(
configurator.NewEcho, AsConfigurator(srvName),
di.Constraint(0, withServerName(srvName)),
func(f *Factory) (*echo.Echo, error) {
return f.Echo(srvName)
},
asEcho(srvName),
),
di.Provide(configurator.NewErrHandler, AsConfigurator(srvName)),
di.Provide(configurator.NewValidator, AsConfigurator(srvName)),
))
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.26.0
github.com/gobwas/glob v0.2.3
github.com/gozix/di v1.0.3
github.com/gozix/di v1.0.4
github.com/gozix/glue/v3 v3.0.0
github.com/gozix/validator/v3 v3.0.0
github.com/gozix/viper/v3 v3.0.3
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gozix/di v1.0.3 h1:HPAcwupunuzABgfWuX4NcspTMEG9yUq1dTgiRSUmb/k=
github.com/gozix/di v1.0.3/go.mod h1:VpR4iuzehn5oXLUaBcn6Mw8VgZlIpqTO/OssNIZaHHc=
github.com/gozix/di v1.0.4 h1:Y0QPlZ+sP4Jsy2h7wEVTl3qePqReK9WdBY14NXGGiHc=
github.com/gozix/di v1.0.4/go.mod h1:VpR4iuzehn5oXLUaBcn6Mw8VgZlIpqTO/OssNIZaHHc=
github.com/gozix/glue/v3 v3.0.0 h1:nnISjcf1n7DDuI3bdabPl6oNk446JoZI3icr72LtmTQ=
github.com/gozix/glue/v3 v3.0.0/go.mod h1:+AdMEhqEnm1q13ouVItuEieGWlcdK4LNDj6IJrEK/J8=
github.com/gozix/universal-translator/v3 v3.0.0 h1:7C3pbv4KzIw1Rfc3EhleVZjLzJJd0HdXQLo4/9tklUE=
Expand Down
75 changes: 43 additions & 32 deletions internal/command/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ package command

import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"

"github.com/gozix/di"
Expand All @@ -17,36 +19,34 @@ import (
"go.uber.org/zap"
"golang.org/x/sync/errgroup"

"github.com/gozix/echo/v4/internal/modifier"
)

const (
tagEcho = "echo.echo"
tagServerName = "echo.server_name"
"github.com/gozix/echo/v4/internal/factory"
"github.com/gozix/echo/v4/internal/filter"
)

// NewHTTPServer is command constructor.
func NewHTTPServer(ctn di.Container) *cobra.Command {
return &cobra.Command{
Use: "http-server [name...]",
Short: "Run http server",
RunE: func(cmd *cobra.Command, args []string) (err error) {
var modServers *modifier.Modifier
if modServers, err = modifier.NewModifier(tagServerName, args); err != nil {
return fmt.Errorf("unable create echo glob modifier : %w", err)
}
RunE: func(cmd *cobra.Command, args []string) error {
return ctn.Call(func(f *factory.Factory, cfg *viper.Viper, logger *zap.Logger) (err error) {
var names []string
if names, err = filter.Names(f.Names(), args); err != nil {
return fmt.Errorf("unable filter echo servers : %w", err)
}

// build all servers before start
var servers = make(map[string]*echo.Echo, len(names))
for _, srvName := range names {
if servers[srvName], err = f.Echo(srvName); err != nil {
return err
}
}

return ctn.Call(func(serverNames []string, cfg *viper.Viper, logger *zap.Logger) error {
var wg, ctx = errgroup.WithContext(cmd.Context())

for _, srvName := range serverNames {
var srvName = srvName
for srvName, e := range servers {
wg.Go(func() (err error) {
var e *echo.Echo
if err = ctn.Resolve(&e, di.WithTags(tagEcho+"."+srvName)); err != nil {
return err
}

var (
subCfg = cfg.Sub("echo." + srvName)
addr = net.JoinHostPort(
Expand All @@ -61,30 +61,41 @@ func NewHTTPServer(ctn di.Container) *cobra.Command {

log.Info("Starting HTTP server")

var stopped = make(chan error, 1)
go func() {
if err = e.Start(addr); err != nil {
log.Info("Gracefully shutting down the HTTP server")
}
stopped <- e.Start(addr)
}()

log.Info("HTTP server started")
select {
case <-ctx.Done():
// graceful shutdown
var timeout = 10 * time.Second
log.Info("Stopping HTTP server", zap.Duration("timeout", timeout))

var timeoutContext, cancel = context.WithTimeout(context.Background(), timeout)
defer cancel()

// wait, context cancellation
<-ctx.Done()
if err = e.Shutdown(timeoutContext); err != nil {
return fmt.Errorf("unable to stop http server %s : %w", srvName, err)
}

// graceful shutdown
var timeout = 10 * time.Second
log.Info("Stopping HTTP server", zap.Duration("timeout", timeout))
err = <-stopped
case err = <-stopped:
// stopped without shutdown, e.g. the address is already in use
}

var timeoutContext, cancel = context.WithTimeout(context.Background(), timeout)
defer cancel()
// http.ErrServerClosed is returned by the start after the shutdown
if errors.Is(err, http.ErrServerClosed) {
log.Info("HTTP server stopped")
return nil
}

return e.Shutdown(timeoutContext)
return fmt.Errorf("http server %s : %w", srvName, err)
})
}

return wg.Wait()
}, di.Constraint(0, modServers.Modifier()))
})
},
}
}
Loading
Loading