@valkyrjaio/valkyrja 26.8.17 → 26.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/Valkyrja/Application/Constant/ApplicationInfo.ts +2 -2
- package/src/Valkyrja/Cli/README.md +42 -6
- package/src/Valkyrja/Cli/Routing/Caster/Caster.ts +32 -0
- package/src/Valkyrja/Cli/Routing/Caster/Contract/CasterContract.ts +13 -0
- package/src/Valkyrja/Cli/Routing/Constant/CliRoutingServiceId.ts +1 -0
- package/src/Valkyrja/Cli/Routing/Data/Abstract/Parameter.ts +1 -21
- package/src/Valkyrja/Cli/Routing/Data/ArgumentParameter.ts +2 -2
- package/src/Valkyrja/Cli/Routing/Data/Contract/ParameterContract.ts +1 -1
- package/src/Valkyrja/Cli/Routing/Data/OptionParameter.ts +2 -2
- package/src/Valkyrja/Cli/Routing/Provider/CliRoutingServiceProvider.ts +7 -0
- package/src/Valkyrja/Container/Manager/ChildContainer.ts +102 -7
- package/src/Valkyrja/Container/Manager/Container.ts +108 -15
- package/src/Valkyrja/Container/Manager/Contract/ContainerContract.ts +1 -0
- package/src/Valkyrja/Container/README.md +141 -16
- package/src/Valkyrja/Container/Throwable/Exception/ContainerCyclicAliasException.ts +21 -0
- package/src/Valkyrja/Http/Message/Uri/Factory/UriFactory.ts +1 -1
- package/src/Valkyrja/Http/README.md +5 -0
- package/src/Valkyrja/Http/Routing/Dispatcher/Router.ts +2 -1
- package/src/Valkyrja/Http/Routing/Matcher/Matcher.ts +8 -10
- package/src/Valkyrja/Http/Routing/Provider/HttpRoutingServiceProvider.ts +4 -1
- package/src/Valkyrja/Type/Contract/TypeContract.ts +13 -0
- package/src/Valkyrja/Type/README.md +91 -35
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export class ApplicationInfo {
|
|
10
|
-
static readonly VERSION = '26.
|
|
11
|
-
static readonly VERSION_BUILD_DATE_TIME = '
|
|
10
|
+
static readonly VERSION = '26.9.0' as const;
|
|
11
|
+
static readonly VERSION_BUILD_DATE_TIME = 'October 19 2026 11:37:33 MST' as const;
|
|
12
12
|
static readonly ASCII = `
|
|
13
13
|
_ _ _
|
|
14
14
|
/\\ /\\__ _| | | ___ _ _ __(_) __ _
|
|
@@ -251,8 +251,9 @@ An empty string given at the call site counts as a default, so it suppresses
|
|
|
251
251
|
the declared one. Omit the default, or pass `null`, to reach step 3.
|
|
252
252
|
|
|
253
253
|
`getArgumentValue()` reads step 1 and step 2, because an argument declares no
|
|
254
|
-
default. Read
|
|
255
|
-
|
|
254
|
+
default. Read `ParameterContract.getValues()` for every raw value of a parameter
|
|
255
|
+
in `ARRAY` value mode, and `Caster.getCastValues()` for every value with the
|
|
256
|
+
cast applied.
|
|
256
257
|
|
|
257
258
|
```ts
|
|
258
259
|
const isShort = route.hasProvidedOption('short');
|
|
@@ -319,11 +320,45 @@ group and keeps the default for the rest.
|
|
|
319
320
|
|
|
320
321
|
### Value casting
|
|
321
322
|
|
|
322
|
-
|
|
323
|
+
Warning: the router applies no cast. `getArgumentValue()` and `getOptionValue()`
|
|
324
|
+
on the route return the raw string, whatever cast the parameter declares. The
|
|
325
|
+
application asks the caster for the cast values.
|
|
323
326
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
+
`Caster` applies the cast, and the parameter applies nothing. `Caster` holds the
|
|
328
|
+
container, so the data object needs none. The parameter holds the cast and the
|
|
329
|
+
raw values, and `getValues()` returns those raw values.
|
|
330
|
+
|
|
331
|
+
`Caster.getCastValues()` reads `cast.type` as a container binding key. It passes
|
|
332
|
+
the raw value as the first entry of the factory's `args`. It returns the
|
|
333
|
+
converted value when `cast.convert` is `true`, and the type itself when
|
|
334
|
+
`cast.convert` is `false`. It returns each raw value for a parameter that holds
|
|
335
|
+
no cast.
|
|
336
|
+
|
|
337
|
+
Warning: register a cast type with `bind`. The caster calls `getService()`, and
|
|
338
|
+
`getService()` skips the singleton cache, so the container builds a type that
|
|
339
|
+
`bindSingleton` registers for each value, and not once for the application. An
|
|
340
|
+
alias, and an instance that `setSingleton` holds, raise
|
|
341
|
+
`ContainerInvalidReferenceException`. See [Type](../Type/README.md).
|
|
342
|
+
|
|
343
|
+
Warning: the factory must return a `TypeContract`. The caster calls `asValue()`
|
|
344
|
+
on the object that the container builds, and an object without that method
|
|
345
|
+
raises a `TypeError`.
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
import { Argument } from '@valkyrjaio/valkyrja/Cli/Interaction/Argument/Argument.ts';
|
|
349
|
+
import { ArgumentParameter } from '@valkyrjaio/valkyrja/Cli/Routing/Data/ArgumentParameter.ts';
|
|
350
|
+
import { CliRoutingServiceId } from '@valkyrjaio/valkyrja/Cli/Routing/Constant/CliRoutingServiceId.ts';
|
|
351
|
+
import { Cast } from '@valkyrjaio/valkyrja/Type/Data/Cast.ts';
|
|
352
|
+
|
|
353
|
+
import type { CasterContract } from '@valkyrjaio/valkyrja/Cli/Routing/Caster/Contract/CasterContract.ts';
|
|
354
|
+
|
|
355
|
+
container.bind('App.Type.Slug', Slug.make);
|
|
356
|
+
|
|
357
|
+
const parameter = new ArgumentParameter('target', 'The target', new Cast('App.Type.Slug')).withArguments(
|
|
358
|
+
new Argument('a-slug'),
|
|
359
|
+
);
|
|
360
|
+
const values = container.getSingleton<CasterContract>(CliRoutingServiceId.CasterContract).getCastValues(parameter);
|
|
361
|
+
```
|
|
327
362
|
|
|
328
363
|
## Input and output
|
|
329
364
|
|
|
@@ -602,6 +637,7 @@ list, because the input handler runs that stage before it matches a route.
|
|
|
602
637
|
| `CliRoutingServiceId.RouterContract` | A `Router` |
|
|
603
638
|
| `CliRoutingServiceId.RouteCollectionContract` | A `RouteCollection` |
|
|
604
639
|
| `CliRoutingServiceId.RouteCollectorContract` | An `AttributeRouteCollector` |
|
|
640
|
+
| `CliRoutingServiceId.CasterContract` | A `Caster`, which applies a cast |
|
|
605
641
|
| `CliRoutingServiceId.CliRoutingData` | The collection's `CliRoutingData` |
|
|
606
642
|
| `CliRoutingServiceId.RouteContract` | The matched route, set at dispatch |
|
|
607
643
|
| `CliInteractionServiceId.CliInteractionConfigContract` | The interaction config |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja Framework package.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2016-present Melech Mizrachi
|
|
5
|
+
*
|
|
6
|
+
* Released under the MIT License. See LICENSE.md for details.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ContainerContract } from '../../../Container/Manager/Contract/ContainerContract.ts';
|
|
10
|
+
import type { TypeContract } from '../../../Type/Contract/TypeContract.ts';
|
|
11
|
+
import type { ParameterContract } from '../Data/Contract/ParameterContract.ts';
|
|
12
|
+
import type { CasterContract } from './Contract/CasterContract.ts';
|
|
13
|
+
|
|
14
|
+
export class Caster implements CasterContract {
|
|
15
|
+
constructor(protected container: ContainerContract) {}
|
|
16
|
+
|
|
17
|
+
getCastValues(parameter: ParameterContract): unknown[] {
|
|
18
|
+
const values = parameter.getValues();
|
|
19
|
+
|
|
20
|
+
if (!parameter.hasCast()) {
|
|
21
|
+
return values;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const cast = parameter.getCast();
|
|
25
|
+
|
|
26
|
+
return values.map((value) => {
|
|
27
|
+
const type = this.container.getService<TypeContract>(cast.type, [value]);
|
|
28
|
+
|
|
29
|
+
return cast.convert ? type.asValue() : type;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja Framework package.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2016-present Melech Mizrachi
|
|
5
|
+
*
|
|
6
|
+
* Released under the MIT License. See LICENSE.md for details.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ParameterContract } from '../../Data/Contract/ParameterContract.ts';
|
|
10
|
+
|
|
11
|
+
export interface CasterContract {
|
|
12
|
+
getCastValues(parameter: ParameterContract): unknown[];
|
|
13
|
+
}
|
|
@@ -10,6 +10,7 @@ export class CliRoutingServiceId {
|
|
|
10
10
|
static readonly RouterContract = 'Valkyrja.Cli.Routing.Dispatcher.RouterContract' as const;
|
|
11
11
|
static readonly RouteCollectionContract = 'Valkyrja.Cli.Routing.Collection.RouteCollectionContract' as const;
|
|
12
12
|
static readonly RouteCollectorContract = 'Valkyrja.Cli.Routing.Collector.RouteCollectorContract' as const;
|
|
13
|
+
static readonly CasterContract = 'Valkyrja.Cli.Routing.Caster.CasterContract' as const;
|
|
13
14
|
static readonly RouteContract = 'Valkyrja.Cli.Routing.Data.RouteContract' as const;
|
|
14
15
|
static readonly CliRoutingData = 'Valkyrja.Cli.Routing.Data.CliRoutingData' as const;
|
|
15
16
|
static readonly CliRoutingConfigContract = 'Valkyrja.Cli.Routing.Data.CliRoutingConfigContract' as const;
|
|
@@ -6,8 +6,6 @@
|
|
|
6
6
|
* Released under the MIT License. See LICENSE.md for details.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { ArgumentContract } from '../../../Interaction/Argument/Contract/ArgumentContract.ts';
|
|
10
|
-
import type { OptionContract } from '../../../Interaction/Option/Contract/OptionContract.ts';
|
|
11
9
|
import type { ParameterContract } from '../Contract/ParameterContract.ts';
|
|
12
10
|
import type { Cast } from '../../../../Type/Data/Cast.ts';
|
|
13
11
|
import { CliRoutingNoCastException } from '../../Throwable/Exception/CliRoutingNoCastException.ts';
|
|
@@ -63,25 +61,7 @@ export abstract class Parameter implements ParameterContract {
|
|
|
63
61
|
return clone;
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
abstract
|
|
67
|
-
|
|
68
|
-
protected getCastValuesForParameters(parameters: Array<ArgumentContract | OptionContract>): unknown[] {
|
|
69
|
-
const values: unknown[] = [];
|
|
70
|
-
const cast = this.cast;
|
|
71
|
-
|
|
72
|
-
for (const param of parameters) {
|
|
73
|
-
const paramValue = param.getValue();
|
|
74
|
-
|
|
75
|
-
if (cast === null) {
|
|
76
|
-
values.push(paramValue);
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
values.push(paramValue);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return values;
|
|
84
|
-
}
|
|
64
|
+
abstract getValues(): string[];
|
|
85
65
|
|
|
86
66
|
abstract isProvided(): boolean;
|
|
87
67
|
abstract hasFirstValue(): boolean;
|
|
@@ -63,8 +63,8 @@ export class ArgumentParameter extends Parameter implements ArgumentParameterCon
|
|
|
63
63
|
return clone;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
return this.
|
|
66
|
+
getValues(): string[] {
|
|
67
|
+
return this.arguments_.map((argument) => argument.getValue());
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
isProvided(): boolean {
|
|
@@ -17,7 +17,7 @@ export interface ParameterContract {
|
|
|
17
17
|
withoutCast(): this;
|
|
18
18
|
getDescription(): string;
|
|
19
19
|
withDescription(description: string): this;
|
|
20
|
-
|
|
20
|
+
getValues(): string[];
|
|
21
21
|
isProvided(): boolean;
|
|
22
22
|
hasFirstValue(): boolean;
|
|
23
23
|
getFirstValue(): string;
|
|
@@ -152,8 +152,8 @@ export class OptionParameter extends Parameter implements OptionParameterContrac
|
|
|
152
152
|
return clone;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
return this.
|
|
155
|
+
getValues(): string[] {
|
|
156
|
+
return this.options.map((option) => option.getValue());
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
isProvided(): boolean {
|
|
@@ -25,6 +25,8 @@ import type { RouteCollectorContract } from '../Collector/Contract/RouteCollecto
|
|
|
25
25
|
import type { RouteContract } from '../Data/Contract/RouteContract.ts';
|
|
26
26
|
import { CliRoutingServiceId } from '../Constant/CliRoutingServiceId.ts';
|
|
27
27
|
import { CliRoutingData } from '../Data/CliRoutingData.ts';
|
|
28
|
+
import { Caster } from '../Caster/Caster.ts';
|
|
29
|
+
import type { CasterContract } from '../Caster/Contract/CasterContract.ts';
|
|
28
30
|
import type { RouterContract } from '../Dispatcher/Contract/RouterContract.ts';
|
|
29
31
|
import { Router } from '../Dispatcher/Router.ts';
|
|
30
32
|
|
|
@@ -34,6 +36,7 @@ export class CliRoutingServiceProvider implements ServiceProviderContract {
|
|
|
34
36
|
[CliRoutingServiceId.RouterContract]: CliRoutingServiceProvider.publishRouter,
|
|
35
37
|
[CliRoutingServiceId.RouteCollectionContract]: CliRoutingServiceProvider.publishRouteCollection,
|
|
36
38
|
[CliRoutingServiceId.RouteCollectorContract]: CliRoutingServiceProvider.publishAttributeRouteCollector,
|
|
39
|
+
[CliRoutingServiceId.CasterContract]: CliRoutingServiceProvider.publishCaster,
|
|
37
40
|
[CliRoutingServiceId.CliRoutingData]: CliRoutingServiceProvider.publishData,
|
|
38
41
|
};
|
|
39
42
|
}
|
|
@@ -87,6 +90,10 @@ export class CliRoutingServiceProvider implements ServiceProviderContract {
|
|
|
87
90
|
);
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
static publishCaster(this: void, container: ContainerContract): void {
|
|
94
|
+
container.setSingleton<CasterContract>(CliRoutingServiceId.CasterContract, new Caster(container));
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
static publishData(this: void, container: ContainerContract): void {
|
|
91
98
|
const collection = container.getSingleton<RouteCollectionContract>(CliRoutingServiceId.RouteCollectionContract);
|
|
92
99
|
const app = container.getSingleton<ApplicationContract>(ApplicationServiceId.ApplicationContract);
|
|
@@ -6,12 +6,16 @@
|
|
|
6
6
|
* Released under the MIT License. See LICENSE.md for details.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { ContainerCyclicAliasException } from '../Throwable/Exception/ContainerCyclicAliasException.ts';
|
|
9
10
|
import { Container } from './Container.ts';
|
|
10
11
|
|
|
11
12
|
import type { ContainerData } from '../Data/ContainerData.ts';
|
|
12
13
|
import type { ContainerContract } from './Contract/ContainerContract.ts';
|
|
13
14
|
|
|
14
15
|
export class ChildContainer extends Container {
|
|
16
|
+
/** The alias targets this container is resolving. */
|
|
17
|
+
protected targetsInFlight = new Set<string>();
|
|
18
|
+
|
|
15
19
|
constructor(
|
|
16
20
|
protected parent: ContainerContract,
|
|
17
21
|
data: ContainerData,
|
|
@@ -34,10 +38,6 @@ export class ChildContainer extends Container {
|
|
|
34
38
|
return super.isSingletonInstance(id) || this.parent.isSingletonInstance(id);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
override isDeferred(id: string): boolean {
|
|
38
|
-
return super.isDeferred(id) || this.parent.isDeferred(id);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
41
|
override isPublished(id: string): boolean {
|
|
42
42
|
return super.isPublished(id) || this.parent.isPublished(id);
|
|
43
43
|
}
|
|
@@ -58,11 +58,106 @@ export class ChildContainer extends Container {
|
|
|
58
58
|
return super.getServiceWithoutChecks<T>(id, args);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
override getAliasedId(alias: string): string | undefined {
|
|
62
|
+
return super.getAliasedId(alias) ?? this.parent.getAliasedId(alias);
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
protected override getAliasedWithoutChecks<T extends object>(id: string, args: unknown[] = []): T | undefined {
|
|
62
|
-
if (
|
|
63
|
-
return
|
|
66
|
+
if (super.isAlias(id)) {
|
|
67
|
+
return super.getAliasedWithoutChecks<T>(id, args);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const target = this.getParentAliasTarget(id);
|
|
71
|
+
|
|
72
|
+
if (target === undefined) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The child holds the same registration. One request must not hold one copy
|
|
77
|
+
// for the alias and another for the target.
|
|
78
|
+
if (this.resolvesInChild(target)) {
|
|
79
|
+
return this.getTargetOnce<T>(id, target, args);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this.parent.getAliased<T>(id, args);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve an alias target, and check a chain that returns to one already in flight.
|
|
87
|
+
*/
|
|
88
|
+
protected getTargetOnce<T extends object>(id: string, target: string, args: unknown[]): T {
|
|
89
|
+
// A chain that closes across two walks returns here rather than to one walk. An
|
|
90
|
+
// instance cached for the target has broken the chain, so read that first.
|
|
91
|
+
if (this.targetsInFlight.has(target)) {
|
|
92
|
+
const registered = this.getSingletonInstance<T>(target);
|
|
93
|
+
|
|
94
|
+
if (registered !== undefined) {
|
|
95
|
+
return registered;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw new ContainerCyclicAliasException(id, target);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.targetsInFlight.add(target);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
return this.get<T>(target, args);
|
|
105
|
+
} finally {
|
|
106
|
+
this.targetsInFlight.delete(target);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Walk the parent's chain of aliases, and return the last hop it reaches.
|
|
112
|
+
*/
|
|
113
|
+
protected getParentAliasTarget(id: string): string | undefined {
|
|
114
|
+
let current = id;
|
|
115
|
+
let target: string | undefined;
|
|
116
|
+
let aliasedId = this.parent.getAliasedId(current);
|
|
117
|
+
const seen = new Set<string>([id]);
|
|
118
|
+
|
|
119
|
+
while (aliasedId !== undefined) {
|
|
120
|
+
// A parent that is itself a child reads its own map and its parent's, and a
|
|
121
|
+
// binding made on either after it was built can close a chain between them.
|
|
122
|
+
if (seen.has(aliasedId)) {
|
|
123
|
+
throw new ContainerCyclicAliasException(current, aliasedId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
seen.add(aliasedId);
|
|
127
|
+
target = aliasedId;
|
|
128
|
+
current = aliasedId;
|
|
129
|
+
|
|
130
|
+
// The parent reads these before it follows an alias, so it can answer at this
|
|
131
|
+
// hop rather than continue the chain.
|
|
132
|
+
if (
|
|
133
|
+
(this.parent.isDeferred(current) && !this.parent.isPublished(current)) ||
|
|
134
|
+
this.parent.isSingleton(current) ||
|
|
135
|
+
this.parent.isService(current)
|
|
136
|
+
) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
aliasedId = this.parent.getAliasedId(current);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return target;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check whether the child resolves the target of a parent-declared alias itself.
|
|
148
|
+
*/
|
|
149
|
+
protected resolvesInChild(target: string): boolean {
|
|
150
|
+
// The parent publishes before it reads any map, so this test comes first. The
|
|
151
|
+
// parent's state and the child's callback each decide one half.
|
|
152
|
+
if (this.parent.isDeferred(target) && !this.parent.isPublished(target) && this.isDeferred(target)) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.parent.isSingletonInstance(target)) {
|
|
157
|
+
return false;
|
|
64
158
|
}
|
|
65
159
|
|
|
66
|
-
|
|
160
|
+
// Both containers answer here, and each marker decides one half.
|
|
161
|
+
return this.parent.isSingletonBinding(target) && this.isSingletonBinding(target);
|
|
67
162
|
}
|
|
68
163
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ContainerData } from '../Data/ContainerData.ts';
|
|
10
|
+
import { ContainerCyclicAliasException } from '../Throwable/Exception/ContainerCyclicAliasException.ts';
|
|
10
11
|
import { ContainerInvalidReferenceException } from '../Throwable/Exception/ContainerInvalidReferenceException.ts';
|
|
11
12
|
import { ContainerInvalidPublishCallbackException } from '../Throwable/Exception/ContainerInvalidPublishCallbackException.ts';
|
|
12
13
|
|
|
@@ -22,6 +23,9 @@ export class Container implements ContainerContract {
|
|
|
22
23
|
protected published: Record<string, boolean> = {};
|
|
23
24
|
|
|
24
25
|
constructor(data: ContainerData = new ContainerData()) {
|
|
26
|
+
// Nothing is installed yet, so past the map there is nothing to read
|
|
27
|
+
this.validateAliasMapIsNotCyclic(data.aliases, () => undefined);
|
|
28
|
+
|
|
25
29
|
this.aliases = { ...data.aliases };
|
|
26
30
|
this.deferredCallback = { ...data.deferredCallback };
|
|
27
31
|
this.services = { ...data.services };
|
|
@@ -38,7 +42,13 @@ export class Container implements ContainerContract {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
setFromData(data: ContainerData): void {
|
|
41
|
-
|
|
45
|
+
const aliases = { ...this.aliases, ...data.aliases };
|
|
46
|
+
|
|
47
|
+
// Only the incoming aliases start a walk, and each walk reads the container past
|
|
48
|
+
// the map it is given. Nothing is installed before the walks end.
|
|
49
|
+
this.validateAliasMapIsNotCyclic(data.aliases, (id) => this.getAliasedId(id));
|
|
50
|
+
|
|
51
|
+
this.aliases = aliases;
|
|
42
52
|
this.deferredCallback = { ...this.deferredCallback, ...data.deferredCallback };
|
|
43
53
|
this.services = { ...this.services, ...data.services };
|
|
44
54
|
this.singletons = { ...this.singletons, ...data.singletons };
|
|
@@ -56,11 +66,81 @@ export class Container implements ContainerContract {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
bindAlias(alias: string, id: string): this {
|
|
69
|
+
this.validateAliasIsNotCyclic(alias, id);
|
|
70
|
+
|
|
59
71
|
this.aliases[alias] = id;
|
|
60
72
|
|
|
61
73
|
return this;
|
|
62
74
|
}
|
|
63
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Validate that an alias does not point at a chain that returns to it.
|
|
78
|
+
*/
|
|
79
|
+
protected validateAliasIsNotCyclic(alias: string, id: string): void {
|
|
80
|
+
if (alias === id) {
|
|
81
|
+
throw new ContainerCyclicAliasException(alias, id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const seen = new Set<string>();
|
|
85
|
+
let current = id;
|
|
86
|
+
let aliasedId = this.getAliasedId(current);
|
|
87
|
+
|
|
88
|
+
while (aliasedId !== undefined) {
|
|
89
|
+
if (aliasedId === alias) {
|
|
90
|
+
throw new ContainerCyclicAliasException(alias, id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// A parent that binds an alias after a child is built checks only its own map,
|
|
94
|
+
// so the two can hold a cycle this alias is no part of. End the walk there.
|
|
95
|
+
if (seen.has(aliasedId)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
seen.add(aliasedId);
|
|
100
|
+
current = aliasedId;
|
|
101
|
+
aliasedId = this.getAliasedId(current);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate that no alias in a map points at a chain that returns to it.
|
|
107
|
+
*/
|
|
108
|
+
protected validateAliasMapIsNotCyclic(
|
|
109
|
+
aliases: Record<string, string>,
|
|
110
|
+
installed: (id: string) => string | undefined,
|
|
111
|
+
): void {
|
|
112
|
+
// `installed` is a parameter rather than a call, because an override reaches a
|
|
113
|
+
// subclass the constructor has not set up yet.
|
|
114
|
+
|
|
115
|
+
// Past the supplied aliases, the walk reads what the container answers already,
|
|
116
|
+
// so it follows a chain the supplied map only reaches into.
|
|
117
|
+
const next = (id: string): string | undefined => (Object.hasOwn(aliases, id) ? aliases[id] : installed(id));
|
|
118
|
+
|
|
119
|
+
for (const alias of Object.keys(aliases)) {
|
|
120
|
+
const seen = new Set<string>([alias]);
|
|
121
|
+
let current = alias;
|
|
122
|
+
let aliasedId = next(current);
|
|
123
|
+
|
|
124
|
+
while (aliasedId !== undefined) {
|
|
125
|
+
// The chain returns to the alias this walk started from, so the map the
|
|
126
|
+
// caller supplied is what closes it. Name the edge that took it there.
|
|
127
|
+
if (aliasedId === alias) {
|
|
128
|
+
throw new ContainerCyclicAliasException(current, aliasedId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// A chain the container already held returns here. `bindAlias()` ends its
|
|
132
|
+
// walk for that state, so this entry point answers it the same way.
|
|
133
|
+
if (seen.has(aliasedId)) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
seen.add(aliasedId);
|
|
138
|
+
current = aliasedId;
|
|
139
|
+
aliasedId = next(current);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
64
144
|
bindSingleton<T extends object>(id: string, factory: (container: ContainerContract, args?: unknown[]) => T): this {
|
|
65
145
|
this.singletons[id] = id;
|
|
66
146
|
this.bind(id, factory);
|
|
@@ -76,11 +156,12 @@ export class Container implements ContainerContract {
|
|
|
76
156
|
}
|
|
77
157
|
|
|
78
158
|
isAlias(id: string): boolean {
|
|
79
|
-
|
|
159
|
+
// The map is a plain object, so only its own keys are aliases the container holds
|
|
160
|
+
return Object.hasOwn(this.aliases, id);
|
|
80
161
|
}
|
|
81
162
|
|
|
82
163
|
isService(id: string): boolean {
|
|
83
|
-
return
|
|
164
|
+
return Object.hasOwn(this.services, id);
|
|
84
165
|
}
|
|
85
166
|
|
|
86
167
|
isSingleton(id: string): boolean {
|
|
@@ -88,11 +169,11 @@ export class Container implements ContainerContract {
|
|
|
88
169
|
}
|
|
89
170
|
|
|
90
171
|
isSingletonBinding(id: string): boolean {
|
|
91
|
-
return
|
|
172
|
+
return Object.hasOwn(this.singletons, id);
|
|
92
173
|
}
|
|
93
174
|
|
|
94
175
|
isSingletonInstance(id: string): boolean {
|
|
95
|
-
return
|
|
176
|
+
return Object.hasOwn(this.instances, id);
|
|
96
177
|
}
|
|
97
178
|
|
|
98
179
|
get<T extends object>(id: string, args: unknown[] = []): T {
|
|
@@ -150,11 +231,11 @@ export class Container implements ContainerContract {
|
|
|
150
231
|
}
|
|
151
232
|
|
|
152
233
|
isDeferred(id: string): boolean {
|
|
153
|
-
return
|
|
234
|
+
return Object.hasOwn(this.deferredCallback, id);
|
|
154
235
|
}
|
|
155
236
|
|
|
156
237
|
isPublished(id: string): boolean {
|
|
157
|
-
return
|
|
238
|
+
return Object.hasOwn(this.published, id);
|
|
158
239
|
}
|
|
159
240
|
|
|
160
241
|
publish(id: string): void {
|
|
@@ -169,7 +250,7 @@ export class Container implements ContainerContract {
|
|
|
169
250
|
}
|
|
170
251
|
|
|
171
252
|
protected getAliasedWithoutChecks<T extends object>(id: string, args: unknown[] = []): T | undefined {
|
|
172
|
-
const aliased = this.
|
|
253
|
+
const aliased = this.getAliasedId(id);
|
|
173
254
|
|
|
174
255
|
if (aliased === undefined) {
|
|
175
256
|
return undefined;
|
|
@@ -191,10 +272,20 @@ export class Container implements ContainerContract {
|
|
|
191
272
|
|
|
192
273
|
const singleton = this.getServiceWithoutChecks<T>(id);
|
|
193
274
|
|
|
194
|
-
if (singleton
|
|
195
|
-
|
|
275
|
+
if (singleton === undefined) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// The map decides which instance every reader gets, because a factory can cache
|
|
280
|
+
// an instance for this id while it runs, before this write.
|
|
281
|
+
const registered = this.getSingletonInstance<T>(id);
|
|
282
|
+
|
|
283
|
+
if (registered !== undefined) {
|
|
284
|
+
return registered;
|
|
196
285
|
}
|
|
197
286
|
|
|
287
|
+
this.instances[id] = singleton;
|
|
288
|
+
|
|
198
289
|
return singleton;
|
|
199
290
|
}
|
|
200
291
|
|
|
@@ -208,20 +299,22 @@ export class Container implements ContainerContract {
|
|
|
208
299
|
return factory(this, args) as T;
|
|
209
300
|
}
|
|
210
301
|
|
|
211
|
-
|
|
212
|
-
|
|
302
|
+
getAliasedId(alias: string): string | undefined {
|
|
303
|
+
// The map is a plain object, so only its own keys are aliases the container holds
|
|
304
|
+
return Object.hasOwn(this.aliases, alias) ? this.aliases[alias] : undefined;
|
|
213
305
|
}
|
|
214
306
|
|
|
215
307
|
protected getSingletonInstance<T extends object>(id: string): T | undefined {
|
|
216
|
-
|
|
308
|
+
// The map is a plain object, so only its own keys are instances the container holds
|
|
309
|
+
return Object.hasOwn(this.instances, id) ? (this.instances[id] as T) : undefined;
|
|
217
310
|
}
|
|
218
311
|
|
|
219
312
|
protected getServiceCallable(id: string): ((container: ContainerContract, args?: unknown[]) => object) | undefined {
|
|
220
|
-
return this.services[id];
|
|
313
|
+
return Object.hasOwn(this.services, id) ? this.services[id] : undefined;
|
|
221
314
|
}
|
|
222
315
|
|
|
223
316
|
protected getDeferredCallback(id: string): ((container: ContainerContract) => void) | undefined {
|
|
224
|
-
return this.deferredCallback[id];
|
|
317
|
+
return Object.hasOwn(this.deferredCallback, id) ? this.deferredCallback[id] : undefined;
|
|
225
318
|
}
|
|
226
319
|
|
|
227
320
|
protected publishUnpublishedProvided(id: string): void {
|
|
@@ -23,6 +23,7 @@ export interface ContainerContract extends ProvidersAwareContract {
|
|
|
23
23
|
isSingletonBinding(id: string): boolean;
|
|
24
24
|
isSingletonInstance(id: string): boolean;
|
|
25
25
|
get<T extends object>(id: string, args?: unknown[]): T;
|
|
26
|
+
getAliasedId(alias: string): string | undefined;
|
|
26
27
|
getAliased<T extends object>(id: string, args?: unknown[]): T;
|
|
27
28
|
getService<T extends object>(id: string, args?: unknown[]): T;
|
|
28
29
|
getSingleton<T extends object>(id: string): T;
|
|
@@ -56,14 +56,15 @@ export interface ContainerContract extends ProvidersAwareContract {
|
|
|
56
56
|
isSingletonBinding(id: string): boolean;
|
|
57
57
|
isSingletonInstance(id: string): boolean;
|
|
58
58
|
get<T extends object>(id: string, args?: unknown[]): T;
|
|
59
|
+
getAliasedId(alias: string): string | undefined;
|
|
59
60
|
getAliased<T extends object>(id: string, args?: unknown[]): T;
|
|
60
61
|
getService<T extends object>(id: string, args?: unknown[]): T;
|
|
61
62
|
getSingleton<T extends object>(id: string): T;
|
|
62
63
|
}
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
`getAliasedId()` returns the id an alias points to, one hop at a time, and
|
|
67
|
+
`undefined` when the id is not an alias.
|
|
67
68
|
|
|
68
69
|
## Service types
|
|
69
70
|
|
|
@@ -139,6 +140,15 @@ container.bindSingleton(CliServerServiceId.InputHandlerContract, (container) =>
|
|
|
139
140
|
The second call returns the stored object. The container writes that object
|
|
140
141
|
into `instances` on the first resolution.
|
|
141
142
|
|
|
143
|
+
Warning: a build keeps the first instance the map holds for an id. A factory
|
|
144
|
+
that caches an instance for the id it is building decides what every reader
|
|
145
|
+
gets. The object that factory returns is discarded then.
|
|
146
|
+
|
|
147
|
+
Warning: that rule holds inside one container. A `ChildContainer` hands a
|
|
148
|
+
parent-held factory to the parent, so the registration lands in the parent and
|
|
149
|
+
the child caches the object the factory returned. The two containers then hold
|
|
150
|
+
different objects for that id.
|
|
151
|
+
|
|
142
152
|
### bindAlias()
|
|
143
153
|
|
|
144
154
|
`bindAlias()` maps one id onto another. The first argument is the alias, and the
|
|
@@ -148,6 +158,33 @@ second argument is the id the container resolves instead:
|
|
|
148
158
|
container.bindAlias('App.Logger', LoggerContractId);
|
|
149
159
|
```
|
|
150
160
|
|
|
161
|
+
An alias that points at a chain that returns to it has no end. Four checks
|
|
162
|
+
reject one with `ContainerCyclicAliasException`:
|
|
163
|
+
|
|
164
|
+
- `bindAlias()` checks the pair it is asked to store.
|
|
165
|
+
- The constructor and `setFromData()` check the aliases they receive, and the
|
|
166
|
+
chain those aliases reach.
|
|
167
|
+
- A child walking the parent's aliases checks the hops of one walk.
|
|
168
|
+
- A child resolving a parent-declared alias checks the target it returns to. The
|
|
169
|
+
check sits on the container that resolves, so a parent which is itself a child
|
|
170
|
+
throws from its own. An instance cached for the target while the lookup ran
|
|
171
|
+
has broken the chain, so the lookup answers with that instance.
|
|
172
|
+
|
|
173
|
+
The first two checks run at registration. A container installs no map before its
|
|
174
|
+
walk ends, so a caller that catches the exception keeps the container it had. A
|
|
175
|
+
container that writes an alias after a child reads through it is outside
|
|
176
|
+
registration. The last two checks see one walk and one return, so a chain that
|
|
177
|
+
reaches neither is unchecked. It has one of four outcomes:
|
|
178
|
+
|
|
179
|
+
- It resolves through the first hop the parent would answer.
|
|
180
|
+
- It ends with a missing reference, when no hop answers.
|
|
181
|
+
- It does not end, when a factory or a publish callback runs in a container that
|
|
182
|
+
carries no such check. A plain `Container` carries none. The child hands the
|
|
183
|
+
lookup to the parent for a target the child does not resolve itself. The
|
|
184
|
+
parent also runs its own factory for a target the child declares none for.
|
|
185
|
+
- It does not end, when an alias the child declares closes a chain through a
|
|
186
|
+
factory or a publish callback the child runs. No check sits on that path.
|
|
187
|
+
|
|
151
188
|
### Every service needs a binding
|
|
152
189
|
|
|
153
190
|
The container does not read a constructor and does not build an unregistered
|
|
@@ -330,7 +367,10 @@ transfer.
|
|
|
330
367
|
object into the container, so an existing key keeps the incoming value:
|
|
331
368
|
|
|
332
369
|
```ts
|
|
333
|
-
|
|
370
|
+
container.bindAlias('App.Logger', FileLoggerId);
|
|
371
|
+
container.setFromData(new ContainerData({ aliases: { 'App.Logger': SlackLoggerId } }));
|
|
372
|
+
|
|
373
|
+
container.getAliasedId('App.Logger'); // SlackLoggerId
|
|
334
374
|
```
|
|
335
375
|
|
|
336
376
|
The `Container` constructor also takes a `ContainerData`, and it replaces the
|
|
@@ -339,7 +379,9 @@ four maps instead of merging.
|
|
|
339
379
|
## Child containers
|
|
340
380
|
|
|
341
381
|
A child container is a per-request container. `WorkerHttp` builds one for each
|
|
342
|
-
request, so request state never reaches the long-lived parent
|
|
382
|
+
request, so request state never reaches the long-lived parent. The parent still
|
|
383
|
+
publishes a deferred id, and caches a singleton, when it answers a lookup a
|
|
384
|
+
child handed to it:
|
|
343
385
|
|
|
344
386
|
```ts
|
|
345
387
|
static getChildContainer(app: ApplicationContract, data: ContainerData): ContainerContract {
|
|
@@ -382,18 +424,24 @@ protected override getServiceWithoutChecks<T extends object>(id: string, args: u
|
|
|
382
424
|
}
|
|
383
425
|
```
|
|
384
426
|
|
|
385
|
-
`isAlias()`, `isService()`, `isSingletonInstance()`, `
|
|
386
|
-
|
|
427
|
+
`isAlias()`, `isService()`, `isSingletonInstance()`, and `isPublished()` each
|
|
428
|
+
report the child state or the parent state. `isSingletonBinding()` is not
|
|
429
|
+
overridden: the child answers for its own markers, which start from the
|
|
430
|
+
snapshot.
|
|
387
431
|
|
|
388
|
-
Warning: `
|
|
389
|
-
|
|
390
|
-
`getData()` ran.
|
|
432
|
+
Warning: `isDeferred()` is not overridden. The child reports the callbacks its
|
|
433
|
+
own map holds, and it does not report a provider the parent registered after
|
|
434
|
+
`getData()` ran. `has()` reads `isDeferred()`, `isSingleton()`, `isService()`,
|
|
435
|
+
and `isAlias()`. It follows the narrowing for a deferred id, and it reports the
|
|
436
|
+
parent for a service, an alias, or a cached instance.
|
|
391
437
|
|
|
392
438
|
### Where a singleton instance lives
|
|
393
439
|
|
|
394
440
|
A singleton binding that the child copied resolves once for each child. The
|
|
395
|
-
child
|
|
396
|
-
|
|
441
|
+
child's own factory runs when the child declares one for that id. Otherwise the
|
|
442
|
+
child asks the parent to run the parent's factory, and the child stores the
|
|
443
|
+
result in its own `instances` map. The parent's map keeps what that factory
|
|
444
|
+
resolved for itself, because that factory receives the parent.
|
|
397
445
|
|
|
398
446
|
An instance the parent built before the request loop is shared. The child
|
|
399
447
|
returns the parent's object:
|
|
@@ -417,11 +465,87 @@ static bootstrapParentServices(app: ApplicationContract): void {
|
|
|
417
465
|
}
|
|
418
466
|
```
|
|
419
467
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
468
|
+
### Where an alias resolves
|
|
469
|
+
|
|
470
|
+
An alias resolves in the container that declares it, so where you declare an
|
|
471
|
+
alias selects the resolution scope. A child lookup of an alias that only the
|
|
472
|
+
parent declares goes to the parent, and the parent answers it as it would for
|
|
473
|
+
any caller:
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
// Once, at boot. The child never declares this alias.
|
|
477
|
+
parent.bind(SlackNotifierId, (c) => SlackNotifier.make(c));
|
|
478
|
+
parent.bindAlias(NotifierContractId, SlackNotifierId);
|
|
479
|
+
|
|
480
|
+
// For each request, the child binds its own.
|
|
481
|
+
child.bind(SlackNotifierId, (c) => SlackNotifier.make(c));
|
|
482
|
+
|
|
483
|
+
child.get(SlackNotifierId); // built by the child's binding
|
|
484
|
+
child.get(NotifierContractId); // built by the parent's binding
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
There is one exception. The child resolves a target the parent would answer for
|
|
488
|
+
the first time, when the child holds that registration too. The request must not
|
|
489
|
+
hold one copy for the alias and another for the target. Three cases:
|
|
490
|
+
|
|
491
|
+
- **A singleton binding the parent never built** — the child resolves it when
|
|
492
|
+
its own `singletons` map carries the marker.
|
|
493
|
+
- **A publisher the parent has not run** — the child resolves it when the child
|
|
494
|
+
reports that callback.
|
|
495
|
+
- **Every other target** — the parent answers the whole lookup, whatever the
|
|
496
|
+
child carries.
|
|
497
|
+
|
|
498
|
+
A worker takes one snapshot after boot, so a request carries every registration.
|
|
499
|
+
The child reuses anything that the parent already built or published.
|
|
500
|
+
|
|
501
|
+
The child reports its own maps, which start from the snapshot, and the parent's
|
|
502
|
+
through the predicates it overrides.
|
|
503
|
+
|
|
504
|
+
Warning: that exception also decides which binding the alias reaches. Give the
|
|
505
|
+
parent a singleton binding it never built. Give the child the marker for that id
|
|
506
|
+
and a factory of its own. The alias then reaches the factory of the child. A
|
|
507
|
+
child that holds the marker and no factory reaches the parent's factory
|
|
508
|
+
instead.
|
|
509
|
+
|
|
510
|
+
Warning: outside that exception, the parent answers the alias, so a factory that
|
|
511
|
+
the parent holds receives the parent. A `bind()` service is outside it, whether
|
|
512
|
+
the parent built one or not.
|
|
513
|
+
|
|
514
|
+
Warning: on that path the parent reads none of the child's maps. An instance the
|
|
515
|
+
child holds for the target does not answer the alias. The parent answers from
|
|
516
|
+
its own maps in one of four ways:
|
|
517
|
+
|
|
518
|
+
- It returns the copy it holds.
|
|
519
|
+
- It runs its own binding.
|
|
520
|
+
- It publishes a provider it holds, and answers with what that publisher
|
|
521
|
+
registered.
|
|
522
|
+
- It throws `ContainerInvalidReferenceException`, when it holds no registration
|
|
523
|
+
for the target.
|
|
524
|
+
|
|
525
|
+
To reach the child's copy through an alias, declare the alias on the child:
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
// Once, at boot.
|
|
529
|
+
parent.setSingleton(ClockContractId, bootClock);
|
|
530
|
+
parent.bindAlias(TimeSourceContractId, ClockContractId);
|
|
531
|
+
|
|
532
|
+
// For each request.
|
|
533
|
+
child.setSingleton(ClockContractId, requestClock);
|
|
534
|
+
|
|
535
|
+
child.get(ClockContractId); // requestClock
|
|
536
|
+
child.get(TimeSourceContractId); // bootClock, answered by the parent
|
|
537
|
+
|
|
538
|
+
child.bindAlias(TimeSourceContractId, ClockContractId);
|
|
539
|
+
|
|
540
|
+
child.get(TimeSourceContractId); // requestClock
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
On the exception path, the child's own factory runs when the child declares one
|
|
544
|
+
for that id. Otherwise the child asks the parent to run the parent's factory,
|
|
545
|
+
as [Where a singleton instance lives](#where-a-singleton-instance-lives)
|
|
546
|
+
states. A singleton caches in the child. A deferred target publishes in the
|
|
547
|
+
child, so its callback receives the child, and what caches is whatever that
|
|
548
|
+
publisher registers.
|
|
425
549
|
|
|
426
550
|
## Exceptions
|
|
427
551
|
|
|
@@ -429,6 +553,7 @@ the child delegates to the parent in every case above.
|
|
|
429
553
|
| :----------------------------------------- | :---------------------------------- | :--------------------------------------- |
|
|
430
554
|
| `ContainerInvalidReferenceException` | `ContainerInvalidArgumentException` | No map holds the id |
|
|
431
555
|
| `ContainerInvalidPublishCallbackException` | `ContainerRuntimeException` | A `publishers()` value is not a function |
|
|
556
|
+
| `ContainerCyclicAliasException` | `ContainerInvalidArgumentException` | An alias reaches a chain with no end |
|
|
432
557
|
|
|
433
558
|
`ContainerRuntimeException` and `ContainerInvalidArgumentException` are the
|
|
434
559
|
abstract bases. Both implement `ContainerThrowable`. See
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja Framework package.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2016-present Melech Mizrachi
|
|
5
|
+
*
|
|
6
|
+
* Released under the MIT License. See LICENSE.md for details.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ContainerInvalidArgumentException } from './Abstract/ContainerInvalidArgumentException.ts';
|
|
10
|
+
|
|
11
|
+
export class ContainerCyclicAliasException extends ContainerInvalidArgumentException {
|
|
12
|
+
constructor(alias: string, id: string, options?: ErrorOptions) {
|
|
13
|
+
super(
|
|
14
|
+
alias === id
|
|
15
|
+
? `Alias \`${alias}\` cannot point at itself.`
|
|
16
|
+
: `Alias \`${alias}\` cannot reach \`${id}\`, because the chain from \`${id}\` returns to \`${alias}\`.`,
|
|
17
|
+
options,
|
|
18
|
+
);
|
|
19
|
+
this.name = 'ContainerCyclicAliasException';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -51,7 +51,7 @@ export abstract class UriFactory {
|
|
|
51
51
|
|
|
52
52
|
static filterScheme(scheme: string): Scheme {
|
|
53
53
|
scheme = scheme.toLowerCase().replace(/:(\/\/)?$/, '');
|
|
54
|
-
return
|
|
54
|
+
return Object.values(Scheme).find((value: string) => value === scheme) ?? Scheme.EMPTY;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
static validatePort(port: number): void {
|
|
@@ -222,6 +222,11 @@ const match = namedGroups[name] ?? parameter.getDefault();
|
|
|
222
222
|
Warning: `processArguments()` throws `HttpRoutingInvalidRoutePathException` when
|
|
223
223
|
a dynamic route declares no parameter.
|
|
224
224
|
|
|
225
|
+
Warning: an unbound cast type raises `ContainerInvalidReferenceException` out of
|
|
226
|
+
`match()`, and out of `matchDynamic()`. The matcher resolves `cast.type` through
|
|
227
|
+
the container for a parameter that carries a cast. Bind that key with `bind`.
|
|
228
|
+
See [Type](../Type/README.md) for casting.
|
|
229
|
+
|
|
225
230
|
### The router
|
|
226
231
|
|
|
227
232
|
`Router.dispatch()` matches, then dispatches:
|
|
@@ -16,6 +16,7 @@ import { RouteNotMatchedHandler } from '../../Middleware/Handler/RouteNotMatched
|
|
|
16
16
|
import { SendingResponseHandler } from '../../Middleware/Handler/SendingResponseHandler.ts';
|
|
17
17
|
import { ResponseSentHandler } from '../../Middleware/Handler/ResponseSentHandler.ts';
|
|
18
18
|
import { ThrowableCaughtHandler } from '../../Middleware/Handler/ThrowableCaughtHandler.ts';
|
|
19
|
+
import { RouteCollection } from '../Collection/RouteCollection.ts';
|
|
19
20
|
import { Matcher } from '../Matcher/Matcher.ts';
|
|
20
21
|
|
|
21
22
|
import type { ContainerContract } from '../../../Container/Manager/Contract/ContainerContract.ts';
|
|
@@ -35,7 +36,7 @@ import type { RouterContract } from './Contract/RouterContract.ts';
|
|
|
35
36
|
export class Router implements RouterContract {
|
|
36
37
|
constructor(
|
|
37
38
|
protected container: ContainerContract = new Container(),
|
|
38
|
-
protected matcher: MatcherContract = new Matcher(),
|
|
39
|
+
protected matcher: MatcherContract = new Matcher(new RouteCollection(), container),
|
|
39
40
|
protected responseFactory: ResponseFactoryContract = new ResponseFactory(),
|
|
40
41
|
protected throwableCaughtHandler: ThrowableCaughtHandlerContract = new ThrowableCaughtHandler(),
|
|
41
42
|
protected routeMatchedHandler: RouteMatchedHandlerContract = new RouteMatchedHandler(),
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* Released under the MIT License. See LICENSE.md for details.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { RouteCollection } from '../Collection/RouteCollection.ts';
|
|
10
9
|
import { HttpRoutingInvalidRoutePathException } from '../Throwable/Exception/HttpRoutingInvalidRoutePathException.ts';
|
|
11
10
|
|
|
11
|
+
import type { ContainerContract } from '../../../Container/Manager/Contract/ContainerContract.ts';
|
|
12
|
+
import type { TypeContract } from '../../../Type/Contract/TypeContract.ts';
|
|
12
13
|
import type { RequestMethod } from '../../Message/Enum/RequestMethod.ts';
|
|
13
14
|
import type { DynamicRouteContract } from '../Data/Contract/DynamicRouteContract.ts';
|
|
14
15
|
import type { ParameterContract } from '../Data/Contract/ParameterContract.ts';
|
|
@@ -17,7 +18,10 @@ import type { RouteCollectionContract } from '../Collection/Contract/RouteCollec
|
|
|
17
18
|
import type { MatcherContract } from './Contract/MatcherContract.ts';
|
|
18
19
|
|
|
19
20
|
export class Matcher implements MatcherContract {
|
|
20
|
-
constructor(
|
|
21
|
+
constructor(
|
|
22
|
+
protected collection: RouteCollectionContract,
|
|
23
|
+
protected container: ContainerContract,
|
|
24
|
+
) {}
|
|
21
25
|
|
|
22
26
|
match(path: string, requestMethod: RequestMethod): RouteContract | null {
|
|
23
27
|
const normalizedPath = '/' + path.replace(/^\/+|\/+$/g, '');
|
|
@@ -95,14 +99,8 @@ export class Matcher implements MatcherContract {
|
|
|
95
99
|
|
|
96
100
|
protected castMatchValue(parameter: ParameterContract, match: string): unknown {
|
|
97
101
|
const cast = parameter.getCast();
|
|
98
|
-
const type = (cast.type
|
|
99
|
-
match,
|
|
100
|
-
);
|
|
102
|
+
const type = this.container.getService<TypeContract>(cast.type, [match]);
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
return type.asValue();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return type;
|
|
104
|
+
return cast.convert ? type.asValue() : type;
|
|
107
105
|
}
|
|
108
106
|
}
|
|
@@ -102,7 +102,10 @@ export class HttpRoutingServiceProvider implements ServiceProviderContract {
|
|
|
102
102
|
static publishMatcher(this: void, container: ContainerContract): void {
|
|
103
103
|
container.setSingleton<MatcherContract>(
|
|
104
104
|
HttpRoutingServiceId.MatcherContract,
|
|
105
|
-
new Matcher(
|
|
105
|
+
new Matcher(
|
|
106
|
+
container.getSingleton<RouteCollectionContract>(HttpRoutingServiceId.RouteCollectionContract),
|
|
107
|
+
container,
|
|
108
|
+
),
|
|
106
109
|
);
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Valkyrja Framework package.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2016-present Melech Mizrachi
|
|
5
|
+
*
|
|
6
|
+
* Released under the MIT License. See LICENSE.md for details.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface TypeContract {
|
|
10
|
+
asValue(): unknown;
|
|
11
|
+
asFlatValue(): string | number | boolean | null;
|
|
12
|
+
modify(closure: (value: unknown) => unknown): this;
|
|
13
|
+
}
|
|
@@ -2,22 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
## Introduction
|
|
4
4
|
|
|
5
|
-
The Type component holds two support classes. `
|
|
6
|
-
|
|
5
|
+
The Type component holds one contract and two support classes. `TypeContract`
|
|
6
|
+
declares a value the framework converts. `Cast` records how to convert a route
|
|
7
|
+
parameter value. `ObjectFactory` copies an object.
|
|
7
8
|
|
|
8
|
-
**This
|
|
9
|
-
no
|
|
10
|
-
|
|
9
|
+
**This component ships no typed value object.** It declares `TypeContract`, and
|
|
10
|
+
it holds no implementation of that contract. There is no primitive wrapper, no
|
|
11
|
+
identifier type, no collection, no JSON wrapper, and no UUID, ULID, or VLID
|
|
12
|
+
support. An application supplies its own type. The component holds three files:
|
|
11
13
|
|
|
12
|
-
| File | Class
|
|
13
|
-
| :-------------------------------- |
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
14
|
+
| File | Class or contract |
|
|
15
|
+
| :-------------------------------- | :---------------- |
|
|
16
|
+
| `Contract/TypeContract.ts` | `TypeContract` |
|
|
17
|
+
| `Data/Cast.ts` | `Cast` |
|
|
18
|
+
| `Object/Factory/ObjectFactory.ts` | `ObjectFactory` |
|
|
19
|
+
|
|
20
|
+
## TypeContract
|
|
21
|
+
|
|
22
|
+
`TypeContract` is the contract for a value that the framework converts. A type
|
|
23
|
+
holds one value. Two methods return that value, and `modify()` returns a new
|
|
24
|
+
type that holds the result of a closure:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
export interface TypeContract {
|
|
28
|
+
asValue(): unknown;
|
|
29
|
+
asFlatValue(): string | number | boolean | null;
|
|
30
|
+
modify(closure: (value: unknown) => unknown): this;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
PHP declares a static `fromValue()` on this contract. A TypeScript interface
|
|
35
|
+
declares instance members only, so a contract cannot carry a static method and
|
|
36
|
+
this port leaves it out. The CLI asks the container for the type instead. See
|
|
37
|
+
[STATIC_METHODS.md](https://github.com/valkyrjaio/architecture/blob/26.x/STATIC_METHODS.md).
|
|
38
|
+
|
|
39
|
+
The contract mirrors PHP's `TypeContract`, which every PHP value object
|
|
40
|
+
implements. PHP's contract also extends `JsonSerializable`, and this port leaves
|
|
41
|
+
that out, because TypeScript serializes an object through `JSON.stringify()`.
|
|
42
|
+
The framework calls `asValue()` only. `asFlatValue()` and `modify()`
|
|
43
|
+
are there for the application, and for the ports that hold value objects
|
|
44
|
+
already.
|
|
16
45
|
|
|
17
46
|
## Cast
|
|
18
47
|
|
|
19
|
-
`Cast` is a data object. It records the
|
|
20
|
-
return the result:
|
|
48
|
+
`Cast` is a data object. It records the container binding key of the type to
|
|
49
|
+
convert a value to, and how to return the result:
|
|
21
50
|
|
|
22
51
|
```ts
|
|
23
52
|
export class Cast {
|
|
@@ -31,7 +60,7 @@ export class Cast {
|
|
|
31
60
|
|
|
32
61
|
| Property | Default | Meaning |
|
|
33
62
|
| :-------- | :------ | :---------------------------------------------- |
|
|
34
|
-
| `type` | — | The
|
|
63
|
+
| `type` | — | The container binding key of the type |
|
|
35
64
|
| `convert` | `true` | Return the converted value, and not the wrapper |
|
|
36
65
|
| `isArray` | `false` | The value holds more than one item |
|
|
37
66
|
|
|
@@ -41,42 +70,69 @@ A route parameter carries the cast. Both routing components declare
|
|
|
41
70
|
`withCast()`, `getCast()`, and `hasCast()` on a parameter:
|
|
42
71
|
|
|
43
72
|
```ts
|
|
73
|
+
import { ArgumentParameter } from '@valkyrjaio/valkyrja/Cli/Routing/Data/ArgumentParameter.ts';
|
|
44
74
|
import { Cast } from '@valkyrjaio/valkyrja/Type/Data/Cast.ts';
|
|
45
75
|
|
|
46
|
-
const parameter = new ArgumentParameter('name', 'description').withCast(new Cast('
|
|
76
|
+
const parameter = new ArgumentParameter('name', 'description').withCast(new Cast('App.Type.Slug'));
|
|
47
77
|
```
|
|
48
78
|
|
|
49
79
|
`getCast()` throws when the parameter carries no cast. The CLI parameter throws
|
|
50
80
|
`CliRoutingNoCastException`.
|
|
51
81
|
|
|
52
|
-
### Where
|
|
82
|
+
### Where a cast applies
|
|
83
|
+
|
|
84
|
+
Two components convert a value, and each one reads `cast.type` as a container
|
|
85
|
+
binding key. The framework calls the HTTP matcher. The application calls the CLI
|
|
86
|
+
caster.
|
|
87
|
+
|
|
88
|
+
`Matcher.castMatchValue()` asks the container for the type that `cast.type`
|
|
89
|
+
names, and it passes the matched text as the first entry of the factory's
|
|
90
|
+
`args`. It returns `asValue()` when `cast.convert` is `true`, and the type itself
|
|
91
|
+
when `cast.convert` is `false`. See [Http](../Http/README.md) for dynamic routes
|
|
92
|
+
and their parameters.
|
|
93
|
+
|
|
94
|
+
`Caster.getCastValues()` asks the container for that type once for each value.
|
|
95
|
+
It passes the raw value as the first entry of the factory's `args`. It returns
|
|
96
|
+
`asValue()` when `cast.convert` is `true`, and the type itself when
|
|
97
|
+
`cast.convert` is `false`. It returns each raw value for a parameter that holds
|
|
98
|
+
no cast.
|
|
53
99
|
|
|
54
|
-
|
|
55
|
-
|
|
100
|
+
A route parameter holds the cast, and it converts nothing. The matcher and the
|
|
101
|
+
caster each hold the container, which is why no parameter needs one.
|
|
102
|
+
|
|
103
|
+
Warning: register a cast type with `bind`. Each reader calls `getService()`,
|
|
104
|
+
which reads only a service binding. An alias, and an instance that
|
|
105
|
+
`setSingleton` holds, raise `ContainerInvalidReferenceException`.
|
|
106
|
+
|
|
107
|
+
`bindSingleton` also registers a callable, so a cast type that `bindSingleton`
|
|
108
|
+
registers still resolves. `getService()` skips the singleton cache, so the
|
|
109
|
+
container builds one instance for each value. That is not the lifetime that
|
|
110
|
+
`bindSingleton` states, which is why a cast type takes `bind`.
|
|
111
|
+
|
|
112
|
+
Warning: the factory must return a `TypeContract`. Each reader calls `asValue()`
|
|
113
|
+
on the object that the container builds, and an object without that method
|
|
114
|
+
raises a `TypeError`.
|
|
115
|
+
|
|
116
|
+
The application binds the type to the key that the cast names, and it asks the
|
|
117
|
+
caster for the values:
|
|
56
118
|
|
|
57
119
|
```ts
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
if (cast.convert) {
|
|
65
|
-
return type.asValue();
|
|
66
|
-
}
|
|
120
|
+
import { Argument } from '@valkyrjaio/valkyrja/Cli/Interaction/Argument/Argument.ts';
|
|
121
|
+
import { ArgumentParameter } from '@valkyrjaio/valkyrja/Cli/Routing/Data/ArgumentParameter.ts';
|
|
122
|
+
import { CliRoutingServiceId } from '@valkyrjaio/valkyrja/Cli/Routing/Constant/CliRoutingServiceId.ts';
|
|
123
|
+
import { Cast } from '@valkyrjaio/valkyrja/Type/Data/Cast.ts';
|
|
67
124
|
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
```
|
|
125
|
+
import type { CasterContract } from '@valkyrjaio/valkyrja/Cli/Routing/Caster/Contract/CasterContract.ts';
|
|
71
126
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
127
|
+
container.bind('App.Type.Slug', Slug.make);
|
|
128
|
+
|
|
129
|
+
const parameter = new ArgumentParameter('target', 'The target', new Cast('App.Type.Slug')).withArguments(
|
|
130
|
+
new Argument('a-slug'),
|
|
131
|
+
);
|
|
132
|
+
const values = container.getSingleton<CasterContract>(CliRoutingServiceId.CasterContract).getCastValues(parameter);
|
|
133
|
+
```
|
|
76
134
|
|
|
77
|
-
|
|
78
|
-
parameter value, and the stored `Cast` does not change the result. See
|
|
79
|
-
[Cli](../Cli/README.md) for CLI arguments and options.
|
|
135
|
+
See [Cli](../Cli/README.md) for CLI arguments and options.
|
|
80
136
|
|
|
81
137
|
Note that no code reads `isArray`.
|
|
82
138
|
|