This message was deleted.
# pulumiverse
s
This message was deleted.
e
I don't think I've heard this suggested before. Have you got a concrete example of using a decorator? I'm struggling to see what exactly you mean by replacing interpolate/apply.
m
interpolate is actually a decent example. before:
Copy code
const someInput = pulumi.interpolate(`string${var}`)
const someArg = 'someWeirdStringOrAnyTypeThatNeedsToBeWrappedInaPulumi_Output<T>'
after:
Copy code
const someInput = @input`string${var}`
const someArg = @input['a','b','c']
etc
a generic decorator that does automatic type wrapping/conversion if the expected target is a pulumi.inputT or pulumi.outputT ...as long as its safe obvi
it bridges the gap between "lol string can't be a pulumi.output<string>" etc
this is especially useful for inline-typed objects like maps and dictionaries
instead of casting the entire world into { [key: string]: string } etc
injection is also really useful to get around importing 10,000 things if you have, for example:
Copy code
const engineeringOu = @input importOrganizationalUnit('Engineering', 'SDLC Environments', 'ou-5r4w-q04lmr3c', @inject rootOuId);
const tenantEnvOu = @input importOrganizationalUnit('Environments', 'Tenant Accounts', 'ou-5r4w-6ui019no', @inject engineeringOu.id);

// AWS Users (Accounts)
const rootAwsAccount = @input importAwsAccount(@inject rootAccountName, 'REDACTED', @inject rootAccountId);
const sharedAwsAccount = @input importAwsAccount(@inject sharedAccountName, 'REDACTED', @inject sharedAccountId);
const tenantAwsAccounts = @input activeTenants.map(tenant => createAwsAccount(`tenant_${tenant}`, @inject tenantEnvOu.id));
const allAwsAccounts = @input tenantAwsAccounts.concat(rootAwsAccount).concat(@inject sharedAwsAccount);

// GitHub CICD Setup
export const crossAccountPermissionPolicyTargets = allAwsAccounts.map(account => {
    return account.id.apply(appliedAccountId => {
        const provider = createAccountPulumiProvider(appliedAccountId);

        const oidcGithubProvider = createGithubOidcProvider(appliedAccountId, @inject provider);
        const cicdIamRole = createGithubCicdRole(appliedAccountId, @inject provider);
        const cicdIamRoleAttachment = createGithubCicdRoleAttachment(appliedAccountId, @inject cicdIamRole, @inject provider);

		// IAM Users
        const cicdIamUser = createGithubCicdUser(appliedAccountId, @inject provider);
        const cicdIamUserAttachment = createGithubCicdUserAttachment(appliedAccountId, @inject cicdIamUser, @inject provider);

        return pulumi.all([cicdIamRole.arn, cicdIamUser.arn]).apply(([arnOne, arnTwo]) => {
            return [arnOne, arnTwo];
        });
    });
}).reduce((accumulator, current)  => {
        return pulumi.all([accumulator, current]).apply(([accArr, currArr]) => accArr.concat(currArr));
    },
    pulumi.output([])
);

export const activeTenantInfo = pulumi.all(tenantAwsAccounts.map(account => pulumi.all([account.id, account.name]).apply(([id, name]) => {
	return { [`${name.match(/_(.*)/)![1]}`] : {
			awsAccountId: id,
			awsAccountName: name,
			iamRoleArn: getGithubCicdRole(id),
			iamUserArn: getGithubCicdUser(id)
		} 
	}
}))).apply(accounts => accounts.reduce((accumulator, current) => {
	accumulator[Object.keys(current)[0]] = current[Object.keys(current)[0]]
	return accumulator;
}));

export const activeSharedInfo = activeSharedInfoConfig;
// TODO - SSO Users. Note: SSO Users may need a policy allowing them to assume an IAM cicd role or user depending on the permission group.
// Here we should stack-create SSO accounts for myself, brett and harris and then state import the existing ones

//TL;DR SSO Permission Sets link a list of AWS Accounts with a list of AWS IAM Policies and have SSO Groups as principals, which in turn link with SSO Users.
mapPermissionDictionaryToAccounts(PERMISSIONS_DICT_BREAKGLASS, allAwsAccounts);
mapPermissionDictionaryToAccounts(PERMISSIONS_DICT_DEVOPS, tenantAwsAccounts.concat(sharedAwsAccount));
mapPermissionDictionaryToAccounts(PERMISSIONS_DICT_ENGINEERS, tenantAwsAccounts);
here is the injection decorator source:
Copy code
import { Abstract } from '../index';
import { INJECTION } from '../symbols';

export type Injectable<T> = T & { [INJECTION]?: InjectionMeta };

export type InjectionMeta = {
	property: PropertyInjectionMeta[];
	constructor: ConstructorInjectionMeta[];
};

export type PropertyInjectionMeta = {
	params: any;
	abstract: any;
	propertyKey: string | symbol;
};

export type ConstructorInjectionMeta = PropertyInjectionMeta & {
	argumentIndex: number;
};

export const inject = <T>(
	abstract: Abstract,
	...params: ConstructorParameters<
        T extends new (...args: any) => any ? T : any
    >
): any => {
	return (
		target: any,
		propertyKey: string | symbol,
		argumentIndex: number,
	): void => {
		const isConstructable =
            typeof target === 'function' && target.prototype !== undefined;

		const metaTarget = isConstructable ? target : target.constructor;
		const injections: InjectionMeta = {
			property: [...(metaTarget[INJECTION]?.property ?? [])],
			constructor: [...(metaTarget[INJECTION]?.constructor ?? [])],
		};

		if (isConstructable) {
			injections.constructor.push({
				params,
				abstract,
				propertyKey,
				argumentIndex,
			});
		} else {
			injections.property.push({ params, abstract, propertyKey });
		}

		Object.defineProperty(metaTarget, INJECTION, {
			value: injections,
			writable: false,
			enumerable: false,
			configurable: false,
		});
	};
};

export default inject;
along with the file it references:
Copy code
/* eslint-disable complexity */
/* eslint-disable max-lines-per-function */
import { inspect } from 'util';
import { Injectable, InjectionMeta } from './annotations/inject';
import Mockery, { Mocks, TemporaryMock } from './mockery';
import * as Symbols from './symbols';

export { default as inject, InjectionMeta } from './annotations/inject';
export { Symbols };
export type Alias = string;
export type Aliases = { [key: string]: Abstract };

export type Abstract = Alias | symbol;

export type Concretion = any;
export type ConstructorType<T = any> = new (...args: any[]) => T;

export type Binding = {
	shared: boolean;
	target: BindTarget;
	onActivation?: any;
};

export type BindTarget = StaticBinding | DynamicBinding;

// eslint-disable-next-line @typescript-eslint/ban-types
export type StaticBinding = Injectable<Function>;
export type DynamicBinding = (container: Container) => Concretion;

export default class Container {

	protected mocks = new Mockery(this);
	protected aliases: Map<Alias, Abstract> = new Map();
	protected bindings: Map<Abstract, Binding> = new Map();
	protected resolving: Set<Abstract | StaticBinding> = new Set();
	protected instances: Map<Abstract, Concretion> = new Map();

	constructor() {
		this.instance(Symbols.CONTAINER, this);
	}

	/**
	 * Adds an already constructed value to the container
	 * @param id Abstract
	 * @param concrete Conretion
	 */
	public instance(id: Abstract, concrete: Concretion): void {
		const abstractId = this.toAbstract(id);

		this.forget(abstractId);
		this.instances.set(abstractId, concrete);
	}

	/**
	 * Adds a singleton to the container
	 * @param id Abstract
	 * @param target BindTarget
	 */
	public singleton<T>(id: Abstract, target: BindTarget): { onActivation: any } {
		return this.bind<T>(id, target, true);
	}

	/**
	 * Adds an abstract ID to the supplied target
	 * @param id Abstract
	 * @param target BindTarget
	 */
	public bind<T>(id: Abstract, target: BindTarget, shared = false): { onActivation: any } {
		if (typeof target !== 'function') {
			throw new Error('Target must be a function or arrow function.');
		}

		id = this.toAbstract(id);

		this.bindings.set(id, { shared, target });

		return {
			onActivation: (callback: (arg: T) => T): void => {
				(this.bindings.get(id) as Binding).onActivation = callback;
			},
		};
	}

	/**
	 * For Testing: allow runtime overrides of items in the container.
	 * @param id Abstract
	 * @param target BindTarget
	 */
	public async mock(mocks: Mocks, callback?: TemporaryMock<this>): Promise<void> {
		this.mocks.addMock(mocks);
		if (typeof callback === 'function') {
			try {
				// eslint-disable-next-line callback-return
				await callback(this);
			} finally {
				this.mocks.deleteMock(mocks);
			}
		}
	}

	public make<T = any>(target: Abstract | StaticBinding, strict = true): T {
		return this.makeWithArgs(target, [], strict);
	}

	/**
	 * Fetches and/or builds the supplied target from the container. When supplied with an ID, the binding
	 * associated to it will be built according to its configuration in the container. When supplied with
	 * a static binding (callable), it will build as a part of the container, resolving any dependencies.
	 * @param target Abstract | StaticBinding
	 * @param args Any. Arguments
	 * @param strict TODODESC
	 */
	public makeWithArgs<T = any>(
		target: Abstract | StaticBinding,
		args: any[],
		strict = true,
	): T {
		let instance;

		switch (typeof target) {
			case 'string':
				const abstract = this.toAbstract(target);
				return this.make(abstract, strict);
			case 'function':
				return this.construct(target as ConstructorType);
			case 'symbol':
				if (this.mocks.has(target)) {
					return this.mocks.get(target);
				}
				if (this.instances.has(target)) {
					return this.instances.get(target);
				}
				if (this.resolving.has(target)) {
					const current = Array.from(this.resolving).pop();
					const error = new Error(
						`Circular dependency detected in [${String(
							current,
						)}] while building [${String(target)}]`,
					);
					Error.captureStackTrace(error, this.makeWithArgs);
					throw error;
				}
				if (this.isBound(target)) {
					this.resolving.add(target);
					try {
						const shared = this.isShared(target);
						const binding = this.bindings.get(target) as Binding;

						if (strict && shared && args.length > 0) {
							console.log({ target, args });
							const error = new Error(
								'Arguments are not supported with singleton construction.',
							);
							Error.captureStackTrace(error, this.makeWithArgs);
							throw error;
						}

						instance = this.build(binding, args);

						if (binding.onActivation) {
							instance = binding.onActivation(instance);
						}
						if (shared) {
							this.instance(target, instance);
						}
					} finally {
						this.resolving.delete(target);
					}
				}
				break;
			default: break;
		}
		if (strict && !instance) {
			const error = new Error(`Failed to resolve abstract: ${String(target)}`);
			Error.captureStackTrace(error, this.makeWithArgs);
			throw error;
		}
		return instance;
	}

	public construct<T extends ConstructorType<any>>(
		target: T,
		args?: ConstructorParameters<T>,
	): InstanceType<T> {
		return this.build({ target, shared: false }, args);
	}

	/**
	 * Assigns an easy to remember string value to fetch things from the container.
	 * @param alias Alias
	 * @param target Abstract
	 */
	public alias(alias: Alias, target: Abstract): void;

	/**
	 * Assigns multiple easy to remember string values to fetch things from the container. Ease intensifies.
	 * @param alias Alias
	 * @param target Abstract
	 */
	public alias(aliases: Aliases): void;

	public alias(...args: any[]): void {
		if (args.length === 1 && typeof args[0] === 'object') {
			const aliases = args[0] as Aliases;
			for (const alias in aliases) {
				this.alias(alias, aliases[alias]);
			}
		} else {
			const alias = this.normalize(args[0]);
			const target = this.toAbstract(args[1]);
			this.aliases.set(alias, target);
		}
	}

	/**
	 * Removes an instance concretion from the container.
	 * @param id Abstract
	 */
	public forget(id: Abstract): void {
		this.instances.delete(this.toAbstract(id));
	}

	/**
	 * Removes all instances (but not bindings) from the container.
	 */
	public forgetAll(): void {
		this.instances.clear();
	}

	/**
	 * Removes a binding and its associated instance, if it exists.
	 * @param id Abstract
	 */
	public unbind(id: Abstract): void {
		const abstract = this.toAbstract(id);
		this.forget(abstract);
		this.bindings.delete(abstract);
	}

	/**
	 * Removes all bindings and associated instances from the container.
	 */
	public unbindAll(): void {
		for (const [id] of this.bindings) {
			this.forget(id);
			this.unbind(id);
		}
		this.bindings.clear();
	}

	/**
	 * Empties the container, except for configured aliases.
	 * @param id Abstract
	 */
	public flush(): void {
		this.forgetAll();
		this.unbindAll();
	}

	/**
	 * Checks whether a configured binding is a singleton (shared).
	 * @param id Abstract
	 */
	public isShared(id: Abstract): boolean {
		const abstract = this.toAbstract(id);
		if (this.isBound(abstract)) {
			const { shared }: Binding =
				this.instances.get(abstract) ?? this.bindings.get(abstract);
			return shared;
		}
		return false;
	}

	/**
	 * Checks if the supplied ID exists within the container
	 * @param id Abstract
	 */
	public isBound(id: Abstract): boolean {
		const abstract = this.toAbstract(id);
		return this.instances.has(abstract) || this.bindings.has(abstract);
	}

	/**
	 * Converts a supplied ID to an abstract which can be used to reference items in the container.
	 * @param id string | symbol
	 */
	public toAbstract(id: string | symbol): Abstract {
		if (typeof id !== 'string') {
			return id;
		}
		const abstract: Abstract = this.aliases.has(id) && this.aliases.get(id) || Symbol.for(`pmbAbstract;${this.normalize(id)}`);
		return abstract;
	}

	/**
	 * Normalizes a string ID for abstract resolution.
	 * @protected
	 */
	protected normalize(id: string): string {
		return id.trim().toLowerCase();
	}

	/**
	 * Calls or constructs a supplied binding, resolving any dependencies in the process.
	 * @protected
	 */
	protected build(binding: Binding, args: unknown[] = []): Concretion {
		//WARNING: If any of this is modified by a linter, abort the commit and add ignores. This code is critical, do not touch it.
		if (binding.target.prototype === undefined) {
			return (binding.target as DynamicBinding)(this);
		}
		const propertyInjections: Concretion[] = [];
		const constructorInjections: Concretion[] = [];
		if (binding.target && Symbols.INJECTION in binding.target) {
			const meta = (binding.target as StaticBinding)[Symbols.INJECTION] as InjectionMeta;

			meta.property.forEach(({ abstract, propertyKey }) =>
				propertyInjections.push([propertyKey, this.make(abstract)]),
			);
			meta.constructor.forEach(({ abstract, argumentIndex }) => {
				constructorInjections[argumentIndex] = this.make(abstract);
			});
		}

		const constructorArgs: unknown[] = args;

		for (const [idx, constructorInjection] of constructorInjections.entries()) {
			constructorArgs[idx] = constructorInjection;
		}

		const instance = Reflect.construct(binding.target, constructorArgs);

		for (const [propertyKey, concrete] of propertyInjections) {
			instance[propertyKey] = concrete;
		}
		return instance;
	}

	/**
	 * Debug: this is for debugging the container
	 * @protected
	 */
	protected [inspect.custom](): {
		aliases: Map<Alias, Abstract>;
		bindings: Map<Abstract, Binding>;
		instances: Map<Abstract, Concretion>;
	} {
		const { aliases, bindings, instances } = this;
		return { aliases, bindings, instances };
	}

}
its basic reflect-metadata cookie cutter stuff w some fancy type inference and casting
but, I'm not really sure it warrants being integrated / supported as a feature. I can tell you that its convenient and in our use cases safe / beneficial
as to whether it even fits into the overall design principles/patterns of pulumi... much better question
its a lot like when you inject a JS object's prototype with a get interceptor if you want to trace it throughout an async series of calls