@fgv/ts-res 5.0.0-21 → 5.0.0-22

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.
@@ -23,6 +23,7 @@
23
23
  /* eslint-disable @rushstack/typedef-var */
24
24
 
25
25
  import { Converters } from '@fgv/ts-utils';
26
+ import { Converters as JsonConverters } from '@fgv/ts-json-base';
26
27
  import { IResourceTypeConfig } from './json';
27
28
 
28
29
  /**
@@ -32,5 +33,6 @@ import { IResourceTypeConfig } from './json';
32
33
  */
33
34
  export const resourceTypeConfig = Converters.strictObject<IResourceTypeConfig>({
34
35
  name: Converters.string,
35
- typeName: Converters.string
36
+ typeName: Converters.string,
37
+ template: JsonConverters.jsonObject.optional()
36
38
  });
@@ -20,6 +20,8 @@
20
20
  * SOFTWARE.
21
21
  */
22
22
 
23
+ import { JsonObject } from '@fgv/ts-json-base';
24
+
23
25
  /**
24
26
  * Configuration for a {@link ResourceTypes.ResourceType | resource type}.
25
27
  * @public
@@ -27,4 +29,5 @@
27
29
  export interface IResourceTypeConfig {
28
30
  name: string;
29
31
  typeName: string;
32
+ template?: JsonObject;
30
33
  }
@@ -41,6 +41,12 @@ export interface IJsonResourceTypeCreateParams {
41
41
  * instance.
42
42
  */
43
43
  index?: number;
44
+
45
+ /**
46
+ * Optional template for new instances of {@link ResourceTypes.JsonResourceType | JsonResourceType}
47
+ * resources.
48
+ */
49
+ template?: JsonObject;
44
50
  }
45
51
 
46
52
  /**
@@ -54,8 +60,8 @@ export class JsonResourceType extends ResourceType<JsonObject> {
54
60
  * @param key - The key for the new {@link ResourceTypes.JsonResourceType | JsonResourceType} instance.
55
61
  * @param index - Optional index for the new {@link ResourceTypes.JsonResourceType | JsonResourceType} instance.
56
62
  */
57
- protected constructor(key: ResourceTypeName, index?: number) {
58
- super(key, index);
63
+ protected constructor(key: ResourceTypeName, index?: number, template?: JsonObject) {
64
+ super(key, index, template);
59
65
  }
60
66
 
61
67
  /**
@@ -66,7 +72,7 @@ export class JsonResourceType extends ResourceType<JsonObject> {
66
72
  */
67
73
  public static create(params?: IJsonResourceTypeCreateParams): Result<JsonResourceType> {
68
74
  return Convert.resourceTypeName.convert(params?.key ?? 'json').onSuccess((key) => {
69
- return captureResult(() => new JsonResourceType(key, params?.index));
75
+ return captureResult(() => new JsonResourceType(key, params?.index, params?.template));
70
76
  });
71
77
  }
72
78
 
@@ -93,12 +99,4 @@ export class JsonResourceType extends ResourceType<JsonObject> {
93
99
  public validate(json: JsonObject, __completeness?: CandidateCompleteness): Result<JsonObject> {
94
100
  return JsonConverters.jsonObject.convert(json);
95
101
  }
96
-
97
- /**
98
- * Gets the default template value for a JSON resource type.
99
- * @returns An empty object as the default JSON value
100
- */
101
- protected getDefaultTemplateValue(): JsonObject {
102
- return {};
103
- }
104
102
  }
@@ -20,8 +20,8 @@
20
20
  * SOFTWARE.
21
21
  */
22
22
 
23
- import { JsonValue, JsonObject } from '@fgv/ts-json-base';
24
- import { Collections, ICollectible, Result } from '@fgv/ts-utils';
23
+ import { isJsonObject, JsonObject, JsonValue } from '@fgv/ts-json-base';
24
+ import { Collections, fail, ICollectible, Result, succeed } from '@fgv/ts-utils';
25
25
  import {
26
26
  CandidateCompleteness,
27
27
  Convert as CommonConvert,
@@ -29,7 +29,8 @@ import {
29
29
  ResourceTypeIndex,
30
30
  ResourceTypeName,
31
31
  ResourceValueMergeMethod,
32
- ResourceId
32
+ ResourceId,
33
+ IResourceResolver
33
34
  } from '../common';
34
35
  import * as ResourceJson from '../resource-json';
35
36
 
@@ -128,11 +129,19 @@ export interface IResourceType<T = unknown> extends ICollectible<ResourceTypeNam
128
129
  /**
129
130
  * Creates a template for a new resource of this type.
130
131
  * The template provides a default structure for creating new resource instances.
131
- * @param resourceId - The id for the new resource
132
- * @returns A loose resource declaration with default values for this resource type
132
+ * @param resourceId - The id for the new resource.
133
+ * @param init - An optional initial value for the resource.
134
+ * @param resolver - An optional resource resolver that can be used to create the template.
135
+ * @param conditions - An optional set of conditions that must be met for the resource to be selected.
136
+ * @returns A loose resource declaration with default values for this resource type.
133
137
  * @public
134
138
  */
135
- createTemplate(resourceId: ResourceId): ResourceJson.Json.ILooseResourceDecl;
139
+ createTemplate(
140
+ resourceId: ResourceId,
141
+ init?: JsonValue,
142
+ conditions?: ResourceJson.Json.ConditionSetDecl,
143
+ resolver?: IResourceResolver
144
+ ): Result<ResourceJson.Json.ILooseResourceDecl>;
136
145
  }
137
146
 
138
147
  /**
@@ -143,6 +152,7 @@ export interface IResourceType<T = unknown> extends ICollectible<ResourceTypeNam
143
152
  */
144
153
  export abstract class ResourceType<T = unknown> implements IResourceType<T> {
145
154
  private _collectible: Collections.Collectible<ResourceTypeName, ResourceTypeIndex>;
155
+ private _template: JsonObject;
146
156
  /**
147
157
  * {@inheritdoc ResourceTypes.IResourceType.key}
148
158
  */
@@ -157,13 +167,14 @@ export abstract class ResourceType<T = unknown> implements IResourceType<T> {
157
167
  return this._collectible.index;
158
168
  }
159
169
 
160
- protected constructor(key: ResourceTypeName, index?: number) {
170
+ protected constructor(key: ResourceTypeName, index?: number, template?: JsonObject) {
161
171
  this._collectible = new Collections.Collectible<ResourceTypeName, ResourceTypeIndex>({
162
172
  key,
163
173
  /* c8 ignore next 1 - coverage having a rough time */
164
174
  index: index !== undefined ? Validate.toResourceTypeIndex(index).orThrow() : undefined,
165
175
  indexConverter: CommonConvert.resourceTypeIndex
166
176
  });
177
+ this._template = template ?? {};
167
178
  }
168
179
 
169
180
  /**
@@ -228,32 +239,54 @@ export abstract class ResourceType<T = unknown> implements IResourceType<T> {
228
239
  * Default implementation provides a basic template.
229
240
  * Subclasses can override to provide type-specific templates.
230
241
  * @param resourceId - The id for the new resource
242
+ * @param init - An optional initial value for the resource.
243
+ * @param conditions - An optional set of conditions that must be met for the resource to be selected.
244
+ * @param resolver - An optional resource resolver that can be used to create the template.
231
245
  * @returns A loose resource declaration with default values for this resource type
232
246
  * @public
233
247
  */
234
- public createTemplate(resourceId: ResourceId): ResourceJson.Json.ILooseResourceDecl {
235
- return {
236
- id: resourceId,
237
- resourceTypeName: this.key,
238
- candidates: [
239
- {
240
- json: this.getDefaultTemplateValue(),
241
- conditions: undefined,
242
- isPartial: false,
243
- mergeMethod: 'replace'
244
- }
245
- ]
246
- };
248
+ public createTemplate(
249
+ resourceId: ResourceId,
250
+ init?: JsonValue,
251
+ conditions?: ResourceJson.Json.ConditionSetDecl,
252
+ resolver?: IResourceResolver
253
+ ): Result<ResourceJson.Json.ILooseResourceDecl> {
254
+ return this.getDefaultTemplateCandidate(init, conditions, resolver).onSuccess((candidate) =>
255
+ succeed({
256
+ id: resourceId,
257
+ resourceTypeName: this.key,
258
+ candidates: [candidate]
259
+ })
260
+ );
247
261
  }
248
262
 
249
263
  /**
250
264
  * Gets the default template value for this resource type.
251
265
  * Subclasses should override this to provide type-specific default values.
252
266
  * @returns The default JSON value for a new resource of this type
267
+ * @public
253
268
  */
254
- protected getDefaultTemplateValue(): JsonObject {
255
- // Default implementation returns an empty object
256
- // Subclasses should override for type-specific defaults
257
- return {};
269
+ public getDefaultTemplateCandidate(
270
+ json?: JsonValue,
271
+ conditions?: ResourceJson.Json.ConditionSetDecl,
272
+ __resolver?: IResourceResolver
273
+ ): Result<ResourceJson.Json.IChildResourceCandidateDecl> {
274
+ json = json ?? this._template;
275
+
276
+ if (!isJsonObject(json)) {
277
+ return fail(`${this.key}: Invalid initial value "${json}" must be JSON object.`);
278
+ }
279
+
280
+ const candidate: ResourceJson.Json.IChildResourceCandidateDecl = {
281
+ json,
282
+ ...(conditions ? { conditions } : {}),
283
+ mergeMethod: 'replace'
284
+ };
285
+
286
+ return this.validate(json, 'full')
287
+ .onSuccess((json) => succeed({ ...candidate, isPartial: false }))
288
+ .onFailure(() =>
289
+ this.validate(json, 'partial').onSuccess((json) => succeed({ ...candidate, isPartial: true }))
290
+ );
258
291
  }
259
292
  }
@@ -23,11 +23,11 @@
23
23
  import { Result, captureResult, fail, succeed } from '@fgv/ts-utils';
24
24
  import { JsonValue, JsonObject, isJsonObject } from '@fgv/ts-json-base';
25
25
  import { JsonEditor } from '@fgv/ts-json';
26
- import { NoMatch } from '../common';
26
+ import { IResourceResolver, NoMatch } from '../common';
27
27
  import { Condition, ConditionSet } from '../conditions';
28
28
  import { AbstractDecision } from '../decisions';
29
29
  import { ReadOnlyQualifierTypeCollector } from '../qualifier-types';
30
- import { IContextQualifierProvider } from './context';
30
+ import { IContextQualifierProvider, ValidatingSimpleContextQualifierProvider } from './context';
31
31
  import { IResourceManager, IResource, IResourceCandidate } from './iResourceManager';
32
32
  import {
33
33
  ConditionMatchType,
@@ -35,6 +35,7 @@ import {
35
35
  IConditionMatchResult
36
36
  } from './conditionSetResolutionResult';
37
37
  import { IResourceResolverCacheListener } from './cacheListener';
38
+ import { IReadOnlyQualifierCollector } from '../qualifiers';
38
39
 
39
40
  /**
40
41
  * Represents the cached result of resolving a decision.
@@ -100,7 +101,7 @@ export interface IResourceResolverCreateParams {
100
101
  * and caching results for optimal performance.
101
102
  * @public
102
103
  */
103
- export class ResourceResolver {
104
+ export class ResourceResolver implements IResourceResolver {
104
105
  /**
105
106
  * The resource manager that defines available resources and provides condition access.
106
107
  */
@@ -121,6 +122,13 @@ export class ResourceResolver {
121
122
  */
122
123
  public readonly options: IResourceResolverOptions;
123
124
 
125
+ /**
126
+ * The readonly qualifier collector that provides qualifier implementations.
127
+ */
128
+ public get qualifiers(): IReadOnlyQualifierCollector {
129
+ return this.contextQualifierProvider.qualifiers;
130
+ }
131
+
124
132
  /**
125
133
  * The cache array for resolved conditions, indexed by condition index for O(1) lookup.
126
134
  * Each entry stores the resolved {@link Runtime.IConditionMatchResult | condition match result} for
@@ -412,7 +420,26 @@ export class ResourceResolver {
412
420
  * or `Failure` with an error message if no candidates match or resolution fails.
413
421
  * @public
414
422
  */
415
- public resolveResource(resource: IResource): Result<IResourceCandidate> {
423
+ public resolveResource(resource: IResource): Result<IResourceCandidate>;
424
+
425
+ /**
426
+ * Resolves a resource by finding the best matching candidate.
427
+ * Uses the resource's associated decision to determine the best match based on the current context.
428
+ * @param resource - The string id of the resource to resolve.
429
+ * @returns `Success` with the best matching candidate if successful,
430
+ * or `Failure` with an error message if no candidates match or resolution fails.
431
+ * @public
432
+ */
433
+ public resolveResource(resource: string): Result<IResourceCandidate>;
434
+ public resolveResource(idOrResource: string | IResource): Result<IResourceCandidate> {
435
+ if (typeof idOrResource === 'string') {
436
+ return this.resourceManager
437
+ .getBuiltResource(idOrResource)
438
+ .onSuccess((resource) => this.resolveResource(resource));
439
+ }
440
+
441
+ const resource = idOrResource;
442
+
416
443
  // Get the abstract decision from the resource's concrete decision
417
444
  const abstractDecision = resource.decision.baseDecision;
418
445
 
@@ -455,7 +482,28 @@ export class ResourceResolver {
455
482
  * or `Failure` with an error message if no candidates match or resolution fails.
456
483
  * @public
457
484
  */
458
- public resolveAllResourceCandidates(resource: IResource): Result<ReadonlyArray<IResourceCandidate>> {
485
+ public resolveAllResourceCandidates(resource: IResource): Result<ReadonlyArray<IResourceCandidate>>;
486
+
487
+ /**
488
+ * Resolves all matching resource candidates in priority order.
489
+ * Uses the resource's associated decision to determine all matching candidates based on the current context.
490
+ * @param resource - The string id of the resource to resolve.
491
+ * @returns `Success` with an array of all matching candidates in priority order if successful,
492
+ * or `Failure` with an error message if no candidates match or resolution fails.
493
+ * @public
494
+ */
495
+ public resolveAllResourceCandidates(resource: string): Result<ReadonlyArray<IResourceCandidate>>;
496
+ public resolveAllResourceCandidates(
497
+ idOrResource: string | IResource
498
+ ): Result<ReadonlyArray<IResourceCandidate>> {
499
+ if (typeof idOrResource === 'string') {
500
+ return this.resourceManager
501
+ .getBuiltResource(idOrResource)
502
+ .onSuccess((resource) => this.resolveAllResourceCandidates(resource));
503
+ }
504
+
505
+ const resource = idOrResource;
506
+
459
507
  // Get the abstract decision from the resource's concrete decision
460
508
  const abstractDecision = resource.decision.baseDecision;
461
509
 
@@ -510,7 +558,27 @@ export class ResourceResolver {
510
558
  * or `Failure` with an error message if no candidates match or resolution fails.
511
559
  * @public
512
560
  */
513
- public resolveComposedResourceValue(resource: IResource): Result<JsonValue> {
561
+ public resolveComposedResourceValue(resource: IResource): Result<JsonValue>;
562
+
563
+ /**
564
+ * Resolves a resource to a composed value by merging matching candidates according to their merge methods.
565
+ * Starting from the highest priority candidates, finds the first "full" candidate and merges all higher
566
+ * priority "partial" candidates into it in ascending order of priority.
567
+ * @param resource - The string id of the resource to resolve.
568
+ * @returns `Success` with the composed JsonValue if successful,
569
+ * or `Failure` with an error message if no candidates match or resolution fails.
570
+ * @public
571
+ */
572
+ public resolveComposedResourceValue(resource: string): Result<JsonValue>;
573
+ public resolveComposedResourceValue(idOrResource: string | IResource): Result<JsonValue> {
574
+ if (typeof idOrResource === 'string') {
575
+ return this.resourceManager
576
+ .getBuiltResource(idOrResource)
577
+ .onSuccess((resource) => this.resolveComposedResourceValue(resource));
578
+ }
579
+
580
+ const resource = idOrResource;
581
+
514
582
  return this.resolveAllResourceCandidates(resource).onSuccess((candidates) => {
515
583
  /* c8 ignore next 3 - defense in depth should never occur */
516
584
  if (candidates.length === 0) {
@@ -581,6 +649,24 @@ export class ResourceResolver {
581
649
  });
582
650
  }
583
651
 
652
+ /**
653
+ * {@inheritDoc IResourceResolver.withContext}
654
+ */
655
+ public withContext(context: Record<string, string>): Result<ResourceResolver> {
656
+ const { resourceManager, qualifierTypes, options } = this;
657
+ return ValidatingSimpleContextQualifierProvider.create({
658
+ qualifiers: this.qualifiers,
659
+ qualifierValues: context
660
+ }).onSuccess((contextQualifierProvider) => {
661
+ return ResourceResolver.create({
662
+ resourceManager,
663
+ qualifierTypes,
664
+ options,
665
+ contextQualifierProvider
666
+ });
667
+ });
668
+ }
669
+
584
670
  /**
585
671
  * Clears all caches (condition, condition set, and decision), forcing all cached items
586
672
  * to be re-evaluated on next access. This should be called when the context changes and cached