@crustjs/validate 0.0.1 → 0.0.2

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.
@@ -1,95 +1,19 @@
1
1
  // @bun
2
2
  import {
3
- buildArgDefinitions,
4
- buildFlagDefinitions,
5
- buildRunHandler,
3
+ buildValidatedRunner,
6
4
  normalizeIssues
7
- } from "../shared/chunk-zfhm7pmv.js";
8
-
9
- // src/effect/command.ts
10
- import { defineCommand } from "@crustjs/core";
11
- import { either, runSync } from "effect/Effect";
12
- import * as Either from "effect/Either";
13
- import * as ParseResult from "effect/ParseResult";
14
- import { decodeUnknown } from "effect/Schema";
15
-
16
- // src/effect/definitions.ts
17
- import { CrustError as CrustError2 } from "@crustjs/core";
18
- import { encodedSchema } from "effect/Schema";
5
+ } from "../shared/chunk-p8pzhj6c.js";
19
6
 
20
7
  // src/effect/schema.ts
21
8
  import { CrustError } from "@crustjs/core";
22
9
  import { isSome } from "effect/Option";
23
- import { isSchema } from "effect/Schema";
10
+ import { encodedSchema, isSchema } from "effect/Schema";
24
11
  import { getDescriptionAnnotation } from "effect/SchemaAST";
25
- function arg(name, schema, options) {
26
- if (!name.trim()) {
27
- throw new CrustError("DEFINITION", "arg(): name is required and must be a non-empty string");
28
- }
29
- if (!isSchema(schema)) {
30
- throw new CrustError("DEFINITION", `arg("${name}"): schema must be an Effect schema`);
31
- }
32
- return {
33
- kind: "arg",
34
- name,
35
- schema,
36
- variadic: options?.variadic
37
- };
38
- }
39
- function flag(schema, options) {
40
- if (!isSchema(schema)) {
41
- throw new CrustError("DEFINITION", "flag(): schema must be an Effect schema");
42
- }
43
- return {
44
- kind: "flag",
45
- schema,
46
- alias: options?.alias
47
- };
48
- }
49
- function resolveDescription(schema) {
50
- return resolveDescriptionFromAst(schema.ast);
51
- }
52
- function resolveDescriptionFromAst(ast) {
53
- const seen = new Set;
54
- let current = ast;
55
- for (;; ) {
56
- if (seen.has(current)) {
57
- return;
58
- }
59
- seen.add(current);
60
- const annotated = getDescriptionAnnotation(current);
61
- if (isSome(annotated) && typeof annotated.value === "string") {
62
- return annotated.value;
63
- }
64
- if (current._tag === "Transformation") {
65
- current = current.from;
66
- continue;
67
- }
68
- if (current._tag === "Refinement") {
69
- current = current.from;
70
- continue;
71
- }
72
- if (current._tag === "Suspend") {
73
- current = current.f();
74
- continue;
75
- }
76
- if (current._tag === "Union") {
77
- for (const member of current.types) {
78
- if (member._tag === "UndefinedKeyword") {
79
- continue;
80
- }
81
- const desc = resolveDescriptionFromAst(member);
82
- if (desc !== undefined) {
83
- return desc;
84
- }
85
- }
86
- return;
87
- }
88
- return;
89
- }
90
- }
91
12
 
92
- // src/effect/definitions.ts
13
+ // src/effect/types.ts
14
+ var EFFECT_SCHEMA = Symbol.for("crustjs.validate.effect");
15
+
16
+ // src/effect/schema.ts
93
17
  function unwrapInputAst(ast) {
94
18
  let current = ast;
95
19
  const seen = new Set;
@@ -180,18 +104,18 @@ function resolveTupleArrayShape(ast, label) {
180
104
  return;
181
105
  }
182
106
  if (ast.elements.length > 0) {
183
- throw new CrustError2("DEFINITION", `${label}: tuple schemas with fixed elements are not supported for CLI parsing. Use Schema.Array(T) for repeatable arguments.`);
107
+ throw new CrustError("DEFINITION", `${label}: tuple schemas with fixed elements are not supported for CLI parsing. Use Schema.Array(T) for repeatable arguments.`);
184
108
  }
185
109
  if (ast.rest.length !== 1) {
186
- throw new CrustError2("DEFINITION", `${label}: tuple schemas are not supported for CLI parsing. Use scalar schemas or array schemas with a single element type.`);
110
+ throw new CrustError("DEFINITION", `${label}: tuple schemas are not supported for CLI parsing. Use scalar schemas or array schemas with a single element type.`);
187
111
  }
188
112
  const rest = ast.rest[0];
189
113
  if (!rest) {
190
- throw new CrustError2("DEFINITION", `${label}: unable to inspect array element schema`);
114
+ throw new CrustError("DEFINITION", `${label}: unable to inspect array element schema`);
191
115
  }
192
116
  const primitive = resolvePrimitiveInputType(rest.type);
193
117
  if (!primitive) {
194
- throw new CrustError2("DEFINITION", `${label}: array element type must be string, number, or boolean`);
118
+ throw new CrustError("DEFINITION", `${label}: array element type must be string, number, or boolean`);
195
119
  }
196
120
  return { type: primitive, multiple: true };
197
121
  }
@@ -205,7 +129,7 @@ function resolveInputShape(schema, label) {
205
129
  if (primitive) {
206
130
  return { type: primitive, multiple: false };
207
131
  }
208
- throw new CrustError2("DEFINITION", `${label}: unsupported schema type for CLI parsing. Use string, number, boolean, enum/literal, or array of these.`);
132
+ throw new CrustError("DEFINITION", `${label}: unsupported schema type for CLI parsing. Use string, number, boolean, enum/literal, or array of these.`);
209
133
  }
210
134
  function acceptsUndefined(ast) {
211
135
  const unwrapped = unwrapInputAst(ast);
@@ -220,21 +144,98 @@ function acceptsUndefined(ast) {
220
144
  function isOptionalInputSchema(schema) {
221
145
  return acceptsUndefined(encodedSchema(schema).ast);
222
146
  }
223
- var effectAdapter = {
224
- resolveInputShape,
225
- isOptionalInputSchema,
226
- resolveDescription,
227
- commandLabel: "defineEffectCommand",
228
- arrayHint: "Schema.Array(...)"
229
- };
230
- function argsToDefinitions(args) {
231
- return buildArgDefinitions(args, effectAdapter);
147
+ function resolveDescription(schema) {
148
+ return resolveDescriptionFromAst(schema.ast);
232
149
  }
233
- function flagsToDefinitions(flags) {
234
- return buildFlagDefinitions(flags, effectAdapter);
150
+ function resolveDescriptionFromAst(ast) {
151
+ const seen = new Set;
152
+ let current = ast;
153
+ for (;; ) {
154
+ if (seen.has(current)) {
155
+ return;
156
+ }
157
+ seen.add(current);
158
+ const annotated = getDescriptionAnnotation(current);
159
+ if (isSome(annotated) && typeof annotated.value === "string") {
160
+ return annotated.value;
161
+ }
162
+ if (current._tag === "Transformation") {
163
+ current = current.from;
164
+ continue;
165
+ }
166
+ if (current._tag === "Refinement") {
167
+ current = current.from;
168
+ continue;
169
+ }
170
+ if (current._tag === "Suspend") {
171
+ current = current.f();
172
+ continue;
173
+ }
174
+ if (current._tag === "Union") {
175
+ for (const member of current.types) {
176
+ if (member._tag === "UndefinedKeyword") {
177
+ continue;
178
+ }
179
+ const desc = resolveDescriptionFromAst(member);
180
+ if (desc !== undefined) {
181
+ return desc;
182
+ }
183
+ }
184
+ return;
185
+ }
186
+ return;
187
+ }
235
188
  }
236
-
237
- // src/effect/command.ts
189
+ function arg(name, schema, options) {
190
+ if (!name.trim()) {
191
+ throw new CrustError("DEFINITION", "arg(): name is required and must be a non-empty string");
192
+ }
193
+ if (!isSchema(schema)) {
194
+ throw new CrustError("DEFINITION", `arg("${name}"): schema must be an Effect schema`);
195
+ }
196
+ const shape = resolveInputShape(schema, `arg "${name}"`);
197
+ const variadic = options?.variadic;
198
+ if (variadic && shape.multiple) {
199
+ throw new CrustError("DEFINITION", `arg "${name}": variadic args must use a scalar schema; do not wrap the schema in Schema.Array(...)`);
200
+ }
201
+ if (!variadic && shape.multiple) {
202
+ throw new CrustError("DEFINITION", `arg "${name}": array schema requires { variadic: true }`);
203
+ }
204
+ const description = resolveDescription(schema);
205
+ const required = !isOptionalInputSchema(schema);
206
+ const def = {
207
+ name,
208
+ type: shape.type,
209
+ ...description !== undefined && { description },
210
+ variadic,
211
+ ...required && { required: true },
212
+ [EFFECT_SCHEMA]: schema
213
+ };
214
+ return def;
215
+ }
216
+ function flag(schema, options) {
217
+ if (!isSchema(schema)) {
218
+ throw new CrustError("DEFINITION", "flag(): schema must be an Effect schema");
219
+ }
220
+ const shape = resolveInputShape(schema, "flag");
221
+ const required = !isOptionalInputSchema(schema);
222
+ const description = resolveDescription(schema);
223
+ const alias = options?.alias === undefined ? undefined : typeof options.alias === "string" ? options.alias : [...options.alias];
224
+ const def = {
225
+ type: shape.type,
226
+ ...shape.multiple && { multiple: true },
227
+ alias,
228
+ ...description !== undefined && { description },
229
+ ...required && { required: true },
230
+ [EFFECT_SCHEMA]: schema
231
+ };
232
+ return def;
233
+ }
234
+ // src/effect/withEffect.ts
235
+ import { either, runSync } from "effect/Effect";
236
+ import * as Either from "effect/Either";
237
+ import * as ParseResult from "effect/ParseResult";
238
+ import { decodeUnknown } from "effect/Schema";
238
239
  function validateValue(schema, value, prefix) {
239
240
  const result = runSync(either(decodeUnknown(schema)(value)));
240
241
  if (Either.isRight(result)) {
@@ -247,28 +248,11 @@ function validateValue(schema, value, prefix) {
247
248
  }));
248
249
  return { ok: false, issues: normalizeIssues(prefixed) };
249
250
  }
250
- function defineEffectCommand(config) {
251
- const {
252
- args: effectArgs,
253
- flags: effectFlags,
254
- run: userRun,
255
- ...passthrough
256
- } = config;
257
- const argSpecs = effectArgs ?? [];
258
- const generatedArgs = argsToDefinitions(argSpecs);
259
- const generatedFlags = flagsToDefinitions(effectFlags);
260
- const command = defineCommand({
261
- ...passthrough,
262
- ...generatedArgs.length > 0 && { args: generatedArgs },
263
- ...Object.keys(generatedFlags).length > 0 && { flags: generatedFlags },
264
- ...userRun && {
265
- run: buildRunHandler(argSpecs, effectFlags, userRun, validateValue)
266
- }
267
- });
268
- return command;
251
+ function withEffect(handler) {
252
+ return buildValidatedRunner(handler, validateValue, EFFECT_SCHEMA, "withEffect");
269
253
  }
270
254
  export {
255
+ withEffect,
271
256
  flag,
272
- defineEffectCommand,
273
257
  arg
274
258
  };
@@ -0,0 +1,120 @@
1
+ // @bun
2
+ // src/validation.ts
3
+ import { CrustError } from "@crustjs/core";
4
+ function formatPath(path) {
5
+ let result = "";
6
+ for (const segment of path) {
7
+ if (typeof segment === "number") {
8
+ result += `[${String(segment)}]`;
9
+ } else {
10
+ const str = String(segment);
11
+ if (result.length > 0) {
12
+ result += `.${str}`;
13
+ } else {
14
+ result = str;
15
+ }
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+ function normalizeIssue(issue) {
21
+ return {
22
+ message: issue.message,
23
+ path: issue.path ? formatPath(issue.path) : ""
24
+ };
25
+ }
26
+ function normalizeIssues(issues) {
27
+ return issues.map(normalizeIssue);
28
+ }
29
+ function renderIssueLine(issue) {
30
+ if (issue.path) {
31
+ return ` - ${issue.path}: ${issue.message}`;
32
+ }
33
+ return ` - ${issue.message}`;
34
+ }
35
+ function renderBulletList(prefix, issues) {
36
+ if (issues.length === 0)
37
+ return prefix;
38
+ const lines = issues.map(renderIssueLine);
39
+ return `${prefix}
40
+ ${lines.join(`
41
+ `)}`;
42
+ }
43
+ function throwValidationError(issues, prefix = "Validation failed") {
44
+ const message = renderBulletList(prefix, issues);
45
+ throw new CrustError("VALIDATION", message, { issues }).withCause(issues);
46
+ }
47
+
48
+ // src/middleware.ts
49
+ function buildValidatedRunner(userRun, validateValue, schemaKey, label) {
50
+ return async (context) => {
51
+ const issues = [];
52
+ const argDefs = context.command.args ?? [];
53
+ const validatedArgs = {};
54
+ for (const def of argDefs) {
55
+ const schema = def[schemaKey];
56
+ if (!schema) {
57
+ validatedArgs[def.name] = context.args[def.name];
58
+ continue;
59
+ }
60
+ const rawValue = context.args[def.name];
61
+ if (def.variadic) {
62
+ const items = Array.isArray(rawValue) ? rawValue : rawValue === undefined ? [] : [rawValue];
63
+ const transformed = [];
64
+ for (let i = 0;i < items.length; i++) {
65
+ const value = items[i];
66
+ const result2 = await validateValue(schema, value, [
67
+ "args",
68
+ def.name,
69
+ i
70
+ ]);
71
+ if (!result2.ok) {
72
+ issues.push(...result2.issues);
73
+ continue;
74
+ }
75
+ transformed.push(result2.value);
76
+ }
77
+ validatedArgs[def.name] = transformed;
78
+ continue;
79
+ }
80
+ const result = await validateValue(schema, rawValue, ["args", def.name]);
81
+ if (!result.ok) {
82
+ issues.push(...result.issues);
83
+ continue;
84
+ }
85
+ validatedArgs[def.name] = result.value;
86
+ }
87
+ const flagDefs = context.command.flags ?? {};
88
+ const validatedFlags = {};
89
+ for (const [name, def] of Object.entries(flagDefs)) {
90
+ const schema = def[schemaKey];
91
+ if (!schema) {
92
+ validatedFlags[name] = context.flags[name];
93
+ continue;
94
+ }
95
+ const rawValue = context.flags[name];
96
+ const result = await validateValue(schema, rawValue, ["flags", name]);
97
+ if (!result.ok) {
98
+ issues.push(...result.issues);
99
+ continue;
100
+ }
101
+ validatedFlags[name] = result.value;
102
+ }
103
+ if (issues.length > 0) {
104
+ throwValidationError(issues, `${label}: validation failed`);
105
+ }
106
+ const validatedContext = {
107
+ args: validatedArgs,
108
+ flags: validatedFlags,
109
+ rawArgs: context.rawArgs,
110
+ command: context.command,
111
+ input: {
112
+ args: context.args,
113
+ flags: context.flags
114
+ }
115
+ };
116
+ return userRun(validatedContext);
117
+ };
118
+ }
119
+
120
+ export { normalizeIssues, buildValidatedRunner };