Skip to content

Commit

Permalink
implemented Provide functions for simple client usage in the typical …
Browse files Browse the repository at this point in the history
…(intended) use cases
  • Loading branch information
johnabass committed Jun 17, 2023
1 parent 146a8a8 commit b63b6db
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 8 deletions.
102 changes: 94 additions & 8 deletions arrangehttp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ import (
"errors"
"net"
"net/http"
"reflect"

"github.com/xmidt-org/arrange"
"go.uber.org/fx"
)

var (
// ErrServerNameRequired indicates that ProvideServer or ProvideServerCustom was called
// with an empty server name.
ErrServerNameRequired = errors.New("A server name is required")
)

// ApplyServerOptions executes options against a server. The original server is returned, along
// with any error(s) that occurred. All options are executed, so the returned error may be an
// aggregate error which can be inspected via go.uber.org/multierr.
Expand All @@ -18,26 +26,35 @@ func ApplyServerOptions(server *http.Server, opts ...ServerOption) (*http.Server
return server, err
}

// NewServer is the primary server constructor for arrange. Use this when you are creating a server
// from a (possibly unmarshaled) ServerConfig. The options can be annotated to come from a value group,
// which is useful when there are multiple servers in a single fx.App.
func NewServer(sc ServerConfig, h http.Handler, opts ...ServerOption) (*http.Server, error) {
return NewServerCustom(sc, h, opts...)
}

// NewServerCustom is a server constructor that allows a client to customize the concrete
// ServerFactory and http.Handler for the server. This function is useful when you have a
// custom (possibly unmarshaled) configuration struct that implements ServerFactory.
func NewServerCustom[F ServerFactory, H http.Handler](sf F, h H, opts ...ServerOption) (s *http.Server, err error) {
s, err = sf.NewServer()
if err == nil {
s.Handler = h
// guard against both the http.Handler being nil and it being
// a non-nil interface tuple that points to a nil instance.
// this allows all types of handlers to be optional components.
hv := reflect.ValueOf(h)
if !hv.IsValid() || (hv.Kind() == reflect.Ptr && hv.IsNil()) {
s.Handler = http.DefaultServeMux
} else {
s.Handler = h
}

s, err = ApplyServerOptions(s, opts...)
}

return
}

// NewServer is the primary server constructor for arrange. Use this when you are creating a server
// from a (possibly unmarshaled) ServerConfig. The options can be annotated to come from a value group,
// which is useful when there are multiple servers in a single fx.App.
func NewServer(sc ServerConfig, h http.Handler, opts ...ServerOption) (*http.Server, error) {
return NewServerCustom(sc, h, opts...)
}

func serve(server *http.Server, listener net.Listener, shutdowner fx.Shutdowner) {
defer shutdowner.Shutdown()
err := server.Serve(listener)
Expand Down Expand Up @@ -102,3 +119,72 @@ func BindServer(server *http.Server, listener net.Listener, lifecycle fx.Lifecyc
newServerHook(server, listener, shutdowner),
)
}

// ProvideServer assembles a server out of application components in a standard, opinionated way.
// The serverName parameter is used as both the name of the *http.Server component and a prefix
// for that server's dependencies:
//
// (1) NewServer is used to create the server as a component named serverName
// (2) ServerConfig is an optional dependency with the name serverName+".config". Making
// this optional allows the provided server to take the package defaults for configuration.
// (3) http.Handler is an optional dependency with the name serverName+".handler". If not supplied,
// http.DefaultServeMux is used, in keeping with the behavior of net/http.
// (4) []ServerOption is an optional value group dependency with the name serverName+".options"
// (5) net.Listener is an optional dependency with the name serverName+".listener"
//
// The external set of options, if supplied, is applied to the server after any injected options.
// This allows for options that come from outside the enclosing fx.App, as might be the case
// for options driven by the command line.
//
// BindServer is used as an fx.Invoke function to bind the resulting server to the enclosing
// application's lifecycle.
func ProvideServer(serverName string, external ...ServerOption) fx.Option {
return ProvideServerCustom[ServerConfig, http.Handler](serverName, external...)
}

// ProvideServerCustom is like ProvideServer, but it allows customization of the concrete
// ServerFactory and http.Handler dependencies.
func ProvideServerCustom[F ServerFactory, H http.Handler](serverName string, external ...ServerOption) fx.Option {
if len(serverName) == 0 {
return fx.Error(ErrServerNameRequired)
}

// Use the named constructor function when possible so that uber/fx's error reporting
// will call out that function in logs.
ctor := NewServerCustom[F, H]
if len(external) > 0 {
ctor = func(sf F, h H, injected ...ServerOption) (s *http.Server, err error) {
s, err = NewServerCustom(sf, h, injected...)
if err == nil {
s, err = ApplyServerOptions(s, external...)
}

return
}
}

prefix := serverName + "."
return fx.Options(
fx.Provide(
fx.Annotate(
ctor,
arrange.Tags().
OptionalName(prefix+"config").
OptionalName(prefix+"handler").
Group(prefix+"options").
OptionalName(prefix+"listener").
ParamTags(),
arrange.Tags().Name(serverName).ResultTags(),
),
),
fx.Invoke(
fx.Annotate(
BindServer,
arrange.Tags().
Name(serverName).
OptionalName(prefix+"listener").
ParamTags(),
),
),
)
}
10 changes: 10 additions & 0 deletions arrangehttp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ func (suite *ServerSuite) TestNewServer() {
suite.Equal(":1234", server.Addr)
}

func (suite *ServerSuite) TestProvideServer() {
app := fxtest.New(
suite.T(),
ProvideServer("main"),
)

app.RequireStart()
app.RequireStop()
}

func TestServer(t *testing.T) {
suite.Run(t, new(ServerSuite))
}

0 comments on commit b63b6db

Please sign in to comment.