InversifyJS icon indicating copy to clipboard operation
InversifyJS copied to clipboard

Complex contextual resolution (multiple tags/tag and name)

Open jshearer opened this issue 5 years ago • 9 comments

In the system I'm building, we have ViewControllers and ViewControllerComponents. Each ViewController type has a name, like form_view, map_view, etc. Each ViewController has specific ViewControllerComponents that correspond to it, like TextArea and Numeric for form_view, and Point and Polygon for map_view.

Expected Behavior

ViewControllers are easy:

context.bind(VIEW_TYPES.ViewController).to(FormViewController).whenTargetNamed("form_view")

And then for the factory I do:

bind<ViewControllerFactory<any>>(VIEW_TYPES.ViewControllerFactory).toFactory<View<any>>(
    context => name => context.container.getNamed(VIEW_TYPES.ViewController, name),
);

Now what I want to do is the following for ViewControllerComponents:

context.bind(VIEW_TYPES.ViewControllerComponent).to(TextArea).whenTargetNamed("text_area").whenTargetTagged("view_type", "form_view")
...
bind<ViewControllerComponentFactory<any>>(VIEW_TYPES.ViewControllerComponentFactory).toFactory<ViewComponent<any>>(
    context => view_type => component_name => context.container.getMultiTagged(VIEW_TYPES.ViewControllerComponent,{[TAGS.NAME_TAG]: component_name, "view_type": view_name})
);

There are two problems: the first is that you can't chain bindingToSyntax (so .whenTargetNamed().whenTargetTagged() won't work). I solved this with the following complex constraint:

    const constraint: interfaces.ConstraintFunction =  (request: interfaces.Request | null) =>
        request !== null && request.target !== null && (
            request.target.matchesTag("view_type")(view_type)
        ) && (
            names.map(name => request.target.matchesNamedTag(name)).some(i=>i) // check that any array element is truthy
        );

But I wasn't able to figure out how to craft a request that can satisfy this request.

Current Behavior

I dug deeper here and found that inside of the plan function in planner.ts, a Target is created, but you're only allowed to specify a single key for its Metadata (despite Metadata being an array).

Possible Solution

If we were somehow allowed to append to this array of Metadatas, then what I'm trying to do would be possible.

Am I missing something, or approaching this the wrong way? There is still a lot about Inversify that I haven't learned yet.

jshearer avatar Jul 16 '19 17:07 jshearer

The root Request from Container getNamed/getAllNamed/getTagged/getAllTagged allows specifying a single tag metadata entry, whereas dependency requests have the tag metadata provided by the named decorator and the tagged decorator and multiple metadata can be created. The metadata entries in Target.metadata have to have unique keys so you can only supply one name but if you wanted multiple 'names' supply the names as an array for the metadata value.

constructor( @tagged(multiNameTag,['One','Two']) public namedDependeny: INamedDependency){}

const oneOfNamedConstraintFactory=function(names:string[]){
            return (request:interfaces.Request)=>{
                let matched=false;
                const customTags=request.target.getCustomTags();
                
                if(customTags){
                    const multiNamedTag=customTags.find(t=>t.key===multiNameTag);
                    if(multiNamedTag){
                        matched= (multiNamedTag.value as string[]).some(n=>names.some(nm=>{
                            return n===nm
                        }))
                    }
                }
                return matched;
            }
        }

container.bind(NamedDependency).toSelf().when(oneOfNamedConstraintFactory(['Zero','Two']))

tonyhallett avatar Aug 15 '19 14:08 tonyhallett

Thanks for getting back to me! I know I can do @tagged(multiNameTag,['One','Two']), but what I really want is getMultiTagged, which is what it seems you're writing in #1134. I'm excited for that to be done! Thank you :)

jshearer avatar Aug 24 '19 05:08 jshearer

I also want getMultiTagged, this is a great feature, thank you for your contribution. But since this is not merged, can you give me some advice on how to implement a workaround? I don't want to create a fork

julia-suarez-deel avatar Mar 29 '20 23:03 julia-suarez-deel

@JSilversun It has been six months since I looked at the source code and I do not remember how it functions ! If I get a chance I will have a look but it might be a few days.

tonyhallett avatar Mar 30 '20 09:03 tonyhallett

This is something I would really want to as well. Is the development of this still a possibility? I could take over your PR and try to make that work. @tonyhallett could you review that for me?

luiz290788 avatar May 12 '21 18:05 luiz290788

@tonyhallett I've took the liberty to work on this. It was a pretty simple solution after all. Do you mind reviewing it?

luiz290788 avatar May 13 '21 14:05 luiz290788

@luiz290788 I need to concentrate on another repo so please try one of the other maintainers. If it is still here when I have finished I will definitely have look.

tonyhallett avatar May 13 '21 17:05 tonyhallett

Thanks @tonyhallett I'll do that!

luiz290788 avatar May 13 '21 17:05 luiz290788

This topic and the related PR's seem to have gone stale, but I found myself with a similar need to simplify the bind constraints. I implemented a simplistic class that implements the Builder pattern, and can be used thus:

container.bind(Parser).toConstantValue(SpecialisedEventParser)
    .when(Constraint.named('EventParser').and.injectedInto(MyController).build())

The purpose here is that I have an abstract Controller which does some boiler plate (in this case input/output parsing) which is inherited by case-by-case controllers (this seems analagous to the OP's ViewController/ViewControllerComponent use case).

@injectable class Controller {
    constructor(
        @inject(Parser) @named('EventParser') eventParser: Parser,
    ) {

}

The existing API seems overly complex, with multiple methods on both the container bind and container get sides (whenTargetNamed, getNamed etc, as well as the standalone functions like namedConstraint, taggedConstraint, typeConstraint etc).

It seems like this could be unified with a single when that accepts a constraint, with the ability to use a builder for complex constraints. if the API accepted a builder, the caller could avoid the need to specify the build() method on each call. The builder can have a fluent interface like exists with the rest of the API. I'm not arguing at the moment that the mix of existing calls should be deprecated, but in time perhaps, they would become less needed.

I can see cases for .or logic and perhaps parenthesising (.paren(builder: Constrained)), and that's where the builder gets more complex/sophisticated - right not I don't have a specific need for that so haven't considered the implementation.

I'm relatively new to Inversify so I can't claim to offer resource to spec/implement this, but it seemed like there might be some use for this approach.

FWIW, I'm actually using it to implement AWS Lambdas, and using Zod as my parser, which allows me to transform from different inputs (AWS IotCore, HTTP Endpoints) where the source event type varies, but the embedded payload is common and therefore can be handled by the same controller after a little massaging.

dmeehan1968 avatar Jun 21 '23 19:06 dmeehan1968