@jarenjs/linq 0.49.2 → 0.56.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 (77) hide show
  1. package/ARCHITECTURE.md +217 -0
  2. package/README.md +559 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1217 -0
  5. package/docs/DB-CLIENT.md +814 -0
  6. package/docs/FLOW-PEN.md +1026 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +771 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1083 -0
  12. package/docs/QUERY-PEN.md +1636 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +255 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +260 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +329 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +9 -4
  24. package/src/contract/define.js +269 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +342 -0
  28. package/src/db/handle.js +86 -0
  29. package/src/db/include.js +316 -0
  30. package/src/db/index.js +19 -0
  31. package/src/db/live.js +43 -0
  32. package/src/db/membership.js +37 -0
  33. package/src/db/open.js +82 -0
  34. package/src/document.js +143 -13
  35. package/src/effect.js +65 -0
  36. package/src/errors.js +69 -6
  37. package/src/expression.js +437 -36
  38. package/src/flow/capture.js +33 -0
  39. package/src/flow/dag.js +302 -0
  40. package/src/flow/fsm.js +328 -0
  41. package/src/flow/index.js +22 -0
  42. package/src/forms/index.js +43 -0
  43. package/src/forms/rules.js +170 -0
  44. package/src/forms/submit.js +177 -0
  45. package/src/index.js +4 -2
  46. package/src/jslt/body.js +226 -0
  47. package/src/jslt/index.js +18 -0
  48. package/src/jslt/rules.js +207 -0
  49. package/src/json-boundary.js +90 -0
  50. package/src/migration/define.js +323 -0
  51. package/src/migration/index.js +15 -0
  52. package/src/migration/steps.js +248 -0
  53. package/src/model/collection.js +171 -0
  54. package/src/model/define.js +125 -0
  55. package/src/model/entity.js +307 -0
  56. package/src/model/index.js +47 -0
  57. package/src/model/relation.js +85 -0
  58. package/src/provider.js +137 -20
  59. package/src/schema/brand.js +31 -0
  60. package/src/schema/builders.js +526 -0
  61. package/src/schema/check.js +29 -0
  62. package/src/schema/emit.js +394 -0
  63. package/src/schema/factories.js +239 -0
  64. package/src/schema/index.js +37 -0
  65. package/src/schema-of.js +24 -0
  66. package/src/sequence.js +233 -103
  67. package/src/sources.js +10 -3
  68. package/types/app.d.ts +293 -0
  69. package/types/contract.d.ts +371 -0
  70. package/types/db.d.ts +188 -0
  71. package/types/flow.d.ts +285 -0
  72. package/types/forms.d.ts +253 -0
  73. package/types/index.d.ts +231 -26
  74. package/types/jslt.d.ts +193 -0
  75. package/types/migration.d.ts +201 -0
  76. package/types/model.d.ts +493 -0
  77. package/types/schema.d.ts +494 -0
@@ -0,0 +1,82 @@
1
+ //@ts-check
2
+ /**
3
+ * @file One capture over a value rooted at `$` with named externals —
4
+ * the shared piece under every pen's query-valued member: a schema
5
+ * `check()` rule (`root`, `path`), a model `compute()` default (no
6
+ * externals), a JSLT rule body (`root`, `path` and the parameters the
7
+ * body declares). The value proxy is rooted at `$`, the instance the
8
+ * query is evaluated at; the externals proxy answers exactly the named
9
+ * expressions and refuses any other name at build time (`JL0104`),
10
+ * earlier than the engine's own error and with the same meaning. One
11
+ * implementation: a pen adds an externals list, never a capture entry
12
+ * point.
13
+ */
14
+
15
+ import { captureExpression } from './expression.js';
16
+ import { LinqBuildError } from './errors.js';
17
+
18
+ /** No parameters are declared for a rule: `p.x` cannot appear. */
19
+ const NO_PARAMS = new Set();
20
+
21
+ /** The indefinite article for a method name (`edge() select` → `an`). */
22
+ const article = (/** @type {string} */ what) =>
23
+ ('aeiou'.includes(what.charAt(0).toLowerCase()) ? 'an' : 'a');
24
+
25
+ /**
26
+ * The externals proxy: exactly the named expressions the capture handed
27
+ * us; anything else has nothing to bind to.
28
+ *
29
+ * The message states only what this file KNOWS — how many externals the
30
+ * evaluator binds and which — because that is all it can know: WHERE the
31
+ * query is evaluated differs per pen (a schema rule over the instance, a
32
+ * model default over the document being written, a flow guard over the
33
+ * step scope at step time), and naming one pen's answer here made the
34
+ * message contradict its own advice for every other pen. A caller that
35
+ * has a scope to name passes it as `advice`.
36
+ * @param {string} what - the method, for the message
37
+ * @param {readonly string[]} names - the externals bound
38
+ * @param {readonly any[]} proxies - one expression proxy per name
39
+ * @param {((name: string) => string) | undefined} advice - the fix, when one exists
40
+ * @param {string} noun - what this pen calls the callback
41
+ */
42
+ function externalsProxy(what, names, proxies, advice, noun) {
43
+ return new Proxy(Object.freeze({}), {
44
+ get(_target, prop) {
45
+ if (typeof prop === 'symbol') return undefined;
46
+ const index = names.indexOf(/** @type {string} */ (prop));
47
+ if (index !== -1) return proxies[index];
48
+ const bound = names.length === 0
49
+ ? 'no externals at all'
50
+ : `exactly ${names.length === 1 ? 'one external' : `${names.length} externals`}, `
51
+ + `${names.map((n) => `'${n}'`).join(' and ')}`;
52
+ throw new LinqBuildError('JL0104',
53
+ `${article(what)} ${what} ${noun} cannot bind '${prop}' — its query evaluates with `
54
+ + `${bound}; anything else has nothing to bind to`
55
+ + (advice === undefined ? '' : advice(prop)));
56
+ },
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Capture one rule into a query document over a value rooted at `$`,
62
+ * with the named externals as the second argument.
63
+ * @param {string} what - the method, for the message
64
+ * @param {readonly string[]} externals - the externals the evaluator binds
65
+ * @param {(value: any, externals: any) => any} rule
66
+ * @param {{ advice?: (name: string) => string, fold?: boolean, noun?: string }} [options] -
67
+ * `advice`: appended to the `JL0104` message — how an unbound name could
68
+ * be declared where it can, or what the callback's own argument already
69
+ * is; `fold`: whether a pure data tree folds into one `$const` (the
70
+ * default) or is spelled as a constructor tree; `noun`: what this pen
71
+ * calls the callback (`'rule'` by default; a pen with no rules passes
72
+ * its own word)
73
+ * @returns {any} the captured query expression (plain JSON)
74
+ */
75
+ export function captureQuery(what, externals, rule, options = {}) {
76
+ const noun = options.noun ?? 'rule';
77
+ return captureExpression(
78
+ (value, ...bound) => rule(value, externalsProxy(what, externals, bound, options.advice, noun)),
79
+ [{ doc: '$', pathable: true }, ...externals],
80
+ NO_PARAMS,
81
+ options.fold !== false);
82
+ }
@@ -1,7 +1,7 @@
1
1
  //@ts-check
2
2
  /**
3
3
  * @file `mapAsync` — the ONE explicit bounded-concurrency boundary
4
- * (LINQ-FORMAT.md §11). Element-wise asynchronous work (an HTTP call, a
4
+ * (QUERY-PEN.md §11). Element-wise asynchronous work (an HTTP call, a
5
5
  * model call, a file read per row) happens here and nowhere else: there
6
6
  * is no parallel universe of `selectAwait`-shaped operators, and a
7
7
  * per-element async *predicate* is `mapAsync` then `where`, by design.
@@ -118,25 +118,30 @@ export async function* applyMapAsync(items, fn, opts) {
118
118
  // parallel: a sliding window of `concurrency` in-flight tasks
119
119
  const window = [];
120
120
  let sourceDone = false;
121
+ // the first rejection anywhere in the window aborts every sibling
122
+ // and stops the pull, at once — not when it reaches the head of the
123
+ // ordered window: the rejection itself still surfaces in order
124
+ let rejected = false;
121
125
  const pull = async () => {
122
126
  const step = await items.next();
123
127
  if (step.done) { sourceDone = true; return null; }
128
+ if (rejected) return null; // a sibling failed while this pull awaited
124
129
  const promise = Promise.resolve(fn(step.value, signal));
125
130
  // a rejection must wait its turn in the ordered window without
126
131
  // firing unhandledRejection while an earlier task is in flight
127
- promise.catch(() => {});
132
+ promise.catch(() => { rejected = true; controller.abort(); });
128
133
  return { promise };
129
134
  };
130
135
  if (opts.ordered) {
131
136
  // completion order = source order; the window buffers at most
132
137
  // `concurrency` results (the documented buffering cost)
133
- while (!sourceDone && window.length < opts.concurrency) {
138
+ while (!sourceDone && !rejected && window.length < opts.concurrency) {
134
139
  const task = await pull();
135
140
  if (task !== null) window.push(task.promise);
136
141
  }
137
142
  while (window.length > 0) {
138
143
  const value = await window.shift();
139
- if (!sourceDone) {
144
+ if (!sourceDone && !rejected) {
140
145
  const task = await pull();
141
146
  if (task !== null) window.push(task.promise);
142
147
  }
@@ -0,0 +1,269 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `defineContract({ id?, version?, compat? }, operations)` — one
4
+ * `$contract` 0.1 document, deep-frozen, that `compileContract` takes
5
+ * unchanged.
6
+ *
7
+ * Two rules make the pen's document and its own public projection
8
+ * comparable member for member. First, the member ORDER is §12.1's, the
9
+ * normative one the revision hashes: root `$contract, id, version,
10
+ * compat, $defs, operations`; operation `kind, input, output, errors,
11
+ * policy, http, doc`; error `status, schema`; http `method, path, in,
12
+ * body, status, media`; `$defs` in first-reference order. Second, no
13
+ * DEFAULT is written: `describe()` marks a default inferred, and a pen
14
+ * that wrote them would turn every default into a declaration and move
15
+ * the revision for nothing.
16
+ *
17
+ * Every `named()` builder any operation reaches becomes one entry of the
18
+ * contract's own `$defs`, referenced `#/$defs/<name>` — the schema pen's
19
+ * hoisting walk over several roots instead of one. Nothing here imports
20
+ * `@jarenjs/contract`: the compiler stays the only judge of what the
21
+ * document means.
22
+ */
23
+
24
+ import { cloneJson, deepFreeze, setObjectMember } from '@jarenjs/core/object';
25
+
26
+ import { LinqBuildError } from '../errors.js';
27
+ import { describeValue, requireJson, requireNameMap } from '../json-boundary.js';
28
+ import { isSchemaBuilder } from '../schema/brand.js';
29
+ import { createHoist, emitInto, hoistedDefs } from '../schema/emit.js';
30
+ import { http as httpBinding, isHttpBinding } from './http.js';
31
+ import { checkOperation, emitPolicy, isOperation, KINDS, readErrors } from './operation.js';
32
+
33
+ const CONTRACT_VERSION = '0.1';
34
+ const HEAD_MEMBERS = ['id', 'version', 'compat'];
35
+
36
+ /** `[A-Za-z_][A-Za-z0-9_-]*` — a contract id (§2.1). */
37
+ const ID = /^[A-Za-z_][A-Za-z0-9_-]*$/;
38
+
39
+ /** @param {any} value */
40
+ function isPlainObject(value) {
41
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
42
+ }
43
+
44
+ /**
45
+ * One schema position — `input`, `output`, an error's `schema` — as the
46
+ * document carries it: a builder emitted into the shared `$defs`
47
+ * context, or a JSON Schema written by hand, copied.
48
+ * @param {any} value
49
+ * @param {any} ctx - the hoisting context
50
+ * @param {string} at
51
+ * @returns {any}
52
+ */
53
+ function schemaAt(value, ctx, at) {
54
+ if (isSchemaBuilder(value)) return emitInto(value, ctx, at);
55
+ const json = requireJson(value, `the schema at ${at}`);
56
+ if (typeof json !== 'boolean' && !isPlainObject(json)) {
57
+ throw new LinqBuildError('JL0101',
58
+ `a schema is an object, true or false — got ${describeValue(value)}`, at);
59
+ }
60
+ return cloneJson(json);
61
+ }
62
+
63
+ /**
64
+ * The kind and spec of one declared operation: a `read()`/`command()`/
65
+ * `subscribe()` declaration, or the same members written by hand with a
66
+ * `kind`.
67
+ *
68
+ * The hand-written form is checked HERE by the same predicate the three
69
+ * declaration functions run, against its own position in the document.
70
+ * It is a second door into one emitter, and a door with no check behind
71
+ * it is worse than no door: an unknown member would be dropped in
72
+ * silence (the pen never sees it again, and the compiler never sees it
73
+ * at all), and a non-string `doc` would be written into a document
74
+ * `jaren-contract` refuses.
75
+ * @param {any} declared
76
+ * @param {string} at
77
+ * @returns {{ kind: string, spec: any }}
78
+ */
79
+ function declarationOf(declared, at) {
80
+ if (isOperation(declared)) return declared;
81
+ if (!isPlainObject(declared)) {
82
+ throw new LinqBuildError('JL0101',
83
+ `an operation is read(), command() or subscribe() — got ${describeValue(declared)}`, at);
84
+ }
85
+ if (!KINDS.includes(declared.kind)) {
86
+ throw new LinqBuildError('JL0102',
87
+ `an operation kind is one of ${KINDS.join(', ')} — got ${describeValue(declared.kind)}`,
88
+ `${at}/kind`);
89
+ }
90
+ const { kind, ...spec } = declared;
91
+ checkOperation(kind, spec, at);
92
+ return { kind, spec };
93
+ }
94
+
95
+ /**
96
+ * Emit one operation, in §12.1's member order, declared members only.
97
+ * @param {string} id
98
+ * @param {any} declared
99
+ * @param {any} ctx - the hoisting context
100
+ * @returns {any}
101
+ */
102
+ function emitOperation(id, declared, ctx) {
103
+ const at = `/operations/${id}`;
104
+ const { kind, spec } = declarationOf(declared, at);
105
+ const out = { kind };
106
+ if (spec.input !== undefined) out.input = schemaAt(spec.input, ctx, `${at}/input`);
107
+ out.output = schemaAt(spec.output, ctx, `${at}/output`);
108
+ if (spec.errors !== undefined) {
109
+ const declaredErrors = readErrors(spec.errors, `${at}/errors`);
110
+ const errors = {};
111
+ for (const [code, entry] of declaredErrors) {
112
+ const e = {};
113
+ if (entry.status !== undefined) e.status = entry.status;
114
+ if (entry.schema !== undefined) {
115
+ e.schema = schemaAt(entry.schema, ctx, `${at}/errors/${code}/schema`);
116
+ }
117
+ setObjectMember(errors, code, e);
118
+ }
119
+ out.errors = errors;
120
+ }
121
+ if (spec.policy !== undefined) out.policy = emitPolicy(spec.policy, `${at}/policy`);
122
+ if (spec.http !== undefined) {
123
+ out.http = isHttpBinding(spec.http) ? { ...spec.http } : { ...httpBinding(spec.http) };
124
+ }
125
+ if (spec.doc !== undefined) out.doc = spec.doc;
126
+ return out;
127
+ }
128
+
129
+ /**
130
+ * The contract under construction: frozen on creation, its document
131
+ * assembled once. `document` and `toJSON()` are the same deep-frozen
132
+ * `$contract` 0.1 JSON; the phantom the declarations carry is what
133
+ * `ContractOf<>` reads.
134
+ */
135
+ export class Contract {
136
+ #document;
137
+
138
+ /**
139
+ * @param {any} document - the assembled document
140
+ */
141
+ constructor(document) {
142
+ this.#document = document;
143
+ Object.freeze(this);
144
+ }
145
+
146
+ /** The deep-frozen `$contract` 0.1 document. */
147
+ get document() { return this.#document; }
148
+
149
+ /** The document — what `JSON.stringify` writes. @returns {any} */
150
+ toJSON() { return this.#document; }
151
+ }
152
+
153
+ /**
154
+ * Write a `$contract` 0.1 document.
155
+ *
156
+ * @param {any} meta - `{ id?, version?, compat? }` (§2.1)
157
+ * @param {any} operations - operation id → `read()` / `command()` / `subscribe()`
158
+ * @returns {Contract} the contract, its `document` deep-frozen JSON
159
+ * @throws {LinqBuildError} `JL0101` a value the pen cannot spell;
160
+ * `JL0102` a kind or path template the format reserves; `JL0103` two
161
+ * distinct builders under one `$defs` name, or a `ref()` nothing defines
162
+ * @example
163
+ * const shop = defineContract({ id: 'shop', version: '5' }, {
164
+ * 'catalog.load': read({ output: Catalog, http: http({ method: 'GET', path: '/api/catalog' }) }),
165
+ * });
166
+ * compileContract(shop.document).ids; // ['catalog.load']
167
+ */
168
+ export function defineContract(meta, operations) {
169
+ if (!isPlainObject(meta)) {
170
+ throw new LinqBuildError('JL0101',
171
+ `defineContract() takes ({ id?, version?, compat? }, operations), got `
172
+ + `${describeValue(meta)} as its first argument`);
173
+ }
174
+ for (const key of Object.keys(meta)) {
175
+ if (!HEAD_MEMBERS.includes(key)) {
176
+ throw new LinqBuildError('JL0101',
177
+ `defineContract() does not take '${key}' — the head is ${HEAD_MEMBERS.join(', ')}; `
178
+ + 'everything else is an operation', `/${key}`);
179
+ }
180
+ }
181
+ if (meta.id !== undefined && (typeof meta.id !== 'string' || !ID.test(meta.id))) {
182
+ throw new LinqBuildError('JL0101',
183
+ `defineContract() id matches [A-Za-z_][A-Za-z0-9_-]*, got ${describeValue(meta.id)}`, '/id');
184
+ }
185
+ if (meta.version !== undefined && typeof meta.version !== 'string') {
186
+ throw new LinqBuildError('JL0101',
187
+ `defineContract() version is a string, got ${describeValue(meta.version)}`, '/version');
188
+ }
189
+ if (meta.compat !== undefined
190
+ && (!Array.isArray(meta.compat) || meta.compat.some((v) => typeof v !== 'string'))) {
191
+ throw new LinqBuildError('JL0101',
192
+ 'defineContract() compat is an array of peer version strings', '/compat');
193
+ }
194
+ if (!isPlainObject(operations)) {
195
+ throw new LinqBuildError('JL0101',
196
+ `defineContract() operations is a plain object of id → operation, got `
197
+ + `${describeValue(operations)}`, '/operations');
198
+ }
199
+ requireNameMap(operations, 'defineContract() operations', '/operations');
200
+ const ids = Object.keys(operations);
201
+ if (ids.length === 0) {
202
+ throw new LinqBuildError('JL0101',
203
+ 'defineContract() needs at least one operation', '/operations');
204
+ }
205
+
206
+ const ctx = createHoist();
207
+ const emitted = ids.map((id) => [id, emitOperation(id, operations[id], ctx)]);
208
+ const defs = hoistedDefs(ctx);
209
+
210
+ const out = { $contract: CONTRACT_VERSION };
211
+ if (meta.id !== undefined) out.id = meta.id;
212
+ if (meta.version !== undefined) out.version = meta.version;
213
+ if (meta.compat !== undefined) out.compat = meta.compat.slice();
214
+ if (defs !== null) out.$defs = defs;
215
+ const ops = {};
216
+ for (const [id, op] of emitted) setObjectMember(ops, id, op);
217
+ out.operations = ops;
218
+ return new Contract(deepFreeze(out));
219
+ }
220
+
221
+ /**
222
+ * Bind a contract client to the contract that types it. Identity at
223
+ * runtime: `invoke`, `url` and `subscribe` are narrowed by the pen's
224
+ * phantoms, and nothing is added to the binding.
225
+ * @template C
226
+ * @param {any} client - any contract client (local, http, port)
227
+ * @param {C} contract - the pen's contract; the type argument only
228
+ * @returns {any}
229
+ * @example
230
+ * const api = typedClient(openLocalClient(compileContract(shop.document), handlers), shop);
231
+ * const outcome = await api.invoke('product.save', { id: 1, revision: 4, product });
232
+ */
233
+ export function typedClient(client, contract) {
234
+ void contract;
235
+ return client;
236
+ }
237
+
238
+ /**
239
+ * Bind a handler table to the contract it serves. Identity at runtime:
240
+ * the table is checked against the operation ids and each handler's
241
+ * input and output typed by the pen's phantoms.
242
+ * @template C
243
+ * @template H
244
+ * @param {C} contract - the pen's contract; the type argument only
245
+ * @param {H} handlers - operation id → handler
246
+ * @returns {H}
247
+ * @example
248
+ * serveHttp(compiled, typedHandlers(shop, { 'catalog.load': () => catalog }));
249
+ */
250
+ export function typedHandlers(contract, handlers) {
251
+ void contract;
252
+ return handlers;
253
+ }
254
+
255
+ /**
256
+ * Bind `contractTools`' output to the contract that types it. Identity
257
+ * at runtime: each tool's `name` and `execute` argument are narrowed by
258
+ * the pen's phantoms.
259
+ * @template C
260
+ * @param {any} tools - what `contractTools(compiled, client)` answered
261
+ * @param {C} contract - the pen's contract; the type argument only
262
+ * @returns {any}
263
+ * @example
264
+ * for (const tool of typedTools(contractTools(compiled, client), shop)) toolbox.add(tool);
265
+ */
266
+ export function typedTools(tools, contract) {
267
+ void contract;
268
+ return tools;
269
+ }
@@ -0,0 +1,247 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `http()` — the REST binding of one operation (CONTRACT-FORMAT
4
+ * §4), written as the format spells it and checked as far as the pen can
5
+ * see. The path template scan mirrors the compiler's own parser: every
6
+ * form §4.2 reserves is refused by name with `JL0102`, at build time,
7
+ * before `compileContract` would answer `JC0008` with the same meaning.
8
+ * Nothing is canonicalized here — a `:name` template is written as the
9
+ * author declared it, and the projections are what show the `{name}`
10
+ * form.
11
+ */
12
+
13
+ import { LinqBuildError } from '../errors.js';
14
+ import { describeValue, requireJson } from '../json-boundary.js';
15
+
16
+ /** The binding brand: how `defineContract` tells a checked binding apart. */
17
+ export const HTTP_BINDING = Symbol.for('@jarenjs/linq/contract-http');
18
+
19
+ /** The members `http` accepts, in the order §12.1 fixes. */
20
+ export const HTTP_MEMBERS = Object.freeze(['method', 'path', 'in', 'body', 'status', 'media']);
21
+
22
+ /** The uppercase tokens §4's table lists. */
23
+ export const HTTP_METHODS = Object.freeze(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']);
24
+
25
+ /** The four places an input member can travel. */
26
+ export const LOCATIONS = Object.freeze(['path', 'query', 'header', 'body']);
27
+
28
+ /** `[A-Za-z_][A-Za-z0-9_]*` — a path variable's name. */
29
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
30
+
31
+ /** The RFC 6570 operators a `{…}` expression may open with; all reserved. */
32
+ const OPERATORS = '+#./;?&=';
33
+
34
+ /**
35
+ * One static segment: any character but the structural ones (`/ { } : *
36
+ * ? #`), whitespace and controls; a `%` must open a well-formed escape.
37
+ * @param {string} segment
38
+ * @param {string} at
39
+ */
40
+ function checkStatic(segment, at) {
41
+ for (let i = 0; i < segment.length; i++) {
42
+ const ch = segment[i];
43
+ if (ch === '{' || ch === '}') {
44
+ throw new LinqBuildError('JL0102',
45
+ `a variable must be a whole segment ("{name}"), found "${segment}" — the format `
46
+ + 'reserves a variable that is only part of a segment', at);
47
+ }
48
+ if (ch === ':') {
49
+ throw new LinqBuildError('JL0102',
50
+ `":" is reserved for a variable segment (":name"), found "${segment}"`, at);
51
+ }
52
+ if (ch === '*') {
53
+ throw new LinqBuildError('JL0102',
54
+ `"*" is a reserved wildcard form; $contract 0.1 has no wildcards, found "${segment}"`, at);
55
+ }
56
+ if (ch === '?' || ch === '#') {
57
+ throw new LinqBuildError('JL0102',
58
+ `"${ch}" cannot appear in a path template (the query and fragment are not part of `
59
+ + 'the path)', at);
60
+ }
61
+ const code = segment.charCodeAt(i);
62
+ if (code <= 0x20 || code === 0x7f) {
63
+ throw new LinqBuildError('JL0102',
64
+ `whitespace or a control character in segment "${segment}"`, at);
65
+ }
66
+ if (ch === '%') {
67
+ if (!/^[0-9A-Fa-f]{2}$/.test(segment.slice(i + 1, i + 3))) {
68
+ throw new LinqBuildError('JL0102',
69
+ `a malformed percent-escape in segment "${segment}"`, at);
70
+ }
71
+ i += 2;
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * The variables a template declares, refusing every reserved form by
78
+ * name (§4.2). Mirrors the compiler's parser; nothing is rewritten.
79
+ * @param {any} source
80
+ * @param {string} at
81
+ * @returns {string[]} the variable names, in order
82
+ */
83
+ export function pathVariables(source, at) {
84
+ if (typeof source !== 'string') {
85
+ throw new LinqBuildError('JL0102',
86
+ `http() path is a path template string, got ${describeValue(source)}`, at);
87
+ }
88
+ if (source.length === 0 || source[0] !== '/') {
89
+ throw new LinqBuildError('JL0102', 'a path template must start with "/"', at);
90
+ }
91
+ /** @type {string[]} */
92
+ const variables = [];
93
+ if (source === '/') return variables;
94
+ const segments = source.slice(1).split('/');
95
+ for (let i = 0; i < segments.length; i++) {
96
+ const segment = segments[i];
97
+ if (segment.length === 0) {
98
+ throw new LinqBuildError('JL0102', i === segments.length - 1
99
+ ? 'a trailing "/" declares an empty segment; the root template "/" is the only empty path'
100
+ : 'an empty segment ("//")', at);
101
+ }
102
+ /** @type {string | null} */
103
+ let name = null;
104
+ if (segment[0] === '{') {
105
+ if (segment[segment.length - 1] !== '}') {
106
+ throw new LinqBuildError('JL0102',
107
+ `a variable must be a whole segment ("{name}"), found "${segment}" — the format `
108
+ + 'reserves a variable that is only part of a segment', at);
109
+ }
110
+ name = segment.slice(1, -1);
111
+ if (name.length > 0 && OPERATORS.includes(name[0])) {
112
+ throw new LinqBuildError('JL0102',
113
+ `"{${name}}" uses the reserved RFC 6570 operator "${name[0]}"; $contract 0.1 `
114
+ + 'supports only "{name}"', at);
115
+ }
116
+ const last = name[name.length - 1];
117
+ if (last === '+' || last === '*') {
118
+ throw new LinqBuildError('JL0102',
119
+ `"{${name}}" uses the reserved "${last}" expansion modifier; $contract 0.1 has no `
120
+ + 'wildcards', at);
121
+ }
122
+ if (name.includes(',') || name.includes(':')) {
123
+ throw new LinqBuildError('JL0102',
124
+ `"{${name}}" uses a reserved RFC 6570 list or prefix form; $contract 0.1 supports `
125
+ + 'only "{name}"', at);
126
+ }
127
+ }
128
+ else if (segment[0] === ':') {
129
+ name = segment.slice(1);
130
+ if (name.length === 0 || name.includes('{') || name.includes('}')) {
131
+ throw new LinqBuildError('JL0102',
132
+ `":name" must be a whole segment with an identifier name, found "${segment}"`, at);
133
+ }
134
+ const last = name[name.length - 1];
135
+ if (last === '*' || last === '+' || last === '?') {
136
+ throw new LinqBuildError('JL0102',
137
+ `":${name}" uses a reserved "${last}" modifier; $contract 0.1 has no wildcards or `
138
+ + 'optional segments', at);
139
+ }
140
+ }
141
+ if (name === null) {
142
+ checkStatic(segment, at);
143
+ continue;
144
+ }
145
+ if (!IDENT.test(name)) {
146
+ throw new LinqBuildError('JL0102',
147
+ `a variable name must match [A-Za-z_][A-Za-z0-9_]*, found "${name}"`, at);
148
+ }
149
+ if (variables.includes(name)) {
150
+ throw new LinqBuildError('JL0102', `the variable "${name}" is declared twice`, at);
151
+ }
152
+ variables.push(name);
153
+ }
154
+ return variables;
155
+ }
156
+
157
+ /**
158
+ * One operation's HTTP binding, checked and branded. The members are
159
+ * written in §12.1's order — `method`, `path`, `in`, `body`, `status`,
160
+ * `media` — and only the ones declared: a default is the compiler's to
161
+ * materialize, never the pen's to write.
162
+ *
163
+ * @param {any} spec - `{ method, path, in?, body?, status?, media? }`
164
+ * @returns {any} the binding, frozen and branded
165
+ * @throws {LinqBuildError} `JL0101` a member that is not what it takes;
166
+ * `JL0102` a path template form the format reserves
167
+ * @example
168
+ * http({ method: 'PUT', path: '/api/products/{id}', in: { revision: 'body' } });
169
+ */
170
+ export function http(spec) {
171
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
172
+ throw new LinqBuildError('JL0101',
173
+ `http() takes { method, path, in?, body?, status?, media? }, got ${describeValue(spec)}`);
174
+ }
175
+ for (const key of Object.keys(spec)) {
176
+ if (!HTTP_MEMBERS.includes(key)) {
177
+ throw new LinqBuildError('JL0101',
178
+ `http() does not take '${key}' — the binding is ${HTTP_MEMBERS.join(', ')} `
179
+ + '(CONTRACT-FORMAT §4)', `/${key}`);
180
+ }
181
+ }
182
+ if (!HTTP_METHODS.includes(spec.method)) {
183
+ throw new LinqBuildError('JL0101',
184
+ `http() method is one uppercase token of ${HTTP_METHODS.join(' ')}, got `
185
+ + `${describeValue(spec.method)}`, '/method');
186
+ }
187
+ const variables = pathVariables(spec.path, '/path');
188
+
189
+ const out = { method: spec.method, path: spec.path };
190
+ if (spec.in !== undefined) {
191
+ const locations = requireJson(spec.in, 'http() in');
192
+ if (locations === null || typeof locations !== 'object' || Array.isArray(locations)) {
193
+ throw new LinqBuildError('JL0101',
194
+ 'http() in is a plain object of input member → path | query | header | body', '/in');
195
+ }
196
+ const placed = {};
197
+ for (const member of Object.keys(locations)) {
198
+ const where = locations[member];
199
+ if (!LOCATIONS.includes(where)) {
200
+ throw new LinqBuildError('JL0101',
201
+ `http() in.${member} is one of ${LOCATIONS.join(', ')}, got ${describeValue(where)}`,
202
+ `/in/${member}`);
203
+ }
204
+ if (where === 'path' && !variables.includes(member)) {
205
+ throw new LinqBuildError('JL0102',
206
+ `http() maps '${member}' to path, but the template declares no {${member}} — a `
207
+ + 'path member is named by the template itself', `/in/${member}`);
208
+ }
209
+ placed[member] = where;
210
+ }
211
+ out.in = placed;
212
+ }
213
+ if (spec.body !== undefined) {
214
+ if (typeof spec.body !== 'string' || spec.body.length === 0) {
215
+ throw new LinqBuildError('JL0101',
216
+ `http() body names the input member whose value IS the request body, got `
217
+ + `${describeValue(spec.body)}`, '/body');
218
+ }
219
+ out.body = spec.body;
220
+ }
221
+ if (spec.status !== undefined) {
222
+ if (!Number.isInteger(spec.status) || spec.status < 200 || spec.status > 299) {
223
+ throw new LinqBuildError('JL0101',
224
+ `http() status is an integer in 200–299, got ${describeValue(spec.status)}`, '/status');
225
+ }
226
+ out.status = spec.status;
227
+ }
228
+ if (spec.media !== undefined) {
229
+ if (typeof spec.media !== 'string' || spec.media.length === 0) {
230
+ throw new LinqBuildError('JL0101',
231
+ `http() media is a media type, got ${describeValue(spec.media)}`, '/media');
232
+ }
233
+ out.media = spec.media;
234
+ }
235
+ Object.defineProperty(out, HTTP_BINDING, { value: true, enumerable: false });
236
+ return Object.freeze(out);
237
+ }
238
+
239
+ /**
240
+ * Whether a value is an `http()` binding (as opposed to the same members
241
+ * written by hand, which `defineContract` accepts and checks the same way).
242
+ * @param {any} value
243
+ * @returns {boolean}
244
+ */
245
+ export function isHttpBinding(value) {
246
+ return value !== null && typeof value === 'object' && value[HTTP_BINDING] === true;
247
+ }
@@ -0,0 +1,23 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `@jarenjs/linq/contract` — `$contract` 0.1 documents by code.
4
+ * `defineContract({ id?, version?, compat? }, operations)` writes the
5
+ * document `compileContract` takes, with every `named()` schema builder
6
+ * an operation reaches hoisted into the contract's own `$defs` and every
7
+ * member in the order CONTRACT-FORMAT §12.1 fixes, so a pen document and
8
+ * its own public projection differ by nothing but the defaults the
9
+ * compiler materializes. `read`/`command`/`subscribe` declare the three
10
+ * kinds, `http()` the REST binding (its path template checked here,
11
+ * earlier than the compiler's `JC0008`), `error()` one entry of an
12
+ * operation's `errors`.
13
+ *
14
+ * The three identity wrappers — `typedClient`, `typedHandlers`,
15
+ * `typedTools` — carry the inferred `Operations` type onto a client, a
16
+ * handler table and an AI toolbox without running the TypeScript
17
+ * projection. The document is the deliverable; nothing here imports
18
+ * `@jarenjs/contract` or an engine.
19
+ */
20
+
21
+ export { defineContract, typedClient, typedHandlers, typedTools, Contract } from './define.js';
22
+ export { read, command, subscribe, error } from './operation.js';
23
+ export { http } from './http.js';