Rethink DI
See original GitHub issueCAUTION: There are still some caveats in the types, I will fix them but already wanted to showcase you the general idea of the API here.
I still feel the urge of rethinking our DI spike and the use of classes. Here is my suggestion on how to wire the Langium parts while maintaining the following features:
- dependency injection (achieved by binding a curried function to an instance of Langium)
- cyclic references are supported (e.g. linking, scoping and typesystem reference each other)
- 100% transparency/documentation of the langium API in one place
- allowing the user to extend the API/adding new objects that will be injected and can be used everywhere
Top level framework API
Here we have an overview what is Langium capable of. Provided the types are well documented in this central location, the user will get a good overview.
export type Langium = {
linkingProvider: LinkingProvider,
scopingProvider: ScopingProvider,
validationProvider: ValidationProvider
}
// just examples
export type LinkingProvider = () => void
export type ScopingProvider = () => void
export type ValidationProvider = () => void
Functional dependency injection
These generic types and one generic function for injection is basically all we need.
// I think this self recursive type still needs to be tweaked a bit.
// We need to ensure that we only have types `Inject<..., ...>`,
// so `K in keyof Langium` might not be sufficient.
export type LangiumAPI<L extends LangiumAPI<L>> = {
[K in keyof Langium]: Inject<L, Langium[K]>
}
export type Inject<L extends LangiumAPI<L>, T> = (api: L) => T
export function createLangium<L extends LangiumAPI<L>> (api: L): Langium {
return Object.fromEntries(
Object.entries(api).map(([key, value]) => [key, value(api)])
) as Langium;
}
Great defaults which can be reused
By exactly knowing our domain, we will provide one or more defaults for our users. This could be even an optional lib to keep the Langium core small.
export function defaultLangiumAPI<L extends LangiumAPI<L>>() { // TODO: declare self-recursive type
return {
linkingProvider: (api: L) => () => {},
scopingProvider: (api: L) => () => {},
validationProvider: (api: L) => () => {}
};
}
Typical use-site
The user typically needs to customize the defaults when developing non-trivial languages.
interface MyLangiumAPI extends LangiumAPI<MyLangiumAPI> {
myCustomProvider: MyCustomProvider
}
type MyCustomProvider = (api: MyLangiumAPI) => unknown
// look ma, custom api is injected into the default ScopingProvider API
const scopingProvider = (api: MyLangiumAPI) => () => {
const { myCustomProvider } = api;
// ...
}
// a user defined feature
const myCustomProvider = (api: MyLangiumAPI) => () => {
// ...
}
const langium = createLangium<MyLangiumAPI>({
...defaultLangiumAPI(), // use the defaults
scopingProvider, // re-define the default scoping provider
myCustomProvider // add a custom provider
});
It would be great if we could consider such an JS-like approach instead of just transferring to Langium what we did in Xtext.
☝️ please take a look @msujew @spoenemann
Issue Analytics
- State:
- Created 2 years ago
- Comments:22 (17 by maintainers)
Top GitHub Comments
Great work, that seems to work really well, although it looks like some black magic.
I tried it with my cyclic dependency example and this approach even works with constructor injection.
The key for DI with Proxies (that allow cycles) like outlined above is using
new Proxy(class {}, handler)
:The
class {}
target allows to be called asIt is a trick but it works. I will update the example above.