@jarenjs/play 0.34.2 → 0.43.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/README.md +2 -2
- package/dist/types/component/index.d.ts +1 -1
- package/dist/types/index.d.ts +18 -5
- package/docs/PLAY-FORMAT.md +8 -1
- package/package.json +6 -5
- package/src/engines.js +71 -0
- package/src/examples.js +42 -0
- package/src/index.js +17 -5
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
**A JSON-engine playground — understand an engine before you compose it.**
|
|
4
4
|
|
|
5
5
|
Pick an engine (JSONPath, JSON Pointer, JSON Patch, `$query`, JSLT,
|
|
6
|
-
markdown, mermaid, …), feed it a source input and
|
|
7
|
-
a curated example library, and watch it run. Where `@jarenjs/studio` is for
|
|
6
|
+
markdown, mermaid, a `$contract` document, …), feed it a source input and
|
|
7
|
+
one or more datasets from a curated example library, and watch it run. Where `@jarenjs/studio` is for
|
|
8
8
|
building an application out of many files, `@jarenjs/play` is for
|
|
9
9
|
learning one engine standalone — a reference bench you experiment on first.
|
|
10
10
|
|
|
@@ -72,7 +72,7 @@ export declare function createPlayComponent(options?: {
|
|
|
72
72
|
engines: Readonly<Record<string, import("../index.js").EngineDescriptor>>;
|
|
73
73
|
examples: readonly import("../index.js").PlayExample[];
|
|
74
74
|
engineIds: typeof engineIds;
|
|
75
|
-
runExample: (engineId: any, source: any, data: any, perCall?: {}) => import("../index.js").PlayResult
|
|
75
|
+
runExample: (engineId: any, source: any, data: any, perCall?: {}) => import("../index.js").PlayResult | Promise<import("../index.js").PlayResult>;
|
|
76
76
|
operators: {
|
|
77
77
|
toOptions: () => any;
|
|
78
78
|
} | undefined;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -154,7 +154,13 @@ export type EngineDescriptor = {
|
|
|
154
154
|
* - live mode selects (may be absent)
|
|
155
155
|
*/
|
|
156
156
|
optionPanes?: OptionPane[];
|
|
157
|
-
|
|
157
|
+
/**
|
|
158
|
+
* most engines answer synchronously; an engine whose run resolves real
|
|
159
|
+
* promises (the contract engine's dispatch panel) answers a thenable
|
|
160
|
+
* Result, which `runExample` passes through with the same never-throw
|
|
161
|
+
* contract (a rejection settles into an error Result)
|
|
162
|
+
*/
|
|
163
|
+
run: (source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult | Promise<PlayResult>;
|
|
158
164
|
};
|
|
159
165
|
export type RunOptions = {
|
|
160
166
|
/**
|
|
@@ -289,7 +295,11 @@ export type PlayExample = {
|
|
|
289
295
|
* @property {EnginePane[]} sourcePanes - the engine INPUT pane(s)
|
|
290
296
|
* @property {EnginePane[]} dataPanes - the JSON it runs against (may be [])
|
|
291
297
|
* @property {OptionPane[]} [optionPanes] - live mode selects (may be absent)
|
|
292
|
-
* @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult} run
|
|
298
|
+
* @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult | Promise<PlayResult>} run
|
|
299
|
+
* most engines answer synchronously; an engine whose run resolves real
|
|
300
|
+
* promises (the contract engine's dispatch panel) answers a thenable
|
|
301
|
+
* Result, which `runExample` passes through with the same never-throw
|
|
302
|
+
* contract (a rejection settles into an error Result)
|
|
293
303
|
*/
|
|
294
304
|
/**
|
|
295
305
|
* @typedef {Object} RunOptions
|
|
@@ -328,12 +338,15 @@ export declare function engineIds(): string[];
|
|
|
328
338
|
export declare const EXAMPLES: readonly PlayExample[];
|
|
329
339
|
/**
|
|
330
340
|
* Run one engine over a source + data. An unknown engine (or a throwing
|
|
331
|
-
* runner) yields an error Result — this never throws.
|
|
341
|
+
* runner) yields an error Result — this never throws. A synchronous
|
|
342
|
+
* engine answers a Result; an async engine (the contract dispatch)
|
|
343
|
+
* answers a Promise of one that never rejects — a host that must know
|
|
344
|
+
* which awaits `Promise.resolve(runExample(…))`.
|
|
332
345
|
* @param {string} engineId
|
|
333
346
|
* @param {Record<string, string>} source
|
|
334
347
|
* @param {Record<string, string>} data
|
|
335
348
|
* @param {RunOptions} [options] - operators (query/jslt), the option-pane
|
|
336
349
|
* config, and host renderers (markdown/mermaid/charts)
|
|
337
|
-
* @returns {PlayResult}
|
|
350
|
+
* @returns {PlayResult | Promise<PlayResult>}
|
|
338
351
|
*/
|
|
339
|
-
export declare function runExample(engineId: string, source: Record<string, string>, data: Record<string, string>, options?: RunOptions): PlayResult
|
|
352
|
+
export declare function runExample(engineId: string, source: Record<string, string>, data: Record<string, string>, options?: RunOptions): PlayResult | Promise<PlayResult>;
|
package/docs/PLAY-FORMAT.md
CHANGED
|
@@ -13,7 +13,7 @@ EngineDescriptor = {
|
|
|
13
13
|
lead?: string, // one line describing the engine
|
|
14
14
|
sourcePanes: EnginePane[], // the engine INPUT (usually one)
|
|
15
15
|
dataPanes: EnginePane[], // the JSON it runs against (may be empty)
|
|
16
|
-
run: (source, data) => PlayResult
|
|
16
|
+
run: (source, data) => PlayResult | Promise<PlayResult>,
|
|
17
17
|
}
|
|
18
18
|
EnginePane = { key: string, label: string, control?: 'code' | 'text' }
|
|
19
19
|
```
|
|
@@ -27,6 +27,13 @@ EnginePane = { key: string, label: string, control?: 'code' | 'text' }
|
|
|
27
27
|
- `run(source, data)` is PURE and NEVER throws: it wraps the real shipped
|
|
28
28
|
compiler and returns a `PlayResult`. `source` / `data` are maps keyed
|
|
29
29
|
by the pane `key`s, holding the raw text.
|
|
30
|
+
- Most engines answer synchronously. An engine whose run resolves real
|
|
31
|
+
promises (the contract engine's dispatch panel invokes a local client)
|
|
32
|
+
answers a **thenable** `PlayResult`; `runExample` passes it through
|
|
33
|
+
under the same never-throw contract — a rejection settles into an
|
|
34
|
+
error Result, so `await Promise.resolve(runExample(…))` is total for
|
|
35
|
+
every engine. A host that renders live must drop a settled result an
|
|
36
|
+
even newer run superseded (the website keys runs by a sequence).
|
|
30
37
|
|
|
31
38
|
## §2 The result
|
|
32
39
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/play",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.43.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -56,9 +56,10 @@
|
|
|
56
56
|
"prepack": "npm run build:types"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@jarenjs/
|
|
60
|
-
"@jarenjs/
|
|
61
|
-
"@jarenjs/
|
|
62
|
-
"@jarenjs/
|
|
59
|
+
"@jarenjs/contract": "^0.43.1",
|
|
60
|
+
"@jarenjs/core": "^0.43.1",
|
|
61
|
+
"@jarenjs/josl": "^0.43.1",
|
|
62
|
+
"@jarenjs/json": "^0.43.1",
|
|
63
|
+
"@jarenjs/validate": "^0.43.1"
|
|
63
64
|
}
|
|
64
65
|
}
|
package/src/engines.js
CHANGED
|
@@ -18,6 +18,9 @@ import { parseXQuery } from '@jarenjs/json/xquery';
|
|
|
18
18
|
import { parseJosl, stringifyJosl, stringifyJsonx } from '@jarenjs/josl';
|
|
19
19
|
import { parseCsvDocument, stringifyCsv, sniffCsvDialect } from '@jarenjs/josl/csv';
|
|
20
20
|
import { createTypeTestCompiler } from '@jarenjs/validate/query';
|
|
21
|
+
import { compileContract } from '@jarenjs/contract';
|
|
22
|
+
import { openLocalClient } from '@jarenjs/contract/local';
|
|
23
|
+
import { toOpenApi, toTypeScript } from '@jarenjs/contract/project';
|
|
21
24
|
import { formatMs } from './format.js';
|
|
22
25
|
|
|
23
26
|
const compileTypeTest = createTypeTestCompiler();
|
|
@@ -555,6 +558,74 @@ export const ENGINE_LIST = [
|
|
|
555
558
|
return okPanels(renderedPanels(view), ...renderTiming(view, t1 - t0));
|
|
556
559
|
},
|
|
557
560
|
},
|
|
561
|
+
{
|
|
562
|
+
// the one ASYNC engine: the dispatch panel resolves a real client
|
|
563
|
+
// outcome, so run() returns a Promise — runExample and the hosts
|
|
564
|
+
// pass a thenable Result through (PLAY-FORMAT §2)
|
|
565
|
+
id: 'contract', label: 'Contract', lead: 'A $contract document — operations two ends may exchange, compiled, projected and dispatched in-process.',
|
|
566
|
+
sourcePanes: [{ key: 'document', label: '$contract document', control: 'code' }],
|
|
567
|
+
dataPanes: [{ key: 'call', label: 'Dispatch — { "op", "input" }' }],
|
|
568
|
+
async run(source, data) {
|
|
569
|
+
const d = parseJson(source.document, 'document'); if (d.error) return failParse(d);
|
|
570
|
+
let contract; const t0 = now();
|
|
571
|
+
// a compile refusal is the engine's lesson: a stable JC00xx code
|
|
572
|
+
// with the JSON Pointer of the member at fault
|
|
573
|
+
try { contract = compileContract(d.value); }
|
|
574
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'document')); }
|
|
575
|
+
const t1 = now();
|
|
576
|
+
let openapi, types;
|
|
577
|
+
try {
|
|
578
|
+
openapi = toOpenApi(contract, {
|
|
579
|
+
info: {
|
|
580
|
+
title: typeof d.value?.id === 'string' ? d.value.id : 'contract',
|
|
581
|
+
version: typeof d.value?.version === 'string' ? d.value.version : '0',
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
types = toTypeScript(contract);
|
|
585
|
+
}
|
|
586
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'document')); }
|
|
587
|
+
// the dispatch pane: an op + input run against trivial echo
|
|
588
|
+
// handlers over the local binding — the whole pipeline with no
|
|
589
|
+
// network. Output validation is DECLARED off (a capability the
|
|
590
|
+
// binding states), so any echo crosses; input validation still
|
|
591
|
+
// refuses a bad input with its outcome, which is the lesson.
|
|
592
|
+
/** @type {import('./index.js').Panel} */
|
|
593
|
+
let dispatch = {
|
|
594
|
+
id: 'dispatch', label: 'Dispatch', kind: 'note', tone: 'info',
|
|
595
|
+
text: 'Type { "op": "<operation id>", "input": { … } } in the Dispatch pane to run an operation against echo handlers (validateOutput declared \'never\').',
|
|
596
|
+
};
|
|
597
|
+
if (String(data.call ?? '').trim() !== '') {
|
|
598
|
+
const call = parseJson(data.call, 'call'); if (call.error) return failParse(call);
|
|
599
|
+
const op = call.value !== null && typeof call.value === 'object' ? call.value.op : undefined;
|
|
600
|
+
if (typeof op !== 'string') {
|
|
601
|
+
dispatch = { id: 'dispatch', label: 'Dispatch', kind: 'note', tone: 'warn', text: 'the call must be an object naming an "op" (and optionally an "input")' };
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
/** @type {Record<string, (input: any) => any>} */
|
|
605
|
+
const handlers = {};
|
|
606
|
+
for (const id of Object.keys(contract.operations)) handlers[id] = (input) => input;
|
|
607
|
+
const client = openLocalClient(contract, handlers, { validateOutput: 'never' });
|
|
608
|
+
try {
|
|
609
|
+
const outcome = await client.invoke(op, call.value.input ?? {});
|
|
610
|
+
dispatch = { id: 'dispatch', label: 'Dispatch (echo)', kind: 'code', text: fmt(outcome) };
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
// a host mistake (JC1005: unknown, opaque or subscribe
|
|
614
|
+
// operation) is the panel's answer, not an engine failure
|
|
615
|
+
dispatch = { id: 'dispatch', label: 'Dispatch', kind: 'note', tone: 'warn', text: msg(err) };
|
|
616
|
+
}
|
|
617
|
+
finally { client.close(); }
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const t2 = now();
|
|
621
|
+
return okPanels([
|
|
622
|
+
{ id: 'describe', label: 'describe()', kind: 'code', text: fmt(contract.describe()) },
|
|
623
|
+
{ id: 'openapi', label: 'OpenAPI', kind: 'code', text: fmt(openapi.document) },
|
|
624
|
+
{ id: 'types', label: 'TypeScript', kind: 'code', text: types },
|
|
625
|
+
dispatch,
|
|
626
|
+
], t1 - t0, t2 - t1);
|
|
627
|
+
},
|
|
628
|
+
},
|
|
558
629
|
// ——— the visual engines: descriptor + examples here, rendering delegated ———
|
|
559
630
|
visual('markdown', 'Markdown', 'CommonMark + GFM + frontmatter → a JSON AST, rendered live.', { sourceLabel: 'Markdown' }),
|
|
560
631
|
visual('mermaid', 'Mermaid', 'Diagrams-as-code → a geometry-free AST → pure-vnode SVG.', { sourceLabel: 'Mermaid' }),
|
package/src/examples.js
CHANGED
|
@@ -543,4 +543,46 @@ is a fixed point.
|
|
|
543
543
|
name: { type: 'string', minLength: 2 }, email: { type: 'string', format: 'email' }, age: { type: 'integer', minimum: 13 },
|
|
544
544
|
}, required: ['name', 'email'] }) },
|
|
545
545
|
datasets: [{ label: 'invalid', data: { data: j({ name: 'A', email: 'not-an-email', age: 7 }) } }] },
|
|
546
|
+
|
|
547
|
+
// ——— Contract ($contract document → describe / OpenAPI / TypeScript / dispatch) ———
|
|
548
|
+
{ id: 'contract-shop', label: 'A shop contract', engine: 'contract',
|
|
549
|
+
source: { document: j({ $contract: '0.1', id: 'shop', version: '1',
|
|
550
|
+
$defs: { Product: { type: 'object', required: ['id', 'name', 'price'], properties: {
|
|
551
|
+
id: { type: 'integer' }, name: { type: 'string', minLength: 1 }, price: { type: 'number', minimum: 0 } } } },
|
|
552
|
+
operations: {
|
|
553
|
+
'catalog.load': { kind: 'read',
|
|
554
|
+
input: { type: 'object', properties: { since: { type: 'string', format: 'date-time' } } },
|
|
555
|
+
output: { type: 'array', items: { $ref: '#/$defs/Product' } },
|
|
556
|
+
http: { method: 'GET', path: '/api/catalog' },
|
|
557
|
+
doc: 'The whole catalog.' },
|
|
558
|
+
'product.save': { kind: 'command',
|
|
559
|
+
input: { type: 'object', required: ['id', 'product'], properties: {
|
|
560
|
+
id: { type: 'integer' }, product: { $ref: '#/$defs/Product' } } },
|
|
561
|
+
output: { $ref: '#/$defs/Product' },
|
|
562
|
+
errors: { conflict: { status: 409 } },
|
|
563
|
+
policy: { idempotency: 'optional' },
|
|
564
|
+
http: { method: 'PUT', path: '/api/products/{id}' } },
|
|
565
|
+
} }) },
|
|
566
|
+
datasets: [
|
|
567
|
+
{ label: 'a valid save', data: { call: j({ op: 'product.save', input: { id: 7, product: { id: 7, name: 'Duck', price: 9.99 } } }) } },
|
|
568
|
+
{ label: 'an invalid input', data: { call: j({ op: 'product.save', input: { id: 'seven' } }) } },
|
|
569
|
+
{ label: 'no dispatch', data: { call: '' } },
|
|
570
|
+
] },
|
|
571
|
+
{ id: 'contract-minimal', label: 'One operation, no http', engine: 'contract',
|
|
572
|
+
source: { document: j({ $contract: '0.1', id: 'echo', operations: {
|
|
573
|
+
'echo.say': { kind: 'command',
|
|
574
|
+
input: { type: 'object', required: ['text'], properties: { text: { type: 'string' } } },
|
|
575
|
+
output: true,
|
|
576
|
+
doc: 'No http member: the binding defaults to POST /echo.say.' },
|
|
577
|
+
} }) },
|
|
578
|
+
datasets: [{ label: 'say something', data: { call: j({ op: 'echo.say', input: { text: 'hello' } }) } }] },
|
|
579
|
+
{ id: 'contract-broken', label: 'A refusal, with its docPath', engine: 'contract',
|
|
580
|
+
source: { document: j({ $contract: '0.1', id: 'broken', operations: {
|
|
581
|
+
'catalog.load': { kind: 'read',
|
|
582
|
+
input: { type: 'object', properties: { since: { type: 'string' } } },
|
|
583
|
+
// no output member: JC00xx at compile, never at request time —
|
|
584
|
+
// the error names /operations/catalog.load with its code
|
|
585
|
+
http: { method: 'GET', path: '/api/catalog' } },
|
|
586
|
+
} }) },
|
|
587
|
+
datasets: [{ label: 'nothing to dispatch', data: { call: '' } }] },
|
|
546
588
|
];
|
package/src/index.js
CHANGED
|
@@ -98,7 +98,11 @@ import { EXAMPLE_LIST } from './examples.js';
|
|
|
98
98
|
* @property {EnginePane[]} sourcePanes - the engine INPUT pane(s)
|
|
99
99
|
* @property {EnginePane[]} dataPanes - the JSON it runs against (may be [])
|
|
100
100
|
* @property {OptionPane[]} [optionPanes] - live mode selects (may be absent)
|
|
101
|
-
* @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult} run
|
|
101
|
+
* @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult | Promise<PlayResult>} run
|
|
102
|
+
* most engines answer synchronously; an engine whose run resolves real
|
|
103
|
+
* promises (the contract engine's dispatch panel) answers a thenable
|
|
104
|
+
* Result, which `runExample` passes through with the same never-throw
|
|
105
|
+
* contract (a rejection settles into an error Result)
|
|
102
106
|
*/
|
|
103
107
|
|
|
104
108
|
/**
|
|
@@ -162,23 +166,31 @@ function withConfig(engine, options) {
|
|
|
162
166
|
|
|
163
167
|
/**
|
|
164
168
|
* Run one engine over a source + data. An unknown engine (or a throwing
|
|
165
|
-
* runner) yields an error Result — this never throws.
|
|
169
|
+
* runner) yields an error Result — this never throws. A synchronous
|
|
170
|
+
* engine answers a Result; an async engine (the contract dispatch)
|
|
171
|
+
* answers a Promise of one that never rejects — a host that must know
|
|
172
|
+
* which awaits `Promise.resolve(runExample(…))`.
|
|
166
173
|
* @param {string} engineId
|
|
167
174
|
* @param {Record<string, string>} source
|
|
168
175
|
* @param {Record<string, string>} data
|
|
169
176
|
* @param {RunOptions} [options] - operators (query/jslt), the option-pane
|
|
170
177
|
* config, and host renderers (markdown/mermaid/charts)
|
|
171
|
-
* @returns {PlayResult}
|
|
178
|
+
* @returns {PlayResult | Promise<PlayResult>}
|
|
172
179
|
*/
|
|
173
180
|
export function runExample(engineId, source, data, options = {}) {
|
|
174
181
|
const engine = ENGINES[engineId];
|
|
175
182
|
if (engine === undefined) {
|
|
176
183
|
return { ok: false, timing: null, error: { message: `unknown engine: ${engineId}` }, panels: [] };
|
|
177
184
|
}
|
|
185
|
+
const failed = (/** @type {unknown} */ err) =>
|
|
186
|
+
/** @type {PlayResult} */ ({ ok: false, timing: null, error: { message: String(/** @type {any} */ (err)?.message ?? err) }, panels: [] });
|
|
178
187
|
try {
|
|
179
|
-
|
|
188
|
+
const result = engine.run(source ?? {}, data ?? {}, withConfig(engine, options));
|
|
189
|
+
return result !== null && typeof result === 'object' && typeof (/** @type {any} */ (result).then) === 'function'
|
|
190
|
+
? /** @type {Promise<PlayResult>} */ (result).then((r) => r, failed)
|
|
191
|
+
: result;
|
|
180
192
|
}
|
|
181
193
|
catch (err) {
|
|
182
|
-
return
|
|
194
|
+
return failed(err);
|
|
183
195
|
}
|
|
184
196
|
}
|