@gjermundgaraba/effect-actions 0.8.0 → 0.10.0

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 (68) hide show
  1. package/README.md +57 -50
  2. package/dist/Action.d.mts +285 -79
  3. package/dist/Action.mjs +124 -23
  4. package/dist/ActionCli.d.mts +102 -31
  5. package/dist/ActionCli.mjs +114 -16
  6. package/dist/ActionHttp.d.mts +170 -51
  7. package/dist/ActionHttp.mjs +130 -79
  8. package/dist/ActionMcp.d.mts +71 -21
  9. package/dist/ActionMcp.mjs +108 -81
  10. package/dist/ActionToolkit.d.mts +56 -27
  11. package/dist/ActionToolkit.mjs +16 -5
  12. package/dist/Authentication.d.mts +129 -33
  13. package/dist/Authentication.mjs +231 -38
  14. package/dist/Testing.d.mts +82 -57
  15. package/dist/Testing.mjs +89 -79
  16. package/dist/internal/actions.d.mts +5 -25
  17. package/dist/internal/actions.mjs +75 -27
  18. package/dist/internal/authentication.d.mts +102 -0
  19. package/dist/internal/authentication.mjs +11 -0
  20. package/dist/internal/call.d.mts +12 -0
  21. package/dist/internal/call.mjs +12 -0
  22. package/dist/internal/cli.d.mts +36 -22
  23. package/dist/internal/cli.mjs +311 -33
  24. package/dist/internal/client.d.mts +43 -0
  25. package/dist/internal/client.mjs +25 -0
  26. package/dist/internal/console.mjs +77 -0
  27. package/dist/internal/declared.d.mts +13 -0
  28. package/dist/internal/declared.mjs +34 -0
  29. package/dist/internal/errors.d.mts +77 -0
  30. package/dist/internal/errors.mjs +79 -0
  31. package/dist/internal/implementation.d.mts +171 -41
  32. package/dist/internal/implementation.mjs +147 -29
  33. package/dist/internal/mcp.d.mts +11 -0
  34. package/dist/internal/mcp.mjs +52 -0
  35. package/dist/internal/memory.d.mts +6 -0
  36. package/dist/internal/memory.mjs +38 -0
  37. package/dist/internal/refusal.mjs +67 -0
  38. package/dist/internal/schema-errors.mjs +20 -0
  39. package/dist/internal/tools.mjs +27 -57
  40. package/docs/Action.md +401 -73
  41. package/docs/ActionCli.md +280 -51
  42. package/docs/ActionHttp.md +266 -82
  43. package/docs/ActionMcp.md +149 -104
  44. package/docs/ActionToolkit.md +97 -35
  45. package/docs/Authentication.md +369 -62
  46. package/docs/CONTEXT.md +23 -36
  47. package/docs/README.md +29 -73
  48. package/docs/Testing.md +154 -77
  49. package/docs/guarantees.md +81 -43
  50. package/docs/setup.md +90 -27
  51. package/package.json +9 -17
  52. package/dist/ActionCatalog.d.mts +0 -30
  53. package/dist/ActionCatalog.mjs +0 -39
  54. package/dist/ActionCliClient.d.mts +0 -48
  55. package/dist/ActionCliClient.mjs +0 -43
  56. package/dist/ActionGroup.d.mts +0 -44
  57. package/dist/ActionGroup.mjs +0 -35
  58. package/dist/ActionHttpClient.d.mts +0 -33
  59. package/dist/ActionHttpClient.mjs +0 -16
  60. package/dist/TestingClient.d.mts +0 -17
  61. package/dist/TestingClient.mjs +0 -22
  62. package/dist/internal/fetchClient.d.mts +0 -6
  63. package/dist/internal/fetchClient.mjs +0 -8
  64. package/dist/internal/tools.d.mts +0 -22
  65. package/docs/ActionCatalog.md +0 -54
  66. package/docs/ActionCliClient.md +0 -63
  67. package/docs/ActionGroup.md +0 -128
  68. package/docs/ActionHttpClient.md +0 -80
@@ -1,50 +1,328 @@
1
- import { Console, Effect, Option, Schema } from "effect";
2
- import { Command, Flag } from "effect/cli";
1
+ import { InvalidInput } from "./errors.mjs";
2
+ import { assertDistinct, literalValues, members, projectedErrors, unsuspended } from "./actions.mjs";
3
+ import { logToStderr } from "./console.mjs";
4
+ import { Console, Effect, Option, Predicate, Record, Runtime, Schema, SchemaAST } from "effect";
5
+ import { CliError, Command, Flag, Param } from "effect/cli";
3
6
  //#region src/internal/cli.ts
4
- const isParametersOptions = (options) => options?.parameters !== void 0;
7
+ /**
8
+ * `getUser` as a command or flag name: `get-user`; `getHTTPUser`: `get-http-user`; `_id`:
9
+ * `id`, since a flag already starts with its dashes. A name of dashes and underscores alone,
10
+ * which would be left empty, is kept as it is.
11
+ */
12
+ const kebab = (name) => {
13
+ const kebabbed = name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
14
+ return kebabbed === "" ? name : kebabbed;
15
+ };
5
16
  /**
6
17
  * A command with a renderer takes `--json` as a flag of its own, so nothing is
7
18
  * claimed tree-wide and a host's `--json`, global or not, is never contested.
8
19
  * Without a renderer the output is JSON already and there is no flag.
9
20
  */
10
21
  const jsonFlag = Flag.Boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Print machine-readable JSON"));
11
- const output = (action, execute, input, render) => Effect.gen(function* () {
12
- const value = yield* execute(input);
13
- const encoded = yield* Schema.encodeEffect(Schema.toCodecJson(action.success))(value);
14
- yield* Console.log(render === void 0 ? JSON.stringify(encoded, null, 2) : render(value));
22
+ /** The kinds of JSON value there are. */
23
+ const jsonKinds = [
24
+ "null",
25
+ "boolean",
26
+ "number",
27
+ "string",
28
+ "array",
29
+ "object"
30
+ ];
31
+ /** The kind of a JSON value, or of a literal's, which JSON has no bigint for. */
32
+ const kindOf = (value) => value === null ? "null" : Predicate.isBoolean(value) ? "boolean" : Predicate.isNumber(value) ? "number" : Predicate.isString(value) ? "string" : Array.isArray(value) ? "array" : "object";
33
+ /**
34
+ * The kinds of JSON value an encoded type accepts at its top, each member's of a union and a
35
+ * suspended type's: its checks and what it nests aside, which the action's schema decodes.
36
+ * A type of no fixed kind accepts all.
37
+ */
38
+ const kindsOf = (ast) => members(ast).flatMap((member) => {
39
+ if (SchemaAST.isLiteral(member)) return [kindOf(member.literal)];
40
+ if (SchemaAST.isEnum(member)) return member.enums.map(([, value]) => kindOf(value));
41
+ if (SchemaAST.isNull(member)) return ["null"];
42
+ if (SchemaAST.isBoolean(member)) return ["boolean"];
43
+ if (SchemaAST.isNumber(member)) return ["number"];
44
+ if (SchemaAST.isString(member) || SchemaAST.isTemplateLiteral(member)) return ["string"];
45
+ if (SchemaAST.isArrays(member)) return ["array"];
46
+ if (SchemaAST.isObjects(member)) return ["object"];
47
+ return jsonKinds;
15
48
  });
16
49
  /**
17
- * One native command. The action's own parameters are one nested config, so the
18
- * flag added for a renderer never meets them in one record.
50
+ * A flag's text as the JSON it holds when the field's `encoded` type accepts that kind of
51
+ * value, or else as itself, for the action's schema to decode: for a number `21` parses,
52
+ * while for `"auto" | string` the text `true` stays text. Only the kind is checked, so JSON
53
+ * breaking a rule of the schema, such as a maximum length, stays JSON, and the action's
54
+ * schema reports the rule and the path to what breaks it.
19
55
  */
20
- const make = (action, execute, options, config, decode) => {
21
- const name = options?.name ?? action.name;
22
- const render = options?.render;
23
- return (render === void 0 ? Command.make(name, { parameters: config }, ({ parameters }) => Effect.flatMap(decode(parameters), (input) => output(action, execute, input, void 0))) : Command.make(name, {
24
- parameters: config,
25
- json: jsonFlag
26
- }, ({ parameters, json }) => Effect.flatMap(decode(parameters), (input) => output(action, execute, input, json ? void 0 : render)))).pipe(Command.withDescription(action.description));
56
+ const jsonOrText = (encoded) => {
57
+ const accepted = new Set(kindsOf(encoded));
58
+ return Schema.Union([Schema.fromJsonString(Schema.Json).check(Schema.makeFilter((json) => accepted.has(kindOf(json)))), Schema.String]);
59
+ };
60
+ /** The strings a union of string literals or a string enum accepts, if it is one. */
61
+ const choices = (ast) => {
62
+ const accepted = members(ast).flatMap(literalValues);
63
+ return accepted.every(Predicate.isString) ? accepted : void 0;
27
64
  };
28
65
  /**
29
- * The whole encoded input, inline or from a file. The native parser decodes both,
30
- * so an invalid value is rendered with the command's help wherever it came from.
66
+ * The native flag or argument parsing one field's encoded JSON value: a string or boolean
67
+ * for those, a choice for string literals and string enums, and JSON or text for anything
68
+ * else, numbers included. A template literal is a string; the action's schema checks its
69
+ * pattern. A suspended type is read as the type it stands for. JSON or text, a flag's or an
70
+ * argument's, is shown as `value`, not as the string it is parsed from.
31
71
  */
32
- const inputFlags = (codec) => ({
33
- input: Flag.String("input").pipe(Flag.withSchema(Schema.fromJsonString(codec)), Flag.optional, Flag.withDescription("Whole canonical action input as JSON")),
34
- inputFile: Flag.FileSchema("input-file", codec, { format: "json" }).pipe(Flag.optional, Flag.withDescription("File containing the whole canonical action input as JSON"))
35
- });
36
- const defaultCommand = (action, execute, options) => {
72
+ const valueParam = (kind, name, field) => {
73
+ const encoded = unsuspended(field);
74
+ const literals = choices(encoded);
75
+ if (literals !== void 0) return Param.Literals(kind, name, literals);
76
+ if (SchemaAST.isString(encoded) || SchemaAST.isTemplateLiteral(encoded)) return Param.String(kind, name);
77
+ if (SchemaAST.isBoolean(encoded)) return Param.Boolean(kind, name);
78
+ return Param.String(kind, name).pipe(Param.withSchema(jsonOrText(encoded)), Param.withMetavar("value"));
79
+ };
80
+ /**
81
+ * An optional field's value without the absence `Schema.optional` adds to it: `undefined`
82
+ * declared, `null` encoded. Omitting the flag already leaves the field out, so
83
+ * `optional(Schema.String)` takes a plain string.
84
+ */
85
+ const present = (ast) => {
86
+ const members = SchemaAST.isUnion(ast) ? ast.types.filter((member) => !SchemaAST.isNull(member) && !SchemaAST.isUndefined(member)) : [];
87
+ return members.length === 1 && members[0] !== void 0 ? members[0] : ast;
88
+ };
89
+ /**
90
+ * Whether `Schema.optional` added the field's JSON `null`: encoded without the JSON codec,
91
+ * its value admits `undefined` but not `null`. A `null` of the field's own schema, as
92
+ * `NullOr` or `OptionFromNullOr` encode, stays.
93
+ */
94
+ const addsNull = (plain) => {
95
+ const types = members(plain);
96
+ return types.some(SchemaAST.isUndefined) && !types.some(SchemaAST.isNull);
97
+ };
98
+ /**
99
+ * The element of an encoded array a flag repeats, and how many times it must: an array of
100
+ * strings or numbers, choices included, whose fixed first elements, if any, are its rest's,
101
+ * as `Schema.NonEmptyArray` writes one. `undefined` for any other array, which takes JSON.
102
+ */
103
+ const repeated = (ast) => {
104
+ const encoded = unsuspended(ast);
105
+ if (!SchemaAST.isArrays(encoded)) return void 0;
106
+ const [element, ...after] = encoded.rest;
107
+ if (element === void 0 || after.length > 0 || !encoded.elements.every((first) => first === element) || !kindsOf(element).every((kind) => kind === "string" || kind === "number")) return;
108
+ return {
109
+ element,
110
+ min: encoded.elements.length
111
+ };
112
+ };
113
+ /** The occurrence of a repeated flag that adds no element, so `--tag '[]'` alone is `[]`. */
114
+ const empty = "[]";
115
+ /**
116
+ * A field's flag or argument, `None` when omitted, from its JSON encoding and its plain one.
117
+ * A required field's is required, so the parser reports it missing; a required boolean flag
118
+ * is a switch instead: omitted, it is `false`, as a switch reads. A boolean argument takes
119
+ * `true` or `false`. A flag or argument of an array of strings or numbers is repeated, one
120
+ * element per occurrence but `[]`, which adds none, and a choice among the element's: none
121
+ * is `[]`, or leaves an optional field out, which `[]` gives instead.
122
+ */
123
+ const fieldParam = (kind, name, encoded, plain) => {
124
+ const optional = SchemaAST.isOptional(encoded);
125
+ const value = optional && plain !== void 0 && addsNull(plain) ? present(encoded) : encoded;
126
+ const repeats = repeated(value);
127
+ if (repeats !== void 0) {
128
+ const literals = choices(unsuspended(repeats.element));
129
+ const elements = literals === void 0 ? valueParam(kind, name, repeats.element) : Param.Literals(kind, name, [...literals, empty]);
130
+ const listed = (values) => Option.some(values.filter((value) => value !== empty));
131
+ return {
132
+ param: optional ? Param.variadic(elements).pipe(Param.map((values) => values.length === 0 ? Option.none() : listed(values))) : Param.variadic(elements, { min: repeats.min }).pipe(Param.map(listed)),
133
+ repeats: true
134
+ };
135
+ }
136
+ if (optional) return {
137
+ param: Param.optional(valueParam(kind, name, value)),
138
+ repeats: false
139
+ };
140
+ return {
141
+ param: kind === Param.flagKind && SchemaAST.isBoolean(unsuspended(encoded)) ? Flag.Boolean(name).pipe(Flag.withDefault(false), Flag.map(Option.some)) : valueParam(kind, name, encoded).pipe(Param.map(Option.some)),
142
+ repeats: false
143
+ };
144
+ };
145
+ /**
146
+ * The fields of a struct, or of a class, which declares them as its one type parameter, by name.
147
+ */
148
+ const fieldsOf = (ast) => {
149
+ const fields = SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast;
150
+ return new Map(fields !== void 0 && SchemaAST.isObjects(fields) ? fields.propertySignatures.map((property) => [property.name, property.type]) : []);
151
+ };
152
+ /** A struct, or a class's, transformed as a whole, as `Schema.encodeKeys` transforms one. */
153
+ const transformedWhole = (ast) => (SchemaAST.isDeclaration(ast) ? ast.typeParameters[0] : ast)?.encoding !== void 0;
154
+ /** A field's description: its own, or the first a suspension it stands for has. */
155
+ const descriptionOf = (ast) => SchemaAST.resolveDescription(ast) ?? (SchemaAST.isSuspend(ast) ? descriptionOf(ast.thunk()) : void 0);
156
+ /**
157
+ * One flag or argument per top-level field of a struct input, named after the field in
158
+ * kebab case, parsed as its encoded value and described by its declared schema: an
159
+ * argument for a field listed in `positional`, a flag otherwise. A field required once
160
+ * encoded is required; the action's schema still decodes what they parse.
161
+ */
162
+ const fieldParams = (encoded, input, positional, aliases) => {
163
+ const plain = fieldsOf(SchemaAST.toEncoded(input));
164
+ const described = fieldsOf(transformedWhole(input) ? encoded : SchemaAST.toType(input));
165
+ return encoded.propertySignatures.map((property) => {
166
+ const field = String(property.name);
167
+ const declaredField = described.get(property.name);
168
+ const kind = positional.includes(field) ? Param.argumentKind : Param.flagKind;
169
+ const { param, repeats } = fieldParam(kind, kebab(field), property.type, plain.get(property.name));
170
+ const alias = kind === Param.flagKind ? aliases[field] : void 0;
171
+ const aliased = alias === void 0 ? param : Param.withAlias(param, alias);
172
+ const documented = declaredField ?? property.type;
173
+ const description = descriptionOf(documented) ?? descriptionOf(present(documented));
174
+ return {
175
+ field,
176
+ param: description === void 0 ? aliased : Param.withDescription(aliased, description),
177
+ optional: SchemaAST.isOptional(property.type),
178
+ repeats
179
+ };
180
+ });
181
+ };
182
+ /**
183
+ * The positional fields in order, checked against the fields there are: each one a field,
184
+ * listed once, and every required one before any optional one, as a parser reads them.
185
+ */
186
+ const positionalOrder = (params, positional) => {
187
+ assertDistinct("positional argument", positional, (field) => field);
188
+ const ordered = positional.map((field) => {
189
+ const found = params.find((param) => param.field === field);
190
+ if (found === void 0) throw new Error(`Not an input field: ${field}`);
191
+ return found;
192
+ });
193
+ const firstOptional = ordered.findIndex((param) => param.optional);
194
+ const misplaced = ordered.slice(firstOptional === -1 ? ordered.length : firstOptional).find((param) => !param.optional);
195
+ if (misplaced !== void 0) throw new Error(`Required positional argument after an optional one: ${misplaced.field}`);
196
+ const greedy = ordered.slice(0, -1).find((param) => param.repeats);
197
+ if (greedy !== void 0) throw new Error(`Repeated positional argument before another one: ${greedy.field}`);
198
+ return ordered;
199
+ };
200
+ /**
201
+ * `--input`, the whole encoded input as JSON, or text, for an input that is not a struct of
202
+ * fields. It is never required: left off, it is `{}`, which the action's schema decodes
203
+ * when the command runs, never when it is built.
204
+ */
205
+ const inputFlag = (encoded) => Flag.String("input").pipe(Flag.withSchema(jsonOrText(encoded)), Flag.withDescription("Whole action input as JSON; omitted, {}"), Flag.optional);
206
+ /**
207
+ * What a success prints on stdout: its JSON, or `render`'s text. It is validated and encoded
208
+ * first, so human output cannot conceal an invalid success, which is a defect, as on a server.
209
+ * An action that returns nothing prints nothing, rather than its encoding, `null`.
210
+ */
211
+ const output = (action, value, render) => Effect.map(Effect.orDie(Schema.encodeEffect(Schema.toCodecJson(action.success))(value)), (encoded) => render === void 0 && SchemaAST.isVoid(action.success.ast) ? Option.none() : Option.some(render === void 0 ? JSON.stringify(encoded, null, 2) : render(value)));
212
+ /**
213
+ * What a command fails with when its action fails: Effect CLI's own error for a handler's
214
+ * failure, which `Command.run` prints on stderr and marks reported, so `runMain` does not
215
+ * print it again. Its `cause` is the action's failure. The process exits with the cause's
216
+ * `Runtime.errorExitCode`, 1 by default.
217
+ */
218
+ var UserError = class extends CliError.UserError {
219
+ /**
220
+ * The action's failure, as Effect names an error's nested one, so the native
221
+ * `Effect.catchReason("UserError", "UserNotFound", f)` and `catchReasons` match it by tag.
222
+ */
223
+ get reason() {
224
+ return this.cause;
225
+ }
226
+ get [Runtime.errorExitCode]() {
227
+ return Runtime.getErrorExitCode(this.cause);
228
+ }
229
+ };
230
+ /** `error`'s own `_tag` or `message`, when it is a string. */
231
+ const text = (error, key) => {
232
+ if (!Predicate.hasProperty(error, key)) return void 0;
233
+ const value = error[key];
234
+ return Predicate.isString(value) ? value : void 0;
235
+ };
236
+ /**
237
+ * A failure no projected schema encodes, without a stack: its tag or an error's name, and
238
+ * its message, or the failure itself when it is a string, then each cause's, so a transport
239
+ * error keeps the refused connection beneath it, up to one already described, as a cycle
240
+ * repeats it. Its other fields are left out, a plain object's `name` too: no schema says
241
+ * they are safe to show, and a builder's failure may hold a connection string.
242
+ */
243
+ const described = (error, seen = /* @__PURE__ */ new Set()) => {
244
+ const name = text(error, "_tag") ?? (error instanceof Error ? error.name : "Error");
245
+ const message = Predicate.isString(error) ? error : text(error, "message") ?? "";
246
+ const named = message === "" ? name : `${name}: ${message}`;
247
+ seen.add(error);
248
+ return Predicate.hasProperty(error, "cause") && error.cause !== void 0 && !seen.has(error.cause) ? `${named}: ${described(error.cause, seen)}` : named;
249
+ };
250
+ /**
251
+ * The `UserError` of each failure of `action`, its message the JSON HTTP sends for it: encoded
252
+ * by the first schema that takes it among the built-in errors, the action's and the
253
+ * surface's own `errors`, as an Effect, as HTTP encodes it. A failure none takes, such as a
254
+ * builder's or the transport's, is described instead.
255
+ */
256
+ const failureOf = (action, errors) => {
257
+ const encode = Schema.encodeUnknownEffect(Schema.fromJsonString(Schema.toCodecJson(Schema.Union(projectedErrors(action, errors)))));
258
+ return (error) => encode(error).pipe(Effect.orElseSucceed(() => described(error)), Effect.flatMap((userMessage) => Effect.fail(new UserError({
259
+ cause: error,
260
+ userMessage
261
+ }))));
262
+ };
263
+ /**
264
+ * The action's input as native flags: one per field of a struct input, named after it,
265
+ * or `--input` taking the whole input as JSON otherwise. The `positional` fields of a
266
+ * struct are arguments instead, in that order.
267
+ */
268
+ const inputConfig = (action, positional, aliases) => {
37
269
  const codec = Schema.toCodecJson(action.input);
38
- return make(action, execute, options, inputFlags(codec), (parsed) => Option.match(Option.orElse(parsed.inputFile, () => parsed.input), {
39
- onNone: () => Schema.decodeUnknownEffect(codec)({}),
40
- onSome: Effect.succeed
41
- }));
270
+ const encoded = unsuspended(SchemaAST.toEncoded(codec.ast));
271
+ const decode = Schema.decodeUnknownEffect(codec, { onExcessProperty: "error" });
272
+ if (SchemaAST.isObjects(encoded) && encoded.indexSignatures.every((signature) => SchemaAST.isNever(signature.type))) {
273
+ const params = fieldParams(encoded, unsuspended(action.input.ast), positional, aliases);
274
+ const flagged = new Set(params.map(({ field }) => field).filter((field) => !positional.includes(field)));
275
+ const stray = Object.keys(aliases).find((field) => !flagged.has(field));
276
+ if (stray !== void 0) throw new Error(`Not a flag's input field: ${stray}`);
277
+ return {
278
+ flags: Object.fromEntries(params.flatMap(({ field, param }) => positional.includes(field) ? [] : [[field, param]])),
279
+ positional: positionalOrder(params, positional),
280
+ decode: (parsed) => decode(Record.getSomes(parsed))
281
+ };
282
+ }
283
+ if (positional.length > 0) throw new Error(`Positional arguments need named input fields: ${positional.join(", ")}`);
284
+ if (Object.keys(aliases).length > 0) throw new Error(`Aliases need named input fields: ${Object.keys(aliases).join(", ")}`);
285
+ return {
286
+ flags: { input: inputFlag(encoded) },
287
+ positional: [],
288
+ decode: (parsed) => decode(Option.getOrElse(parsed["input"] ?? Option.none(), () => ({})))
289
+ };
42
290
  };
43
- const parametersCommand = (action, execute, options) => make(action, execute, options, options.parameters, (parsed) => Schema.decodeUnknownEffect(Schema.toCodecJson(action.input))(options.input(parsed)));
44
- /** Build one native command around an action-bound operation. */
45
- const command = (action, execute, options) => {
46
- if (isParametersOptions(options)) return parametersCommand(action, execute, options);
47
- return defaultCommand(action, execute, options);
291
+ /**
292
+ * One native command around an action-bound operation, its flags and positional
293
+ * arguments derived from the action's input. A field's flag shadows a global flag of the
294
+ * same name; two flags of the command itself with one name are refused when it is built.
295
+ * Input that does not decode is `InvalidInput`, as over HTTP, and the operation writes its
296
+ * logs and console output to stderr, since stdout carries the result. Every failure is a
297
+ * `UserError`, encoded with the surface's `errors` too, such as a binding's.
298
+ */
299
+ const command = (action, execute, options, errors = []) => {
300
+ const name = options?.name ?? kebab(action.name);
301
+ const render = options?.render;
302
+ const { flags, positional, decode } = inputConfig(action, options?.positional ?? [], options?.aliases ?? {});
303
+ const failure = failureOf(action, errors);
304
+ assertDistinct("flag", [
305
+ ...Object.keys(flags).map((field) => [`--${kebab(field)}`, `field ${field}`]),
306
+ ...render === void 0 ? [] : [["--json", "render's --json"]],
307
+ ...Object.entries(options?.aliases ?? {}).flatMap(([field, alias]) => Predicate.isString(alias) ? [[`--${alias.replace(/^-+/, "")}`, `alias of field ${field}`]] : [])
308
+ ], ([flag]) => flag, ([, claimant]) => claimant);
309
+ const args = positional.map(({ param }) => param);
310
+ const parsedFields = (input, values) => ({
311
+ ...input,
312
+ ...Object.fromEntries(positional.map(({ field }, index) => [field, values[index] ?? Option.none()]))
313
+ });
314
+ const run = (parsed, rendered) => decode(parsed).pipe(Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap(execute), Effect.catch(failure), Effect.flatMap((value) => output(action, value, rendered)), logToStderr, Effect.flatMap(Option.match({
315
+ onNone: () => Effect.void,
316
+ onSome: Console.log
317
+ })));
318
+ return (render === void 0 ? Command.make(name, {
319
+ input: flags,
320
+ args
321
+ }, ({ input, args: values }) => run(parsedFields(input, values), void 0)) : Command.make(name, {
322
+ input: flags,
323
+ args,
324
+ json: jsonFlag
325
+ }, ({ input, args: values, json }) => run(parsedFields(input, values), json ? void 0 : render))).pipe(Command.withDescription(action.description));
48
326
  };
49
327
  //#endregion
50
- export { command };
328
+ export { UserError, command, kebab };
@@ -0,0 +1,43 @@
1
+ import { BuiltIns } from "./errors.mjs";
2
+ import "./implementation.mjs";
3
+ import { Call } from "./call.mjs";
4
+ import { Action_d_exports, Any, Errors } from "../Action.mjs";
5
+ import { Any as Any$1, VerifierError } from "./authentication.mjs";
6
+ import { Effect, Schema } from "effect";
7
+ import { HttpClient, HttpClientError } from "effect/http";
8
+ import { HttpApi, HttpApiClient } from "effect/http-api";
9
+ //#region src/internal/client.d.ts
10
+ /**
11
+ * The native `HttpApiClient.make` options except `transformResponse`: `baseUrl` and
12
+ * `transformClient`. `transformResponse` may change a call's success, failure or
13
+ * services, which the method types cannot follow.
14
+ */
15
+ export type Options = Omit<NonNullable<Parameters<typeof HttpApiClient.make>[1]>, "transformResponse">;
16
+ /** An error schema the binding declares on every endpoint. */
17
+ type BindingError = Errors[number];
18
+ /**
19
+ * What one call of `A` fails with: a declared error value (the action's own, one `E` of the
20
+ * binding's, or a built-in `InvalidInput`, `Unauthenticated` or `Forbidden`), a native
21
+ * `HttpClientError` for a failed request or an undeclared answer, or a `SchemaError` when
22
+ * the success does not decode. Input that does not encode is `InvalidInput`, and nothing is sent.
23
+ */
24
+ export type MethodError<A extends Any, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | HttpClientError.HttpClientError | Schema.SchemaError;
25
+ /** One action as an Effect of its decoded success, failing with `MethodError`. */
26
+ type Method<A extends Any, E extends BindingError> = Call<A, Effect.Effect<A["success"]["Type"], MethodError<A, E>>>;
27
+ /**
28
+ * Any HTTP binding. The API is a native constraint here because native groups are
29
+ * invariant in their endpoints; `actions` carries the types.
30
+ */
31
+ export interface AnyHttp {
32
+ readonly authentication?: Any$1 | undefined;
33
+ readonly actions: ReadonlyArray<Any>;
34
+ readonly error: ReadonlyArray<BindingError>;
35
+ readonly prefix: `/${string}`;
36
+ readonly api: HttpApi.Constraint;
37
+ }
38
+ /**
39
+ * Every action of the binding `H`, as `client.<action>(input)`: a protected one may also fail
40
+ * with what its descriptor's verifier declares.
41
+ */
42
+ export type Client<H extends AnyHttp> = { readonly [A in H["actions"][number] as A["name"]]: Method<A, H["error"][number] | VerifierError<A, H["authentication"]>>; };
43
+ //#endregion
@@ -0,0 +1,25 @@
1
+ import { InvalidInput } from "./errors.mjs";
2
+ import { inputOf } from "./call.mjs";
3
+ import { Effect, Schema } from "effect";
4
+ import { HttpApiClient } from "effect/http-api";
5
+ //#region src/internal/client.ts
6
+ /** Refuse an action a binding of `actions` does not hold, matched by identity. */
7
+ const assertInBinding = (actions, action) => {
8
+ if (!actions.includes(action)) throw new Error(`Action "${action.name}" is not in this HTTP binding`);
9
+ };
10
+ /**
11
+ * Build the native client of a binding once and look up each action's method, taking the
12
+ * action's input directly. Every binding API is a native `HttpApi` of one top-level
13
+ * group built from `http.actions`, so each action has a native method of its name.
14
+ */
15
+ const methods = (http, options = {}) => Effect.map(HttpApiClient.make(http.api, options), (client) => {
16
+ const native = client;
17
+ return (action) => {
18
+ const method = native[action.name];
19
+ if (method === void 0) throw new Error(`No client method for ${action.name}`);
20
+ const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(action.input), { errors: "all" });
21
+ return (...args) => inputOf(action, args).pipe(Effect.tap(encode), Effect.mapError(InvalidInput.fromSchemaError), Effect.flatMap((payload) => method({ payload })));
22
+ };
23
+ });
24
+ //#endregion
25
+ export { assertInBinding, methods };
@@ -0,0 +1,77 @@
1
+ import { Clock, Console, Effect, Function, Predicate } from "effect";
2
+ //#region src/internal/console.ts
3
+ /**
4
+ * The console of code whose program's stdout carries data: a stdio server's protocol, or a
5
+ * CLI command's result. Every method writes through `console.error` alone, so every console
6
+ * logger, `Console.log` and the default logger alike, leaves stdout intact, and so do
7
+ * counters, timers and group labels, which Node's console prints on stdout. A counter or a
8
+ * timer prints its label as given, never as a format, then its count or the milliseconds
9
+ * since it started, read on `clock` and rounded to three decimals; a group prints its label
10
+ * without indenting what follows, and `clear` clears nothing.
11
+ */
12
+ const stderrConsole = (console, clock) => {
13
+ const counts = /* @__PURE__ */ new Map();
14
+ const timers = /* @__PURE__ */ new Map();
15
+ const write = (...args) => console.error(...args);
16
+ const elapsed = (label, data) => {
17
+ const start = timers.get(label);
18
+ if (start !== void 0) {
19
+ const millis = Number(clock.monotonicTimeNanosUnsafe() - start) / 1e6;
20
+ write("%s: %sms", label, Number(millis.toFixed(3)), ...data);
21
+ }
22
+ };
23
+ const labeled = (label, args) => {
24
+ const [first, ...rest] = args;
25
+ write(...Predicate.isString(first) ? [`${label}: ${first}`, ...rest] : [label, ...args]);
26
+ };
27
+ const group = (...label) => {
28
+ if (label.some((part) => part !== void 0)) write(...label);
29
+ };
30
+ return {
31
+ assert: (condition, ...args) => {
32
+ if (!condition) labeled("Assertion failed", args);
33
+ },
34
+ clear: Function.constVoid,
35
+ count: (label = "default") => {
36
+ const count = (counts.get(label) ?? 0) + 1;
37
+ counts.set(label, count);
38
+ write("%s: %d", label, count);
39
+ },
40
+ countReset: (label = "default") => {
41
+ counts.delete(label);
42
+ },
43
+ debug: write,
44
+ dir: (item) => write("%O", item),
45
+ dirxml: write,
46
+ error: write,
47
+ group,
48
+ groupCollapsed: group,
49
+ groupEnd: Function.constVoid,
50
+ info: write,
51
+ log: write,
52
+ table: (data) => write(data),
53
+ time: (label = "default") => {
54
+ timers.set(label, clock.monotonicTimeNanosUnsafe());
55
+ },
56
+ timeEnd: (label = "default") => {
57
+ elapsed(label, []);
58
+ timers.delete(label);
59
+ },
60
+ timeLog: (label = "default", ...data) => elapsed(label, data),
61
+ trace: (...args) => {
62
+ const { stack = "" } = /* @__PURE__ */ new Error();
63
+ const frames = stack.split("\n").slice(2).join("\n");
64
+ labeled("Trace", args);
65
+ if (frames !== "") write(frames);
66
+ },
67
+ warn: write
68
+ };
69
+ };
70
+ /**
71
+ * `effect` writing its console output and Effect logs to stderr alone, through the console
72
+ * it runs with: for what a surface runs where stdout carries data, a stdio server's program
73
+ * or a CLI command's builder, authorizer and handler.
74
+ */
75
+ const logToStderr = (effect) => Effect.provideServiceEffect(effect, Console.Console, Effect.zipWith(Effect.service(Console.Console), Effect.service(Clock.Clock), stderrConsole));
76
+ //#endregion
77
+ export { logToStderr };
@@ -0,0 +1,13 @@
1
+ import { Action_d_exports, Errors } from "../Action.mjs";
2
+ import { HttpServerResponse } from "effect/http";
3
+ //#region src/internal/declared.d.ts
4
+ /** The list an `error` option of type `G` stands for: one schema is a list of one. */
5
+ type ListOf<G> = G extends Errors ? G : readonly [G];
6
+ /**
7
+ * The errors options `O` declare, as at run time: those `error` always gives, or, where it may
8
+ * be absent, those it gives or none, so clients decode each that may arrive.
9
+ */
10
+ export type ErrorsOf<O> = O extends unknown ? "error" extends keyof O ? O extends {
11
+ readonly error: infer G extends Errors | Errors[number];
12
+ } ? ListOf<G> : ListOf<Extract<O["error" & keyof O], Errors | Errors[number]>> | [] : [] : never;
13
+ //#endregion
@@ -0,0 +1,34 @@
1
+ import { unsuspended } from "./actions.mjs";
2
+ import { Schema, SchemaAST } from "effect";
3
+ import { HttpServerResponse } from "effect/http";
4
+ import { HttpApiSchema } from "effect/http-api";
5
+ //#region src/internal/declared.ts
6
+ /** The status an error schema states, as `HttpApi` reads it. */
7
+ const statusOf = SchemaAST.resolveAt("httpApiStatus");
8
+ /** The status a schema states, or the first one a suspension it wraps states. */
9
+ const statusThrough = (ast) => statusOf(ast) ?? (SchemaAST.isSuspend(ast) ? statusThrough(ast.thunk()) : void 0);
10
+ /**
11
+ * The schemas an endpoint declares for one error, each with the status it is sent with. `HttpApi`
12
+ * reads a status off each declared schema, never off a union's members nor through a suspension, so
13
+ * a plain union without a status of its own declares each member, and a suspended error, as a
14
+ * recursive one is written, without a status of its own states the status of what it suspends. An
15
+ * error without a status is an outcome the action expects, not a server fault: it is sent as 422,
16
+ * rather than `HttpApi`'s 500, which clients and proxies read as the server failing.
17
+ */
18
+ const declared = (error) => {
19
+ const status = statusThrough(error.ast);
20
+ if (status !== void 0) return [statusOf(error.ast) === void 0 ? HttpApiSchema.status(status)(error) : error];
21
+ const resolved = unsuspended(error.ast);
22
+ if (SchemaAST.isUnion(resolved) && resolved.checks === void 0 && resolved.encoding === void 0) return resolved.types.flatMap((member) => declared(Schema.make(member)));
23
+ return [HttpApiSchema.status(422)(error)];
24
+ };
25
+ /**
26
+ * `error` as an endpoint declaring `errors` sends it: its JSON, encoded by the first declared
27
+ * schema that accepts it, with that schema's status. None for a value no schema accepts.
28
+ */
29
+ const declaredResponse = (errors, error) => {
30
+ const schema = errors.flatMap(declared).find((each) => Schema.is(each)(error));
31
+ return schema === void 0 ? void 0 : HttpServerResponse.jsonUnsafe(Schema.encodeUnknownSync(Schema.toCodecJson(schema))(error), { status: statusOf(schema.ast) ?? 422 });
32
+ };
33
+ //#endregion
34
+ export { declared, declaredResponse };
@@ -0,0 +1,77 @@
1
+ import { Schema } from "effect";
2
+ //#region src/internal/errors.d.ts
3
+ /**
4
+ * One thing wrong with an input: where, as the keys and indexes leading to it from the input's
5
+ * root, none for the input itself, and what the schema expects there.
6
+ */
7
+ declare const Issue: Schema.Struct<{
8
+ readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
9
+ readonly message: Schema.String;
10
+ }>;
11
+ /** One thing wrong with an input, as `InvalidInput.issues` lists it. */
12
+ type Issue = (typeof Issue)["Type"];
13
+ declare const InvalidInput_base: Schema.Class<InvalidInput, Schema.TaggedStruct<"InvalidInput", {
14
+ readonly message: Schema.withConstructorDefault<Schema.String>;
15
+ readonly issues: Schema.withConstructorDefault<Schema.$Array<Schema.Struct<{
16
+ readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
17
+ readonly message: Schema.String;
18
+ }>>>;
19
+ }>, import("effect/Cause").YieldableError>;
20
+ /**
21
+ * The request's input does not decode against the action's input. HTTP answers every such
22
+ * request with it, its message the schema's own description of what is wrong, and `issues`
23
+ * the same description an issue at a time, for a caller that points at a field. A handler
24
+ * failing with it for input that decodes but cannot be served may name its own.
25
+ */
26
+ export declare class InvalidInput extends InvalidInput_base {
27
+ private readonly "~builtIn";
28
+ /**
29
+ * The `InvalidInput` for input that did not decode, as every surface answers it: the
30
+ * schema's message, and each of its issues by path, for code that decodes input of its own,
31
+ * such as a header or a route parameter. Neither carries a value of the input: decoding
32
+ * runs without `reportInput`, so no issue holds one to format. A symbol key is sent as its
33
+ * string form, `Symbol(name)`, as JSON has no symbols, which tells it from a string key of
34
+ * that name.
35
+ */
36
+ static readonly fromSchemaError: ({ issue, message }: Schema.SchemaError) => InvalidInput;
37
+ }
38
+ declare const Unauthenticated_base: Schema.Class<Unauthenticated, Schema.TaggedStruct<"Unauthenticated", {
39
+ readonly message: Schema.withConstructorDefault<Schema.String>;
40
+ }>, import("effect/Cause").YieldableError>;
41
+ /** The caller is not authenticated: authentication or an implementation's `authorize` refuses. */
42
+ export declare class Unauthenticated extends Unauthenticated_base {
43
+ private readonly "~builtIn";
44
+ }
45
+ declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbidden", {
46
+ readonly message: Schema.withConstructorDefault<Schema.String>;
47
+ readonly scopes: Schema.optionalKey<Schema.NonEmptyArray<Schema.String>>;
48
+ }>, import("effect/Cause").YieldableError>;
49
+ /**
50
+ * The caller may not run this action: an implementation's `authorize` refuses. `scopes`,
51
+ * when given, are every OAuth scope the call needs: a refusal naming them is answered with
52
+ * the `insufficient_scope` challenge an OAuth client re-authorizes on.
53
+ */
54
+ export declare class Forbidden extends Forbidden_base {
55
+ private readonly "~builtIn";
56
+ }
57
+ /**
58
+ * What authentication or an implementation's `authorize` refuses a caller with, instead of
59
+ * running a handler. An error an action declares, such as a rate limit, is not a refusal. As a
60
+ * schema, it tells a refusal apart: `Schema.is(Action.Refusal)(error)`.
61
+ */
62
+ export declare const Refusal: Schema.Union<readonly [typeof Unauthenticated, typeof Forbidden]>;
63
+ /** A refusal: `Unauthenticated | Forbidden`. */
64
+ export type Refusal = (typeof Refusal)["Type"];
65
+ /**
66
+ * The failures every endpoint and tool declares beyond its action's own, and any handler may
67
+ * fail with: bad input, and every refusal. No action or binding declares them itself.
68
+ */
69
+ export declare const builtIns: readonly [typeof InvalidInput, typeof Unauthenticated, typeof Forbidden];
70
+ /** The built-in failures' schemas. */
71
+ export type BuiltIns = (typeof builtIns)[number];
72
+ /** The built-in failures as one schema: `Schema.is(Action.BuiltIn)(error)`. */
73
+ export declare const BuiltIn: Schema.Union<readonly [typeof InvalidInput, typeof Unauthenticated, typeof Forbidden]>;
74
+ /** A built-in failure: `InvalidInput | Refusal`. */
75
+ export type BuiltIn = (typeof BuiltIn)["Type"];
76
+ //#endregion
77
+ export { Issue };