Skip to content

Commit

Permalink
improve parameter handling at runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
jahudka committed Nov 13, 2024
1 parent 3d47e92 commit acd253b
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 108 deletions.
8 changes: 4 additions & 4 deletions core/cli/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,9 +459,9 @@ export class Compiler {
}
case 'string': {
const [method, arg] = /^%[a-z0-9_.]+$/i.test(info)
? ['resolveParameter', info.slice(1, -1)]
? ['resolve', info.slice(1, -1)]
: ['expand', info];
return [name, `di.${method}('${arg}')`];
return [name, `di.parameters.${method}('${arg}')`];
}
default:
return [name, join('.', source, path, 'args', name)];
Expand Down Expand Up @@ -506,11 +506,11 @@ export class Compiler {
}

if ('nestedTypes' in parameters) {
return 'di.getParameters()';
return 'di.parameters.getAll()';
}

const optional = arg.flags & TypeFlag.Optional ? `, false` : '';
return `di.resolveParameter('${parameters.path}'${optional})`;
return `di.parameters.resolve('${parameters.path}'${optional})`;
}

private compileServiceInjection(type: Type, flags: TypeFlag, id: string): string {
Expand Down
3 changes: 2 additions & 1 deletion core/cli/src/containerBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { SourceFile, Type } from 'ts-morph';
import { DefinitionError } from './errors';
import {
ContainerOptions,
ContainerParametersInfo, NestedParameterInfo,
ContainerParametersInfo,
NestedParameterInfo,
ServiceDecoratorInfo,
ServiceDefinitionInfo,
ServiceRegistrationInfo,
Expand Down
5 changes: 1 addition & 4 deletions core/cli/src/definitionScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,7 @@ export class DefinitionScanner {

if (Node.isStringLiteral(initializer)) {
const value = initializer.getLiteralValue();

if (/%[a-z0-9_.]+%/i.test(value)) {
args[name] = value;
}
args[name] = /%[a-z0-9_.]+%/i.test(value) ? value : undefined;
} else {
args[name] = this.resolveCallbackInfo(arg);
}
Expand Down
6 changes: 3 additions & 3 deletions core/cli/src/typeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class TypeHelper {
if (args.length === 0) {
flags |= TypeFlag.Accessor;
type = returnType;
} else if (args.length === 1 && returnType.getText() === 'void') {
} else if (args.length === 1 && returnType.isVoid()) {
flags |= TypeFlag.Injector;
type = args[0].getValueDeclarationOrThrow().getType();
}
Expand All @@ -86,10 +86,10 @@ export class TypeHelper {
[type, flags] = this.resolveNullable(type.getTypeArguments()[0], flags | TypeFlag.Async);
} else if (target === this.refs.get('GlobalIterable', SyntaxKind.TypeAliasDeclaration)) {
flags |= TypeFlag.Iterable;
type = type.getTypeArguments()[0];
[type] = type.getTypeArguments();
} else if (target === this.refs.get('GlobalAsyncIterable', SyntaxKind.TypeAliasDeclaration)) {
flags |= TypeFlag.Async | TypeFlag.Iterable;
type = type.getTypeArguments()[0];
[type] = type.getTypeArguments();
} else if (this.isContainer(target)) {
flags |= TypeFlag.Container;
type = target;
Expand Down
110 changes: 18 additions & 92 deletions core/dicc/src/container.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AsyncLocalStorage } from 'async_hooks';
import { Store } from './store';
import { ParameterStore } from './parameterStore';
import { ServiceStore } from './serviceStore';
import {
CompiledAsyncServiceDefinition,
CompiledServiceDefinition,
Expand All @@ -9,12 +10,9 @@ import {
ContainerParameters,
FindResult,
GetResult,
InvalidParameterPathError,
IterateResult,
Parameter,
ServiceMap,
ServiceScope,
UnknownParameterError,
} from './types';
import {
createAsyncIterator,
Expand All @@ -24,17 +22,17 @@ import {

export class Container<Services extends Record<string, any> = {}, Parameters extends ContainerParameters = {}> {
readonly [ServiceMap]?: Services;
readonly parameters: ParameterStore<Parameters>;

private readonly parameters: Parameters;
private readonly definitions: Map<string, CompiledServiceDefinition<any, Services, Parameters>> = new Map();
private readonly aliases: Map<string, string[]> = new Map();
private readonly globalServices: Store = new Store();
private readonly localServices: AsyncLocalStorage<Store> = new AsyncLocalStorage();
private readonly globalServices: ServiceStore = new ServiceStore();
private readonly localServices: AsyncLocalStorage<ServiceStore> = new AsyncLocalStorage();
private readonly forkHooks: [string, CompiledServiceForkHook<any, Services, Parameters>][] = [];
private readonly creating: Set<string> = new Set();

constructor(parameters: Parameters, definitions: CompiledServiceDefinitionMap<Services, Parameters>) {
this.parameters = parameters;
this.parameters = new ParameterStore(parameters);
this.importDefinitions(definitions);
}

Expand All @@ -44,15 +42,6 @@ export class Container<Services extends Record<string, any> = {}, Parameters ext
return this.getOrCreate(this.resolve(id), need);
}

iterate<Id extends keyof Services>(alias: Id): IterateResult<Services, Id>;
iterate(alias: string): Iterable<any> | AsyncIterable<any> {
const ids = this.resolve(alias, false);
const async = ids.some((id) => this.definitions.get(id)?.async);
return async
? createAsyncIterator(ids, async (id) => this.getOrCreate(id, false))
: createIterator(ids, (id) => this.getOrCreate(id, false));
}

find<Id extends keyof Services>(alias: Id): FindResult<Services, Id>;
find(alias: string): Promise<any[]> | any[] {
const ids = this.resolve(alias, false);
Expand All @@ -64,6 +53,15 @@ export class Container<Services extends Record<string, any> = {}, Parameters ext
: ids.map((id) => this.getOrCreate(id, false)).filter((service) => service !== undefined);
}

iterate<Id extends keyof Services>(alias: Id): IterateResult<Services, Id>;
iterate(alias: string): Iterable<any> | AsyncIterable<any> {
const ids = this.resolve(alias, false);
const async = ids.some((id) => this.definitions.get(id)?.async);
return async
? createAsyncIterator(ids, async (id) => this.getOrCreate(id, false))
: createIterator(ids, (id) => this.getOrCreate(id, false));
}

register<Id extends keyof Services>(alias: Id, service: Services[Id], force?: boolean): PromiseLike<void> | void;
register(alias: string, service: any): PromiseLike<void> | void {
const id = this.resolve(alias);
Expand Down Expand Up @@ -94,81 +92,9 @@ export class Container<Services extends Record<string, any> = {}, Parameters ext
}
}

getParameters(): Parameters {
return this.parameters;
}

resolveParameter<P extends string>(path: P, need?: true): Parameter<Parameters, P>;
resolveParameter<P extends string>(path: P, need: false): Parameter<Parameters, P> | undefined;
resolveParameter<P extends string>(path: P, need?: boolean): Parameter<Parameters, P> | undefined {
try {
return this.doResolveParameter(path);
} catch (e: unknown) {
if (e instanceof UnknownParameterError && need === false) {
return undefined;
}

throw e;
}
}

expand(value: string, need?: true): string;
expand(value: string, need: false): string | undefined;
expand(value: string, need?: boolean): string | undefined {
try {
return value.replace(/%([a-z0-9_.]+)%/gi, (_, path) => this.doResolveParameter(path));
} catch (e: unknown) {
if (e instanceof UnknownParameterError && need === false) {
return undefined;
}

throw e;
}
}

private doResolveParameter(path: string): any {
const tokens = path.split(/\./g);
let cursor: any = this.parameters;

for (let i = 0; i < tokens.length; ++i) {
const token = tokens[i];

if (typeof cursor !== 'object' || cursor === null) {
throw new InvalidParameterPathError(tokens.slice(0, i).join('.'));
}

if (!(token in cursor)) {
throw new UnknownParameterError(tokens.slice(0, i).join('.'));
}

cursor = cursor[token];
}

return this.expandTree(cursor);
}

private expandTree(value: any): any {
if (typeof value === 'string') {
return this.expand(value);
}

if (Array.isArray(value)) {
return value.map((v) => this.expandTree(v));
}

if (typeof value !== 'object' || value === null) {
return value;
}

return Object.fromEntries(Object.entries(value).map(([k, v]) => [
this.expand(k),
this.expandTree(v),
]));
}

async fork<R>(cb: () => Promise<R>): Promise<R> {
const parent = this.currentStore;
const store = new Store(parent);
const store = new ServiceStore(parent);
const chain = this.forkHooks.reduceRight((next, [id, hook]) => {
return async () => {
const callback = async (fork?: any) => {
Expand Down Expand Up @@ -299,11 +225,11 @@ export class Container<Services extends Record<string, any> = {}, Parameters ext
return servicePromise;
}

private get currentStore(): Store {
private get currentStore(): ServiceStore {
return this.localServices.getStore() ?? this.globalServices;
}

private getStore(scope: ServiceScope = 'global'): Store | undefined {
private getStore(scope: ServiceScope = 'global'): ServiceStore | undefined {
if (scope === 'global') {
return this.globalServices;
} else if (scope === 'local') {
Expand Down
98 changes: 98 additions & 0 deletions core/dicc/src/parameterStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
Parameter,
InvalidParameterPathError,
ParameterExpansionError,
UnknownParameterError,
} from './types';

export class ParameterStore<Parameters extends Record<string, any> = {}> {
private readonly resolving: Set<string> = new Set();

constructor(
private readonly parameters: Parameters,
) {}

getAll(): Parameters {
return this.expandValue(this.parameters);
}

resolve<P extends string>(path: P, need?: true): Parameter<Parameters, P>;
resolve<P extends string>(path: P, need: false): Parameter<Parameters, P> | undefined;
resolve<P extends string>(path: P, need?: boolean): Parameter<Parameters, P> | undefined {
try {
return this.resolvePath(path);
} catch (e: unknown) {
if (e instanceof UnknownParameterError && need === false) {
return undefined;
}

throw e;
}
}

expand(value: string, need?: true): string;
expand(value: string, need: false): string | undefined;
expand(value: string, need?: boolean): string | undefined {
try {
return this.expandString(value);
} catch (e: unknown) {
if (e instanceof UnknownParameterError && need === false) {
return undefined;
}

throw e;
}
}

private resolvePath(path: string): any {
if (this.resolving.has(path)) {
throw new ParameterExpansionError(`Cannot resolve parameter '${path}': cyclic parameter dependency detected`);
}

const tokens = path.split(/\./g);
let cursor: any = this.parameters;

for (let i = 0; i < tokens.length; ++i) {
const token = tokens[i];

if (typeof cursor !== 'object' || cursor === null) {
throw new InvalidParameterPathError(tokens.slice(0, i).join('.'));
}

if (!(token in cursor)) {
throw new UnknownParameterError(tokens.slice(0, i).join('.'));
}

cursor = cursor[token];
}

try {
this.resolving.add(path);
return this.expandValue(cursor);
} finally {
this.resolving.delete(path);
}
}

private expandString(value: string): string {
return value.replace(/%([a-z0-9_.]+)%/gi, (_, path) => this.resolvePath(path));
}

private expandValue(value: any): any {
const type = typeof value;

if (type === 'string') {
return this.expandString(value);
}

if (Array.isArray(value)) {
return value.map((v) => this.expandValue(v));
}

if (type !== 'object' || value === null) {
return value;
}

return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.expandValue(v)]));
}
}
6 changes: 3 additions & 3 deletions core/dicc/src/store.ts → core/dicc/src/serviceStore.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export class Store {
export class ServiceStore {
private readonly services: Map<string, any> = new Map();
private readonly parent?: Store;
private readonly parent?: ServiceStore;

constructor(parent?: Store) {
constructor(parent?: ServiceStore) {
this.parent = parent;
}

Expand Down
2 changes: 1 addition & 1 deletion core/dicc/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export type ServiceType<D> =
export const ServiceMap = Symbol('ServiceMap');

export type ForeignServiceType<C extends Container<any, any>, Id extends string> =
C extends {[ServiceMap]?: infer Map}
C extends { [ServiceMap]?: infer Map }
? Id extends keyof Map ? Map[Id] : never
: never;

Expand Down

0 comments on commit acd253b

Please sign in to comment.