@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.
Files changed (134) hide show
  1. package/dist/cjs/index.cjs +2396 -0
  2. package/dist/cjs/index.cjs.map +1 -0
  3. package/dist/index.legacy-esm.js +2276 -0
  4. package/dist/index.mjs +2276 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +38 -0
  7. package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2423 -0
  8. package/src/core/base-builder/__tests__/fluent-partial.test.ts +179 -0
  9. package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
  10. package/src/core/base-builder/__tests__/registry.test.ts +534 -0
  11. package/src/core/base-builder/__tests__/resolution-mixed-arrays.test.ts +319 -0
  12. package/src/core/base-builder/__tests__/resolution-pipeline.test.ts +416 -0
  13. package/src/core/base-builder/__tests__/resolution-switches.test.ts +468 -0
  14. package/src/core/base-builder/__tests__/resolution-templates.test.ts +255 -0
  15. package/src/core/base-builder/__tests__/switch.test.ts +815 -0
  16. package/src/core/base-builder/__tests__/template.test.ts +596 -0
  17. package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
  18. package/src/core/base-builder/__tests__/value-storage.test.ts +459 -0
  19. package/src/core/base-builder/conditional/index.ts +64 -0
  20. package/src/core/base-builder/context.ts +152 -0
  21. package/src/core/base-builder/errors.ts +69 -0
  22. package/src/core/base-builder/fluent-builder-base.ts +308 -0
  23. package/src/core/base-builder/guards.ts +137 -0
  24. package/src/core/base-builder/id/generator.ts +290 -0
  25. package/src/core/base-builder/id/registry.ts +152 -0
  26. package/src/core/base-builder/index.ts +72 -0
  27. package/src/core/base-builder/resolution/path-resolver.ts +116 -0
  28. package/src/core/base-builder/resolution/pipeline.ts +103 -0
  29. package/src/core/base-builder/resolution/steps/__tests__/nested-asset-wrappers.test.ts +206 -0
  30. package/src/core/base-builder/resolution/steps/asset-id.ts +77 -0
  31. package/src/core/base-builder/resolution/steps/asset-wrappers.ts +64 -0
  32. package/src/core/base-builder/resolution/steps/builders.ts +84 -0
  33. package/src/core/base-builder/resolution/steps/mixed-arrays.ts +95 -0
  34. package/src/core/base-builder/resolution/steps/nested-asset-wrappers.ts +124 -0
  35. package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
  36. package/src/core/base-builder/resolution/steps/switches.ts +71 -0
  37. package/src/core/base-builder/resolution/steps/templates.ts +40 -0
  38. package/src/core/base-builder/resolution/value-resolver.ts +333 -0
  39. package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
  40. package/src/core/base-builder/storage/value-storage.ts +282 -0
  41. package/src/core/base-builder/types.ts +266 -0
  42. package/src/core/base-builder/utils.ts +10 -0
  43. package/src/core/flow/__tests__/index.test.ts +292 -0
  44. package/src/core/flow/index.ts +118 -0
  45. package/src/core/index.ts +8 -0
  46. package/src/core/mocks/generated/action.builder.ts +92 -0
  47. package/src/core/mocks/generated/choice-item.builder.ts +120 -0
  48. package/src/core/mocks/generated/choice.builder.ts +134 -0
  49. package/src/core/mocks/generated/collection.builder.ts +93 -0
  50. package/src/core/mocks/generated/field-collection.builder.ts +86 -0
  51. package/src/core/mocks/generated/index.ts +10 -0
  52. package/src/core/mocks/generated/info.builder.ts +64 -0
  53. package/src/core/mocks/generated/input.builder.ts +63 -0
  54. package/src/core/mocks/generated/overview-collection.builder.ts +65 -0
  55. package/src/core/mocks/generated/splash-collection.builder.ts +93 -0
  56. package/src/core/mocks/generated/text.builder.ts +47 -0
  57. package/src/core/mocks/index.ts +1 -0
  58. package/src/core/mocks/types/action.ts +92 -0
  59. package/src/core/mocks/types/choice.ts +129 -0
  60. package/src/core/mocks/types/collection.ts +140 -0
  61. package/src/core/mocks/types/info.ts +7 -0
  62. package/src/core/mocks/types/input.ts +7 -0
  63. package/src/core/mocks/types/text.ts +5 -0
  64. package/src/core/schema/__tests__/index.test.ts +127 -0
  65. package/src/core/schema/index.ts +195 -0
  66. package/src/core/schema/types.ts +7 -0
  67. package/src/core/switch/__tests__/index.test.ts +156 -0
  68. package/src/core/switch/index.ts +81 -0
  69. package/src/core/tagged-template/README.md +448 -0
  70. package/src/core/tagged-template/__tests__/extract-bindings-from-schema.test.ts +207 -0
  71. package/src/core/tagged-template/__tests__/index.test.ts +190 -0
  72. package/src/core/tagged-template/__tests__/schema-std-integration.test.ts +580 -0
  73. package/src/core/tagged-template/binding.ts +95 -0
  74. package/src/core/tagged-template/expression.ts +92 -0
  75. package/src/core/tagged-template/extract-bindings-from-schema.ts +120 -0
  76. package/src/core/tagged-template/index.ts +5 -0
  77. package/src/core/tagged-template/std.ts +472 -0
  78. package/src/core/tagged-template/types.ts +123 -0
  79. package/src/core/template/__tests__/index.test.ts +380 -0
  80. package/src/core/template/index.ts +196 -0
  81. package/src/core/utils/index.ts +160 -0
  82. package/src/fp/README.md +411 -0
  83. package/src/fp/__tests__/index.test.ts +1178 -0
  84. package/src/fp/index.ts +386 -0
  85. package/src/gen/common.ts +15 -0
  86. package/src/index.ts +5 -0
  87. package/src/types.ts +203 -0
  88. package/types/core/base-builder/conditional/index.d.ts +21 -0
  89. package/types/core/base-builder/context.d.ts +39 -0
  90. package/types/core/base-builder/errors.d.ts +45 -0
  91. package/types/core/base-builder/fluent-builder-base.d.ts +147 -0
  92. package/types/core/base-builder/guards.d.ts +58 -0
  93. package/types/core/base-builder/id/generator.d.ts +69 -0
  94. package/types/core/base-builder/id/registry.d.ts +93 -0
  95. package/types/core/base-builder/index.d.ts +9 -0
  96. package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
  97. package/types/core/base-builder/resolution/pipeline.d.ts +27 -0
  98. package/types/core/base-builder/resolution/steps/asset-id.d.ts +14 -0
  99. package/types/core/base-builder/resolution/steps/asset-wrappers.d.ts +14 -0
  100. package/types/core/base-builder/resolution/steps/builders.d.ts +14 -0
  101. package/types/core/base-builder/resolution/steps/mixed-arrays.d.ts +14 -0
  102. package/types/core/base-builder/resolution/steps/nested-asset-wrappers.d.ts +14 -0
  103. package/types/core/base-builder/resolution/steps/static-values.d.ts +14 -0
  104. package/types/core/base-builder/resolution/steps/switches.d.ts +15 -0
  105. package/types/core/base-builder/resolution/steps/templates.d.ts +14 -0
  106. package/types/core/base-builder/resolution/value-resolver.d.ts +62 -0
  107. package/types/core/base-builder/storage/auxiliary-storage.d.ts +50 -0
  108. package/types/core/base-builder/storage/value-storage.d.ts +82 -0
  109. package/types/core/base-builder/types.d.ts +183 -0
  110. package/types/core/base-builder/utils.d.ts +2 -0
  111. package/types/core/flow/index.d.ts +23 -0
  112. package/types/core/index.d.ts +8 -0
  113. package/types/core/mocks/index.d.ts +2 -0
  114. package/types/core/mocks/types/action.d.ts +58 -0
  115. package/types/core/mocks/types/choice.d.ts +95 -0
  116. package/types/core/mocks/types/collection.d.ts +102 -0
  117. package/types/core/mocks/types/info.d.ts +7 -0
  118. package/types/core/mocks/types/input.d.ts +7 -0
  119. package/types/core/mocks/types/text.d.ts +5 -0
  120. package/types/core/schema/index.d.ts +34 -0
  121. package/types/core/schema/types.d.ts +5 -0
  122. package/types/core/switch/index.d.ts +21 -0
  123. package/types/core/tagged-template/binding.d.ts +19 -0
  124. package/types/core/tagged-template/expression.d.ts +11 -0
  125. package/types/core/tagged-template/extract-bindings-from-schema.d.ts +7 -0
  126. package/types/core/tagged-template/index.d.ts +6 -0
  127. package/types/core/tagged-template/std.d.ts +174 -0
  128. package/types/core/tagged-template/types.d.ts +69 -0
  129. package/types/core/template/index.d.ts +97 -0
  130. package/types/core/utils/index.d.ts +47 -0
  131. package/types/fp/index.d.ts +149 -0
  132. package/types/gen/common.d.ts +6 -0
  133. package/types/index.d.ts +3 -0
  134. 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
+ }