Giter VIP home page Giter VIP logo

Comments (17)

jeffijoe avatar jeffijoe commented on July 25, 2024

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.

jeffijoe avatar jeffijoe commented on July 25, 2024

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.

Vincz avatar Vincz commented on July 25, 2024

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.

jeffijoe avatar jeffijoe commented on July 25, 2024

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.

Vincz avatar Vincz commented on July 25, 2024

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

jeffijoe avatar jeffijoe commented on July 25, 2024

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.

Vincz avatar Vincz commented on July 25, 2024

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.

jeffijoe avatar jeffijoe commented on July 25, 2024

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.

jeffijoe avatar jeffijoe commented on July 25, 2024

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.

Vincz avatar Vincz commented on July 25, 2024

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.

Vincz avatar Vincz commented on July 25, 2024

Yes, you're right. I could wrap Awilix and make an Awilix container builder.

from awilix.

jeffijoe avatar jeffijoe commented on July 25, 2024

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.

Vincz avatar Vincz commented on July 25, 2024

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.

jeffijoe avatar jeffijoe commented on July 25, 2024

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.

jeffijoe avatar jeffijoe commented on July 25, 2024

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.

Vincz avatar Vincz commented on July 25, 2024

Yes, exactly. I was thinking about a generate() method on the container that would freeze it, making it immutable.

from awilix.

jeffijoe avatar jeffijoe commented on July 25, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.