@loutrejs/loutre 0.4.0 → 0.5.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.
- package/dist/application/openapi.js +2 -1
- package/dist/core/context-key.d.ts +3 -1
- package/dist/core/context-key.js +3 -0
- package/dist/core/contract-internal.d.ts +22 -0
- package/dist/core/contract-internal.js +61 -0
- package/dist/core/contract.d.ts +34 -32
- package/dist/core/contract.js +35 -26
- package/dist/core/implementation.d.ts +25 -15
- package/dist/core/implementation.js +8 -3
- package/dist/core/layer.d.ts +30 -2
- package/dist/core/layer.js +29 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/graph.d.ts +2 -1
- package/dist/graph/graph.js +39 -24
- package/dist/http/application.js +10 -1
- package/dist/http/client.d.ts +4 -4
- package/dist/http/client.js +7 -2
- package/dist/http/definitions.d.ts +155 -17
- package/dist/http/definitions.js +244 -3
- package/dist/http/internal.d.ts +8 -0
- package/dist/http/internal.js +1 -0
- package/dist/http/path.d.ts +4 -0
- package/dist/http/path.js +6 -0
- package/package.json +1 -1
package/dist/http/definitions.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { childPipelineOf
|
|
2
|
-
import {
|
|
1
|
+
import { childPipelineOf } from '../core/index.js';
|
|
2
|
+
import { contractNodeBinding, contractNodeMetadata, protocolNamespaceBuilder, protocolNamespaceType, } from '../core/contract-internal.js';
|
|
3
|
+
import { httpNodeMetadata } from './internal.js';
|
|
4
|
+
import { assertValidHttpMethod, createHttpDispatchKey, parseHttpPath, } from './path.js';
|
|
3
5
|
const controller = Object.freeze({
|
|
4
6
|
kind: 'terminal',
|
|
5
7
|
name: 'http.controller',
|
|
@@ -7,6 +9,14 @@ const controller = Object.freeze({
|
|
|
7
9
|
protocol: 'http',
|
|
8
10
|
});
|
|
9
11
|
function defineHttp(definition) {
|
|
12
|
+
assertValidHttpMethod(definition.method);
|
|
13
|
+
for (const response of Object.values(definition.responses)) {
|
|
14
|
+
if (!Number.isInteger(response.status) ||
|
|
15
|
+
response.status < 200 ||
|
|
16
|
+
response.status > 599) {
|
|
17
|
+
throw new Error(`Invalid HTTP response status: ${response.status}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
const segments = parseHttpPath(definition.path);
|
|
11
21
|
const paramsSchemas = definition.request?.params;
|
|
12
22
|
if (paramsSchemas) {
|
|
@@ -38,7 +48,238 @@ function hasParamsValidation(pipeline) {
|
|
|
38
48
|
hasParamsValidation(childPipelineOf(item) ?? [])));
|
|
39
49
|
}
|
|
40
50
|
function defineHttpGroup(definitions) {
|
|
41
|
-
|
|
51
|
+
const resolution = resolveHttpTree(definitions, {
|
|
52
|
+
path: '',
|
|
53
|
+
pipeline: [],
|
|
54
|
+
responses: {},
|
|
55
|
+
procedurePrefix: '',
|
|
56
|
+
});
|
|
57
|
+
const group = {
|
|
58
|
+
kind: 'protocol-group',
|
|
59
|
+
protocol: 'http',
|
|
60
|
+
procedures: Object.freeze({ ...resolution.procedures }),
|
|
61
|
+
};
|
|
62
|
+
Object.defineProperty(group, protocolNamespaceBuilder, {
|
|
63
|
+
enumerable: false,
|
|
64
|
+
value: (root) => buildHttpNamespace(resolution.tree, root, []),
|
|
65
|
+
});
|
|
66
|
+
return Object.freeze(group);
|
|
67
|
+
}
|
|
68
|
+
const HTTP_LEAF_PROPERTIES = new Set([
|
|
69
|
+
'method',
|
|
70
|
+
'path',
|
|
71
|
+
'summary',
|
|
72
|
+
'description',
|
|
73
|
+
'tags',
|
|
74
|
+
'deprecated',
|
|
75
|
+
'request',
|
|
76
|
+
'responses',
|
|
77
|
+
'pipeline',
|
|
78
|
+
'interaction',
|
|
79
|
+
]);
|
|
80
|
+
const HTTP_BRANCH_PROPERTIES = new Set([
|
|
81
|
+
'path',
|
|
82
|
+
'pipeline',
|
|
83
|
+
'responses',
|
|
84
|
+
'routes',
|
|
85
|
+
]);
|
|
86
|
+
function assertHttpRouteTreeObject(definitions) {
|
|
87
|
+
const prototype = Object.getPrototypeOf(definitions);
|
|
88
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
89
|
+
throw new Error('HTTP route tree must be a plain object');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function assertHttpRouteName(name) {
|
|
93
|
+
if (name.length === 0 ||
|
|
94
|
+
name === '__proto__' ||
|
|
95
|
+
name.includes('.') ||
|
|
96
|
+
Number.isFinite(Number(name))) {
|
|
97
|
+
throw new Error(`Invalid HTTP Contract node name: ${JSON.stringify(name)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function assertHttpNodeProperties(source) {
|
|
101
|
+
const allowed = 'method' in source ? HTTP_LEAF_PROPERTIES : HTTP_BRANCH_PROPERTIES;
|
|
102
|
+
for (const property of Object.keys(source)) {
|
|
103
|
+
if (!allowed.has(property)) {
|
|
104
|
+
throw new Error(`Unknown HTTP Contract node property: ${property}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function resolveHttpTree(definitions, context) {
|
|
109
|
+
assertHttpRouteTreeObject(definitions);
|
|
110
|
+
const procedures = {};
|
|
111
|
+
const tree = {};
|
|
112
|
+
for (const [name, candidate] of Object.entries(definitions)) {
|
|
113
|
+
assertHttpRouteName(name);
|
|
114
|
+
const metadata = typeof candidate === 'object' &&
|
|
115
|
+
candidate !== null &&
|
|
116
|
+
httpNodeMetadata in candidate
|
|
117
|
+
? candidate[httpNodeMetadata]
|
|
118
|
+
: undefined;
|
|
119
|
+
const source = (metadata?.source ?? candidate);
|
|
120
|
+
assertHttpNodeProperties(source);
|
|
121
|
+
const procedureName = context.procedurePrefix === ''
|
|
122
|
+
? name
|
|
123
|
+
: `${context.procedurePrefix}.${name}`;
|
|
124
|
+
if ('method' in source) {
|
|
125
|
+
const responses = mergeHttpResponses(context.responses, source.responses);
|
|
126
|
+
const definition = {
|
|
127
|
+
...source,
|
|
128
|
+
path: joinHttpPath(context.path, source.path),
|
|
129
|
+
responses,
|
|
130
|
+
pipeline: [...context.pipeline, ...source.pipeline],
|
|
131
|
+
};
|
|
132
|
+
const protocol = defineHttp(definition);
|
|
133
|
+
procedures[procedureName] = protocol;
|
|
134
|
+
tree[name] = {
|
|
135
|
+
kind: 'leaf',
|
|
136
|
+
source,
|
|
137
|
+
handlerName: metadata?.kind === 'leaf' && metadata.handlerName
|
|
138
|
+
? metadata.handlerName
|
|
139
|
+
: name,
|
|
140
|
+
protocol,
|
|
141
|
+
};
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
assertBranchPipeline(source.pipeline ?? []);
|
|
145
|
+
const branchResponses = mergeHttpResponses(context.responses, source.responses ?? {});
|
|
146
|
+
const branch = resolveHttpTree(source.routes, {
|
|
147
|
+
path: joinHttpPath(context.path, source.path ?? ''),
|
|
148
|
+
pipeline: [...context.pipeline, ...(source.pipeline ?? [])],
|
|
149
|
+
responses: branchResponses,
|
|
150
|
+
procedurePrefix: procedureName,
|
|
151
|
+
});
|
|
152
|
+
Object.assign(procedures, branch.procedures);
|
|
153
|
+
tree[name] = {
|
|
154
|
+
kind: 'branch',
|
|
155
|
+
source,
|
|
156
|
+
children: branch.tree,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return { procedures, tree: Object.freeze(tree) };
|
|
160
|
+
}
|
|
161
|
+
function joinHttpPath(parent, child) {
|
|
162
|
+
if (parent === '')
|
|
163
|
+
return child;
|
|
164
|
+
if (child === '' || child === '/')
|
|
165
|
+
return parent;
|
|
166
|
+
return `${parent}${child}`;
|
|
167
|
+
}
|
|
168
|
+
function mergeHttpResponses(parent, local) {
|
|
169
|
+
for (const name of Object.keys(local)) {
|
|
170
|
+
if (Object.hasOwn(parent, name)) {
|
|
171
|
+
throw new Error(`Duplicate inherited HTTP response variant: ${name}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return Object.freeze({ ...parent, ...local });
|
|
175
|
+
}
|
|
176
|
+
function assertBranchPipeline(pipeline) {
|
|
177
|
+
for (const item of pipeline) {
|
|
178
|
+
if (item.kind === 'terminal') {
|
|
179
|
+
throw new Error('HTTP branch pipeline must not contain a terminal layer');
|
|
180
|
+
}
|
|
181
|
+
if (item.kind === 'layer') {
|
|
182
|
+
const child = childPipelineOf(item);
|
|
183
|
+
if (child)
|
|
184
|
+
assertBranchPipeline(child);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function buildHttpNamespace(tree, root, parentPath) {
|
|
189
|
+
const namespace = {};
|
|
190
|
+
for (const [name, template] of Object.entries(tree)) {
|
|
191
|
+
namespace[name] = buildHttpNode(template, root, [...parentPath, name]);
|
|
192
|
+
}
|
|
193
|
+
return Object.freeze(namespace);
|
|
194
|
+
}
|
|
195
|
+
function buildHttpNode(template, root, path) {
|
|
196
|
+
if (template.kind === 'leaf') {
|
|
197
|
+
const binding = {
|
|
198
|
+
kind: 'contract',
|
|
199
|
+
procedures: Object.freeze({
|
|
200
|
+
[template.handlerName]: Object.freeze({
|
|
201
|
+
kind: 'procedure',
|
|
202
|
+
protocols: Object.freeze({ http: template.protocol }),
|
|
203
|
+
}),
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
Object.defineProperty(binding, contractNodeMetadata, {
|
|
207
|
+
enumerable: false,
|
|
208
|
+
value: Object.freeze({
|
|
209
|
+
kind: 'leaf',
|
|
210
|
+
root,
|
|
211
|
+
path: Object.freeze(['http', ...path]),
|
|
212
|
+
procedures: Object.freeze({
|
|
213
|
+
[template.handlerName]: path.join('.'),
|
|
214
|
+
}),
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
Object.freeze(binding);
|
|
218
|
+
const node = {};
|
|
219
|
+
Object.defineProperty(node, contractNodeBinding, {
|
|
220
|
+
enumerable: false,
|
|
221
|
+
value: binding,
|
|
222
|
+
});
|
|
223
|
+
Object.defineProperty(node, httpNodeMetadata, {
|
|
224
|
+
enumerable: false,
|
|
225
|
+
value: Object.freeze({
|
|
226
|
+
kind: 'leaf',
|
|
227
|
+
root,
|
|
228
|
+
path: Object.freeze([...path]),
|
|
229
|
+
source: template.source,
|
|
230
|
+
handlerName: template.handlerName,
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
return Object.freeze(node);
|
|
234
|
+
}
|
|
235
|
+
const children = buildHttpNamespace(template.children, root, path);
|
|
236
|
+
const procedures = {};
|
|
237
|
+
collectHttpNodeProcedures(template.children, '', procedures);
|
|
238
|
+
const binding = {
|
|
239
|
+
kind: 'contract',
|
|
240
|
+
procedures: Object.freeze(procedures),
|
|
241
|
+
};
|
|
242
|
+
Object.defineProperty(binding, contractNodeMetadata, {
|
|
243
|
+
enumerable: false,
|
|
244
|
+
value: Object.freeze({
|
|
245
|
+
kind: 'branch',
|
|
246
|
+
root,
|
|
247
|
+
path: Object.freeze(['http', ...path]),
|
|
248
|
+
procedures: Object.freeze(Object.fromEntries(Object.keys(procedures).map((procedure) => [
|
|
249
|
+
procedure,
|
|
250
|
+
[...path, procedure].join('.'),
|
|
251
|
+
]))),
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
Object.freeze(binding);
|
|
255
|
+
const node = { ...children };
|
|
256
|
+
Object.defineProperty(node, contractNodeBinding, {
|
|
257
|
+
enumerable: false,
|
|
258
|
+
value: binding,
|
|
259
|
+
});
|
|
260
|
+
Object.defineProperty(node, httpNodeMetadata, {
|
|
261
|
+
enumerable: false,
|
|
262
|
+
value: Object.freeze({
|
|
263
|
+
kind: 'branch',
|
|
264
|
+
root,
|
|
265
|
+
path: Object.freeze([...path]),
|
|
266
|
+
source: template.source,
|
|
267
|
+
}),
|
|
268
|
+
});
|
|
269
|
+
return Object.freeze(node);
|
|
270
|
+
}
|
|
271
|
+
function collectHttpNodeProcedures(tree, prefix, procedures) {
|
|
272
|
+
for (const [name, template] of Object.entries(tree)) {
|
|
273
|
+
const path = prefix === '' ? name : `${prefix}.${name}`;
|
|
274
|
+
if (template.kind === 'leaf') {
|
|
275
|
+
procedures[path] = Object.freeze({
|
|
276
|
+
kind: 'procedure',
|
|
277
|
+
protocols: Object.freeze({ http: template.protocol }),
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
collectHttpNodeProcedures(template.children, path, procedures);
|
|
282
|
+
}
|
|
42
283
|
}
|
|
43
284
|
export const http = Object.assign(defineHttpGroup, {
|
|
44
285
|
protocol: 'http',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const httpNodeMetadata = Symbol('loutre.http-node-metadata');
|
package/dist/http/path.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ type AsciiLetter = AsciiLowercase | Uppercase<AsciiLowercase>;
|
|
|
3
3
|
type AsciiDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
|
|
4
4
|
type ParamNameStart = AsciiLetter | '_';
|
|
5
5
|
type ParamNameContinuation = ParamNameStart | AsciiDigit;
|
|
6
|
+
type HttpMethodTokenCharacter = AsciiLetter | AsciiDigit | '!' | '#' | '$' | '%' | '&' | "'" | '*' | '+' | '-' | '.' | '^' | '_' | '`' | '|' | '~';
|
|
7
|
+
type IsHttpMethodTokenRest<TMethod extends string> = TMethod extends '' ? true : TMethod extends `${HttpMethodTokenCharacter}${infer TRest}` ? IsHttpMethodTokenRest<TRest> : false;
|
|
8
|
+
export type IsValidHttpMethod<TMethod extends string> = string extends TMethod ? false : TMethod extends `${HttpMethodTokenCharacter}${infer TRest}` ? IsHttpMethodTokenRest<TRest> : false;
|
|
6
9
|
type IsParamNameRest<TName extends string> = TName extends '' ? true : TName extends `${ParamNameContinuation}${infer TRest}` ? IsParamNameRest<TRest> : false;
|
|
7
10
|
type IsParamName<TName extends string> = TName extends `${ParamNameStart}${infer TRest}` ? IsParamNameRest<TRest> : false;
|
|
8
11
|
type IsStaticSegment<TSegment extends string> = TSegment extends '' ? false : TSegment extends `${string}{${string}` | `${string}}${string}` ? false : TSegment extends `${string}?${string}` | `${string}#${string}` ? false : true;
|
|
@@ -43,6 +46,7 @@ export type HttpPathSegment = {
|
|
|
43
46
|
readonly kind: 'param';
|
|
44
47
|
readonly name: string;
|
|
45
48
|
};
|
|
49
|
+
export declare function assertValidHttpMethod(method: string): void;
|
|
46
50
|
export declare function parseHttpPath(path: string): readonly HttpPathSegment[];
|
|
47
51
|
export declare function normalizeHttpPath(segments: readonly HttpPathSegment[]): string;
|
|
48
52
|
export declare function createHttpDispatchKey(method: string, segments: readonly HttpPathSegment[]): string;
|
package/dist/http/path.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
const PARAM_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2
|
+
const HTTP_METHOD_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
3
|
+
export function assertValidHttpMethod(method) {
|
|
4
|
+
if (!HTTP_METHOD_PATTERN.test(method)) {
|
|
5
|
+
throw new Error(`Invalid HTTP method: ${JSON.stringify(method)}`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
2
8
|
export function parseHttpPath(path) {
|
|
3
9
|
if (path === '/')
|
|
4
10
|
return Object.freeze([]);
|