Skip to content

Commit

Permalink
[v3] Service API cleanup and comments (#4024)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
fbbdev and leaanthony authored Jan 23, 2025
1 parent 547e30f commit 16ce1d3
Show file tree
Hide file tree
Showing 12 changed files with 113 additions and 49 deletions.
3 changes: 3 additions & 0 deletions docs/src/content/docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Expand Down
28 changes: 24 additions & 4 deletions docs/src/content/docs/learn/services.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion v3/examples/services/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion v3/internal/commands/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions v3/internal/generator/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
".Service10",
".Service11",
".Service12",
"/other.Service13"
".Service13",
"/other.Service14"
]
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]()
Expand All @@ -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]()...),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
34 changes: 0 additions & 34 deletions v3/pkg/application/application_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
72 changes: 72 additions & 0 deletions v3/pkg/application/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 16ce1d3

Please sign in to comment.