@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valkyrjaio/valkyrja",
3
- "version": "26.8.17",
3
+ "version": "26.9.0",
4
4
  "description": "The Valkyrja Framework.",
5
5
  "keywords": [
6
6
  "framework",
@@ -7,8 +7,8 @@
7
7
  */
8
8
 
9
9
  export class ApplicationInfo {
10
- static readonly VERSION = '26.8.17' as const;
11
- static readonly VERSION_BUILD_DATE_TIME = 'September 26 2026 09:12:25 MST' as const;
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 the parameter's own `getCastValues()` for every value of a
255
- parameter in `ARRAY` value mode.
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
- A parameter carries an optional `Cast`. See [Type](../Type/README.md).
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
- Note that the CLI stores the cast and does not apply it.
325
- `getCastValuesForParameters()` returns each raw value, whether the parameter
326
- carries a cast or not.
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 getCastValues(): unknown[];
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
- getCastValues(): unknown[] {
67
- return this.getCastValuesForParameters(this.arguments_);
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
- getCastValues(): unknown[];
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
- getCastValues(): unknown[] {
156
- return this.getCastValuesForParameters(this.options);
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 (!super.isAlias(id) && this.parent.isAlias(id)) {
63
- return this.parent.getAliased<T>(id, args);
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
- return super.getAliasedWithoutChecks<T>(id, args);
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
- this.aliases = { ...this.aliases, ...data.aliases };
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
- return id in this.aliases;
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 id in this.services;
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 id in this.singletons;
172
+ return Object.hasOwn(this.singletons, id);
92
173
  }
93
174
 
94
175
  isSingletonInstance(id: string): boolean {
95
- return id in this.instances;
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 id in this.deferredCallback;
234
+ return Object.hasOwn(this.deferredCallback, id);
154
235
  }
155
236
 
156
237
  isPublished(id: string): boolean {
157
- return id in this.published;
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.getAlias(id);
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 !== undefined) {
195
- this.instances[id] = singleton;
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
- protected getAlias(id: string): string | undefined {
212
- return this.aliases[id];
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
- return this.instances[id] as T | undefined;
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
- Note that this port declares no `getAliasedId()`. The PHP reference declares
66
- one.
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
- this.aliases = { ...this.aliases, ...data.aliases };
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()`, `isDeferred()`, and
386
- `isPublished()` each report the child state or the parent state.
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: `isSingletonBinding()` is not overridden. The child reports its own
389
- copied bindings, and it does not report a binding that the parent added after
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 asks the parent to run the factory, and the child stores the result in its
396
- own `instances` map. The parent's map does not change.
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
- Note that this port declares no guard against a parent that writes while it
421
- answers a child. The PHP reference throws
422
- `ContainerUnpublishedParentTargetException` and
423
- `ContainerUnresolvedParentAliasException`. This port has neither exception, and
424
- the child delegates to the parent in every case above.
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 (Object.values(Scheme) as string[]).includes(scheme) ? (scheme as Scheme) : Scheme.EMPTY;
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(protected collection: RouteCollectionContract = new RouteCollection()) {}
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 as unknown as { fromValue: (v: unknown) => { asValue: () => unknown } }).fromValue(
99
- match,
100
- );
102
+ const type = this.container.getService<TypeContract>(cast.type, [match]);
101
103
 
102
- if (cast.convert) {
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(container.getSingleton<RouteCollectionContract>(HttpRoutingServiceId.RouteCollectionContract)),
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. `Cast` records how to convert a
6
- route parameter value. `ObjectFactory` copies an object.
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 port ships no typed value objects.** The component has no `TypeContract`,
9
- no primitive wrapper, no identifier type, no collection, no JSON wrapper, and no
10
- UUID, ULID, or VLID support. The component holds two files:
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
- | `Data/Cast.ts` | `Cast` |
15
- | `Object/Factory/ObjectFactory.ts` | `ObjectFactory` |
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 type to convert a value to, and how to
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 type to convert the value to |
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('string'));
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 the framework applies a cast
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
- The HTTP `Matcher` is the one place that converts a matched value. It reads
55
- `cast.type` as a class, and it calls the static `fromValue()` on that class:
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
- protected castMatchValue(parameter: ParameterContract, match: string): unknown {
59
- const cast = parameter.getCast();
60
- const type = (cast.type as unknown as { fromValue: (v: unknown) => { asValue: () => unknown } }).fromValue(
61
- match,
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
- return type;
69
- }
70
- ```
125
+ import type { CasterContract } from '@valkyrjaio/valkyrja/Cli/Routing/Caster/Contract/CasterContract.ts';
71
126
 
72
- Warning: the port ships no class with a `fromValue()` method. An application
73
- that sets a cast on an HTTP route parameter supplies that class itself. The
74
- class declares a static `fromValue()` that returns an object with `asValue()`.
75
- See [Http](../Http/README.md) for dynamic routes and their parameters.
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
- Note that the CLI does not apply a cast. `getCastValues()` returns each raw
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