Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEP] Service tooling proposal #3496

Draft
wants to merge 2 commits into
base: v3-alpha
Choose a base branch
from

Conversation

leaanthony
Copy link
Member

@leaanthony leaanthony commented May 19, 2024

Description

This proposal outlines new CLI functionality for the new "Services" concept in v3, which can be user to create new services in a standard way. It offers a foundation for future service management, such as installing 3rd party services.

I'm willing to implement this myself.

@leaanthony leaanthony marked this pull request as draft May 19, 2024 05:54
Copy link

cloudflare-pages bot commented May 19, 2024

Deploying wails with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2866852
Status: ✅  Deploy successful!
Preview URL: https://b110c04f.wails.pages.dev
Branch Preview URL: https://v3-alpha-proposals-service-t.wails.pages.dev

View logs

@fbbdev
Copy link

fbbdev commented Jun 8, 2024

First of all thank you for your work on this and sorry for the very late feedback.

It is true, as you wrote in the proposal, that people can manage services by hand without too much effort.

Nonetheless, I think this contribution fits very well wails' mentality of providing ergonomic, moderately opinionated development tools on top of the go distribution, and it is definitely going to play a part, however small, in making the developer experience smoother and simpler.

I would like to object to the currently proposed code layout, and suggest an alternative.

The main problem IMO is that it does not fit very well the layout of generated JS code, and it is going to introduce friction for the developers on the frontend side.

A secondary problem is that I don't think having a per-service go.mod file makes sense for application-level services, which are most probably going to import internal subpackages of the application module, and are probably never going to be reused.

Regarding the main problem, the binding generator currently outputs:

  • per-service files <servicename>.js/ts;
  • per-package index files index.js;

Service files can be imported with the usual syntax

import * as ServiceName from "<package>/<servicename>.js"

whereas index files are designed to facilitate imports like this

import {ServiceName1, ServiceName2} from "<package>";

Having one folder per service is gonna make indexes redundant and practically useless, as well as complicate JS imports, which need to be typed by hand. The former pattern is going to become:

import * as ServiceName from "<package>/<servicename>/<servicename>.js"

The latter will look like this:

import {ServiceName1} "<package>/<servicename1>";
import {ServiceName2} "<package>/<servicename2>";

Not nice...

I understand that having per-service folders was meant to facilitate method handling, and I am gonna propose a couple alternative solutions below.

Regarding the secondary problem, I explained my reasons above. I'd just like to add this: I remember reading around the internet that having nested go.mod files should always be avoided, except for versioning purposes. I always try to abide by this rule, but maybe it was just someone's personal opinion? Aren't there any drawbacks to this?

On to the alternatives.

I'd like to propose the following layout for applications:

<application root>
  +- services
     +- <servicename1>.go
     +- <servicename2>.go
     +- ...
     +- index.go

For reusable, non application-specific services, I propose adding a new plugin template to the init command that would result in the following layout:

<plugin root>
  +- <servicename1>.go
  +- <servicename2>.go
  +- ...
  +- index.go
  +- go.mod
  +- go.sum
  +- plugin.yml
  +- Taskfile.yml

The service tool would use the former layout if the current package is main, the latter if it is non-main. This logic has the added benefit of making the service tool work seamlessly in the services folder, as well as in any other subpackage that might want to export its own services.

The plugin.yml file replaces the proposed service.yml file, keeping the same content. I propose delegating this entirely to the template system, the service tool in my view should not be concerned with this detail.

A service file <servicename>.go would contain by default the type and lifecycle methods (names are hypothetical)

type ServiceName struct { /* ... */ }

func (s *ServiceName) InitService() { /* ... */ }
func (s *ServiceName) ShutdownService() { /* ... */ }

as well as any user-defined methods.

The index.go file would contain the following function:

func Services() application.Service {
    return application.CombineServices(
        application.NewService(&ServiceName1{}),
        application.NewService(&ServiceName2{}),
    )
}

The name Services is tentative, alternative names could be Instances, ServiceInstances, GetServices, GetInstances, GetServiceInstances...

Then application templates would be amended to include the following default configuration:

import "<module path>/services"

// ...

func main() {
    // ...
    application.New(application.Options{
        // ...
        Bind: []application.Service{
            services.Services(),
        },
        // ...
    })
    // ...
}

application.CombineServices would be a new function that combines multiple service instances into one application.Service value. Thanks to the struct being opaque, this can be done without any disruption to current code.

Let me explain the rationale behind this approach:

  • it would be nice for the tool to automatically keep bound services in sync with the services folder;
  • however, editing the main.go file directly would be cumbersome, more error-prone and less user-friendly then editing the body of a simple secondary function;
  • the reason why the Services thingy has to be a function is that users might want to add some parameters there for service configuration;
  • as an added benefit, plugins (i.e. reusable service packages) gain better encapsulation and present a uniform interface to consumers.

Two problems still have to be handled:

  1. how to edit the Services function, but still allow users to customise it;
  2. how to manage service methods.

Problem 1 can be easily solved by parsing the package, then editing the Go AST and printing it back instead of manipulating source code directly. The only constraint being that the Services function must have exactly one return statement, whose expression is a call to application.CombineServices, and if not we throw an error. Moreover, one could consider using go/types to retrieve the list of services defined by any package and its methods. This would be very easy.

Problem 2 can be solved in the same way, but I was actually thinking that maybe method handling could be omitted entirely. Service methods are not the same as routes/actions in a web framework: they are just plain simple Go methods, with zero boilerplate. I don't see this feature being used much, and I feel like it would be a bit overkill.

The only case where we'd still need to edit methods would be service renaming and removal, and it can be easily done with the type-checker + AST editing.

I am of course available for help or discussion in implementing AST/type manipulation. In this regard, I was having a look at things to understand how hard it would be and I found this interesting lib: https://pkg.go.dev/github.com/dave/dst

@leaanthony
Copy link
Member Author

leaanthony commented Jun 9, 2024

Thanks @fbbdev for the insightful feedback 🙏
I guess the reason I'm keen for one service per package is that it makes it exactly the same as Go modules, which everyone is familiar with. However there are 2 use cases we need to consider:

I'm making services for myself

This is almost certainly the default use case and the one we should consider the most. In this scenario, there is no need for go.mod as local imports are sufficient via the base module path.

I'm making a sharable service

This is where someone has written a service and would like to share it. Sharing should absolutely be done via Go modules. So then the question is, how do we get from a local service to a shareable one? Perhaps it's just adding a go.mod and hosting it.

The only issue with composing multiple services into the same folder is that each 3rd party library you pull in may have different package names (they might not all be package service).

We should also be clear on terms 😅 For me a service is discrete & shareable code that can be given to the Services (was Bind) list and be callable from the front end. Each service could, in theory, have different namespaces or contain multiple pieces of differing functionality and using the "Combine" idea you presented would be fine for that.

I would be very wary of manipulating source code or copying code from 3rd party packages to local directories as, honestly, I think it's going to be very hard with lots of opportunities to go wrong. The ideal scenario is where someone uses import to pull in a 3rd party service and calls whatever method to create a new instance of it for the Services field. I think, so long as we don't attempt to combine 3rd party and local services at the source code level, then we should be pretty good. The parser would generate the code for the 3rd party lib so I think it's ok that the structure of that is whatever is best for front end. It's ok if the 2 don't match, we should just do whatever feels the most natural from a dev perspective.

Again, thanks for taking the time to provide feedback. What a great community we have! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants