@sundaeswap/sprinkles 0.6.0 → 0.6.1
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/Sprinkle/__tests__/fill-in-struct.test.js +138 -0
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/cjs/Sprinkle/index.js +39 -3
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +138 -0
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/esm/Sprinkle/index.js +39 -3
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/Sprinkle/__tests__/fill-in-struct.test.ts +144 -0
- package/src/Sprinkle/index.ts +44 -3
package/package.json
CHANGED
|
@@ -150,6 +150,150 @@ describe("FillInStruct", () => {
|
|
|
150
150
|
);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
test("discriminated union propagates default when variant matches", async () => {
|
|
154
|
+
// When default has type "a" and user selects variant A, the default values
|
|
155
|
+
// for non-literal fields should be pre-populated (field starts as "set" status).
|
|
156
|
+
// Submitting without editing should return the default values.
|
|
157
|
+
const schema = Type.Union([
|
|
158
|
+
Type.Object({
|
|
159
|
+
type: Type.Literal("a"),
|
|
160
|
+
value: Type.String(),
|
|
161
|
+
}),
|
|
162
|
+
Type.Object({
|
|
163
|
+
type: Type.Literal("b"),
|
|
164
|
+
count: Type.BigInt(),
|
|
165
|
+
}),
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
const defaultValue = { type: "a" as const, value: "existing-value" };
|
|
169
|
+
|
|
170
|
+
// Select the first variant (type "a") - matches the default
|
|
171
|
+
mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
|
|
172
|
+
return opts.choices[0].value;
|
|
173
|
+
});
|
|
174
|
+
// Both fields are "set" (type auto-filled, value pre-populated from default).
|
|
175
|
+
// allRequiredFieldsFilled is true so Submit is enabled. Submit immediately.
|
|
176
|
+
mockSelectCancellable.mockResolvedValueOnce("submit");
|
|
177
|
+
|
|
178
|
+
const result = await sprinkle.FillInStruct(schema, defaultValue as any);
|
|
179
|
+
|
|
180
|
+
// The default value for the "value" field should be included in the result
|
|
181
|
+
expect(result).toEqual({ type: "a", value: "existing-value" });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("discriminated union with nested object propagates nested defaults", async () => {
|
|
185
|
+
// When a discriminated union default has a nested settings object, the nested
|
|
186
|
+
// fields should also be pre-populated from the default.
|
|
187
|
+
const schema = Type.Union([
|
|
188
|
+
Type.Object({
|
|
189
|
+
type: Type.Literal("config"),
|
|
190
|
+
settings: Type.Object({
|
|
191
|
+
timeout: Type.BigInt(),
|
|
192
|
+
retries: Type.BigInt(),
|
|
193
|
+
}),
|
|
194
|
+
}),
|
|
195
|
+
Type.Object({
|
|
196
|
+
type: Type.Literal("other"),
|
|
197
|
+
name: Type.String(),
|
|
198
|
+
}),
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
const defaultValue = {
|
|
202
|
+
type: "config" as const,
|
|
203
|
+
settings: { timeout: 30n, retries: 3n },
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Select the first variant (type "config") - matches the default
|
|
207
|
+
mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
|
|
208
|
+
return opts.choices[0].value;
|
|
209
|
+
});
|
|
210
|
+
// Outer object: "type" is auto-filled literal, "settings" is pre-populated from default.
|
|
211
|
+
// Both required fields are "set", so Submit is available.
|
|
212
|
+
mockSelectCancellable.mockResolvedValueOnce("submit");
|
|
213
|
+
|
|
214
|
+
const result = await sprinkle.FillInStruct(schema, defaultValue as any);
|
|
215
|
+
|
|
216
|
+
// Both the outer and nested default values should be returned
|
|
217
|
+
expect(result).toEqual({
|
|
218
|
+
type: "config",
|
|
219
|
+
settings: { timeout: 30n, retries: 3n },
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("discriminated union does not propagate default when variant does not match", async () => {
|
|
224
|
+
// When default has type "a" but user selects variant B, no default should be passed.
|
|
225
|
+
// The variant B field should start as "unset" and require user input.
|
|
226
|
+
const schema = Type.Union([
|
|
227
|
+
Type.Object({
|
|
228
|
+
type: Type.Literal("a"),
|
|
229
|
+
value: Type.String(),
|
|
230
|
+
}),
|
|
231
|
+
Type.Object({
|
|
232
|
+
type: Type.Literal("b"),
|
|
233
|
+
name: Type.String(),
|
|
234
|
+
}),
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
// Default has type "a" but user selects variant "b"
|
|
238
|
+
const defaultValue = { type: "a" as const, value: "should-not-appear" };
|
|
239
|
+
|
|
240
|
+
// Select the second variant (type "b") - does NOT match the default
|
|
241
|
+
mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
|
|
242
|
+
return opts.choices[1].value;
|
|
243
|
+
});
|
|
244
|
+
// Variant B: "type" is auto-filled, "name" starts as unset (no default propagated).
|
|
245
|
+
// Select "name" field and fill it in.
|
|
246
|
+
mockSelectCancellable.mockResolvedValueOnce("field:name");
|
|
247
|
+
mockInputCancellable.mockResolvedValueOnce("new-name");
|
|
248
|
+
mockSelectCancellable.mockResolvedValueOnce("submit");
|
|
249
|
+
|
|
250
|
+
const result = await sprinkle.FillInStruct(schema, defaultValue as any);
|
|
251
|
+
|
|
252
|
+
expect(result).toEqual({ type: "b", name: "new-name" });
|
|
253
|
+
// The input prompt for "name" should have no default (default from variant A was not passed)
|
|
254
|
+
expect(mockInputCancellable.mock.calls[0][0].default).toBeUndefined();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("non-discriminated union propagates default when value structurally matches selected variant", async () => {
|
|
258
|
+
// For a union of simple types, when the default matches the selected variant
|
|
259
|
+
// structurally (via Value.Check), it should be passed through.
|
|
260
|
+
const schema = Type.Union([Type.String(), Type.BigInt()]);
|
|
261
|
+
|
|
262
|
+
const defaultValue = "default-string";
|
|
263
|
+
|
|
264
|
+
// Select the first variant (String)
|
|
265
|
+
mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
|
|
266
|
+
return opts.choices[0].value;
|
|
267
|
+
});
|
|
268
|
+
mockInputCancellable.mockResolvedValueOnce("new-value");
|
|
269
|
+
|
|
270
|
+
await sprinkle.FillInStruct(schema, defaultValue as any);
|
|
271
|
+
|
|
272
|
+
// The input prompt should show the string default
|
|
273
|
+
expect(mockInputCancellable.mock.calls[0][0].default).toBe("default-string");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("non-discriminated union does not propagate default when value does not match selected variant", async () => {
|
|
277
|
+
// When a string default is provided but BigInt variant is selected,
|
|
278
|
+
// the default must not be passed (it would fail structural validation).
|
|
279
|
+
const schema = Type.Union([Type.String(), Type.BigInt()]);
|
|
280
|
+
|
|
281
|
+
// Default is a string but user selects BigInt variant
|
|
282
|
+
const defaultValue = "not-a-bigint";
|
|
283
|
+
|
|
284
|
+
// Select the second variant (BigInt)
|
|
285
|
+
mockSelectCancellable.mockImplementationOnce(async (opts: any) => {
|
|
286
|
+
return opts.choices[1].value;
|
|
287
|
+
});
|
|
288
|
+
mockInputCancellable.mockResolvedValueOnce("42");
|
|
289
|
+
|
|
290
|
+
const result = await sprinkle.FillInStruct(schema, defaultValue as any);
|
|
291
|
+
|
|
292
|
+
expect(result).toBe(42n);
|
|
293
|
+
// The string default must not be passed to the BigInt prompt
|
|
294
|
+
expect(mockInputCancellable.mock.calls[0][0].default).toBeUndefined();
|
|
295
|
+
});
|
|
296
|
+
|
|
153
297
|
// --- Object types (menu-based) ---
|
|
154
298
|
|
|
155
299
|
test("fills an object with multiple fields", async () => {
|
package/src/Sprinkle/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./prompts.js";
|
|
20
20
|
import colors from "yoctocolors-cjs";
|
|
21
21
|
import { type TSchema, Type, OptionalKind } from "@sinclair/typebox";
|
|
22
|
+
import { Value } from "@sinclair/typebox/value";
|
|
22
23
|
import * as fs from "fs";
|
|
23
24
|
import * as path from "path";
|
|
24
25
|
export * from "@sinclair/typebox";
|
|
@@ -590,8 +591,15 @@ export class Sprinkle<S extends TSchema> {
|
|
|
590
591
|
{
|
|
591
592
|
title: "Edit settings",
|
|
592
593
|
action: async () => {
|
|
593
|
-
|
|
594
|
-
|
|
594
|
+
try {
|
|
595
|
+
this.settings = await this.EditStruct(this.type, this.settings);
|
|
596
|
+
this.saveSettings();
|
|
597
|
+
} catch (e) {
|
|
598
|
+
if (e instanceof UserCancelledError) {
|
|
599
|
+
return; // User cancelled, return to menu
|
|
600
|
+
}
|
|
601
|
+
throw e;
|
|
602
|
+
}
|
|
595
603
|
},
|
|
596
604
|
},
|
|
597
605
|
{
|
|
@@ -1137,7 +1145,40 @@ export class Sprinkle<S extends TSchema> {
|
|
|
1137
1145
|
throw new UserCancelledError();
|
|
1138
1146
|
}
|
|
1139
1147
|
const selection = selectionResult as TSchema;
|
|
1140
|
-
|
|
1148
|
+
// Determine if the provided default value matches the selected variant.
|
|
1149
|
+
// For discriminated unions (objects with literal fields like `type`), check
|
|
1150
|
+
// if the literal field values in the selected variant match those in `def`.
|
|
1151
|
+
// For non-discriminated unions, fall back to structural matching with Value.Check.
|
|
1152
|
+
let matchedDef: unknown = undefined;
|
|
1153
|
+
if (def !== undefined) {
|
|
1154
|
+
if (isObject(selection)) {
|
|
1155
|
+
// Check if all literal fields in the selected variant match def
|
|
1156
|
+
const literalFields = Object.entries(selection.properties ?? {}).filter(
|
|
1157
|
+
([, fieldSchema]) => isLiteral(fieldSchema as TSchema),
|
|
1158
|
+
);
|
|
1159
|
+
if (literalFields.length > 0) {
|
|
1160
|
+
const defRecord = def as Record<string, unknown>;
|
|
1161
|
+
const allLiteralsMatch = literalFields.every(
|
|
1162
|
+
([fieldName, fieldSchema]) =>
|
|
1163
|
+
defRecord[fieldName] === (fieldSchema as unknown as { const: unknown }).const,
|
|
1164
|
+
);
|
|
1165
|
+
if (allLiteralsMatch) {
|
|
1166
|
+
matchedDef = def;
|
|
1167
|
+
}
|
|
1168
|
+
} else {
|
|
1169
|
+
// No literal discriminators - use structural check
|
|
1170
|
+
if (Value.Check(selection, def)) {
|
|
1171
|
+
matchedDef = def;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
} else {
|
|
1175
|
+
// Non-object variant - use structural check
|
|
1176
|
+
if (Value.Check(selection, def)) {
|
|
1177
|
+
matchedDef = def;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return this._fillInStruct(selection, path, defs, matchedDef as TExact<typeof selection>) as Promise<TExact<U>>;
|
|
1141
1182
|
}
|
|
1142
1183
|
|
|
1143
1184
|
if (isString(type)) {
|