diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index 7ad0571f4..39cb27011 100644 --- a/docs/src/content/docs/changelog.mdx +++ b/docs/src/content/docs/changelog.mdx @@ -37,6 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `window-call` example to demonstrate how to know which window is calling a service by [@leaanthony](https://github.com/leaanthony) - Better panic handling by [@leaanthony](https://github.com/leaanthony) - New Menu guide by [@leaanthony](https://github.com/leaanthony) +- Add doc comments for Service API by [@fbbdev](https://github.com/fbbdev) in [#4024](https://github.com/wailsapp/wails/pull/4024) +- Add function `application.NewServiceWithOptions` to initialise services with additional configuration by [@leaanthony](https://github.com/leaanthony) in [#4024](https://github.com/wailsapp/wails/pull/4024) ### Fixed @@ -52,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed `application.WindowIDKey` and `application.WindowNameKey` (replaced by `application.WindowKey`) by [@leaanthony](https://github.com/leaanthony) - In JS/TS bindings, class fields of fixed-length array types are now initialized with their expected length instead of being empty by [@fbbdev](https://github.com/fbbdev) in [#4001](https://github.com/wailsapp/wails/pull/4001) - ContextMenuData now returns a string instead of any by [@leaanthony](https://github.com/leaanthony) +- `application.NewService` does not accept options as an optional parameter anymore (use `application.NewServiceWithOptions` instead) by [@leaanthony](https://github.com/leaanthony) in [#4024](https://github.com/wailsapp/wails/pull/4024) ## v3.0.0-alpha.9 - 2025-01-13 diff --git a/docs/src/content/docs/learn/services.mdx b/docs/src/content/docs/learn/services.mdx index f555fc856..2e82e944b 100644 --- a/docs/src/content/docs/learn/services.mdx +++ b/docs/src/content/docs/learn/services.mdx @@ -52,6 +52,24 @@ app := application.New(application.Options{ }) ``` +This registers the `NewMyService` function as a service with the application. + +Services may also be registered with additional options: + +```go +app := application.New(application.Options{ + Services: []application.Service{ + application.NewServiceWithOptions(NewMyService(), application.ServiceOptions{ + // ... + }) + } +}) +``` + +ServiceOptions has the following fields: + - Name - Specify a custom name for the Service + - Route - A route to bind the Service to the frontend (more on this below) + ## Optional Methods Services can implement optional methods to hook into the application lifecycle. @@ -67,8 +85,10 @@ bindings generated for a service, so they are not exposed to your frontend. func (s *Service) ServiceName() string ``` -This method returns the name of the service. It is used for logging purposes -only. +This method returns the name of the service. By default, it will the struct name of the Service but can be +overridden with the `Name` field of the `ServiceOptions`. + +It is used for logging purposes only. ### ServiceStartup @@ -101,7 +121,7 @@ your service to act as an HTTP handler. The route of the handler is defined in the service options: ```go -application.NewService(fileserver.New(&fileserver.Config{ +application.NewServiceWithOptions(fileserver.New(&fileserver.Config{ RootPath: rootPath, }), application.ServiceOptions{ Route: "/files", @@ -144,7 +164,7 @@ We can now use this service in our application: ```go app := application.New(application.Options{ Services: []application.Service{ - application.NewService(fileserver.New(&fileserver.Config{ + application.NewServiceWithOptions(fileserver.New(&fileserver.Config{ RootPath: rootPath, }), application.ServiceOptions{ Route: "/files", diff --git a/v3/examples/services/main.go b/v3/examples/services/main.go index 74668cb91..a4ebb6bf4 100644 --- a/v3/examples/services/main.go +++ b/v3/examples/services/main.go @@ -43,7 +43,7 @@ func main() { AutoSave: true, })), application.NewService(log.New()), - application.NewService(fileserver.New(&fileserver.Config{ + application.NewServiceWithOptions(fileserver.New(&fileserver.Config{ RootPath: rootPath, }), application.ServiceOptions{ Route: "/files", diff --git a/v3/internal/commands/bindings.go b/v3/internal/commands/bindings.go index 478c16b21..f73285a29 100644 --- a/v3/internal/commands/bindings.go +++ b/v3/internal/commands/bindings.go @@ -77,7 +77,7 @@ func GenerateBindings(options *flags.GenerateBindingsOptions, patterns []string) if spinner != nil { spinner.Info(resultMessage) } else { - term.Infofln(resultMessage) + term.Infofln("%s", resultMessage) } // Report output directory. diff --git a/v3/internal/generator/errors.go b/v3/internal/generator/errors.go index d5b8acfd6..cb000d1ad 100644 --- a/v3/internal/generator/errors.go +++ b/v3/internal/generator/errors.go @@ -13,16 +13,16 @@ import ( // ErrNoContextPackage indicates that // the canonical path for the standard context package // did not match any actual package. -var ErrNoContextPackage = errors.New("standard context package not found at canonical import path ('context'): is the Wails v3 module properly installed?") +var ErrNoContextPackage = errors.New("standard context package not found at canonical import path ('context'): is the Wails v3 module properly installed? ") // ErrNoApplicationPackage indicates that // the canonical path for the Wails application package // did not match any actual package. -var ErrNoApplicationPackage = errors.New("Wails application package not found at canonical import path ('" + config.WailsAppPkgPath + "'): is the Wails v3 module properly installed?") +var ErrNoApplicationPackage = errors.New("Wails application package not found at canonical import path ('" + config.WailsAppPkgPath + "'): is the Wails v3 module properly installed? ") // ErrBadApplicationPackage indicates that // the Wails application package has invalid content. -var ErrBadApplicationPackage = errors.New("package " + config.WailsAppPkgPath + ": function NewService has wrong signature: is the Wails v3 module properly installed?") +var ErrBadApplicationPackage = errors.New("package " + config.WailsAppPkgPath + ": function NewService has wrong signature: is the Wails v3 module properly installed? ") // ErrNoPackages is returned by [Generator.Generate] // when [LoadPackages] returns no error and no packages. @@ -43,7 +43,7 @@ type ErrorReport struct { errors map[string]bool } -// NewError report initialises an ErrorReport instance +// NewErrorReport report initialises an ErrorReport instance // with the provided Logger implementation. // // If logger is nil, messages will be accumulated but not logged. diff --git a/v3/internal/generator/testcases/complex_instantiations/bound_types.json b/v3/internal/generator/testcases/complex_instantiations/bound_types.json index eeb776de3..ece1b9220 100644 --- a/v3/internal/generator/testcases/complex_instantiations/bound_types.json +++ b/v3/internal/generator/testcases/complex_instantiations/bound_types.json @@ -11,5 +11,6 @@ ".Service10", ".Service11", ".Service12", - "/other.Service13" + ".Service13", + "/other.Service14" ] diff --git a/v3/internal/generator/testcases/complex_instantiations/funcs.go b/v3/internal/generator/testcases/complex_instantiations/funcs.go index 73788baf6..7aea0e7e0 100644 --- a/v3/internal/generator/testcases/complex_instantiations/funcs.go +++ b/v3/internal/generator/testcases/complex_instantiations/funcs.go @@ -2,7 +2,7 @@ package main import "github.com/wailsapp/wails/v3/pkg/application" -func ServiceInitialiser[T any]() func(*T, ...application.ServiceOptions) application.Service { +func ServiceInitialiser[T any]() func(*T) application.Service { return application.NewService[T] } diff --git a/v3/internal/generator/testcases/complex_instantiations/main.go b/v3/internal/generator/testcases/complex_instantiations/main.go index 872a029d0..74e03ba6c 100644 --- a/v3/internal/generator/testcases/complex_instantiations/main.go +++ b/v3/internal/generator/testcases/complex_instantiations/main.go @@ -20,6 +20,7 @@ type Service9 struct{} type Service10 struct{} type Service11 struct{} type Service12 struct{} +type Service13 struct{} func main() { factory := NewFactory[Service1, Service2]() @@ -36,6 +37,7 @@ func main() { ServiceInitialiser[Service6]()(&Service6{}), other.CustomNewService(Service7{}), other.ServiceInitialiser[Service8]()(&Service8{}), + application.NewServiceWithOptions(&Service13{}, application.ServiceOptions{Name: "custom name"}), other.LocalService, }, CustomNewServices[Service9, Service10]()...), diff --git a/v3/internal/generator/testcases/complex_instantiations/other/funcs.go b/v3/internal/generator/testcases/complex_instantiations/other/funcs.go index 1f413998e..daf1ad066 100644 --- a/v3/internal/generator/testcases/complex_instantiations/other/funcs.go +++ b/v3/internal/generator/testcases/complex_instantiations/other/funcs.go @@ -6,7 +6,7 @@ func CustomNewService[T any](srv T) application.Service { return application.NewService(&srv) } -func ServiceInitialiser[T any]() func(*T, ...application.ServiceOptions) application.Service { +func ServiceInitialiser[T any]() func(*T) application.Service { return application.NewService[T] } diff --git a/v3/internal/generator/testcases/complex_instantiations/other/local.go b/v3/internal/generator/testcases/complex_instantiations/other/local.go index dea5d0561..99cf173d2 100644 --- a/v3/internal/generator/testcases/complex_instantiations/other/local.go +++ b/v3/internal/generator/testcases/complex_instantiations/other/local.go @@ -2,6 +2,6 @@ package other import "github.com/wailsapp/wails/v3/pkg/application" -type Service13 int +type Service14 int -var LocalService = application.NewService(new(Service13)) +var LocalService = application.NewService(new(Service14)) diff --git a/v3/pkg/application/application_options.go b/v3/pkg/application/application_options.go index 2f52996ed..d3f02f3ff 100644 --- a/v3/pkg/application/application_options.go +++ b/v3/pkg/application/application_options.go @@ -8,40 +8,6 @@ import ( "github.com/wailsapp/wails/v3/internal/assetserver" ) -// Service wraps a bound type instance. -// The zero value of Service is invalid. -// Valid values may only be obtained by calling [NewService]. -type Service struct { - instance any - options ServiceOptions -} - -type ServiceOptions struct { - // Name can be set to override the name of the service - // This is useful for logging and debugging purposes - Name string - // Route is the path to the assets - Route string -} - -var DefaultServiceOptions = ServiceOptions{ - Route: "", -} - -// NewService returns a Service value wrapping the given pointer. -// If T is not a named type, the returned value is invalid. -// The prefix is used if Service implements a http.Handler only one allowed -func NewService[T any](instance *T, options ...ServiceOptions) Service { - if len(options) == 1 { - return Service{instance, options[0]} - } - return Service{instance, DefaultServiceOptions} -} - -func (s Service) Instance() any { - return s.instance -} - // Options contains the options for the application type Options struct { // Name is the name of the application (used in the default about box) diff --git a/v3/pkg/application/services.go b/v3/pkg/application/services.go index 446746299..02ac6f049 100644 --- a/v3/pkg/application/services.go +++ b/v3/pkg/application/services.go @@ -5,14 +5,86 @@ import ( "reflect" ) +// Service wraps a bound type instance. +// The zero value of Service is invalid. +// Valid values may only be obtained by calling [NewService]. +type Service struct { + instance any + options ServiceOptions +} + +// ServiceOptions provides optional parameters for calls to [NewService]. +type ServiceOptions struct { + // Name can be set to override the name of the service + // for logging and debugging purposes. + // + // If empty, it will default + // either to the value obtained through the [ServiceName] interface, + // or to the type name. + Name string + + // If the service instance implements [http.Handler], + // it will be mounted on the internal asset server + // at the prefix specified by Route. + Route string +} + +// DefaultServiceOptions specifies the default values of service options, +// used when no [ServiceOptions] instance is provided to [NewService]. +var DefaultServiceOptions = ServiceOptions{} + +// NewService returns a Service value wrapping the given pointer. +// If T is not a concrete named type, the returned value is invalid. +func NewService[T any](instance *T) Service { + return Service{instance, DefaultServiceOptions} +} + +// NewServiceWithOptions returns a Service value wrapping the given pointer +// and specifying the given service options. +// If T is not a concrete named type, the returned value is invalid. +func NewServiceWithOptions[T any](instance *T, options ServiceOptions) Service { + service := NewService(instance) // Delegate to NewService so that the static analyser may detect T. Do not remove this call. + service.options = options + return service +} + +// Instance returns the service instance provided to [NewService]. +func (s Service) Instance() any { + return s.instance +} + +// ServiceName returns the name of the service +// +// This is an *optional* method that may be implemented by service instances. +// It is used for logging and debugging purposes. +// +// If a non-empty name is provided with [ServiceOptions], +// it takes precedence over the one returned by the ServiceName method. type ServiceName interface { ServiceName() string } +// ServiceStartup is an *optional* method that may be implemented by service instances. +// +// This method will be called during application startup and will receive a copy of the options +// specified at creation time. It can be used for initialising resources. +// +// The context will be valid as long as the application is running, +// and will be canceled right before shutdown. +// +// If the return value is non-nil, it is logged along with the service name, +// the startup process aborts and the application quits. +// When that happens, service instances that have been already initialised +// receive a shutdown notification. type ServiceStartup interface { ServiceStartup(ctx context.Context, options ServiceOptions) error } +// ServiceShutdown is an *optional* method that may be implemented by service instances. +// +// This method will be called during application shutdown. It can be used for cleaning up resources. +// +// If the return value is non-nil, it is logged along with the service name. type ServiceShutdown interface { ServiceShutdown() error }