@player-tools/fluent 0.12.1--canary.241.6077
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/dist/cjs/index.cjs +2396 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +2276 -0
- package/dist/index.mjs +2276 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
- package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2423 -0
- package/src/core/base-builder/__tests__/fluent-partial.test.ts +179 -0
- package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
- package/src/core/base-builder/__tests__/registry.test.ts +534 -0
- package/src/core/base-builder/__tests__/resolution-mixed-arrays.test.ts +319 -0
- package/src/core/base-builder/__tests__/resolution-pipeline.test.ts +416 -0
- package/src/core/base-builder/__tests__/resolution-switches.test.ts +468 -0
- package/src/core/base-builder/__tests__/resolution-templates.test.ts +255 -0
- package/src/core/base-builder/__tests__/switch.test.ts +815 -0
- package/src/core/base-builder/__tests__/template.test.ts +596 -0
- package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
- package/src/core/base-builder/__tests__/value-storage.test.ts +459 -0
- package/src/core/base-builder/conditional/index.ts +64 -0
- package/src/core/base-builder/context.ts +152 -0
- package/src/core/base-builder/errors.ts +69 -0
- package/src/core/base-builder/fluent-builder-base.ts +308 -0
- package/src/core/base-builder/guards.ts +137 -0
- package/src/core/base-builder/id/generator.ts +290 -0
- package/src/core/base-builder/id/registry.ts +152 -0
- package/src/core/base-builder/index.ts +72 -0
- package/src/core/base-builder/resolution/path-resolver.ts +116 -0
- package/src/core/base-builder/resolution/pipeline.ts +103 -0
- package/src/core/base-builder/resolution/steps/__tests__/nested-asset-wrappers.test.ts +206 -0
- package/src/core/base-builder/resolution/steps/asset-id.ts +77 -0
- package/src/core/base-builder/resolution/steps/asset-wrappers.ts +64 -0
- package/src/core/base-builder/resolution/steps/builders.ts +84 -0
- package/src/core/base-builder/resolution/steps/mixed-arrays.ts +95 -0
- package/src/core/base-builder/resolution/steps/nested-asset-wrappers.ts +124 -0
- package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
- package/src/core/base-builder/resolution/steps/switches.ts +71 -0
- package/src/core/base-builder/resolution/steps/templates.ts +40 -0
- package/src/core/base-builder/resolution/value-resolver.ts +333 -0
- package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
- package/src/core/base-builder/storage/value-storage.ts +282 -0
- package/src/core/base-builder/types.ts +266 -0
- package/src/core/base-builder/utils.ts +10 -0
- package/src/core/flow/__tests__/index.test.ts +292 -0
- package/src/core/flow/index.ts +118 -0
- package/src/core/index.ts +8 -0
- package/src/core/mocks/generated/action.builder.ts +92 -0
- package/src/core/mocks/generated/choice-item.builder.ts +120 -0
- package/src/core/mocks/generated/choice.builder.ts +134 -0
- package/src/core/mocks/generated/collection.builder.ts +93 -0
- package/src/core/mocks/generated/field-collection.builder.ts +86 -0
- package/src/core/mocks/generated/index.ts +10 -0
- package/src/core/mocks/generated/info.builder.ts +64 -0
- package/src/core/mocks/generated/input.builder.ts +63 -0
- package/src/core/mocks/generated/overview-collection.builder.ts +65 -0
- package/src/core/mocks/generated/splash-collection.builder.ts +93 -0
- package/src/core/mocks/generated/text.builder.ts +47 -0
- package/src/core/mocks/index.ts +1 -0
- package/src/core/mocks/types/action.ts +92 -0
- package/src/core/mocks/types/choice.ts +129 -0
- package/src/core/mocks/types/collection.ts +140 -0
- package/src/core/mocks/types/info.ts +7 -0
- package/src/core/mocks/types/input.ts +7 -0
- package/src/core/mocks/types/text.ts +5 -0
- package/src/core/schema/__tests__/index.test.ts +127 -0
- package/src/core/schema/index.ts +195 -0
- package/src/core/schema/types.ts +7 -0
- package/src/core/switch/__tests__/index.test.ts +156 -0
- package/src/core/switch/index.ts +81 -0
- package/src/core/tagged-template/README.md +448 -0
- package/src/core/tagged-template/__tests__/extract-bindings-from-schema.test.ts +207 -0
- package/src/core/tagged-template/__tests__/index.test.ts +190 -0
- package/src/core/tagged-template/__tests__/schema-std-integration.test.ts +580 -0
- package/src/core/tagged-template/binding.ts +95 -0
- package/src/core/tagged-template/expression.ts +92 -0
- package/src/core/tagged-template/extract-bindings-from-schema.ts +120 -0
- package/src/core/tagged-template/index.ts +5 -0
- package/src/core/tagged-template/std.ts +472 -0
- package/src/core/tagged-template/types.ts +123 -0
- package/src/core/template/__tests__/index.test.ts +380 -0
- package/src/core/template/index.ts +196 -0
- package/src/core/utils/index.ts +160 -0
- package/src/fp/README.md +411 -0
- package/src/fp/__tests__/index.test.ts +1178 -0
- package/src/fp/index.ts +386 -0
- package/src/gen/common.ts +15 -0
- package/src/index.ts +5 -0
- package/src/types.ts +203 -0
- package/types/core/base-builder/conditional/index.d.ts +21 -0
- package/types/core/base-builder/context.d.ts +39 -0
- package/types/core/base-builder/errors.d.ts +45 -0
- package/types/core/base-builder/fluent-builder-base.d.ts +147 -0
- package/types/core/base-builder/guards.d.ts +58 -0
- package/types/core/base-builder/id/generator.d.ts +69 -0
- package/types/core/base-builder/id/registry.d.ts +93 -0
- package/types/core/base-builder/index.d.ts +9 -0
- package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
- package/types/core/base-builder/resolution/pipeline.d.ts +27 -0
- package/types/core/base-builder/resolution/steps/asset-id.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/asset-wrappers.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/builders.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/mixed-arrays.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/nested-asset-wrappers.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/static-values.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/switches.d.ts +15 -0
- package/types/core/base-builder/resolution/steps/templates.d.ts +14 -0
- package/types/core/base-builder/resolution/value-resolver.d.ts +62 -0
- package/types/core/base-builder/storage/auxiliary-storage.d.ts +50 -0
- package/types/core/base-builder/storage/value-storage.d.ts +82 -0
- package/types/core/base-builder/types.d.ts +183 -0
- package/types/core/base-builder/utils.d.ts +2 -0
- package/types/core/flow/index.d.ts +23 -0
- package/types/core/index.d.ts +8 -0
- package/types/core/mocks/index.d.ts +2 -0
- package/types/core/mocks/types/action.d.ts +58 -0
- package/types/core/mocks/types/choice.d.ts +95 -0
- package/types/core/mocks/types/collection.d.ts +102 -0
- package/types/core/mocks/types/info.d.ts +7 -0
- package/types/core/mocks/types/input.d.ts +7 -0
- package/types/core/mocks/types/text.d.ts +5 -0
- package/types/core/schema/index.d.ts +34 -0
- package/types/core/schema/types.d.ts +5 -0
- package/types/core/switch/index.d.ts +21 -0
- package/types/core/tagged-template/binding.d.ts +19 -0
- package/types/core/tagged-template/expression.d.ts +11 -0
- package/types/core/tagged-template/extract-bindings-from-schema.d.ts +7 -0
- package/types/core/tagged-template/index.d.ts +6 -0
- package/types/core/tagged-template/std.d.ts +174 -0
- package/types/core/tagged-template/types.d.ts +69 -0
- package/types/core/template/index.d.ts +97 -0
- package/types/core/utils/index.d.ts +47 -0
- package/types/fp/index.d.ts +149 -0
- package/types/gen/common.d.ts +6 -0
- package/types/index.d.ts +3 -0
- package/types/types.d.ts +163 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ValuePath } from "../types";
|
|
2
|
+
import { isAssetWrapperValue, isPlainObject } from "../guards";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Safely gets a value as a Record<string, unknown>, returning empty object if not.
|
|
6
|
+
* This avoids unsafe `as` assertions by using the isPlainObject type guard.
|
|
7
|
+
*/
|
|
8
|
+
function asRecordOrEmpty(value: unknown): Record<string, unknown> {
|
|
9
|
+
return isPlainObject(value) ? value : {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sets a value at a nested path in an object
|
|
14
|
+
* Handles nested objects, arrays, and AssetWrapper structures
|
|
15
|
+
*
|
|
16
|
+
* @param obj - The target object to modify
|
|
17
|
+
* @param path - Array of keys/indices representing the path
|
|
18
|
+
* @param value - The value to set at the path
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* setValueAtPath(obj, ["actions", 0, "label"], "Click me")
|
|
22
|
+
* // Sets obj.actions[0].label = "Click me"
|
|
23
|
+
*/
|
|
24
|
+
export function setValueAtPath(
|
|
25
|
+
obj: Record<string, unknown>,
|
|
26
|
+
path: ValuePath,
|
|
27
|
+
value: unknown,
|
|
28
|
+
): void {
|
|
29
|
+
if (path.length === 0) return;
|
|
30
|
+
|
|
31
|
+
if (path.length === 1) {
|
|
32
|
+
obj[path[0]] = value;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [currentKey, ...restPath] = path;
|
|
37
|
+
const nextKey = restPath[0];
|
|
38
|
+
const currentValue = obj[currentKey];
|
|
39
|
+
|
|
40
|
+
// Check if current value is an AssetWrapper containing an array
|
|
41
|
+
const isAssetWrapperWithArray =
|
|
42
|
+
isAssetWrapperValue(currentValue) && Array.isArray(currentValue.asset);
|
|
43
|
+
|
|
44
|
+
if (isAssetWrapperWithArray && typeof nextKey === "number") {
|
|
45
|
+
setValueInAssetWrapperArray(
|
|
46
|
+
obj,
|
|
47
|
+
currentKey,
|
|
48
|
+
currentValue as { asset: unknown[] },
|
|
49
|
+
nextKey,
|
|
50
|
+
restPath,
|
|
51
|
+
value,
|
|
52
|
+
);
|
|
53
|
+
} else if (Array.isArray(currentValue) && typeof nextKey === "number") {
|
|
54
|
+
setValueInArray(obj, currentKey, currentValue, nextKey, restPath, value);
|
|
55
|
+
} else {
|
|
56
|
+
setValueInObject(obj, currentKey, restPath, value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sets value in an array within an AssetWrapper
|
|
62
|
+
*/
|
|
63
|
+
function setValueInAssetWrapperArray(
|
|
64
|
+
obj: Record<string, unknown>,
|
|
65
|
+
currentKey: string | number,
|
|
66
|
+
wrappedArray: { asset: unknown[] },
|
|
67
|
+
nextKey: number,
|
|
68
|
+
restPath: ValuePath,
|
|
69
|
+
value: unknown,
|
|
70
|
+
): void {
|
|
71
|
+
const arrayResult = [...wrappedArray.asset];
|
|
72
|
+
if (restPath.length === 1) {
|
|
73
|
+
arrayResult[nextKey] = value;
|
|
74
|
+
} else {
|
|
75
|
+
const nestedObj = asRecordOrEmpty(arrayResult[nextKey]);
|
|
76
|
+
setValueAtPath(nestedObj, restPath.slice(1), value);
|
|
77
|
+
arrayResult[nextKey] = nestedObj;
|
|
78
|
+
}
|
|
79
|
+
obj[currentKey] = { asset: arrayResult };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sets value in a regular array
|
|
84
|
+
*/
|
|
85
|
+
function setValueInArray(
|
|
86
|
+
obj: Record<string, unknown>,
|
|
87
|
+
currentKey: string | number,
|
|
88
|
+
array: unknown[],
|
|
89
|
+
nextKey: number,
|
|
90
|
+
restPath: ValuePath,
|
|
91
|
+
value: unknown,
|
|
92
|
+
): void {
|
|
93
|
+
const arrayResult = [...array];
|
|
94
|
+
if (restPath.length === 1) {
|
|
95
|
+
arrayResult[nextKey] = value;
|
|
96
|
+
} else {
|
|
97
|
+
const nestedObj = asRecordOrEmpty(arrayResult[nextKey]);
|
|
98
|
+
setValueAtPath(nestedObj, restPath.slice(1), value);
|
|
99
|
+
arrayResult[nextKey] = nestedObj;
|
|
100
|
+
}
|
|
101
|
+
obj[currentKey] = arrayResult;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sets value in a nested object
|
|
106
|
+
*/
|
|
107
|
+
function setValueInObject(
|
|
108
|
+
obj: Record<string, unknown>,
|
|
109
|
+
currentKey: string | number,
|
|
110
|
+
restPath: ValuePath,
|
|
111
|
+
value: unknown,
|
|
112
|
+
): void {
|
|
113
|
+
const nestedObj = asRecordOrEmpty(obj[currentKey]);
|
|
114
|
+
setValueAtPath(nestedObj, restPath, value);
|
|
115
|
+
obj[currentKey] = nestedObj;
|
|
116
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { BaseBuildContext } from "../types";
|
|
2
|
+
import type { ValueStorage } from "../storage/value-storage";
|
|
3
|
+
import type { AuxiliaryStorage } from "../storage/auxiliary-storage";
|
|
4
|
+
import { resolveStaticValues } from "./steps/static-values";
|
|
5
|
+
import { generateAssetIdForBuilder } from "./steps/asset-id";
|
|
6
|
+
import { resolveAssetWrappers } from "./steps/asset-wrappers";
|
|
7
|
+
import { resolveMixedArrays } from "./steps/mixed-arrays";
|
|
8
|
+
import { resolveBuilders } from "./steps/builders";
|
|
9
|
+
import { resolveSwitches } from "./steps/switches";
|
|
10
|
+
import { resolveTemplates } from "./steps/templates";
|
|
11
|
+
import { resolveNestedAssetWrappers } from "./steps/nested-asset-wrappers";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a nested context for child assets
|
|
15
|
+
* This is Step 3 of the build process
|
|
16
|
+
*/
|
|
17
|
+
function createNestedParentContext<C extends BaseBuildContext>(
|
|
18
|
+
result: Record<string, unknown>,
|
|
19
|
+
context: C | undefined,
|
|
20
|
+
): C | undefined {
|
|
21
|
+
if (!context) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Extract parent ID with type checking
|
|
26
|
+
const parentId =
|
|
27
|
+
"id" in result && typeof result.id === "string" ? result.id : undefined;
|
|
28
|
+
|
|
29
|
+
// Create context for nested assets
|
|
30
|
+
// We clear the branch to avoid double-nesting
|
|
31
|
+
return {
|
|
32
|
+
...context,
|
|
33
|
+
parentId,
|
|
34
|
+
branch: undefined,
|
|
35
|
+
} as C;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Executes the complete build pipeline
|
|
40
|
+
*
|
|
41
|
+
* The pipeline consists of 9 steps that transform builder state into a final object:
|
|
42
|
+
* 1. Resolve static values (extract TaggedTemplateValue, resolve simple builders)
|
|
43
|
+
* 2. Generate asset ID if needed
|
|
44
|
+
* 3. Create nested context for child assets
|
|
45
|
+
* 4. Resolve AssetWrapper values
|
|
46
|
+
* 5. Resolve mixed arrays
|
|
47
|
+
* 6. Resolve builders
|
|
48
|
+
* 7. Resolve nested AssetWrapper paths
|
|
49
|
+
* 8. Resolve switches
|
|
50
|
+
* 9. Resolve templates
|
|
51
|
+
*
|
|
52
|
+
* @param valueStorage - Storage containing property values
|
|
53
|
+
* @param auxiliaryStorage - Storage containing metadata (templates, switches)
|
|
54
|
+
* @param defaults - Optional default values to merge into result
|
|
55
|
+
* @param context - Optional build context
|
|
56
|
+
* @param arrayProperties - Set of property names that are array types
|
|
57
|
+
* @param assetWrapperPaths - Paths to nested AssetWrapper properties
|
|
58
|
+
* @returns The fully built object
|
|
59
|
+
*/
|
|
60
|
+
export function executeBuildPipeline<T, C extends BaseBuildContext>(
|
|
61
|
+
valueStorage: ValueStorage<T>,
|
|
62
|
+
auxiliaryStorage: AuxiliaryStorage,
|
|
63
|
+
defaults: Partial<T> | undefined,
|
|
64
|
+
context: C | undefined,
|
|
65
|
+
arrayProperties: ReadonlySet<string>,
|
|
66
|
+
assetWrapperPaths: ReadonlyArray<ReadonlyArray<string>> = [],
|
|
67
|
+
): T {
|
|
68
|
+
const result: Record<string, unknown> = defaults ? { ...defaults } : {};
|
|
69
|
+
|
|
70
|
+
// Step 1: Resolve static values
|
|
71
|
+
resolveStaticValues(valueStorage, result, context);
|
|
72
|
+
|
|
73
|
+
// Step 2: Generate asset ID if needed
|
|
74
|
+
generateAssetIdForBuilder(valueStorage, result, context);
|
|
75
|
+
|
|
76
|
+
// Step 3: Create nested context for child assets
|
|
77
|
+
const nestedParentContext = createNestedParentContext(result, context);
|
|
78
|
+
|
|
79
|
+
// Step 4: Resolve AssetWrapper values
|
|
80
|
+
resolveAssetWrappers(valueStorage, result, nestedParentContext);
|
|
81
|
+
|
|
82
|
+
// Step 5: Resolve mixed arrays
|
|
83
|
+
resolveMixedArrays(valueStorage, result, nestedParentContext);
|
|
84
|
+
|
|
85
|
+
// Step 6: Resolve builders
|
|
86
|
+
resolveBuilders(valueStorage, result, nestedParentContext);
|
|
87
|
+
|
|
88
|
+
// Step 7: Resolve nested AssetWrapper paths
|
|
89
|
+
resolveNestedAssetWrappers(result, nestedParentContext, assetWrapperPaths);
|
|
90
|
+
|
|
91
|
+
// Step 8: Resolve switches
|
|
92
|
+
resolveSwitches(
|
|
93
|
+
auxiliaryStorage,
|
|
94
|
+
result,
|
|
95
|
+
nestedParentContext,
|
|
96
|
+
arrayProperties,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Step 9: Resolve templates
|
|
100
|
+
resolveTemplates(auxiliaryStorage, result, context);
|
|
101
|
+
|
|
102
|
+
return result as T;
|
|
103
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { resolveNestedAssetWrappers } from "../nested-asset-wrappers";
|
|
3
|
+
import type { BaseBuildContext } from "../../../types";
|
|
4
|
+
import { FLUENT_BUILDER_SYMBOL } from "../../../types";
|
|
5
|
+
|
|
6
|
+
describe("resolveNestedAssetWrappers", () => {
|
|
7
|
+
test("skips paths with length < 2", () => {
|
|
8
|
+
const result: Record<string, unknown> = {
|
|
9
|
+
label: { type: "text", value: "Hello" },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
resolveNestedAssetWrappers(result, undefined, [["label"]]);
|
|
13
|
+
|
|
14
|
+
// Should not wrap - this is handled by direct AssetWrapper resolution
|
|
15
|
+
expect(result.label).toEqual({ type: "text", value: "Hello" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("wraps asset at nested path", () => {
|
|
19
|
+
const result: Record<string, unknown> = {
|
|
20
|
+
header: {
|
|
21
|
+
left: { type: "text", value: "Hello" },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
resolveNestedAssetWrappers(result, undefined, [["header", "left"]]);
|
|
26
|
+
|
|
27
|
+
expect(result.header).toEqual({
|
|
28
|
+
left: { asset: { type: "text", value: "Hello" } },
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("wraps multiple nested paths", () => {
|
|
33
|
+
const result: Record<string, unknown> = {
|
|
34
|
+
header: {
|
|
35
|
+
left: { type: "text", value: "Left" },
|
|
36
|
+
right: { type: "text", value: "Right" },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
resolveNestedAssetWrappers(result, undefined, [
|
|
41
|
+
["header", "left"],
|
|
42
|
+
["header", "right"],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
expect(result.header).toEqual({
|
|
46
|
+
left: { asset: { type: "text", value: "Left" } },
|
|
47
|
+
right: { asset: { type: "text", value: "Right" } },
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("handles deeply nested paths", () => {
|
|
52
|
+
const result: Record<string, unknown> = {
|
|
53
|
+
section: {
|
|
54
|
+
content: {
|
|
55
|
+
slot: { type: "text", value: "Deep" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
resolveNestedAssetWrappers(result, undefined, [
|
|
61
|
+
["section", "content", "slot"],
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
expect(result.section).toEqual({
|
|
65
|
+
content: {
|
|
66
|
+
slot: { asset: { type: "text", value: "Deep" } },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("skips already wrapped assets", () => {
|
|
72
|
+
const result: Record<string, unknown> = {
|
|
73
|
+
header: {
|
|
74
|
+
left: { asset: { type: "text", value: "Already wrapped" } },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
resolveNestedAssetWrappers(result, undefined, [["header", "left"]]);
|
|
79
|
+
|
|
80
|
+
expect(result.header).toEqual({
|
|
81
|
+
left: { asset: { type: "text", value: "Already wrapped" } },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("handles missing intermediate path", () => {
|
|
86
|
+
const result: Record<string, unknown> = {
|
|
87
|
+
// header.left doesn't exist
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Should not throw
|
|
91
|
+
resolveNestedAssetWrappers(result, undefined, [["header", "left"]]);
|
|
92
|
+
|
|
93
|
+
expect(result).toEqual({});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("handles null value in path", () => {
|
|
97
|
+
const result: Record<string, unknown> = {
|
|
98
|
+
header: {
|
|
99
|
+
left: null,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
resolveNestedAssetWrappers(result, undefined, [["header", "left"]]);
|
|
104
|
+
|
|
105
|
+
expect(result.header).toEqual({ left: null });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("wraps arrays of assets", () => {
|
|
109
|
+
const result: Record<string, unknown> = {
|
|
110
|
+
content: {
|
|
111
|
+
items: [
|
|
112
|
+
{ type: "text", value: "Item 1" },
|
|
113
|
+
{ type: "text", value: "Item 2" },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
resolveNestedAssetWrappers(result, undefined, [["content", "items"]]);
|
|
119
|
+
|
|
120
|
+
expect(result.content).toEqual({
|
|
121
|
+
items: [
|
|
122
|
+
{ asset: { type: "text", value: "Item 1" } },
|
|
123
|
+
{ asset: { type: "text", value: "Item 2" } },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("filters null/undefined from arrays", () => {
|
|
129
|
+
const result: Record<string, unknown> = {
|
|
130
|
+
content: {
|
|
131
|
+
items: [{ type: "text", value: "Valid" }, null, undefined],
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
resolveNestedAssetWrappers(result, undefined, [["content", "items"]]);
|
|
136
|
+
|
|
137
|
+
expect(result.content).toEqual({
|
|
138
|
+
items: [{ asset: { type: "text", value: "Valid" } }],
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("skips non-asset values", () => {
|
|
143
|
+
const result: Record<string, unknown> = {
|
|
144
|
+
config: {
|
|
145
|
+
name: "test",
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
resolveNestedAssetWrappers(result, undefined, [["config", "name"]]);
|
|
150
|
+
|
|
151
|
+
// String value should not be wrapped
|
|
152
|
+
expect(result.config).toEqual({ name: "test" });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("generates slot names from path", () => {
|
|
156
|
+
const context: BaseBuildContext = {
|
|
157
|
+
parentId: "parent",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const result: Record<string, unknown> = {
|
|
161
|
+
header: {
|
|
162
|
+
left: { type: "text", value: "Hello", id: "" },
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
resolveNestedAssetWrappers(result, context, [["header", "left"]]);
|
|
167
|
+
|
|
168
|
+
// The ID should be generated using the path as slot name
|
|
169
|
+
const header = result.header as Record<string, unknown>;
|
|
170
|
+
const left = header.left as { asset: { id: string } };
|
|
171
|
+
expect(left.asset.id).toBe("parent-header-left");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("resolves intermediate builders before continuing traversal", () => {
|
|
175
|
+
// Create a mock builder for the intermediate object
|
|
176
|
+
const mockBuilder = {
|
|
177
|
+
[FLUENT_BUILDER_SYMBOL]: true as const,
|
|
178
|
+
build: () => ({
|
|
179
|
+
left: { type: "text", value: "From builder" },
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const result: Record<string, unknown> = {
|
|
184
|
+
header: mockBuilder,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
resolveNestedAssetWrappers(result, undefined, [["header", "left"]]);
|
|
188
|
+
|
|
189
|
+
// The intermediate builder should be resolved, and the leaf should be wrapped
|
|
190
|
+
expect(result.header).toEqual({
|
|
191
|
+
left: { asset: { type: "text", value: "From builder" } },
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("handles empty path gracefully", () => {
|
|
196
|
+
const result: Record<string, unknown> = {
|
|
197
|
+
label: { type: "text", value: "Hello" },
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Should not throw for empty path
|
|
201
|
+
resolveNestedAssetWrappers(result, undefined, [[]]);
|
|
202
|
+
|
|
203
|
+
// Result should be unchanged
|
|
204
|
+
expect(result.label).toEqual({ type: "text", value: "Hello" });
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { BaseBuildContext, AssetMetadata } from "../../types";
|
|
2
|
+
import type { ValueStorage } from "../../storage/value-storage";
|
|
3
|
+
import { isStringOrUndefined } from "../../guards";
|
|
4
|
+
import { generateAssetId } from "../../id/generator";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Step 2: Generates ID for this asset if needed
|
|
8
|
+
*
|
|
9
|
+
* Note: This runs AFTER resolveStaticValues, so we use values from result
|
|
10
|
+
* rather than from storage.
|
|
11
|
+
*
|
|
12
|
+
* @param storage - The value storage (used to check if ID was explicitly set)
|
|
13
|
+
* @param result - The result object being built (contains resolved values)
|
|
14
|
+
* @param context - Optional build context
|
|
15
|
+
*/
|
|
16
|
+
export function generateAssetIdForBuilder<T, C extends BaseBuildContext>(
|
|
17
|
+
storage: ValueStorage<T>,
|
|
18
|
+
result: Record<string, unknown>,
|
|
19
|
+
context: C | undefined,
|
|
20
|
+
): void {
|
|
21
|
+
const hasIdSet = storage.has("id" as keyof T);
|
|
22
|
+
const hasType = storage.has("type" as keyof T) || "type" in result;
|
|
23
|
+
const hasIdField = "id" in result;
|
|
24
|
+
|
|
25
|
+
// Case 1: Asset with type field (standard case)
|
|
26
|
+
if (!hasIdSet && hasType) {
|
|
27
|
+
// Use values from result since they're already extracted
|
|
28
|
+
const typeValue = result.type;
|
|
29
|
+
const assetValue = result.value;
|
|
30
|
+
const assetBinding = result.binding;
|
|
31
|
+
|
|
32
|
+
// Validate types with type guards
|
|
33
|
+
if (!isStringOrUndefined(typeValue)) {
|
|
34
|
+
return; // Skip ID generation if type is not a string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!isStringOrUndefined(assetValue)) {
|
|
38
|
+
return; // Skip if value is not string/undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isStringOrUndefined(assetBinding)) {
|
|
42
|
+
return; // Skip if binding is not string/undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const assetMetadata: AssetMetadata = {
|
|
46
|
+
type: typeValue as string,
|
|
47
|
+
...(assetValue ? { value: assetValue as string } : {}),
|
|
48
|
+
...(assetBinding ? { binding: assetBinding as string } : {}),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const generatedId = generateAssetId({
|
|
52
|
+
context,
|
|
53
|
+
parameterName: typeValue as string,
|
|
54
|
+
assetMetadata,
|
|
55
|
+
explicitId: undefined,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (generatedId) {
|
|
59
|
+
result.id = generatedId;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Case 2: Object with id field but no type (e.g., ChoiceItem)
|
|
63
|
+
// Generate ID based on context branch if available
|
|
64
|
+
else if (!hasIdSet && hasIdField && !hasType && context && context.branch) {
|
|
65
|
+
// Use genId directly with the context to generate ID based on branch
|
|
66
|
+
const generatedId = generateAssetId({
|
|
67
|
+
context,
|
|
68
|
+
parameterName: "item",
|
|
69
|
+
assetMetadata: undefined,
|
|
70
|
+
explicitId: undefined,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (generatedId) {
|
|
74
|
+
result.id = generatedId;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { BaseBuildContext } from "../../types";
|
|
2
|
+
import type { ValueStorage } from "../../storage/value-storage";
|
|
3
|
+
import { isAssetWrapperValue } from "../../guards";
|
|
4
|
+
import { resolveAndWrapAsset } from "../value-resolver";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Step 4: Resolves AssetWrapper values in static storage
|
|
8
|
+
*
|
|
9
|
+
* Processes values that were marked as AssetWrapper during value setting.
|
|
10
|
+
* These values contain assets or builders that need special context handling.
|
|
11
|
+
*
|
|
12
|
+
* @param storage - The value storage
|
|
13
|
+
* @param result - The result object being built
|
|
14
|
+
* @param nestedParentContext - Context for nested assets
|
|
15
|
+
*/
|
|
16
|
+
export function resolveAssetWrappers<T, C extends BaseBuildContext>(
|
|
17
|
+
storage: ValueStorage<T>,
|
|
18
|
+
result: Record<string, unknown>,
|
|
19
|
+
nestedParentContext: C | undefined,
|
|
20
|
+
): void {
|
|
21
|
+
if (!nestedParentContext) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const values = storage.getValues();
|
|
26
|
+
|
|
27
|
+
for (const key of Object.keys(values)) {
|
|
28
|
+
const value = values[key as keyof T];
|
|
29
|
+
|
|
30
|
+
if (isAssetWrapperValue(value)) {
|
|
31
|
+
const unwrapped = value.asset;
|
|
32
|
+
|
|
33
|
+
if (Array.isArray(unwrapped)) {
|
|
34
|
+
result[key] = resolveAssetWrapperArray(
|
|
35
|
+
unwrapped,
|
|
36
|
+
nestedParentContext,
|
|
37
|
+
key,
|
|
38
|
+
);
|
|
39
|
+
} else {
|
|
40
|
+
result[key] = resolveAndWrapAsset(unwrapped, {
|
|
41
|
+
context: nestedParentContext,
|
|
42
|
+
slotName: key,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Helper: Resolves an array within an AssetWrapper
|
|
51
|
+
*/
|
|
52
|
+
function resolveAssetWrapperArray<C extends BaseBuildContext>(
|
|
53
|
+
array: readonly unknown[],
|
|
54
|
+
context: C,
|
|
55
|
+
key: string,
|
|
56
|
+
): unknown[] {
|
|
57
|
+
// Filter out null/undefined values before processing
|
|
58
|
+
return array
|
|
59
|
+
.filter((item) => item !== null && item !== undefined)
|
|
60
|
+
.map((item, index) => {
|
|
61
|
+
const slotName = `${key}-${index}`;
|
|
62
|
+
return resolveAndWrapAsset(item, { context, slotName });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { BaseBuildContext } from "../../types";
|
|
2
|
+
import type { ValueStorage } from "../../storage/value-storage";
|
|
3
|
+
import { isAssetWrapperValue } from "../../guards";
|
|
4
|
+
import { resolveAndWrapAsset, resolveValue } from "../value-resolver";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Step 6: Resolves regular builders (non-array)
|
|
8
|
+
*
|
|
9
|
+
* Processes FluentBuilder instances and objects containing builders.
|
|
10
|
+
* Creates nested contexts for proper ID generation.
|
|
11
|
+
*
|
|
12
|
+
* @param storage - The value storage
|
|
13
|
+
* @param result - The result object being built
|
|
14
|
+
* @param nestedParentContext - Context for nested builders
|
|
15
|
+
*/
|
|
16
|
+
export function resolveBuilders<T, C extends BaseBuildContext>(
|
|
17
|
+
storage: ValueStorage<T>,
|
|
18
|
+
result: Record<string, unknown>,
|
|
19
|
+
nestedParentContext: C | undefined,
|
|
20
|
+
): void {
|
|
21
|
+
const builders = storage.getBuilders();
|
|
22
|
+
|
|
23
|
+
builders.forEach((value, key) => {
|
|
24
|
+
// Check if this is an AssetWrapper
|
|
25
|
+
if (isAssetWrapperValue(value)) {
|
|
26
|
+
resolveBuilderAsAssetWrapper(result, key, value, nestedParentContext);
|
|
27
|
+
} else {
|
|
28
|
+
resolveBuilderNormal(result, key, value, nestedParentContext);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Helper: Resolves builder that is wrapped in AssetWrapper
|
|
35
|
+
*/
|
|
36
|
+
function resolveBuilderAsAssetWrapper<C extends BaseBuildContext>(
|
|
37
|
+
result: Record<string, unknown>,
|
|
38
|
+
key: string,
|
|
39
|
+
wrapperValue: { asset: unknown },
|
|
40
|
+
nestedParentContext: C | undefined,
|
|
41
|
+
): void {
|
|
42
|
+
const unwrapped = wrapperValue.asset;
|
|
43
|
+
|
|
44
|
+
if (nestedParentContext) {
|
|
45
|
+
if (Array.isArray(unwrapped)) {
|
|
46
|
+
// Filter out null/undefined values before processing
|
|
47
|
+
result[key] = unwrapped
|
|
48
|
+
.filter((item) => item !== null && item !== undefined)
|
|
49
|
+
.map((item, index) => {
|
|
50
|
+
const slotName = `${key}-${index}`;
|
|
51
|
+
return resolveAndWrapAsset(item, {
|
|
52
|
+
context: nestedParentContext,
|
|
53
|
+
slotName,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
result[key] = resolveAndWrapAsset(unwrapped, {
|
|
58
|
+
context: nestedParentContext,
|
|
59
|
+
slotName: key,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// No context, just resolve the wrapper
|
|
64
|
+
result[key] = resolveValue(wrapperValue, {});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Helper: Resolves builder normally (not wrapped)
|
|
70
|
+
* Uses resolveAndWrapAsset to properly wrap Asset builders in { asset: ... } format
|
|
71
|
+
*/
|
|
72
|
+
function resolveBuilderNormal<C extends BaseBuildContext>(
|
|
73
|
+
result: Record<string, unknown>,
|
|
74
|
+
key: string,
|
|
75
|
+
value: unknown,
|
|
76
|
+
nestedParentContext: C | undefined,
|
|
77
|
+
): void {
|
|
78
|
+
// Use resolveAndWrapAsset which correctly wraps Asset builders
|
|
79
|
+
// This ensures slot properties like 'label' get wrapped in { asset: ... }
|
|
80
|
+
result[key] = resolveAndWrapAsset(value, {
|
|
81
|
+
context: nestedParentContext,
|
|
82
|
+
slotName: key,
|
|
83
|
+
});
|
|
84
|
+
}
|