diff --git a/gateway/config.go b/gateway/config.go index b1789e31..a542f7b6 100644 --- a/gateway/config.go +++ b/gateway/config.go @@ -115,6 +115,7 @@ func (c *Config) SyncFrequency() time.Duration { } func NewConfigFromURL(ctx context.Context, fs afs.Service, URL string) (*Config, error) { + fs = NewFs(URL, fs) data, err := fs.DownloadWithURL(ctx, URL) if err != nil { return nil, err diff --git a/gateway/option.go b/gateway/option.go new file mode 100644 index 00000000..45a5bad9 --- /dev/null +++ b/gateway/option.go @@ -0,0 +1,120 @@ +package gateway + +import ( + "context" + "embed" + "github.com/viant/datly/repository" + "github.com/viant/datly/view/extension" + "github.com/viant/gmetric" + "net/http" +) + +type options struct { + config *Config + authorizer Authorizer + extensions *extension.Registry + metrics *gmetric.Service + repository *repository.Service + statusHandler http.Handler + embedFs *embed.FS + configURL string + authProvider func(config *Config, fs *embed.FS) (Authorizer, error) +} + +func newOptions(ctx context.Context, opts ...Option) (*options, error) { + result := &options{} + for _, option := range opts { + option(result) + } + if result.metrics == nil { + result.metrics = gmetric.New() + } + if ext := result.extensions; ext == nil { + result.extensions = extension.NewRegistry() + } + + if result.config == nil { + if result.configURL != "" { + var err error + + if result.config, err = NewConfigFromURL(ctx, fs, result.configURL); err != nil { + return nil, err + } + } + } + if result.config == nil { + result.config = &Config{} + } + + if result.authorizer == nil && result.authProvider != nil { + var err error + if result.authorizer, err = result.authProvider(result.config, result.embedFs); err != nil { + return nil, err + } + } + + return result, nil +} + +// Option represents a service option +type Option func(*options) + +// WithConfig sets a config +func WithConfig(config *Config) Option { + return func(o *options) { + o.config = config + } +} + +// WithAuthorizer sets an authorizer +func WithAuthorizer(authorizer Authorizer) Option { + return func(o *options) { + o.authorizer = authorizer + } +} + +// WithExtensions sets an extension registry +func WithExtensions(registry *extension.Registry) Option { + return func(o *options) { + o.extensions = registry + } +} + +// WithMetrics sets a metrics service +func WithMetrics(metrics *gmetric.Service) Option { + return func(o *options) { + o.metrics = metrics + } +} + +// WithRepository sets a repository service +func WithRepository(repository *repository.Service) Option { + return func(o *options) { + o.repository = repository + } +} + +// WithEmbedFs sets an embed file system +func WithEmbedFs(embedFs *embed.FS) Option { + return func(o *options) { + o.embedFs = embedFs + } +} + +func WithStatusHandler(handler http.Handler) Option { + return func(o *options) { + o.statusHandler = handler + } +} + +func WithAuthProvider(authProvider func(config *Config, fs *embed.FS) (Authorizer, error)) Option { + return func(o *options) { + o.authProvider = authProvider + } +} + +func WithConfigURL(configURL string) Option { + return func(o *options) { + o.configURL = configURL + } +} diff --git a/gateway/runtime/gcf/handler.go b/gateway/runtime/gcf/handler.go index d43fed53..fb59c5f0 100644 --- a/gateway/runtime/gcf/handler.go +++ b/gateway/runtime/gcf/handler.go @@ -1,6 +1,7 @@ package gcf import ( + "context" "fmt" _ "github.com/go-sql-driver/mysql" _ "github.com/viant/afs/embed" @@ -38,8 +39,8 @@ func handleRequest(w http.ResponseWriter, r *http.Request) error { if configURL == "" { return fmt.Errorf("config was emrty") } - - service, err := gateway.Singleton(configURL, nil, nil, extension.Config, nil) + ctx := context.Background() + service, err := gateway.Singleton(ctx, gateway.WithConfigURL(configURL), gateway.WithExtensions(extension.Config)) if err != nil { return err } diff --git a/gateway/runtime/serverless/service.go b/gateway/runtime/serverless/service.go index 503e421d..5c8785b1 100644 --- a/gateway/runtime/serverless/service.go +++ b/gateway/runtime/serverless/service.go @@ -2,16 +2,15 @@ package serverless import ( "context" + "embed" "fmt" "github.com/viant/datly/gateway" "github.com/viant/datly/service/auth/jwt" "github.com/viant/datly/view/extension" "os" - "sync" ) var gatewayConfig *gateway.Config -var configInit sync.Once var _service *gateway.Service @@ -23,30 +22,15 @@ func GetService() (*gateway.Service, error) { if configURL == "" { return nil, fmt.Errorf("config was emty") } - - var err error - fs := gateway.NewFs(configURL) - configInit.Do(func() { - gatewayConfig, err = gateway.NewConfigFromURL(context.Background(), fs, configURL) - }) - - if err != nil { - configInit = sync.Once{} - return nil, err - } - - var authorizer gateway.Authorizer - if jwtAuthorizer, err := jwt.Init(gatewayConfig, nil); err == nil { - authorizer = jwtAuthorizer - } else { - return nil, err - } - - service, err := gateway.SingletonWithConfig(gatewayConfig, nil, authorizer, extension.Config, nil) + service, err := gateway.Singleton(context.Background(), + gateway.WithAuthProvider(func(config *gateway.Config, fs *embed.FS) (gateway.Authorizer, error) { + return jwt.Init(gatewayConfig, fs) + }), + gateway.WithConfigURL(configURL), + gateway.WithExtensions(extension.Config)) if err != nil { return nil, err } - _service = service return service, nil } diff --git a/gateway/runtime/standalone/option.go b/gateway/runtime/standalone/option.go index a6681bb5..228e8e61 100644 --- a/gateway/runtime/standalone/option.go +++ b/gateway/runtime/standalone/option.go @@ -16,6 +16,13 @@ type Options struct { useSingleton *bool } +func (o *Options) UseSingleton() bool { + if o.useSingleton == nil { + return true + } + return *o.useSingleton +} + func NewOptions(ctx context.Context, opts ...Option) (*Options, error) { options := &Options{} for _, opt := range opts { @@ -44,18 +51,21 @@ func NewOptions(ctx context.Context, opts ...Option) (*Options, error) { // Option represents standalone option type Option func(*Options) +// WithAuth sets an authorizer func WithAuth(auth gateway.Authorizer) Option { return func(o *Options) { o.auth = auth } } +// WithConfig sets a config func WithConfig(config *Config) Option { return func(o *Options) { o.config = config } } +// WithVersion sets a version func WithUseSingleton(useSingleton *bool) Option { return func(o *Options) { o.useSingleton = useSingleton diff --git a/gateway/runtime/standalone/server.go b/gateway/runtime/standalone/server.go index 3ced859d..ed64c11f 100644 --- a/gateway/runtime/standalone/server.go +++ b/gateway/runtime/standalone/server.go @@ -47,14 +47,20 @@ func New(ctx context.Context, opts ...Option) (*Server, error) { if config.Config == nil { return nil, fmt.Errorf("gateway config was empty") } - service, err := gateway.SingletonWithConfig( - config.Config, - handler.NewStatus(config.Version, &config.Meta), - options.auth, - extension.Config, - metric, - ) + var service *gateway.Service + var gOptions = []gateway.Option{ + gateway.WithExtensions(extension.Config), + gateway.WithConfig(config.Config), + gateway.WithMetrics(metric), + gateway.WithStatusHandler(handler.NewStatus(config.Version, &config.Meta)), + gateway.WithAuthorizer(options.auth), + } + if options.UseSingleton() { + service, err = gateway.Singleton(ctx, gOptions...) + } else { + service, err = gateway.New(ctx, gOptions...) + } if err != nil { if service != nil { _ = service.Close() @@ -62,7 +68,6 @@ func New(ctx context.Context, opts ...Option) (*Server, error) { return nil, err } - server := &Server{ Service: service, Server: http.Server{ diff --git a/gateway/service.go b/gateway/service.go index 74965272..cec95843 100644 --- a/gateway/service.go +++ b/gateway/service.go @@ -14,7 +14,6 @@ import ( "github.com/viant/datly/shared" "github.com/viant/datly/utils/httputils" "github.com/viant/datly/view" - "github.com/viant/datly/view/extension" "github.com/viant/gmetric" "github.com/viant/scy/auth/jwt/signer" "net/http" @@ -76,46 +75,48 @@ func (r *Service) Close() error { } // New creates gateway Service. It is important to call Service.Close before Service got Garbage collected. -func New(ctx context.Context, aConfig *Config, statusHandler http.Handler, authorizer Authorizer, extensions *extension.Registry, metrics *gmetric.Service) (*Service, error) { +func New(ctx context.Context, opts ...Option) (*Service, error) { start := time.Now() - if err := aConfig.Init(ctx); err != nil { - return nil, err - } - err := aConfig.Validate() + options, err := newOptions(ctx, opts...) if err != nil { return nil, err } + aConfig := options.config + if err := aConfig.Init(ctx); err != nil { + return nil, err + } fs, err := newFileService(aConfig) if err != nil { return nil, err } - - repository, err := repository.New(ctx, repository.WithComponentURL(aConfig.RouteURL), - repository.WithResourceURL(aConfig.DependencyURL), - repository.WithPluginURL(aConfig.PluginsURL), - repository.WithApiPrefix(aConfig.APIPrefix), - repository.WithExtensions(extensions), - repository.WithMetrics(metrics), - repository.WithRefreshFrequency(aConfig.SyncFrequency()), - repository.WithDispatcher(dispatcher.New), - ) - if err != nil { - return nil, fmt.Errorf("failed to initialise component service: %w", err) + componentRepository := options.repository + if componentRepository == nil { + componentRepository, err = repository.New(ctx, repository.WithComponentURL(aConfig.RouteURL), + repository.WithResourceURL(aConfig.DependencyURL), + repository.WithPluginURL(aConfig.PluginsURL), + repository.WithApiPrefix(aConfig.APIPrefix), + repository.WithExtensions(options.extensions), + repository.WithMetrics(options.metrics), + repository.WithRefreshFrequency(aConfig.SyncFrequency()), + repository.WithDispatcher(dispatcher.New), + ) + if err != nil { + return nil, fmt.Errorf("failed to initialise component service: %w", err) + } } - - mainRouter, err := NewRouter(ctx, repository, aConfig, metrics, statusHandler, authorizer) + mainRouter, err := NewRouter(ctx, componentRepository, aConfig, options.metrics, options.statusHandler, options.authorizer) if err != nil { return nil, err } srv := &Service{ - metrics: metrics, - repository: repository, + metrics: options.metrics, + repository: componentRepository, Config: aConfig, mux: sync.RWMutex{}, fs: fs, - statusHandler: statusHandler, - authorizer: authorizer, + statusHandler: options.statusHandler, + authorizer: options.authorizer, mainRouter: mainRouter, } if aConfig.JwtSigner != nil { diff --git a/gateway/singleton.go b/gateway/singleton.go index 1ec1c00d..137a8948 100644 --- a/gateway/singleton.go +++ b/gateway/singleton.go @@ -5,9 +5,6 @@ import ( "github.com/viant/afs" "github.com/viant/afs/file" "github.com/viant/afs/url" - "github.com/viant/datly/view/extension" - "github.com/viant/gmetric" - "net/http" "os" "sync" ) @@ -15,42 +12,23 @@ import ( var service *Service var once sync.Once -func Singleton(configURL string, statusHandler http.Handler, authorizer Authorizer, registry *extension.Registry, metric *gmetric.Service) (*Service, error) { +func Singleton(ctx context.Context, options ...Option) (*Service, error) { var err error - fs := NewFs(configURL) once.Do(func() { - ctx := context.Background() - var config *Config - if config, err = NewConfigFromURL(ctx, fs, configURL); err != nil { - return - } - service, err = New(ctx, config, statusHandler, authorizer, registry, metric) + service, err = New(ctx, options...) }) if err != nil { once = sync.Once{} } - return service, err } -func NewFs(configURL string) afs.Service { +func NewFs(configURL string, fs afs.Service) afs.Service { if os.Getenv("DATLY_FS") == "cfs" { ParentURL, _ := url.Split(configURL, file.Scheme) return NewCacheFs(ParentURL) } - return afs.New() -} - -func SingletonWithConfig(config *Config, statusHandler http.Handler, authorizer Authorizer, registry *extension.Registry, metric *gmetric.Service) (*Service, error) { - var err error - once.Do(func() { - ctx := context.Background() - service, err = New(ctx, config, statusHandler, authorizer, registry, metric) - }) - if err != nil { - once = sync.Once{} - } - return service, err + return fs } func ResetSingleton() { diff --git a/internal/codegen/tmpl/handler/handler_init.gox b/internal/codegen/tmpl/handler/handler_init.gox index cfdaea4a..7d4fc716 100644 --- a/internal/codegen/tmpl/handler/handler_init.gox +++ b/internal/codegen/tmpl/handler/handler_init.gox @@ -10,7 +10,7 @@ import ( var ${Prefix}PathURI = "$URI" -func define${Prefix}Component(ctx context.Context, srv *datly.Service) (*repository.Component, error) { +func Define${Prefix}Component(ctx context.Context, srv *datly.Service) (*repository.Component, error) { return srv.AddHandler(ctx, contract.NewPath("$Method", ${Prefix}PathURI), &${Prefix}Handler{}, repository.WithContract( reflect.TypeOf(&${Prefix}Input{}), diff --git a/repository/codegen/contract.gox b/repository/codegen/contract.gox index 65cce27e..991a7ee7 100644 --- a/repository/codegen/contract.gox +++ b/repository/codegen/contract.gox @@ -1,7 +1,7 @@ var ${Name}PathURI = "${URI}" -func define${Name}Component(ctx context.Context, srv *datly.Service) error { +func Define${Name}Component(ctx context.Context, srv *datly.Service) error { aComponent, err := repository.NewComponent( contract.NewPath("${Method}",${Name}PathURI), repository.WithResource(srv.Resource()),$WithNamedResource