@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.
@@ -37,8 +37,8 @@ class JsonResourceType extends resourceType_1.ResourceType {
37
37
  * @param key - The key for the new {@link ResourceTypes.JsonResourceType | JsonResourceType} instance.
38
38
  * @param index - Optional index for the new {@link ResourceTypes.JsonResourceType | JsonResourceType} instance.
39
39
  */
40
- constructor(key, index) {
41
- super(key, index);
40
+ constructor(key, index, template) {
41
+ super(key, index, template);
42
42
  }
43
43
  /**
44
44
  * Factory method to create a new {@link ResourceTypes.JsonResourceType | JsonResourceType} instance.
@@ -49,7 +49,7 @@ class JsonResourceType extends resourceType_1.ResourceType {
49
49
  static create(params) {
50
50
  var _a;
51
51
  return common_1.Convert.resourceTypeName.convert((_a = params === null || params === void 0 ? void 0 : params.key) !== null && _a !== void 0 ? _a : 'json').onSuccess((key) => {
52
- return (0, ts_utils_1.captureResult)(() => new JsonResourceType(key, params === null || params === void 0 ? void 0 : params.index));
52
+ return (0, ts_utils_1.captureResult)(() => new JsonResourceType(key, params === null || params === void 0 ? void 0 : params.index, params === null || params === void 0 ? void 0 : params.template));
53
53
  });
54
54
  }
55
55
  /**
@@ -61,13 +61,6 @@ class JsonResourceType extends resourceType_1.ResourceType {
61
61
  validate(json, __completeness) {
62
62
  return ts_json_base_1.Converters.jsonObject.convert(json);
63
63
  }
64
- /**
65
- * Gets the default template value for a JSON resource type.
66
- * @returns An empty object as the default JSON value
67
- */
68
- getDefaultTemplateValue() {
69
- return {};
70
- }
71
64
  }
72
65
  exports.JsonResourceType = JsonResourceType;
73
66
  //# sourceMappingURL=jsonResourceType.js.map
@@ -1,6 +1,6 @@
1
- import { JsonValue, JsonObject } from '@fgv/ts-json-base';
1
+ import { JsonObject, JsonValue } from '@fgv/ts-json-base';
2
2
  import { ICollectible, Result } from '@fgv/ts-utils';
3
- import { CandidateCompleteness, ResourceTypeIndex, ResourceTypeName, ResourceValueMergeMethod, ResourceId } from '../common';
3
+ import { CandidateCompleteness, ResourceTypeIndex, ResourceTypeName, ResourceValueMergeMethod, ResourceId, IResourceResolver } from '../common';
4
4
  import * as ResourceJson from '../resource-json';
5
5
  /**
6
6
  * Parameters used to validate a {@link ResourceJson.Json.ILooseResourceCandidateDecl | resource candidate declaration}.
@@ -86,11 +86,14 @@ export interface IResourceType<T = unknown> extends ICollectible<ResourceTypeNam
86
86
  /**
87
87
  * Creates a template for a new resource of this type.
88
88
  * The template provides a default structure for creating new resource instances.
89
- * @param resourceId - The id for the new resource
90
- * @returns A loose resource declaration with default values for this resource type
89
+ * @param resourceId - The id for the new resource.
90
+ * @param init - An optional initial value for the resource.
91
+ * @param resolver - An optional resource resolver that can be used to create the template.
92
+ * @param conditions - An optional set of conditions that must be met for the resource to be selected.
93
+ * @returns A loose resource declaration with default values for this resource type.
91
94
  * @public
92
95
  */
93
- createTemplate(resourceId: ResourceId): ResourceJson.Json.ILooseResourceDecl;
96
+ createTemplate(resourceId: ResourceId, init?: JsonValue, conditions?: ResourceJson.Json.ConditionSetDecl, resolver?: IResourceResolver): Result<ResourceJson.Json.ILooseResourceDecl>;
94
97
  }
95
98
  /**
96
99
  * Abstract base class for resource types which are responsible for
@@ -100,6 +103,7 @@ export interface IResourceType<T = unknown> extends ICollectible<ResourceTypeNam
100
103
  */
101
104
  export declare abstract class ResourceType<T = unknown> implements IResourceType<T> {
102
105
  private _collectible;
106
+ private _template;
103
107
  /**
104
108
  * {@inheritdoc ResourceTypes.IResourceType.key}
105
109
  */
@@ -108,7 +112,7 @@ export declare abstract class ResourceType<T = unknown> implements IResourceType
108
112
  * {@inheritdoc ResourceTypes.IResourceType.index}
109
113
  */
110
114
  get index(): ResourceTypeIndex | undefined;
111
- protected constructor(key: ResourceTypeName, index?: number);
115
+ protected constructor(key: ResourceTypeName, index?: number, template?: JsonObject);
112
116
  /**
113
117
  * Validates properties of a {@link ResourceJson.Json.ILooseResourceCandidateDecl | resource candidate declaration} for
114
118
  * a resource instance value.
@@ -163,15 +167,19 @@ export declare abstract class ResourceType<T = unknown> implements IResourceType
163
167
  * Default implementation provides a basic template.
164
168
  * Subclasses can override to provide type-specific templates.
165
169
  * @param resourceId - The id for the new resource
170
+ * @param init - An optional initial value for the resource.
171
+ * @param conditions - An optional set of conditions that must be met for the resource to be selected.
172
+ * @param resolver - An optional resource resolver that can be used to create the template.
166
173
  * @returns A loose resource declaration with default values for this resource type
167
174
  * @public
168
175
  */
169
- createTemplate(resourceId: ResourceId): ResourceJson.Json.ILooseResourceDecl;
176
+ createTemplate(resourceId: ResourceId, init?: JsonValue, conditions?: ResourceJson.Json.ConditionSetDecl, resolver?: IResourceResolver): Result<ResourceJson.Json.ILooseResourceDecl>;
170
177
  /**
171
178
  * Gets the default template value for this resource type.
172
179
  * Subclasses should override this to provide type-specific default values.
173
180
  * @returns The default JSON value for a new resource of this type
181
+ * @public
174
182
  */
175
- protected getDefaultTemplateValue(): JsonObject;
183
+ getDefaultTemplateCandidate(json?: JsonValue, conditions?: ResourceJson.Json.ConditionSetDecl, __resolver?: IResourceResolver): Result<ResourceJson.Json.IChildResourceCandidateDecl>;
176
184
  }
177
185
  //# sourceMappingURL=resourceType.d.ts.map
@@ -22,6 +22,7 @@
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
24
  exports.ResourceType = void 0;
25
+ const ts_json_base_1 = require("@fgv/ts-json-base");
25
26
  const ts_utils_1 = require("@fgv/ts-utils");
26
27
  const common_1 = require("../common");
27
28
  /**
@@ -43,13 +44,14 @@ class ResourceType {
43
44
  get index() {
44
45
  return this._collectible.index;
45
46
  }
46
- constructor(key, index) {
47
+ constructor(key, index, template) {
47
48
  this._collectible = new ts_utils_1.Collections.Collectible({
48
49
  key,
49
50
  /* c8 ignore next 1 - coverage having a rough time */
50
51
  index: index !== undefined ? common_1.Validate.toResourceTypeIndex(index).orThrow() : undefined,
51
52
  indexConverter: common_1.Convert.resourceTypeIndex
52
53
  });
54
+ this._template = template !== null && template !== void 0 ? template : {};
53
55
  }
54
56
  /**
55
57
  * Sets the index for this resource type. Once set, the index cannot be changed.
@@ -62,32 +64,34 @@ class ResourceType {
62
64
  * Default implementation provides a basic template.
63
65
  * Subclasses can override to provide type-specific templates.
64
66
  * @param resourceId - The id for the new resource
67
+ * @param init - An optional initial value for the resource.
68
+ * @param conditions - An optional set of conditions that must be met for the resource to be selected.
69
+ * @param resolver - An optional resource resolver that can be used to create the template.
65
70
  * @returns A loose resource declaration with default values for this resource type
66
71
  * @public
67
72
  */
68
- createTemplate(resourceId) {
69
- return {
73
+ createTemplate(resourceId, init, conditions, resolver) {
74
+ return this.getDefaultTemplateCandidate(init, conditions, resolver).onSuccess((candidate) => (0, ts_utils_1.succeed)({
70
75
  id: resourceId,
71
76
  resourceTypeName: this.key,
72
- candidates: [
73
- {
74
- json: this.getDefaultTemplateValue(),
75
- conditions: undefined,
76
- isPartial: false,
77
- mergeMethod: 'replace'
78
- }
79
- ]
80
- };
77
+ candidates: [candidate]
78
+ }));
81
79
  }
82
80
  /**
83
81
  * Gets the default template value for this resource type.
84
82
  * Subclasses should override this to provide type-specific default values.
85
83
  * @returns The default JSON value for a new resource of this type
84
+ * @public
86
85
  */
87
- getDefaultTemplateValue() {
88
- // Default implementation returns an empty object
89
- // Subclasses should override for type-specific defaults
90
- return {};
86
+ getDefaultTemplateCandidate(json, conditions, __resolver) {
87
+ json = json !== null && json !== void 0 ? json : this._template;
88
+ if (!(0, ts_json_base_1.isJsonObject)(json)) {
89
+ return (0, ts_utils_1.fail)(`${this.key}: Invalid initial value "${json}" must be JSON object.`);
90
+ }
91
+ const candidate = Object.assign(Object.assign({ json }, (conditions ? { conditions } : {})), { mergeMethod: 'replace' });
92
+ return this.validate(json, 'full')
93
+ .onSuccess((json) => (0, ts_utils_1.succeed)(Object.assign(Object.assign({}, candidate), { isPartial: false })))
94
+ .onFailure(() => this.validate(json, 'partial').onSuccess((json) => (0, ts_utils_1.succeed)(Object.assign(Object.assign({}, candidate), { isPartial: true }))));
91
95
  }
92
96
  }
93
97
  exports.ResourceType = ResourceType;
@@ -1,5 +1,6 @@
1
1
  import { Result } from '@fgv/ts-utils';
2
2
  import { JsonValue } from '@fgv/ts-json-base';
3
+ import { IResourceResolver } from '../common';
3
4
  import { Condition, ConditionSet } from '../conditions';
4
5
  import { AbstractDecision } from '../decisions';
5
6
  import { ReadOnlyQualifierTypeCollector } from '../qualifier-types';
@@ -7,6 +8,7 @@ import { IContextQualifierProvider } from './context';
7
8
  import { IResourceManager, IResource, IResourceCandidate } from './iResourceManager';
8
9
  import { ConditionSetResolutionResult, IConditionMatchResult } from './conditionSetResolutionResult';
9
10
  import { IResourceResolverCacheListener } from './cacheListener';
11
+ import { IReadOnlyQualifierCollector } from '../qualifiers';
10
12
  /**
11
13
  * Represents the cached result of resolving a decision.
12
14
  * Contains either a failure indicator or a list of instance indices for matching condition sets,
@@ -68,7 +70,7 @@ export interface IResourceResolverCreateParams {
68
70
  * and caching results for optimal performance.
69
71
  * @public
70
72
  */
71
- export declare class ResourceResolver {
73
+ export declare class ResourceResolver implements IResourceResolver {
72
74
  /**
73
75
  * The resource manager that defines available resources and provides condition access.
74
76
  */
@@ -85,6 +87,10 @@ export declare class ResourceResolver {
85
87
  * The configuration options for this resource resolver.
86
88
  */
87
89
  readonly options: IResourceResolverOptions;
90
+ /**
91
+ * The readonly qualifier collector that provides qualifier implementations.
92
+ */
93
+ get qualifiers(): IReadOnlyQualifierCollector;
88
94
  /**
89
95
  * The cache array for resolved conditions, indexed by condition index for O(1) lookup.
90
96
  * Each entry stores the resolved {@link Runtime.IConditionMatchResult | condition match result} for
@@ -169,6 +175,15 @@ export declare class ResourceResolver {
169
175
  * @public
170
176
  */
171
177
  resolveResource(resource: IResource): Result<IResourceCandidate>;
178
+ /**
179
+ * Resolves a resource by finding the best matching candidate.
180
+ * Uses the resource's associated decision to determine the best match based on the current context.
181
+ * @param resource - The string id of the resource to resolve.
182
+ * @returns `Success` with the best matching candidate if successful,
183
+ * or `Failure` with an error message if no candidates match or resolution fails.
184
+ * @public
185
+ */
186
+ resolveResource(resource: string): Result<IResourceCandidate>;
172
187
  /**
173
188
  * Resolves all matching resource candidates in priority order.
174
189
  * Uses the resource's associated decision to determine all matching candidates based on the current context.
@@ -178,6 +193,15 @@ export declare class ResourceResolver {
178
193
  * @public
179
194
  */
180
195
  resolveAllResourceCandidates(resource: IResource): Result<ReadonlyArray<IResourceCandidate>>;
196
+ /**
197
+ * Resolves all matching resource candidates in priority order.
198
+ * Uses the resource's associated decision to determine all matching candidates based on the current context.
199
+ * @param resource - The string id of the resource to resolve.
200
+ * @returns `Success` with an array of all matching candidates in priority order if successful,
201
+ * or `Failure` with an error message if no candidates match or resolution fails.
202
+ * @public
203
+ */
204
+ resolveAllResourceCandidates(resource: string): Result<ReadonlyArray<IResourceCandidate>>;
181
205
  /**
182
206
  * Resolves a resource to a composed value by merging matching candidates according to their merge methods.
183
207
  * Starting from the highest priority candidates, finds the first "full" candidate and merges all higher
@@ -188,6 +212,20 @@ export declare class ResourceResolver {
188
212
  * @public
189
213
  */
190
214
  resolveComposedResourceValue(resource: IResource): Result<JsonValue>;
215
+ /**
216
+ * Resolves a resource to a composed value by merging matching candidates according to their merge methods.
217
+ * Starting from the highest priority candidates, finds the first "full" candidate and merges all higher
218
+ * priority "partial" candidates into it in ascending order of priority.
219
+ * @param resource - The string id of the resource to resolve.
220
+ * @returns `Success` with the composed JsonValue if successful,
221
+ * or `Failure` with an error message if no candidates match or resolution fails.
222
+ * @public
223
+ */
224
+ resolveComposedResourceValue(resource: string): Result<JsonValue>;
225
+ /**
226
+ * {@inheritDoc IResourceResolver.withContext}
227
+ */
228
+ withContext(context: Record<string, string>): Result<ResourceResolver>;
191
229
  /**
192
230
  * Clears all caches (condition, condition set, and decision), forcing all cached items
193
231
  * to be re-evaluated on next access. This should be called when the context changes and cached
@@ -26,6 +26,7 @@ const ts_utils_1 = require("@fgv/ts-utils");
26
26
  const ts_json_base_1 = require("@fgv/ts-json-base");
27
27
  const ts_json_1 = require("@fgv/ts-json");
28
28
  const common_1 = require("../common");
29
+ const context_1 = require("./context");
29
30
  const conditionSetResolutionResult_1 = require("./conditionSetResolutionResult");
30
31
  /**
31
32
  * High-performance runtime resource resolver with O(1) condition caching.
@@ -34,6 +35,12 @@ const conditionSetResolutionResult_1 = require("./conditionSetResolutionResult")
34
35
  * @public
35
36
  */
36
37
  class ResourceResolver {
38
+ /**
39
+ * The readonly qualifier collector that provides qualifier implementations.
40
+ */
41
+ get qualifiers() {
42
+ return this.contextQualifierProvider.qualifiers;
43
+ }
37
44
  /**
38
45
  * The cache array for resolved conditions, indexed by condition index for O(1) lookup.
39
46
  * Each entry stores the resolved {@link Runtime.IConditionMatchResult | condition match result} for
@@ -268,15 +275,13 @@ class ResourceResolver {
268
275
  (_c = this._listener) === null || _c === void 0 ? void 0 : _c.onCacheMiss('decision', decisionIndex);
269
276
  return (0, ts_utils_1.succeed)(successResult);
270
277
  }
271
- /**
272
- * Resolves a resource by finding the best matching candidate.
273
- * Uses the resource's associated decision to determine the best match based on the current context.
274
- * @param resource - The {@link Resources.Resource | resource} to resolve.
275
- * @returns `Success` with the best matching candidate if successful,
276
- * or `Failure` with an error message if no candidates match or resolution fails.
277
- * @public
278
- */
279
- resolveResource(resource) {
278
+ resolveResource(idOrResource) {
279
+ if (typeof idOrResource === 'string') {
280
+ return this.resourceManager
281
+ .getBuiltResource(idOrResource)
282
+ .onSuccess((resource) => this.resolveResource(resource));
283
+ }
284
+ const resource = idOrResource;
280
285
  // Get the abstract decision from the resource's concrete decision
281
286
  const abstractDecision = resource.decision.baseDecision;
282
287
  // Resolve the decision to get candidate indices in priority order
@@ -301,15 +306,13 @@ class ResourceResolver {
301
306
  const bestCandidate = resource.candidates[candidateIndex];
302
307
  return (0, ts_utils_1.succeed)(bestCandidate);
303
308
  }
304
- /**
305
- * Resolves all matching resource candidates in priority order.
306
- * Uses the resource's associated decision to determine all matching candidates based on the current context.
307
- * @param resource - The {@link Resources.Resource | resource} to resolve.
308
- * @returns `Success` with an array of all matching candidates in priority order if successful,
309
- * or `Failure` with an error message if no candidates match or resolution fails.
310
- * @public
311
- */
312
- resolveAllResourceCandidates(resource) {
309
+ resolveAllResourceCandidates(idOrResource) {
310
+ if (typeof idOrResource === 'string') {
311
+ return this.resourceManager
312
+ .getBuiltResource(idOrResource)
313
+ .onSuccess((resource) => this.resolveAllResourceCandidates(resource));
314
+ }
315
+ const resource = idOrResource;
313
316
  // Get the abstract decision from the resource's concrete decision
314
317
  const abstractDecision = resource.decision.baseDecision;
315
318
  // Resolve the decision to get candidate indices in priority order
@@ -345,16 +348,13 @@ class ResourceResolver {
345
348
  }
346
349
  return (0, ts_utils_1.succeed)(candidates);
347
350
  }
348
- /**
349
- * Resolves a resource to a composed value by merging matching candidates according to their merge methods.
350
- * Starting from the highest priority candidates, finds the first "full" candidate and merges all higher
351
- * priority "partial" candidates into it in ascending order of priority.
352
- * @param resource - The {@link Resources.Resource | resource} to resolve.
353
- * @returns `Success` with the composed JsonValue if successful,
354
- * or `Failure` with an error message if no candidates match or resolution fails.
355
- * @public
356
- */
357
- resolveComposedResourceValue(resource) {
351
+ resolveComposedResourceValue(idOrResource) {
352
+ if (typeof idOrResource === 'string') {
353
+ return this.resourceManager
354
+ .getBuiltResource(idOrResource)
355
+ .onSuccess((resource) => this.resolveComposedResourceValue(resource));
356
+ }
357
+ const resource = idOrResource;
358
358
  return this.resolveAllResourceCandidates(resource).onSuccess((candidates) => {
359
359
  /* c8 ignore next 3 - defense in depth should never occur */
360
360
  if (candidates.length === 0) {
@@ -415,6 +415,23 @@ class ResourceResolver {
415
415
  .withErrorFormat((err) => `${resource.id}: Composition failed: ${err}`);
416
416
  });
417
417
  }
418
+ /**
419
+ * {@inheritDoc IResourceResolver.withContext}
420
+ */
421
+ withContext(context) {
422
+ const { resourceManager, qualifierTypes, options } = this;
423
+ return context_1.ValidatingSimpleContextQualifierProvider.create({
424
+ qualifiers: this.qualifiers,
425
+ qualifierValues: context
426
+ }).onSuccess((contextQualifierProvider) => {
427
+ return ResourceResolver.create({
428
+ resourceManager,
429
+ qualifierTypes,
430
+ options,
431
+ contextQualifierProvider
432
+ });
433
+ });
434
+ }
418
435
  /**
419
436
  * Clears all caches (condition, condition set, and decision), forcing all cached items
420
437
  * to be re-evaluated on next access. This should be called when the context changes and cached
@@ -0,0 +1,2 @@
1
+ import '@fgv/ts-utils-jest';
2
+ //# sourceMappingURL=derivedResourceType.test.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { JsonObject, JsonValue } from '@fgv/ts-json-base';
2
+ import { Result } from '@fgv/ts-utils';
3
+ import { CandidateCompleteness, ResourceTypeName, ResourceId, IResourceResolver } from '../../../packlets/common';
4
+ import { IResourceCandidateValidationProperties, ResourceType } from '../../../packlets/resource-types/resourceType';
5
+ import * as ResourceJson from '../../../packlets/resource-json';
6
+ /**
7
+ * Parameters to create a TestDerivedResourceType instance.
8
+ */
9
+ export interface ITestDerivedResourceTypeCreateParams {
10
+ /**
11
+ * Optional key for the new TestDerivedResourceType instance.
12
+ * Defaults to 'test-derived'.
13
+ */
14
+ key?: string;
15
+ /**
16
+ * Optional index for the new TestDerivedResourceType instance.
17
+ */
18
+ index?: number;
19
+ /**
20
+ * Optional template for new instances.
21
+ */
22
+ template?: JsonObject;
23
+ /**
24
+ * Optional resource ID to use as a template source.
25
+ * When provided, the resource resolver will be used to look up
26
+ * this resource and use its value as the template.
27
+ */
28
+ templateResourceId?: string;
29
+ }
30
+ /**
31
+ * Test data structure for the derived resource type
32
+ */
33
+ export interface ITestData {
34
+ title: string;
35
+ description?: string;
36
+ metadata?: Record<string, JsonValue>;
37
+ }
38
+ /**
39
+ * Partial test data structure for the derived resource type
40
+ */
41
+ export interface IPartialTestData {
42
+ title?: string;
43
+ description?: string;
44
+ metadata?: Record<string, JsonValue>;
45
+ }
46
+ /**
47
+ * A derived resource type for testing that demonstrates:
48
+ * 1. Custom validation logic
49
+ * 2. Using a resource resolver to look up template values
50
+ * 3. Type-specific defaults and error handling
51
+ */
52
+ export declare class TestDerivedResourceType extends ResourceType<ITestData> {
53
+ private _templateResourceId?;
54
+ protected constructor(key: ResourceTypeName, index?: number, template?: JsonObject, templateResourceId?: string);
55
+ /**
56
+ * Factory method to create a new TestDerivedResourceType instance.
57
+ */
58
+ static create(params?: ITestDerivedResourceTypeCreateParams): Result<TestDerivedResourceType>;
59
+ /**
60
+ * Validates a resource candidate declaration with custom logic.
61
+ */
62
+ validateDeclaration(props: IResourceCandidateValidationProperties): Result<ITestData | IPartialTestData>;
63
+ /**
64
+ * Validates JSON for this resource type with custom business logic.
65
+ */
66
+ validate(json: JsonValue, completeness: CandidateCompleteness): Result<IPartialTestData>;
67
+ validate(json: JsonValue, completeness: 'full'): Result<ITestData>;
68
+ validate(json: JsonValue, completeness: 'partial'): Result<IPartialTestData>;
69
+ /**
70
+ * Override to demonstrate looking up template values from another resource.
71
+ */
72
+ getDefaultTemplateCandidate(json?: JsonValue, conditions?: ResourceJson.Json.ConditionSetDecl, resolver?: IResourceResolver): Result<ResourceJson.Json.IChildResourceCandidateDecl>;
73
+ /**
74
+ * Override createTemplate to add additional validation for the derived type.
75
+ */
76
+ createTemplate(resourceId: ResourceId, init?: JsonValue, conditions?: ResourceJson.Json.ConditionSetDecl, resolver?: IResourceResolver): Result<ResourceJson.Json.ILooseResourceDecl>;
77
+ }
78
+ //# sourceMappingURL=testDerivedResourceType.d.ts.map
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2025 Erik Fortune
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ * SOFTWARE.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.TestDerivedResourceType = void 0;
25
+ const ts_utils_1 = require("@fgv/ts-utils");
26
+ const common_1 = require("../../../packlets/common");
27
+ // eslint-disable-next-line @rushstack/packlets/mechanics
28
+ const resourceType_1 = require("../../../packlets/resource-types/resourceType");
29
+ /**
30
+ * A derived resource type for testing that demonstrates:
31
+ * 1. Custom validation logic
32
+ * 2. Using a resource resolver to look up template values
33
+ * 3. Type-specific defaults and error handling
34
+ */
35
+ class TestDerivedResourceType extends resourceType_1.ResourceType {
36
+ constructor(key, index, template, templateResourceId) {
37
+ super(key, index, template);
38
+ this._templateResourceId = templateResourceId;
39
+ }
40
+ /**
41
+ * Factory method to create a new TestDerivedResourceType instance.
42
+ */
43
+ static create(params) {
44
+ var _a;
45
+ return common_1.Convert.resourceTypeName.convert((_a = params === null || params === void 0 ? void 0 : params.key) !== null && _a !== void 0 ? _a : 'test-derived').onSuccess((key) => {
46
+ return (0, ts_utils_1.captureResult)(() => new TestDerivedResourceType(key, params === null || params === void 0 ? void 0 : params.index, params === null || params === void 0 ? void 0 : params.template, params === null || params === void 0 ? void 0 : params.templateResourceId));
47
+ });
48
+ }
49
+ /**
50
+ * Validates a resource candidate declaration with custom logic.
51
+ */
52
+ validateDeclaration(props) {
53
+ // Custom validation: merge method must be 'replace' for full candidates
54
+ if (props.completeness === 'full' && props.mergeMethod !== 'replace') {
55
+ return (0, ts_utils_1.fail)(`${this.key}: Full candidates must use 'replace' merge method, got '${props.mergeMethod}'`);
56
+ }
57
+ return this.validate(props.json, props.completeness);
58
+ }
59
+ validate(json, completeness) {
60
+ if (typeof json !== 'object' || json === null || Array.isArray(json)) {
61
+ return (0, ts_utils_1.fail)(`${this.key}: Expected JSON object, got ${typeof json}`);
62
+ }
63
+ const obj = json;
64
+ // Validate required title field for full resources
65
+ if (completeness === 'full' && (typeof obj.title !== 'string' || !obj.title.trim())) {
66
+ return (0, ts_utils_1.fail)(`${this.key}: 'title' field is required and must be a non-empty string`);
67
+ }
68
+ // Validate optional description
69
+ if (obj.description !== undefined && typeof obj.description !== 'string') {
70
+ return (0, ts_utils_1.fail)(`${this.key}: 'description' field must be a string if provided`);
71
+ }
72
+ // Validate optional metadata
73
+ if (obj.metadata !== undefined) {
74
+ if (typeof obj.metadata !== 'object' || obj.metadata === null || Array.isArray(obj.metadata)) {
75
+ return (0, ts_utils_1.fail)(`${this.key}: 'metadata' field must be an object if provided`);
76
+ }
77
+ }
78
+ const result = Object.assign(Object.assign(Object.assign({}, (obj.title && { title: obj.title })), (obj.description && { description: obj.description })), (obj.metadata && { metadata: obj.metadata }));
79
+ return (0, ts_utils_1.succeed)(result);
80
+ }
81
+ /**
82
+ * Override to demonstrate looking up template values from another resource.
83
+ */
84
+ getDefaultTemplateCandidate(json, conditions, resolver) {
85
+ // If a template resource ID is configured and resolver is available, try to resolve it
86
+ if (this._templateResourceId && resolver && !json) {
87
+ return resolver
88
+ .resolveComposedResourceValue(this._templateResourceId)
89
+ .onFailure((error) => (0, ts_utils_1.fail)(`${this.key}: Failed to resolve template resource '${this._templateResourceId}': ${error}`))
90
+ .onSuccess((resolvedValue) => {
91
+ // Use the resolved value as our JSON template
92
+ return super.getDefaultTemplateCandidate(resolvedValue, conditions, resolver);
93
+ });
94
+ }
95
+ // Use provided JSON or fall back to base implementation
96
+ return super.getDefaultTemplateCandidate(json, conditions, resolver);
97
+ }
98
+ /**
99
+ * Override createTemplate to add additional validation for the derived type.
100
+ */
101
+ createTemplate(resourceId, init, conditions, resolver) {
102
+ // Additional validation: warn if trying to use resolver functionality without a resolver
103
+ if (this._templateResourceId && !resolver) {
104
+ return (0, ts_utils_1.fail)(`${this.key}: Template resource ID '${this._templateResourceId}' is configured but no resolver provided`);
105
+ }
106
+ return super.createTemplate(resourceId, init, conditions, resolver);
107
+ }
108
+ }
109
+ exports.TestDerivedResourceType = TestDerivedResourceType;
110
+ //# sourceMappingURL=testDerivedResourceType.js.map
@@ -0,0 +1,2 @@
1
+ import '@fgv/ts-utils-jest';
2
+ //# sourceMappingURL=resourceResolver.coverage.test.d.ts.map
@@ -0,0 +1,2 @@
1
+ import '@fgv/ts-utils-jest';
2
+ //# sourceMappingURL=resourceResolver.newFeatures.test.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fgv/ts-res",
3
- "version": "5.0.0-21",
3
+ "version": "5.0.0-22",
4
4
  "description": "Multi-dimensional Resource Runtime",
5
5
  "main": "lib/index.js",
6
6
  "types": "dist/ts-res.d.ts",
@@ -44,16 +44,16 @@
44
44
  "@rushstack/heft-node-rig": "~2.9.3",
45
45
  "@types/heft-jest": "1.0.6",
46
46
  "eslint-plugin-tsdoc": "~0.4.0",
47
- "@fgv/ts-utils-jest": "5.0.0-21",
48
- "@fgv/ts-extras": "5.0.0-21"
47
+ "@fgv/ts-utils-jest": "5.0.0-22",
48
+ "@fgv/ts-extras": "5.0.0-22"
49
49
  },
50
50
  "dependencies": {
51
51
  "luxon": "^3.7.1",
52
52
  "fflate": "~0.8.2",
53
- "@fgv/ts-utils": "5.0.0-21",
54
- "@fgv/ts-json-base": "5.0.0-21",
55
- "@fgv/ts-bcp47": "5.0.0-21",
56
- "@fgv/ts-json": "5.0.0-21"
53
+ "@fgv/ts-bcp47": "5.0.0-22",
54
+ "@fgv/ts-utils": "5.0.0-22",
55
+ "@fgv/ts-json-base": "5.0.0-22",
56
+ "@fgv/ts-json": "5.0.0-22"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "heft build --clean",
@@ -20,7 +20,8 @@
20
20
  * SOFTWARE.
21
21
  */
22
22
 
23
- import { Brand } from '@fgv/ts-utils';
23
+ import { JsonValue } from '@fgv/ts-json-base';
24
+ import { Brand, Result } from '@fgv/ts-utils';
24
25
 
25
26
  /**
26
27
  * Branded string representing a validated resource id. A resource ID
@@ -76,3 +77,29 @@ export const allResourceValueMergeMethods: ResourceValueMergeMethod[] = ['augmen
76
77
  * @public
77
78
  */
78
79
  export type CandidateCompleteness = 'full' | 'partial';
80
+
81
+ /**
82
+ * Minimal resource resolver
83
+ * @public
84
+ */
85
+ export interface IResourceResolver {
86
+ /**
87
+ * Resolves a resource to a composed value by merging matching candidates according to their merge methods.
88
+ * Starting from the highest priority candidates, finds the first "full" candidate and merges all higher
89
+ * priority "partial" candidates into it in ascending order of priority.
90
+ * @param resource - The string id of the resource to resolve.
91
+ * @returns `Success` with the composed JsonValue if successful,
92
+ * or `Failure` with an error message if no candidates match or resolution fails.
93
+ * @public
94
+ */
95
+ resolveComposedResourceValue(resource: string): Result<JsonValue>;
96
+
97
+ /**
98
+ * Creates a new {@link IResourceResolver | resource resolver} with the given context.
99
+ * @param context - The context to use for the new resource resolver.
100
+ * @returns `Success` with the new resource resolver if successful,
101
+ * or `Failure` with an error message if the context is invalid.
102
+ * @public
103
+ */
104
+ withContext(context: Record<string, string>): Result<IResourceResolver>;
105
+ }