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

Add ability to unbind a specific class #1035

Open
manfred4 opened this issue Jan 18, 2019 · 3 comments
Open

Add ability to unbind a specific class #1035

manfred4 opened this issue Jan 18, 2019 · 3 comments

Comments

@manfred4
Copy link

manfred4 commented Jan 18, 2019

I propose adding the ability to unbind a specific class from any previously bound interface.
If for example some project I depend on already binds the classes Katana and Shuriken to the interface Weapon, whereas I only want a multiinject to inject the Katana and other self-defined weapons and not shurikens, because they are just too sneaky.

Expected Behavior

I unbind the shuriken from the weapon interface and bind a new weapon.
When using multiinject on the Weapon class only the Katana and my new weapon should be in the injected objects.

Current Behavior

unbind unbinds all previously defined classes from the interface and only my new defined weapon would be in the multiinjected list.

Possible Solution

The project I depend on already binds like this:
bind(Katana).to(Weapon)
bind(Shuriken).to(Weapon)
Add some new syntax to allow this:
unbind(Shuriken).from(Weapon)
bind(Hammer).to(Weapon)

@dominic-simplan
Copy link

Did you find a workaround?

@manfred4
Copy link
Author

manfred4 commented Aug 7, 2019

As this was not too big of an issue for my use case, i simply ignored the problem.
A very hacky workaround would maybe be to unbind everything and just bind everything needed again. This may however add a lot of unnecessary dependencies.

@tonyhallett
Copy link
Contributor

tonyhallett commented Aug 17, 2019

You could use ContainerModule and unload, but this is better. Note that it has not been carefully considered, but works for your example. It relies upon the internal binding dictionary.

rebindContainer.ts

import {Container, interfaces, METADATA_KEY, BindingTypeEnum} from 'inversify'
import { getBindingDictionary} from 'inversify/dts/planning/planner'
const plannerExports=require('inversify/lib/planning/planner.js')
const gbd:typeof getBindingDictionary=plannerExports.getBindingDictionary;
type ServiceIdentifierOrArray=interfaces.ServiceIdentifier<any>|interfaces.ServiceIdentifier<any>[]
export class RebindContainer extends Container{
    private bd:interfaces.Lookup<interfaces.Binding<any>>
    constructor(options?:interfaces.ContainerOptions){
        super(options);
        this.bd=gbd(this);
    }
    private bindingMatchesTarget(binding:interfaces.Binding<any>,key:string | number | symbol,value:string){
        let found=false;
        if(binding.constraint&&binding.constraint.metaData){
            if(binding.constraint.metaData.key===key&&binding.constraint.metaData.value===value){
                found=true;
            }
        }
        return found;
    }
    private replaceBindingValue(binding:interfaces.Binding<any>,newValue:any){
        let clearCache=true;
        switch(binding.type){
            case BindingTypeEnum.ConstantValue:
                binding.cache=newValue;
                clearCache=false;
                break;
            case BindingTypeEnum.Constructor:
                binding.implementationType=newValue;
                break;
            case BindingTypeEnum.DynamicValue:
                binding.dynamicValue=newValue;
                break;
            case BindingTypeEnum.Factory:
                binding.factory=newValue;
                break;
            case BindingTypeEnum.Function:
                binding.cache=newValue;
                clearCache=false;
                break;
            case BindingTypeEnum.Instance:
                binding.implementationType=newValue;
                break;
            case BindingTypeEnum.Provider:
                binding.provider=newValue;
        }
        if(clearCache) binding.cache=undefined;
        binding.activated=false;
    }
    private getSidMatcher(serviceIdentifier?:ServiceIdentifierOrArray):(sid:interfaces.ServiceIdentifier<any>)=>boolean{
        let matchesSid=(sid:interfaces.ServiceIdentifier<any>)=>true;
        if(serviceIdentifier){
            let serviceIdentifiers:interfaces.ServiceIdentifier<any>[]=[];
            if(!Array.isArray(serviceIdentifier)){
                serviceIdentifiers.push(serviceIdentifier)
            }else{
                serviceIdentifiers=serviceIdentifier;
            }
            matchesSid=(sid)=>serviceIdentifiers.indexOf(sid)!==-1
        }
        return matchesSid;
    }
    unbindBindings(filter:(binding:interfaces.Binding<any>)=>boolean){
        this.bd.removeByCondition(filter);
        return this;
    }
    unbindTo(ctor:interfaces.Newable<any>,serviceIdentifer?:ServiceIdentifierOrArray){
        const matchesSid=this.getSidMatcher(serviceIdentifer);
        this.unbindBindings((b)=>{
            let remove=false;
            if(matchesSid(b.serviceIdentifier)){
                if(b.type===BindingTypeEnum.Instance){
                    remove=b.implementationType===ctor;
                }
            }
            return remove;
        })
        return this;
    }
    
    replaceToValue(oldCtor:interfaces.Newable<any>,newCtor:interfaces.Newable<any>,serviceIdentifer?:ServiceIdentifierOrArray){
        const matchesSid=this.getSidMatcher(serviceIdentifer);
        this.bd.traverse((sid,bindings)=>{
            if(matchesSid(sid)){
                for(let i=0;i<bindings.length;i++){
                    const binding=bindings[i];
                    if(binding.type===BindingTypeEnum.Instance&&binding.implementationType===oldCtor){
                        binding.cache=undefined;
                        binding.activated=false;
                        binding.implementationType=newCtor;
                        break;
                    }
                }
            }
        })
        return this;
    }
    unbindTarget(key:string | number | symbol,value:string,serviceIdentifer?:ServiceIdentifierOrArray){
        const matchesSid=this.getSidMatcher(serviceIdentifer);
        this.unbindBindings((b)=>{
            let found=false;
            if(matchesSid(b.serviceIdentifier)){
                found=this.bindingMatchesTarget(b,key,value);
            }
            return found;
        })
        return this;
    }
    
    unbindNamed(name:string,serviceIdentifer?:ServiceIdentifierOrArray){
        return this.unbindTarget(METADATA_KEY.NAMED_TAG,name,serviceIdentifer);
    }
    
    
    replaceTargetValue(key:string | number | symbol,value:string,newValue:any,serviceIdentifier?:ServiceIdentifierOrArray){
        const matchesSid=this.getSidMatcher(serviceIdentifier);
        this.bd.traverse((sid,bindings)=>{
            if(matchesSid(sid)){
                for(let i=0;i<bindings.length;i++){
                    const binding=bindings[i];
                    if(this.bindingMatchesTarget(binding,key,value)){
                        this.replaceBindingValue(binding,newValue);
                    }
                }
            }
        })
        return this;
    }
    replaceNamedValue(name:string,newValue:any,serviceIdentifier?:ServiceIdentifierOrArray){
        return this.replaceTargetValue(METADATA_KEY.NAMED_TAG,name,newValue,serviceIdentifier);
    }

}
describe('unbind specific class issue',()=>{
    it('should work with RebindContainer replaceToValue',()=>{
        interface IWeapon{}
        @injectable()
        class Katana implements IWeapon{
            
        }
        @injectable()
        class Shuriken implements IWeapon{
            
        }
        @injectable()
        class Hammer implements IWeapon{
            
        }
        const IWeaponSymbol=Symbol.for('IWeapon');
        
        
        const container=new RebindContainer();
        container.bind(IWeaponSymbol).to(Katana);
        container.bind(IWeaponSymbol).to(Shuriken);
        container.replaceToValue(Shuriken,Hammer);
        const allWeapons=container.getAll(IWeaponSymbol);
        expect(allWeapons[0]).toBeInstanceOf(Katana);
        expect(allWeapons[1]).toBeInstanceOf(Hammer);
    })
})

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

No branches or pull requests

3 participants