@dusted/anqst 0.1.2 → 1.0.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/src/app.js +86 -103
- package/dist/src/build-stamp.js +5 -0
- package/dist/src/emit.js +530 -89
- package/dist/src/parser.js +159 -8
- package/dist/src/verify.js +6 -1
- package/package.json +3 -2
- package/spec/AnQst-Spec-DSL.d.ts +323 -213
package/dist/src/parser.js
CHANGED
|
@@ -55,26 +55,168 @@ function parseMemberKindFromAnQstType(typeNode) {
|
|
|
55
55
|
if (!typeName.startsWith("AnQst."))
|
|
56
56
|
return null;
|
|
57
57
|
const kind = typeName.slice("AnQst.".length);
|
|
58
|
-
if (!["Call", "Slot", "Emitter", "Output", "Input"].includes(kind))
|
|
58
|
+
if (!["Call", "Slot", "Emitter", "Output", "Input", "DropTarget", "HoverTarget"].includes(kind))
|
|
59
59
|
return null;
|
|
60
60
|
if (kind === "Emitter")
|
|
61
61
|
return { kind, payload: null };
|
|
62
62
|
const arg = typeNode.typeArguments?.[0];
|
|
63
63
|
return { kind, payload: arg ? arg.getText() : null };
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
const DEFAULT_MEMBER_TIMEOUT_MS = 120000;
|
|
66
|
+
const MAX_MEMBER_TIMEOUT_MS = 2147483647;
|
|
67
|
+
function parseMemberKindWithConfig(typeNode) {
|
|
68
|
+
if (!typescript_1.default.isTypeReferenceNode(typeNode))
|
|
69
|
+
return null;
|
|
70
|
+
const typeName = qNameToText(typeNode.typeName);
|
|
71
|
+
if (!typeName.startsWith("AnQst."))
|
|
72
|
+
return null;
|
|
73
|
+
const kind = typeName.slice("AnQst.".length);
|
|
74
|
+
if (!["Call", "Slot", "Emitter", "Output", "Input", "DropTarget", "HoverTarget"].includes(kind))
|
|
75
|
+
return null;
|
|
76
|
+
const typeArgs = typeNode.typeArguments ?? [];
|
|
77
|
+
if (kind === "Emitter") {
|
|
78
|
+
return {
|
|
79
|
+
kind,
|
|
80
|
+
payload: null,
|
|
81
|
+
configTypeNode: typeArgs[0] ?? null
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
kind,
|
|
86
|
+
payload: typeArgs[0] ? typeArgs[0].getText() : null,
|
|
87
|
+
configTypeNode: (kind === "Call" || kind === "HoverTarget") ? (typeArgs[1] ?? null) : null
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function parseNumericLiteralType(node) {
|
|
91
|
+
if (!typescript_1.default.isLiteralTypeNode(node))
|
|
92
|
+
return null;
|
|
93
|
+
if (typescript_1.default.isNumericLiteral(node.literal)) {
|
|
94
|
+
return Number(node.literal.text);
|
|
95
|
+
}
|
|
96
|
+
if (typescript_1.default.isPrefixUnaryExpression(node.literal)
|
|
97
|
+
&& node.literal.operator === typescript_1.default.SyntaxKind.MinusToken
|
|
98
|
+
&& typescript_1.default.isNumericLiteral(node.literal.operand)) {
|
|
99
|
+
return -Number(node.literal.operand.text);
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function resolveMemberTimeoutMs(source, serviceName, memberName, kind, configTypeNode, warnings, memberLoc) {
|
|
104
|
+
if (kind !== "Call")
|
|
105
|
+
return DEFAULT_MEMBER_TIMEOUT_MS;
|
|
106
|
+
if (!configTypeNode)
|
|
107
|
+
return DEFAULT_MEMBER_TIMEOUT_MS;
|
|
108
|
+
if (!typescript_1.default.isTypeLiteralNode(configTypeNode)) {
|
|
109
|
+
throw new errors_1.VerifyError(`${kind} config for '${memberName}' must be an inline object literal type.`, locFromNode(source, configTypeNode));
|
|
110
|
+
}
|
|
111
|
+
let timeoutSeconds = null;
|
|
112
|
+
let timeoutMilliseconds = null;
|
|
113
|
+
const memberPath = `${serviceName}.${memberName}`;
|
|
114
|
+
for (const prop of configTypeNode.members) {
|
|
115
|
+
if (!typescript_1.default.isPropertySignature(prop) || !prop.name) {
|
|
116
|
+
throw new errors_1.VerifyError(`${kind} config for '${memberName}' only supports named properties.`, locFromNode(source, prop));
|
|
117
|
+
}
|
|
118
|
+
if (!typescript_1.default.isIdentifier(prop.name)) {
|
|
119
|
+
throw new errors_1.VerifyError(`${kind} config for '${memberName}' only supports identifier keys.`, locFromNode(source, prop.name));
|
|
120
|
+
}
|
|
121
|
+
const key = prop.name.text;
|
|
122
|
+
if (!prop.type) {
|
|
123
|
+
throw new errors_1.VerifyError(`${kind} config key '${key}' in '${memberName}' must declare a numeric literal value.`, locFromNode(source, prop));
|
|
124
|
+
}
|
|
125
|
+
if (key !== "timeoutSeconds" && key !== "timeoutMilliseconds") {
|
|
126
|
+
warnings.push({
|
|
127
|
+
severity: "warn",
|
|
128
|
+
message: `Unknown ${kind} config key '${key}' ignored for '${memberPath}'.`,
|
|
129
|
+
loc: locFromNode(source, prop.name),
|
|
130
|
+
memberPath
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const numericValue = parseNumericLiteralType(prop.type);
|
|
135
|
+
if (numericValue === null || !Number.isInteger(numericValue)) {
|
|
136
|
+
throw new errors_1.VerifyError(`${kind} config key '${key}' in '${memberName}' must be an integer literal >= 0.`, locFromNode(source, prop.type));
|
|
137
|
+
}
|
|
138
|
+
if (numericValue < 0) {
|
|
139
|
+
throw new errors_1.VerifyError(`${kind} config key '${key}' in '${memberName}' must be >= 0.`, locFromNode(source, prop.type));
|
|
140
|
+
}
|
|
141
|
+
if (key === "timeoutSeconds")
|
|
142
|
+
timeoutSeconds = numericValue;
|
|
143
|
+
if (key === "timeoutMilliseconds")
|
|
144
|
+
timeoutMilliseconds = numericValue;
|
|
145
|
+
}
|
|
146
|
+
if (timeoutSeconds !== null && timeoutMilliseconds !== null) {
|
|
147
|
+
throw new errors_1.VerifyError(`${kind} config for '${memberName}' must specify only one of 'timeoutSeconds' or 'timeoutMilliseconds'.`, memberLoc);
|
|
148
|
+
}
|
|
149
|
+
const effectiveMs = timeoutMilliseconds !== null
|
|
150
|
+
? timeoutMilliseconds
|
|
151
|
+
: timeoutSeconds !== null
|
|
152
|
+
? timeoutSeconds * 1000
|
|
153
|
+
: DEFAULT_MEMBER_TIMEOUT_MS;
|
|
154
|
+
if (effectiveMs > MAX_MEMBER_TIMEOUT_MS) {
|
|
155
|
+
throw new errors_1.VerifyError(`${kind} timeout for '${memberName}' exceeds max supported value (${MAX_MEMBER_TIMEOUT_MS} ms).`, memberLoc);
|
|
156
|
+
}
|
|
157
|
+
return effectiveMs;
|
|
158
|
+
}
|
|
159
|
+
const DEFAULT_HOVER_RATE_HZ = 60;
|
|
160
|
+
const DEFAULT_HOVER_THROTTLE_MS = Math.round(1000 / DEFAULT_HOVER_RATE_HZ);
|
|
161
|
+
function resolveHoverThrottleMs(source, serviceName, memberName, kind, configTypeNode, warnings, memberLoc) {
|
|
162
|
+
if (kind !== "HoverTarget")
|
|
163
|
+
return 0;
|
|
164
|
+
if (!configTypeNode)
|
|
165
|
+
return DEFAULT_HOVER_THROTTLE_MS;
|
|
166
|
+
if (!typescript_1.default.isTypeLiteralNode(configTypeNode)) {
|
|
167
|
+
throw new errors_1.VerifyError(`HoverTarget config for '${memberName}' must be an inline object literal type.`, locFromNode(source, configTypeNode));
|
|
168
|
+
}
|
|
169
|
+
let maxRateHz = null;
|
|
170
|
+
const memberPath = `${serviceName}.${memberName}`;
|
|
171
|
+
for (const prop of configTypeNode.members) {
|
|
172
|
+
if (!typescript_1.default.isPropertySignature(prop) || !prop.name) {
|
|
173
|
+
throw new errors_1.VerifyError(`HoverTarget config for '${memberName}' only supports named properties.`, locFromNode(source, prop));
|
|
174
|
+
}
|
|
175
|
+
if (!typescript_1.default.isIdentifier(prop.name)) {
|
|
176
|
+
throw new errors_1.VerifyError(`HoverTarget config for '${memberName}' only supports identifier keys.`, locFromNode(source, prop.name));
|
|
177
|
+
}
|
|
178
|
+
const key = prop.name.text;
|
|
179
|
+
if (!prop.type) {
|
|
180
|
+
throw new errors_1.VerifyError(`HoverTarget config key '${key}' in '${memberName}' must declare a numeric literal value.`, locFromNode(source, prop));
|
|
181
|
+
}
|
|
182
|
+
if (key !== "maxRateHz") {
|
|
183
|
+
warnings.push({
|
|
184
|
+
severity: "warn",
|
|
185
|
+
message: `Unknown HoverTarget config key '${key}' ignored for '${memberPath}'.`,
|
|
186
|
+
loc: locFromNode(source, prop.name),
|
|
187
|
+
memberPath
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const numericValue = parseNumericLiteralType(prop.type);
|
|
192
|
+
if (numericValue === null || !Number.isFinite(numericValue)) {
|
|
193
|
+
throw new errors_1.VerifyError(`HoverTarget config key '${key}' in '${memberName}' must be a numeric literal >= 0.`, locFromNode(source, prop.type));
|
|
194
|
+
}
|
|
195
|
+
if (numericValue < 0) {
|
|
196
|
+
throw new errors_1.VerifyError(`HoverTarget config key '${key}' in '${memberName}' must be >= 0.`, locFromNode(source, prop.type));
|
|
197
|
+
}
|
|
198
|
+
maxRateHz = numericValue;
|
|
199
|
+
}
|
|
200
|
+
if (maxRateHz === null)
|
|
201
|
+
return DEFAULT_HOVER_THROTTLE_MS;
|
|
202
|
+
return maxRateHz === 0 ? 0 : Math.round(1000 / maxRateHz);
|
|
203
|
+
}
|
|
204
|
+
function parseServiceMember(source, serviceName, member, warnings) {
|
|
66
205
|
if (typescript_1.default.isMethodSignature(member)) {
|
|
67
206
|
if (member.questionToken)
|
|
68
207
|
throw new errors_1.VerifyError("Optional service methods are not allowed.", locFromNode(source, member));
|
|
69
208
|
const returnType = member.type;
|
|
70
209
|
if (!returnType)
|
|
71
210
|
throw new errors_1.VerifyError("Service method must declare return type.", locFromNode(source, member));
|
|
72
|
-
const parsed =
|
|
211
|
+
const parsed = parseMemberKindWithConfig(returnType);
|
|
73
212
|
if (!parsed)
|
|
74
213
|
throw new errors_1.VerifyError(`Unsupported service method return type '${returnType.getText()}'.`, locFromNode(source, member));
|
|
75
|
-
if (parsed.kind === "Input" || parsed.kind === "Output") {
|
|
214
|
+
if (parsed.kind === "Input" || parsed.kind === "Output" || parsed.kind === "DropTarget" || parsed.kind === "HoverTarget") {
|
|
76
215
|
throw new errors_1.VerifyError(`${parsed.kind} must be declared as property, not method.`, locFromNode(source, member));
|
|
77
216
|
}
|
|
217
|
+
if (parsed.kind === "Emitter" && parsed.configTypeNode !== null) {
|
|
218
|
+
throw new errors_1.VerifyError(`Emitter '${member.name.getText(source)}' does not support config parameters; use plain AnQst.Emitter.`, locFromNode(source, parsed.configTypeNode));
|
|
219
|
+
}
|
|
78
220
|
if (!member.name || !typescript_1.default.isIdentifier(member.name)) {
|
|
79
221
|
throw new errors_1.VerifyError("Only identifier service method names are supported.", locFromNode(source, member));
|
|
80
222
|
}
|
|
@@ -87,11 +229,14 @@ function parseServiceMember(source, member) {
|
|
|
87
229
|
throw new errors_1.VerifyError("Service parameters must declare type.", locFromNode(source, param));
|
|
88
230
|
return { name: param.name.text, typeText: param.type.getText() };
|
|
89
231
|
});
|
|
232
|
+
const timeoutMs = resolveMemberTimeoutMs(source, serviceName, member.name.text, parsed.kind, parsed.configTypeNode, warnings, locFromNode(source, member));
|
|
90
233
|
return {
|
|
91
234
|
kind: parsed.kind,
|
|
92
235
|
name: member.name.text,
|
|
93
236
|
payloadTypeText: parsed.payload,
|
|
94
237
|
parameters,
|
|
238
|
+
timeoutMs,
|
|
239
|
+
hoverThrottleMs: 0,
|
|
95
240
|
loc: locFromNode(source, member)
|
|
96
241
|
};
|
|
97
242
|
}
|
|
@@ -100,20 +245,24 @@ function parseServiceMember(source, member) {
|
|
|
100
245
|
throw new errors_1.VerifyError("Service property must declare type.", locFromNode(source, member));
|
|
101
246
|
if (member.questionToken)
|
|
102
247
|
throw new errors_1.VerifyError("Optional service properties are not allowed.", locFromNode(source, member));
|
|
103
|
-
const parsed =
|
|
248
|
+
const parsed = parseMemberKindWithConfig(member.type);
|
|
104
249
|
if (!parsed)
|
|
105
250
|
throw new errors_1.VerifyError(`Unsupported service property type '${member.type.getText()}'.`, locFromNode(source, member));
|
|
106
|
-
if (parsed.kind !== "Input" && parsed.kind !== "Output") {
|
|
251
|
+
if (parsed.kind !== "Input" && parsed.kind !== "Output" && parsed.kind !== "DropTarget" && parsed.kind !== "HoverTarget") {
|
|
107
252
|
throw new errors_1.VerifyError(`${parsed.kind} must be declared as method, not property.`, locFromNode(source, member));
|
|
108
253
|
}
|
|
109
254
|
if (!member.name || !typescript_1.default.isIdentifier(member.name)) {
|
|
110
255
|
throw new errors_1.VerifyError("Only identifier service property names are supported.", locFromNode(source, member));
|
|
111
256
|
}
|
|
257
|
+
const timeoutMs = resolveMemberTimeoutMs(source, serviceName, member.name.text, parsed.kind, parsed.configTypeNode, warnings, locFromNode(source, member));
|
|
258
|
+
const hoverThrottleMs = resolveHoverThrottleMs(source, serviceName, member.name.text, parsed.kind, parsed.configTypeNode, warnings, locFromNode(source, member));
|
|
112
259
|
return {
|
|
113
260
|
kind: parsed.kind,
|
|
114
261
|
name: member.name.text,
|
|
115
262
|
payloadTypeText: parsed.payload,
|
|
116
263
|
parameters: [],
|
|
264
|
+
timeoutMs,
|
|
265
|
+
hoverThrottleMs,
|
|
117
266
|
loc: locFromNode(source, member)
|
|
118
267
|
};
|
|
119
268
|
}
|
|
@@ -228,12 +377,13 @@ function parseSpecFileAst(specFilePath) {
|
|
|
228
377
|
throw new errors_1.VerifyError("Namespace body must be a block.", locFromNode(source, ns));
|
|
229
378
|
const services = [];
|
|
230
379
|
const namespaceTypeDecls = [];
|
|
380
|
+
const warnings = [];
|
|
231
381
|
let supportsDevelopmentModeTransport = false;
|
|
232
382
|
for (const stmt of ns.body.statements) {
|
|
233
383
|
if (typescript_1.default.isInterfaceDeclaration(stmt)) {
|
|
234
384
|
const baseType = serviceBaseType(stmt);
|
|
235
385
|
if (baseType !== null) {
|
|
236
|
-
const members = stmt.members.map((member) => parseServiceMember(source, member));
|
|
386
|
+
const members = stmt.members.map((member) => parseServiceMember(source, stmt.name.text, member, warnings));
|
|
237
387
|
if (baseType === "AngularHTTPBaseServerClass") {
|
|
238
388
|
supportsDevelopmentModeTransport = true;
|
|
239
389
|
}
|
|
@@ -257,7 +407,8 @@ function parseSpecFileAst(specFilePath) {
|
|
|
257
407
|
namespaceTypeDecls,
|
|
258
408
|
importedTypeDecls: importInfo.importedTypeDecls,
|
|
259
409
|
importedTypeSymbols: importInfo.importedTypeSymbols,
|
|
260
|
-
specImports: importInfo.specImports
|
|
410
|
+
specImports: importInfo.specImports,
|
|
411
|
+
warnings
|
|
261
412
|
};
|
|
262
413
|
}
|
|
263
414
|
function parseSpecFile(specFilePath) {
|
package/dist/src/verify.js
CHANGED
|
@@ -194,9 +194,14 @@ function verifySpecSemantics(spec) {
|
|
|
194
194
|
reachableGeneratedTypes: reachable.size,
|
|
195
195
|
serviceCount: spec.services.length
|
|
196
196
|
};
|
|
197
|
+
const warnings = [...spec.warnings];
|
|
198
|
+
const warningSummary = warnings.length === 0
|
|
199
|
+
? ""
|
|
200
|
+
: `\nWarnings:\n${warnings.map((w) => ` [warn] ${w.loc.file}:${w.loc.line}:${w.loc.column} ${w.memberPath} - ${w.message}`).join("\n")}`;
|
|
197
201
|
return {
|
|
198
202
|
stats,
|
|
199
|
-
message: `AnQst spec valid:\n ${stats.namespaceDeclaredTypes} types.\n ${stats.serviceCount} services
|
|
203
|
+
message: `AnQst spec valid:\n ${stats.namespaceDeclaredTypes} types.\n ${stats.serviceCount} services.${warningSummary}`,
|
|
204
|
+
warnings
|
|
200
205
|
};
|
|
201
206
|
}
|
|
202
207
|
function verifySpec(spec) {
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dusted/anqst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Opinionated backend generator for webapps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nodejs",
|
|
7
7
|
"qt",
|
|
8
8
|
"angular",
|
|
9
|
-
"webapp"
|
|
9
|
+
"webapp",
|
|
10
|
+
"apigenerator"
|
|
10
11
|
],
|
|
11
12
|
"homepage": "https://github.com/DusteDdk/AnQst#readme",
|
|
12
13
|
"bugs": {
|