Comments (17)
Thanks for the kind words!
Awilix already supports inject
, which works regardless of resolution mode. It lets you add per-module arguments, although you still need to know the argument name up-front.
Does that solve your issue?
Also, keep in mind that you can create your own registration types that resolves the way you want it to. Take a look at asValue
for a simple example.
EDIT: in fact, you would be able to implement your requested behavior entirely by writing a custom registration type! ๐
With that said, I strongly suggest you use inject
as it's pretty powerful. The primary reason I added inject
was that I needed to inject some strings but I didn't want every module to be able to access them.
from awilix.
Also, the whole idea with a container is that you shouldn't have to worry about each implementation's dependencies; that's what the container does for you. If I were you, I'd use your args
as a way to use inject
for isolated per-module dependencies and let Awilix figure out the rest. ๐
from awilix.
You're right. I think I should be able to achieve what I want by defining a asDefinition() function just like asValue, asClass or asFunction.
The purpose of this service definitions is to be able to modify the behavior of services that can be define in the project dependencies (for a plugin system).
For example, let's say, that I create a plugin to expose a mailer service (a module myframework-mailer-plugin).
I have multiple ways to do so:
Create the service manually in my project
const MailerClass = require('myframework-mailer-plugin').Mailer;
container.registerClass("mailer", MailerClass);
I don't like it because I would prefer that my plugin define their own services.
Give the container to the plugin at some point
const populateContainer = require('myframework-mailer-plugin').servicePopulator;
populateContainer(container);
I don't like it either because the plugin is able to do anything with the container and it'll define the service is own way without anyway to "customize" the service creation process.
With the service definitions, we can create and customize the service in the plugin are define.
// myframework-mailer-plugin.js
const MailerClass = require('myframework-mailer-plugin').Mailer;
module.exports.services = {
mailer: {
class: MailerClass,
args: ["@mailer.transport"]
}
}
And in my project, I'll grab the definition of services exposed by the plugin and be able to change them.
const MyMailerClass = require('./MyMailerClass'); // My custom class that extends the MailerClass from the plugin
const definitions = getAllPluginsDefinitions();
definitions.mailer.class = MyMailerClass
container.processDefinitions(definitions);
This definitions would allow to customize the service generations process in many ways.
I think the auto-wiring of the services based on the args is the best way for the personal codebase.
from awilix.
Well like I said you can do basically anything you want by writing using a custom registration ๐
The registration API isn't documented other than with code comments cause I didn't think anyone would ever want to write any, but glad I implemented it in a modular fashion now! ๐
Can I close this? ๐
from awilix.
@jeffijoe Yes, of course !
I've made some progress but I still have design questions.
The api looks like this :
container.registerDefinition({
target: aClass|aFunction|aString,
args: array|object
})
The target can be a class, a function or a string.
If it's a string, it can be of two forms:
"xxxxxx" == require("xxxxx")
or
"xxxxxx::a.b.c" == require("xxxxx")[a][b][c]
this way, we can resolve exported function or classe locally or in modules. This behavior could be customise by providing a custom targetResolver function.
If the args is provided, it must be an array if the resolution mode is classic, and an object if the resolution is a proxy.
If it is defined, it'll replace the corresponding keys in the constructor (using inject).
It will also be recursively parse to resolve string prefixed by an "@".
For example:
const service = ({blue, red, logger}) => () => console.log(blue("hello blue"), red("hello red"));
registerDefinition({
target: service,
args: {
blue: (msg) => "<blue>" + msg + "</blue>",
logger: "@logger2"
}
})
Will replace blue by the function define in args
Will leave the auto-resolve of red
Will replace logger by resolve(logger2)
So in the end, the asDefinition function, just wrap the asFunction or asClass resolver function (by checking the type of the target definition), after setting an injector if some args are provided. But everything is done in the resolve (as I need the container to resolve the args).
The goal is to be able to load a set of definitions, and to be able to change this definitions before the services are created.
For example, something like that:
container.getDefinition('myservice').replaceArgs("logger", "@logger3")
I realise that It's a lot of changes if I want to cover my use cases and I'm wondering if I should submit PRs on your project or if I should start my own. What do you think about that ?
from awilix.
You can access the object returned by the as*
functions by using container.registrations.myservice
, so you can alter it if you want. You could just wrap Awilix and expose an API that uses it.
And tread carefully when using magic strings; what if the user really wants to inject a string starting with @
? Better use a function returning a known object shape (perhaps with a Symbol) if you ask me. ๐
from awilix.
With the new API their is many ways to get the same result:
asClass == asDefinition(target: class)
asFunction == asDefinition(target: class)
and asClass is almost the same as asFunction except the class is wrapped in asClass and the target is the constructor instead of the function itself.
Also, you do already have the isClass() function. So, you're able to guess, given an argument, if it's a class or a function.
And the current version of asDefinition, well... it's just resolving the target, checking if the resolved module is a class or a function, and calling generateResolver accordingly.
So in the end, we could remove asClass and asFunction, and just keep asDefinition (or whatever name) and auto-detect everything (is it a class ? is it as function ? is it a module that need to be require ?) and embedding the service name in the definition.
We could have a Definition class with a nice api like:
definition.getServiceName()
definition.replaceArg()
definition.setTarget()
definition.setOption(xxx)
And we could interact with it through the container
container.getDefinition(serviceName)
About the magic strings, this is how Symfony handle them:
http://symfony.com/doc/current/service_container/parameters.html
Their is also a notion of container parameters, resolved by the magic string "%%"
So as the magic string would only be used in the args definition, I don't think it would be a problem. Symbol could be great, but the idea is to be able to define this "definitions" from a nice readable config file.
from awilix.
I would prefer to keep the asClass
and asFunction
as-is to ensure you always have the choice to chose one over the other in case the isClass
heuristic failsโwhich is possible. While it is common to use PascalCase
for constructor functions, not everyone follows this pattern and the option to manually choose the registration type is favorable to them.
Awilix does not expose any "official" way to mutate registrations other than by explicitly overwriting them with new ones.
What exactly is it you are trying to achieve with your proposed mutable API? Why would you need/want to use definition.replaceArg()
?
from awilix.
I'd like to stay in the spirit of module composition, and so I suggest you author an npm module that wraps Awilix to support your definitions proposalโthis will allow for the most flexibility to bring your idea to life. I completely understand where you are coming from regarding the Symfony container, but I'd like Awilix to remain lightweight. The README is already getting quite big, and adding even more ways to do this to core might scare people off.
Here's an example usage and implementation demoing how easily you can build a declarative framework on top of Awilix:
import { createContainer, asClass, asFunction } from 'awilix'
import isClass from 'awilix/lib/isClass'
import { declarative } from 'yournewpackage'
// Pass in a container instance and configure away
const container = declarative(createContainer())
.define({
name: 'my-mail-builder',
target: service,
args: {
blue: (msg) => "<blue>" + msg + "</blue>",
logger: "@logger2"
}
})
.container
// example implementation
function declarative (container) {
return {
container,
define (spec) {
const reg = isClass(spec.target)
? asClass(spec.target)
: asFunction(spec.target)
container.register(
spec.name,
// Custom injector to resolve arguments
// "scope" is the relevant (child) container to use, important!!
reg.inject((scope) => resolveArgs(scope, spec.args))
)
return this
}
}
function resolveArgs (scope, args) {
const result = {}
Object.keys(args).forEach(k => {
const val = args[k]
if (typeof val === 'string' && val.startsWith('@')) {
// Resolve the value from the container
result[k] = scope.resolve(val.substring(1))
} else {
result[k] = val
}
})
return result
}
}
What do you think? This way users can opt-in to it by installing your package. ๐
Also, keep in mind the above is just an example implementation! You can defer registration until you've collected all your definitions, completely up to you. I hope you can see the power you get by wrapping Awilix rather than thinking of a way to put this into core. ๐
from awilix.
The purpose of this mutable API would be the modularity.
$ npm install myframework-plugin
// in my app
const definitions = require("myframework-plugin").definitions;
const container = awilix.createContainer();
container->loadDefinitions(definitions);
// the exposed services are amesome, but I just need to customise one of them
container->getDefinition('super_service')->setTarget(myCustomClass)
from awilix.
Yes, you're right. I could wrap Awilix and make an Awilix container builder.
from awilix.
container->getDefinition('super_service')->setTarget(myCustomClass)
You can do this already ๐
container.register('super_service', asClass(MyCustomClass))
You can overwrite/replace any service by simply registering something else โ I use this heavily to provide a default singleton implementation for a service, but I replace it with a scoped one that includes more context per request using awilix-koa
. For example, you can register a generic logger as singleton, but in the request scope you overwrite it with a logger configured to prepend user info to each message. ๐ก
from awilix.
Yes, you're right. But there is others use cases, where it could be nice to just customise part of the service declaration without having to redefine the complete service.
from awilix.
In that case, you can resort to:
container.register('myService', {
...container.registrations.myService,
injector: () => ({ woo: 'hoo' }), // add new injector
lifetime: Lifetime.SCOPED // add new lifetime
})
It will overwrite the registration by merging the old one into the new one. ๐
from awilix.
In your container builder wrapper, you can defer building the container until all your mutations on your definitions have taken place. That's what I recommend, at least.
I just want to enforce the point that the building blocks are there for you to go crazy ๐
from awilix.
Yes, exactly. I was thinking about a generate() method on the container that would freeze it, making it immutable.
from awilix.
That'll be difficult because child containers (scopes) rely on being able to add registrations right before resolving a serviceโthat's how you can inject the current user into a service.
You can of course omit exposing the container in your framework, but being able to add registrations to scopes at runtime is a fundamental feature of Awilix.
from awilix.
Related Issues (20)
- Problem with resolve Resolution Path HOT 1
- Auto loading modules customization HOT 7
- Can not resolve via Symbol() HOT 2
- Exporting instance of the container gives undefined HOT 10
- Clear, Concrete introduction in README.md needed. HOT 2
- Awilix is not creating instance of the class i pass as an dependency HOT 17
- Is anyone try to use tree shaking on a NodeJS app with Awilix ? HOT 1
- Using generic classes with awilix HOT 1
- Shouldn't `asClass` type-check the constructor parameters? HOT 4
- Is awilix compatible with TypeScript version 5+? HOT 2
- container.resolve type auto-inference feature HOT 1
- Whiteboard pattern HOT 7
- [Question] Support for assisted inject HOT 2
- awilix object is undefined when using default import, but named import of functions work HOT 1
- Support for ESM modules HOT 3
- Is it possible to reinforce constructor param name/types? HOT 1
- Can't resolve HOT 4
- How to unregister a dependency from the container? HOT 6
- Default value not working in class constructor in Proxy mode HOT 2
- asFunction -> asClass Cyclic dependency HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. ๐๐๐
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google โค๏ธ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from awilix.