@dusted/anqst 0.1.2 → 0.1.3
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 +326 -104
- package/dist/src/parser.js +108 -5
- package/dist/src/verify.js +6 -1
- package/package.json +1 -1
- package/spec/AnQst-Spec-DSL.d.ts +50 -18
package/dist/src/parser.js
CHANGED
|
@@ -62,19 +62,116 @@ function parseMemberKindFromAnQstType(typeNode) {
|
|
|
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"].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" ? (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
|
+
function parseServiceMember(source, serviceName, member, warnings) {
|
|
66
160
|
if (typescript_1.default.isMethodSignature(member)) {
|
|
67
161
|
if (member.questionToken)
|
|
68
162
|
throw new errors_1.VerifyError("Optional service methods are not allowed.", locFromNode(source, member));
|
|
69
163
|
const returnType = member.type;
|
|
70
164
|
if (!returnType)
|
|
71
165
|
throw new errors_1.VerifyError("Service method must declare return type.", locFromNode(source, member));
|
|
72
|
-
const parsed =
|
|
166
|
+
const parsed = parseMemberKindWithConfig(returnType);
|
|
73
167
|
if (!parsed)
|
|
74
168
|
throw new errors_1.VerifyError(`Unsupported service method return type '${returnType.getText()}'.`, locFromNode(source, member));
|
|
75
169
|
if (parsed.kind === "Input" || parsed.kind === "Output") {
|
|
76
170
|
throw new errors_1.VerifyError(`${parsed.kind} must be declared as property, not method.`, locFromNode(source, member));
|
|
77
171
|
}
|
|
172
|
+
if (parsed.kind === "Emitter" && parsed.configTypeNode !== null) {
|
|
173
|
+
throw new errors_1.VerifyError(`Emitter '${member.name.getText(source)}' does not support config parameters; use plain AnQst.Emitter.`, locFromNode(source, parsed.configTypeNode));
|
|
174
|
+
}
|
|
78
175
|
if (!member.name || !typescript_1.default.isIdentifier(member.name)) {
|
|
79
176
|
throw new errors_1.VerifyError("Only identifier service method names are supported.", locFromNode(source, member));
|
|
80
177
|
}
|
|
@@ -87,11 +184,13 @@ function parseServiceMember(source, member) {
|
|
|
87
184
|
throw new errors_1.VerifyError("Service parameters must declare type.", locFromNode(source, param));
|
|
88
185
|
return { name: param.name.text, typeText: param.type.getText() };
|
|
89
186
|
});
|
|
187
|
+
const timeoutMs = resolveMemberTimeoutMs(source, serviceName, member.name.text, parsed.kind, parsed.configTypeNode, warnings, locFromNode(source, member));
|
|
90
188
|
return {
|
|
91
189
|
kind: parsed.kind,
|
|
92
190
|
name: member.name.text,
|
|
93
191
|
payloadTypeText: parsed.payload,
|
|
94
192
|
parameters,
|
|
193
|
+
timeoutMs,
|
|
95
194
|
loc: locFromNode(source, member)
|
|
96
195
|
};
|
|
97
196
|
}
|
|
@@ -100,7 +199,7 @@ function parseServiceMember(source, member) {
|
|
|
100
199
|
throw new errors_1.VerifyError("Service property must declare type.", locFromNode(source, member));
|
|
101
200
|
if (member.questionToken)
|
|
102
201
|
throw new errors_1.VerifyError("Optional service properties are not allowed.", locFromNode(source, member));
|
|
103
|
-
const parsed =
|
|
202
|
+
const parsed = parseMemberKindWithConfig(member.type);
|
|
104
203
|
if (!parsed)
|
|
105
204
|
throw new errors_1.VerifyError(`Unsupported service property type '${member.type.getText()}'.`, locFromNode(source, member));
|
|
106
205
|
if (parsed.kind !== "Input" && parsed.kind !== "Output") {
|
|
@@ -109,11 +208,13 @@ function parseServiceMember(source, member) {
|
|
|
109
208
|
if (!member.name || !typescript_1.default.isIdentifier(member.name)) {
|
|
110
209
|
throw new errors_1.VerifyError("Only identifier service property names are supported.", locFromNode(source, member));
|
|
111
210
|
}
|
|
211
|
+
const timeoutMs = resolveMemberTimeoutMs(source, serviceName, member.name.text, parsed.kind, parsed.configTypeNode, warnings, locFromNode(source, member));
|
|
112
212
|
return {
|
|
113
213
|
kind: parsed.kind,
|
|
114
214
|
name: member.name.text,
|
|
115
215
|
payloadTypeText: parsed.payload,
|
|
116
216
|
parameters: [],
|
|
217
|
+
timeoutMs,
|
|
117
218
|
loc: locFromNode(source, member)
|
|
118
219
|
};
|
|
119
220
|
}
|
|
@@ -228,12 +329,13 @@ function parseSpecFileAst(specFilePath) {
|
|
|
228
329
|
throw new errors_1.VerifyError("Namespace body must be a block.", locFromNode(source, ns));
|
|
229
330
|
const services = [];
|
|
230
331
|
const namespaceTypeDecls = [];
|
|
332
|
+
const warnings = [];
|
|
231
333
|
let supportsDevelopmentModeTransport = false;
|
|
232
334
|
for (const stmt of ns.body.statements) {
|
|
233
335
|
if (typescript_1.default.isInterfaceDeclaration(stmt)) {
|
|
234
336
|
const baseType = serviceBaseType(stmt);
|
|
235
337
|
if (baseType !== null) {
|
|
236
|
-
const members = stmt.members.map((member) => parseServiceMember(source, member));
|
|
338
|
+
const members = stmt.members.map((member) => parseServiceMember(source, stmt.name.text, member, warnings));
|
|
237
339
|
if (baseType === "AngularHTTPBaseServerClass") {
|
|
238
340
|
supportsDevelopmentModeTransport = true;
|
|
239
341
|
}
|
|
@@ -257,7 +359,8 @@ function parseSpecFileAst(specFilePath) {
|
|
|
257
359
|
namespaceTypeDecls,
|
|
258
360
|
importedTypeDecls: importInfo.importedTypeDecls,
|
|
259
361
|
importedTypeSymbols: importInfo.importedTypeSymbols,
|
|
260
|
-
specImports: importInfo.specImports
|
|
362
|
+
specImports: importInfo.specImports,
|
|
363
|
+
warnings
|
|
261
364
|
};
|
|
262
365
|
}
|
|
263
366
|
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
package/spec/AnQst-Spec-DSL.d.ts
CHANGED
|
@@ -10,20 +10,20 @@
|
|
|
10
10
|
* - This is not an input file to the generator, it is the description of the language that describes the widget.
|
|
11
11
|
* Not for use by TypeScript application implementation.
|
|
12
12
|
* @example
|
|
13
|
-
* package.json:
|
|
14
|
-
* - "AnQst": "./AnQst/MyUserMgmtWidget.settings.json"
|
|
13
|
+
* package.json:
|
|
14
|
+
* - "AnQst": "./AnQst/MyUserMgmtWidget.settings.json"
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
export namespace AnQst {
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Declare service `InterfaceName`
|
|
21
|
-
*
|
|
21
|
+
*
|
|
22
22
|
* @remarks
|
|
23
23
|
* Multiple allowed.
|
|
24
24
|
* Affords developers of advanced widgets the ability to create domain-informed categories.
|
|
25
25
|
* - Duplicate method declarations with identical parameter lists are invalid in normative AnQst-Spec input.
|
|
26
|
-
*
|
|
26
|
+
*
|
|
27
27
|
* @example
|
|
28
28
|
* export interface UserService extends Widget.Service { }
|
|
29
29
|
* // Generates UserService.
|
|
@@ -44,6 +44,7 @@ export namespace AnQst {
|
|
|
44
44
|
*/
|
|
45
45
|
interface AngularHTTPBaseServerClass extends Service { }
|
|
46
46
|
|
|
47
|
+
type CallConfig = { timeoutSeconds: number } | { timeoutMilliseconds: number } | {};
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Declare non-blocking service method `MethodName`(`MethodArguments`): Promise<`MethodReturnType`>.
|
|
@@ -52,27 +53,38 @@ export namespace AnQst {
|
|
|
52
53
|
* - Flow:
|
|
53
54
|
* - Widget: Call to service method `MethodName`(`MethodArguments`).
|
|
54
55
|
* - Widget: Returns Promise<`MethodReturnType`>.
|
|
55
|
-
* - Parent: Registered
|
|
56
|
-
* -
|
|
57
|
-
*
|
|
56
|
+
* - Parent: Registered callback receives args and returns `T` synchronously.
|
|
57
|
+
* - Widget: Promise resolves with payload `T` or rejects with plain error object.
|
|
58
|
+
* - Optional config supports timeout tuning:
|
|
59
|
+
* - `AnQst.Call<T, { timeoutSeconds: N }>`
|
|
60
|
+
* - `AnQst.Call<T, { timeoutMilliseconds: N }>`
|
|
61
|
+
* - Exactly one timeout key is allowed. Value must be integer >= 0.
|
|
62
|
+
* - Default timeout is 120s. `0` means wait forever.
|
|
58
63
|
* @example
|
|
59
64
|
* // AnQst spec:
|
|
60
65
|
* getUserById(userId: string): AnQst.Call<User>
|
|
61
66
|
* //Angular app:
|
|
62
67
|
* const user: User = await this.userService.getUserById("abc");
|
|
63
68
|
*/
|
|
64
|
-
interface Call<T> { dummy: T }
|
|
69
|
+
interface Call<T, Config extends CallConfig = {}> { dummy: T; config?: Config }
|
|
65
70
|
|
|
66
|
-
/**
|
|
67
|
-
* Declare blocking service method onSlot.`MethodName`( handler(`MethodArguments`):`MethodReturnType` ): void
|
|
71
|
+
/**
|
|
72
|
+
* Declare blocking service method onSlot.`MethodName`( handler(`MethodArguments`):`MethodReturnType` ): void
|
|
68
73
|
* @remarks
|
|
69
74
|
* - **Parent** -> Widget
|
|
70
75
|
* - Impl note: Autogenerated stub handler queues until handler is set (set method calls spools queue through handler)
|
|
71
76
|
* - Flow:
|
|
72
77
|
* - Parent: Call to generated widget method `MethodName`(`MethodArguments`).
|
|
73
|
-
* - Widget: Registered handler(`MethodArguments`)
|
|
74
|
-
* - Widget: Handler
|
|
78
|
+
* - Widget: Registered handler(`MethodArguments`) is called.
|
|
79
|
+
* - Widget: Handler return forms:
|
|
80
|
+
* - `T` -> success payload
|
|
81
|
+
* - `Promise<T>` -> awaited, success payload on resolve
|
|
82
|
+
* - `Error` -> failure
|
|
83
|
+
* - throw -> failure
|
|
84
|
+
* - rejected promise -> failure
|
|
75
85
|
* - Parent: `MethodName` call returns with result.
|
|
86
|
+
* - Generated C++ Slot methods do not expose `ok/error` out parameters.
|
|
87
|
+
* - Default Slot timeout is 1000ms.
|
|
76
88
|
* Note: One active handler, calling will replace existing and is valid and allowed.
|
|
77
89
|
* @example
|
|
78
90
|
* // AnQst spec:
|
|
@@ -95,6 +107,7 @@ export namespace AnQst {
|
|
|
95
107
|
* - Widget: Call to service method `MethodName`(`MethodArguments`).
|
|
96
108
|
* - Widget: Returns void.
|
|
97
109
|
* - Parent: Might have something connected to the signal, might not.
|
|
110
|
+
* - If no listener is connected, event is dropped.
|
|
98
111
|
* @example
|
|
99
112
|
* // AnQst spec:
|
|
100
113
|
* complain(whine: string): AnQst.Emitter;
|
|
@@ -144,20 +157,20 @@ export namespace AnQst {
|
|
|
144
157
|
|
|
145
158
|
/**
|
|
146
159
|
* AnQst-Spec type mapping overview and control.
|
|
147
|
-
*
|
|
160
|
+
*
|
|
148
161
|
* @remarks
|
|
149
162
|
* Any Type that be mapped between TypeScript and Qt, C++ standard types or
|
|
150
163
|
* Plain Old Data (POD) types (in that order of preference) will be mapped by default.
|
|
151
|
-
*
|
|
164
|
+
*
|
|
152
165
|
* Canonical mapping directive namespace is AnQst.Type.<type>.
|
|
153
166
|
* To express advisory mapping preference, use AnQst.Type.<type>.
|
|
154
167
|
* Advisory means generator SHOULD honor it, but MAY fall back to inferred/default mapping and emit diagnostic.
|
|
155
168
|
* Array forms are equivalent: T[] == Array<T>, and AnQst.Type.X[] == Array<AnQst.Type.X>.
|
|
156
|
-
*
|
|
169
|
+
*
|
|
157
170
|
* TypeScript definitions+classes and C++ structs are generated for each
|
|
158
171
|
* structured TypeScript type ( type = {...} or interface { ... } )
|
|
159
172
|
* referenced in an AnQst spec.
|
|
160
|
-
*
|
|
173
|
+
*
|
|
161
174
|
*/
|
|
162
175
|
enum Type {
|
|
163
176
|
object = "JavaScript Object <-> QVariantMap (JSON.stringify/parse semantics)",
|
|
@@ -195,13 +208,13 @@ export namespace AnQst {
|
|
|
195
208
|
|
|
196
209
|
/**
|
|
197
210
|
* These are explicitly forbidden argument/return types.
|
|
198
|
-
*
|
|
211
|
+
*
|
|
199
212
|
* @remarks
|
|
200
213
|
* - They may not be referenced in AnQst specs or imported types.
|
|
201
214
|
* - Service methods cannot accept them as arguments
|
|
202
215
|
* - Service methods cannot return them.
|
|
203
216
|
* - Service properties of their type cannot be declared.
|
|
204
|
-
*
|
|
217
|
+
*
|
|
205
218
|
* - For Objects/Maps/Sets use AnQst.Type.<Type> instead.
|
|
206
219
|
*/
|
|
207
220
|
export enum ForbiddenType {
|
|
@@ -213,5 +226,24 @@ export namespace AnQst {
|
|
|
213
226
|
any = "Passing 'any' type across the boundary is not allowed",
|
|
214
227
|
}
|
|
215
228
|
|
|
229
|
+
/**
|
|
230
|
+
* JS/TS Error instances can be returned, but they have special meaning.
|
|
231
|
+
*
|
|
232
|
+
* @remarks
|
|
233
|
+
* AnQst is opinionated, Errors are not for control flow. They are for signalling unrecoverable and unhandled circumstance.
|
|
234
|
+
* Use: To indicate unrecoverable program error/wrong use.
|
|
235
|
+
* Don't use: To communicate expected and/or ignorable/handable situations, define a domain-specific transport type instad.
|
|
236
|
+
* When else encountered: Unhandled Errors.
|
|
237
|
+
* Effect: The receiving end throws an exception with message `<service>.<member> emitted error: <WidgetStackTrace>`
|
|
238
|
+
* AnQst internal behavior:
|
|
239
|
+
* When AnQst type translation/mapping encounters a true Error object instance, the Error object is not transported, instead
|
|
240
|
+
* the sending AnQst code signals to the receiving AnQst code a message, and the receiving AnQst code throws a runtime exception on
|
|
241
|
+
* the call or callback site in the Parent.
|
|
242
|
+
|
|
243
|
+
*/
|
|
244
|
+
export enum ExceptionalType {
|
|
245
|
+
Error = "AnQst will not transport an Error object, but will cause an exception to be thrown on reception site."
|
|
246
|
+
}
|
|
247
|
+
|
|
216
248
|
}
|
|
217
249
|
|