@jarenjs/contract 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 +508 -0
- package/dist/types/adapters/fetch.d.ts +27 -0
- package/dist/types/adapters/node.d.ts +47 -0
- package/dist/types/app/binding.d.ts +122 -0
- package/dist/types/app/effect.d.ts +77 -0
- package/dist/types/app/index.d.ts +31 -0
- package/dist/types/app/subscription.d.ts +82 -0
- package/dist/types/bundle.d.ts +43 -0
- package/dist/types/cli.d.ts +15 -0
- package/dist/types/client/http.d.ts +242 -0
- package/dist/types/client/outcome.d.ts +289 -0
- package/dist/types/compat.d.ts +36 -0
- package/dist/types/compile.d.ts +196 -0
- package/dist/types/describe.d.ts +115 -0
- package/dist/types/diff.d.ts +91 -0
- package/dist/types/errors.d.ts +205 -0
- package/dist/types/http/dispatch.d.ts +148 -0
- package/dist/types/http/serve.d.ts +154 -0
- package/dist/types/http/wire.d.ts +334 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/ledger.d.ts +207 -0
- package/dist/types/local/index.d.ts +127 -0
- package/dist/types/messages.d.ts +63 -0
- package/dist/types/path.d.ts +119 -0
- package/dist/types/pipeline.d.ts +157 -0
- package/dist/types/port/client.d.ts +142 -0
- package/dist/types/port/frame.d.ts +195 -0
- package/dist/types/port/serve.d.ts +102 -0
- package/dist/types/project/index.d.ts +34 -0
- package/dist/types/project/markdown.d.ts +28 -0
- package/dist/types/project/openapi.d.ts +102 -0
- package/dist/types/project/tools.d.ts +57 -0
- package/dist/types/project/typescript.d.ts +59 -0
- package/dist/types/public.d.ts +73 -0
- package/dist/types/revision.d.ts +36 -0
- package/dist/types/stream/client.d.ts +104 -0
- package/dist/types/stream/server.d.ts +106 -0
- package/dist/types/stream/sse.d.ts +62 -0
- package/docs/APP-INTEGRATION.md +301 -0
- package/docs/CONTRACT-FORMAT.md +1923 -0
- package/package.json +110 -0
- package/schemas/jaren-contract-port.draft-07.schema.json +241 -0
- package/schemas/jaren-contract-port.schema.json +241 -0
- package/schemas/jaren-contract.draft-07.schema.json +287 -0
- package/schemas/jaren-contract.schema.json +287 -0
- package/src/adapters/fetch.js +109 -0
- package/src/adapters/node.js +238 -0
- package/src/app/binding.js +426 -0
- package/src/app/effect.js +190 -0
- package/src/app/index.js +26 -0
- package/src/app/subscription.js +130 -0
- package/src/bundle.js +168 -0
- package/src/cli.js +264 -0
- package/src/client/http.js +1150 -0
- package/src/client/outcome.js +364 -0
- package/src/compat.js +62 -0
- package/src/compile.js +1162 -0
- package/src/describe.js +109 -0
- package/src/diff.js +610 -0
- package/src/errors.js +236 -0
- package/src/http/dispatch.js +1054 -0
- package/src/http/serve.js +301 -0
- package/src/http/wire.js +469 -0
- package/src/index.js +33 -0
- package/src/ledger.js +225 -0
- package/src/local/index.js +363 -0
- package/src/messages.js +68 -0
- package/src/path.js +471 -0
- package/src/pipeline.js +241 -0
- package/src/port/client.js +518 -0
- package/src/port/frame.js +196 -0
- package/src/port/serve.js +442 -0
- package/src/project/index.js +29 -0
- package/src/project/markdown.js +244 -0
- package/src/project/openapi.js +564 -0
- package/src/project/openapi.jslt.json +149 -0
- package/src/project/tools.js +139 -0
- package/src/project/typescript.js +152 -0
- package/src/project/typescript.jtlt.json +72 -0
- package/src/public.js +206 -0
- package/src/revision.js +90 -0
- package/src/stream/client.js +212 -0
- package/src/stream/server.js +306 -0
- package/src/stream/sse.js +67 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `toMarkdown`: a compiled contract as one reference document —
|
|
4
|
+
* the title and version, an operations table, one section per public
|
|
5
|
+
* operation (its doc, parameters, body, responses, errors) and the types
|
|
6
|
+
* rendered through `@jarenjs/emit`'s Markdown target over the SAME type
|
|
7
|
+
* model the TypeScript projection uses (`contractTypeModel`), so the
|
|
8
|
+
* names a reader meets in the operation sections are the names the
|
|
9
|
+
* `.d.ts` declares. The document shape is a JTLT stylesheet (compiled
|
|
10
|
+
* once at module scope); the JavaScript half derives the rows.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isJsonObject } from '@jarenjs/core/object';
|
|
14
|
+
import { compileJtltStylesheet } from '@jarenjs/json/jtlt';
|
|
15
|
+
import { createTypeTestCompiler } from '@jarenjs/validate/query';
|
|
16
|
+
import { renderMarkdown } from '@jarenjs/emit/markdown';
|
|
17
|
+
|
|
18
|
+
import { ContractHostError } from '../errors.js';
|
|
19
|
+
import { retainedOperations } from '../public.js';
|
|
20
|
+
import { contractTypeModel } from './typescript.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {import('../compile.js').Contract} Contract
|
|
24
|
+
* @typedef {import('../compile.js').CompiledOperation} CompiledOperation
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} MarkdownOptions
|
|
29
|
+
* @property {string} [title] - the document title (default: the contract id, or `jaren-contract`)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** The document shape. */
|
|
33
|
+
const MARKDOWN_STYLESHEET = {
|
|
34
|
+
$jtlt: '0.1',
|
|
35
|
+
output: 'text',
|
|
36
|
+
rules: [
|
|
37
|
+
{
|
|
38
|
+
match: '$',
|
|
39
|
+
body: [
|
|
40
|
+
'# ', '$.title', '\n\n',
|
|
41
|
+
{ $if: [{ $exists: '$.version' }, { $concat: ['Version `', '$.version', '`'] }, 'Unversioned'] },
|
|
42
|
+
{ $if: [{ $exists: '$.compat[*]' }, { $concat: [', compatible with `', { '$string-join': ['$.compat[*]', '`, `'] }, '`'] }, ''] },
|
|
43
|
+
'. Generated from the jaren-contract document by @jarenjs/contract — do not edit; regenerate instead.\n\n',
|
|
44
|
+
'## Operations\n\n',
|
|
45
|
+
'| Operation | Method | Path | Kind | Task | Idempotency |\n| --- | --- | --- | --- | --- | --- |\n',
|
|
46
|
+
[{ $apply: ['$.operations[*]', 'row'] }],
|
|
47
|
+
'\n',
|
|
48
|
+
[{ $apply: ['$.operations[*]', 'section'] }],
|
|
49
|
+
'# Types\n\n',
|
|
50
|
+
'$.types',
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
mode: 'row',
|
|
55
|
+
body: ['| [`', '$.id', '`](#', '$.anchor', ') | `', '$.method', '` | `', '$.path', '` | ', '$.kind', ' | `', '$.task', '` | `', '$.idempotency', '` |\n'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
mode: 'section',
|
|
59
|
+
body: [
|
|
60
|
+
'## ', '$.id', '\n\n',
|
|
61
|
+
{ $if: [{ $exists: '$.doc' }, { $concat: ['$.doc', '\n\n'] }, ''] },
|
|
62
|
+
'`', '$.method', ' ', '$.path', '` — a ', '$.kind', ' operation',
|
|
63
|
+
{ $if: ['$.opaque', ' (opaque: the response bytes are not decoded by the contract)', ''] },
|
|
64
|
+
'; task `', '$.task', '`, idempotency `', '$.idempotency', '`, cache `', '$.cache', '`',
|
|
65
|
+
{ $if: [{ $exists: '$.revision' }, { $concat: [', revision `', '$.revision', '`'] }, ''] },
|
|
66
|
+
{ $if: [{ $exists: '$.retry' }, { $concat: [', retry up to ', '$.retry.max', ' time(s) on `', { '$string-join': ['$.retry.on[*]', '`, `'] }, '`'] }, ''] },
|
|
67
|
+
'.\n\n',
|
|
68
|
+
'### Parameters\n\n',
|
|
69
|
+
{
|
|
70
|
+
$if: [
|
|
71
|
+
{ $exists: '$.parameters[*]' },
|
|
72
|
+
{ $concat: ['| Name | In | Required | Schema |\n| --- | --- | --- | --- |\n'] },
|
|
73
|
+
'None.\n',
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
[{ $apply: ['$.parameters[*]', 'parameter'] }],
|
|
77
|
+
'\n### Body\n\n',
|
|
78
|
+
{ $if: [{ $exists: '$.body' }, { $concat: ['$.body.description', '\n'] }, 'None.\n'] },
|
|
79
|
+
'\n### Responses\n\n',
|
|
80
|
+
'| Status | Description | Schema |\n| --- | --- | --- |\n',
|
|
81
|
+
'| ', '$.success.status', ' | Success | ', '$.success.schema', ' |\n',
|
|
82
|
+
[{ $apply: ['$.errors[*]', 'response'] }],
|
|
83
|
+
'\n### Errors\n\n',
|
|
84
|
+
{
|
|
85
|
+
$if: [
|
|
86
|
+
{ $exists: '$.errors[*]' },
|
|
87
|
+
{ $concat: ['| Code | Status | Details |\n| --- | --- | --- |\n'] },
|
|
88
|
+
'None declared.\n',
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
[{ $apply: ['$.errors[*]', 'error'] }],
|
|
92
|
+
'\n',
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{ mode: 'parameter', body: ['| `', '$.name', '` | ', '$.in', ' | ', '$.required', ' | ', '$.schema', ' |\n'] },
|
|
96
|
+
{ mode: 'response', body: ['| ', '$.status', ' | Declared failure `', '$.code', '` | wire error |\n'] },
|
|
97
|
+
{ mode: 'error', body: ['| `', '$.code', '` | ', '$.status', ' | ', '$.details', ' |\n'] },
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** The stylesheet, compiled once. */
|
|
102
|
+
const render = compileJtltStylesheet(MARKDOWN_STYLESHEET, { compileTypeTest: createTypeTestCompiler() });
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A GitHub-style heading anchor of an operation section: lowercase,
|
|
106
|
+
* punctuation dropped — `product.save` → `productsave`.
|
|
107
|
+
* @param {string} id
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
function anchorOf(id) {
|
|
111
|
+
return id.replace(/[^a-z0-9]/g, '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A Markdown link to a declared type's section in the Types part.
|
|
116
|
+
* @param {string} name
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
function typeLink(name) {
|
|
120
|
+
return `[\`${name}\`](#${name.toLowerCase()})`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A short label of a member schema for a table cell: the type (with a
|
|
125
|
+
* format), a `$ref` target, an enum, a constant, or the schema as JSON
|
|
126
|
+
* when it is none of those. Detail lives in the Types section.
|
|
127
|
+
* @param {unknown} schema
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
function schemaLabel(schema) {
|
|
131
|
+
if (schema === true || schema === undefined) return 'any';
|
|
132
|
+
if (schema === false) return 'never';
|
|
133
|
+
if (!isJsonObject(schema)) return 'any';
|
|
134
|
+
if (typeof schema.$ref === 'string') {
|
|
135
|
+
const ref = schema.$ref;
|
|
136
|
+
const name = ref.startsWith('#/$defs/') ? ref.slice('#/$defs/'.length).split('/')[0] : null;
|
|
137
|
+
return name !== null && name.length > 0 ? typeLink(name) : `\`${ref}\``;
|
|
138
|
+
}
|
|
139
|
+
if (schema.const !== undefined) return `\`${JSON.stringify(schema.const)}\``;
|
|
140
|
+
if (Array.isArray(schema.enum)) return `one of ${schema.enum.map((v) => `\`${JSON.stringify(v)}\``).join(', ')}`;
|
|
141
|
+
const type = Array.isArray(schema.type) ? schema.type.join(' \\| ') : (typeof schema.type === 'string' ? schema.type : null);
|
|
142
|
+
if (type !== null) {
|
|
143
|
+
let label = `\`${type}\``;
|
|
144
|
+
if (type === 'array' && isJsonObject(schema.items)) label += ` of ${schemaLabel(schema.items)}`;
|
|
145
|
+
if (typeof schema.format === 'string') label += ` (${schema.format})`;
|
|
146
|
+
return label;
|
|
147
|
+
}
|
|
148
|
+
return `\`${JSON.stringify(schema)}\``;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The rows of one operation section.
|
|
153
|
+
* @param {CompiledOperation} op
|
|
154
|
+
* @param {Record<string, any>} row - the type-model row of the operation (names)
|
|
155
|
+
* @returns {Record<string, unknown>}
|
|
156
|
+
*/
|
|
157
|
+
function operationRows(op, row) {
|
|
158
|
+
const http = op.http;
|
|
159
|
+
/** @type {Record<string, unknown>[]} */
|
|
160
|
+
const parameters = [];
|
|
161
|
+
/** @type {string[]} */
|
|
162
|
+
const bodyMembers = [];
|
|
163
|
+
const transport = op.input === null ? null : op.input.transport;
|
|
164
|
+
const required = op.input !== null && Array.isArray(op.input.effective.required) ? op.input.effective.required : [];
|
|
165
|
+
const members = Object.keys(http.in);
|
|
166
|
+
for (let i = 0; i < members.length; i++) {
|
|
167
|
+
const name = members[i];
|
|
168
|
+
const loc = http.in[name];
|
|
169
|
+
if (loc === 'body') {
|
|
170
|
+
bodyMembers.push(name);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
parameters.push({
|
|
174
|
+
name, in: loc, required: loc === 'path' || required.includes(name) ? 'yes' : 'no',
|
|
175
|
+
schema: schemaLabel(/** @type {any} */ (transport).schemas[name]),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (op.policy.idempotency !== 'none') {
|
|
179
|
+
parameters.push({
|
|
180
|
+
name: 'Idempotency-Key', in: 'header', required: op.policy.idempotency === 'required' ? 'yes' : 'no',
|
|
181
|
+
schema: '`string` (the caller-generated idempotency key)',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** @type {Record<string, unknown>} */
|
|
185
|
+
const out = {
|
|
186
|
+
id: op.id, anchor: anchorOf(op.id), method: http.method, path: http.path, kind: op.kind,
|
|
187
|
+
task: op.policy.task, idempotency: op.policy.idempotency, cache: op.policy.cache, opaque: http.opaque,
|
|
188
|
+
};
|
|
189
|
+
if (op.policy.revision !== null) out.revision = op.policy.revision;
|
|
190
|
+
if (op.policy.retry !== null) out.retry = { max: op.policy.retry.max, on: op.policy.retry.on.slice() };
|
|
191
|
+
if (op.doc !== null) out.doc = op.doc;
|
|
192
|
+
out.parameters = parameters;
|
|
193
|
+
const inputLink = typeof row.input === 'string' && row.input !== 'null' ? typeLink(row.input) : null;
|
|
194
|
+
if (http.body !== null) {
|
|
195
|
+
out.body = { description: `\`${http.media}\` — the \`${http.body}\` member of ${inputLink} is the whole body${required.includes(http.body) ? ' (required)' : ''}.` };
|
|
196
|
+
}
|
|
197
|
+
else if (bodyMembers.length > 0) {
|
|
198
|
+
out.body = {
|
|
199
|
+
description: bodyMembers.length === members.length
|
|
200
|
+
? `\`${http.media}\` — ${inputLink}.`
|
|
201
|
+
: `\`${http.media}\` — an object of the ${inputLink} members ${bodyMembers.map((m) => `\`${m}\``).join(', ')}.`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
out.success = {
|
|
205
|
+
status: String(http.status),
|
|
206
|
+
schema: http.opaque ? `\`${http.media}\` bytes` : (http.status === 204 || http.method === 'HEAD' ? 'no body' : typeLink(row.output)),
|
|
207
|
+
};
|
|
208
|
+
/** @type {Record<string, unknown>[]} */
|
|
209
|
+
const errors = [];
|
|
210
|
+
const codes = Object.keys(op.errors);
|
|
211
|
+
for (let i = 0; i < codes.length; i++) {
|
|
212
|
+
const detailsName = row.details[codes[i]];
|
|
213
|
+
errors.push({ code: codes[i], status: String(op.errors[codes[i]].status), details: detailsName === null ? '—' : typeLink(detailsName) });
|
|
214
|
+
}
|
|
215
|
+
out.errors = errors;
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Project a compiled contract to Markdown reference documentation.
|
|
221
|
+
* @param {Contract} contract
|
|
222
|
+
* @param {MarkdownOptions} [options]
|
|
223
|
+
* @returns {string} Markdown
|
|
224
|
+
* @throws {ContractHostError} `JC1008` — not a compiled contract, or a malformed option
|
|
225
|
+
* @example
|
|
226
|
+
* writeFileSync('shop.md', toMarkdown(contract, { title: 'Shop API' }));
|
|
227
|
+
*/
|
|
228
|
+
export function toMarkdown(contract, options = {}) {
|
|
229
|
+
if (!isJsonObject(options)) throw new ContractHostError('JC1008', 'toMarkdown: options must be an object');
|
|
230
|
+
const ops = retainedOperations(contract, undefined, 'toMarkdown');
|
|
231
|
+
if (options.title !== undefined && typeof options.title !== 'string') {
|
|
232
|
+
throw new ContractHostError('JC1008', 'toMarkdown: options.title must be a string');
|
|
233
|
+
}
|
|
234
|
+
const { rows, model } = contractTypeModel(contract, ops, contract.id === null ? 'a jaren-contract document' : `the jaren-contract '${contract.id}'`);
|
|
235
|
+
/** @type {Record<string, unknown>} */
|
|
236
|
+
const input = {
|
|
237
|
+
title: options.title ?? (contract.id === null ? 'jaren-contract' : contract.id),
|
|
238
|
+
compat: contract.compat.slice(),
|
|
239
|
+
operations: ops.map((op, i) => operationRows(op, rows[i])),
|
|
240
|
+
types: renderMarkdown(model),
|
|
241
|
+
};
|
|
242
|
+
if (contract.version !== null) input.version = contract.version;
|
|
243
|
+
return render(input);
|
|
244
|
+
}
|