[v3] Service API cleanup and comments (#4024)

* Gather and document service API

* Update changelog

* Add NewServiceWithOptions

* Revert static analyser change

* Remove infinite loop in NewService[WithOptions]

* Fix compiler warning in bindings command

* Add test for NewServiceWithOptions

* Update changelog

* Fix service example

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
This commit is contained in:
Fabio Massaioli 2025-01-23 11:53:48 +01:00 committed by GitHub
commit 16ce1d3448
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 113 additions and 49 deletions

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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.

View file

@ -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.

View file

@ -11,5 +11,6 @@
".Service10",
".Service11",
".Service12",
"/other.Service13"
".Service13",
"/other.Service14"
]

View file

@ -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]
}

View file

@ -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]()...),

View file

@ -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]
}

View file

@ -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))

View file

@ -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)

View file

@ -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
}