carrick 0.3.70 → 0.3.72
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 +10 -2
- package/bin/carrick.mjs +4 -1
- package/dist/contract.d.ts +3 -0
- package/dist/contract.js.map +1 -1
- package/dist/init/doctor.d.ts +3 -1
- package/dist/init/doctor.js +33 -6
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/install-id.d.ts +32 -0
- package/dist/init/install-id.js +118 -0
- package/dist/init/install-id.js.map +1 -0
- package/dist/init/mcp.d.ts +59 -5
- package/dist/init/mcp.js +133 -27
- package/dist/init/mcp.js.map +1 -1
- package/dist/init/remove.d.ts +4 -0
- package/dist/init/remove.js +26 -5
- package/dist/init/remove.js.map +1 -1
- package/dist/init/run.d.ts +8 -0
- package/dist/init/run.js +20 -2
- package/dist/init/run.js.map +1 -1
- package/dist/native.d.ts +25 -5
- package/dist/native.js +41 -10
- package/dist/native.js.map +1 -1
- package/dist/render.js +7 -2
- package/dist/render.js.map +1 -1
- package/package.json +6 -6
- package/sidecar/dist/src/capture/api.d.ts +5 -1
- package/sidecar/dist/src/type-inferrer.d.ts +119 -19
- package/sidecar/dist/src/type-inferrer.js +413 -54
- package/sidecar/dist/src/type-structural-expander.d.ts +32 -3
- package/sidecar/dist/src/type-structural-expander.js +67 -16
|
@@ -861,7 +861,7 @@ export class TypeInferrer {
|
|
|
861
861
|
if (atLine) {
|
|
862
862
|
const requestType = this.requestContractFromRegistration(atLine.registration, atLine.handler);
|
|
863
863
|
if (requestType) {
|
|
864
|
-
return this.
|
|
864
|
+
return this.declaredRequestInferredType(request, requestType, this.getNodeLocation(atLine.handler));
|
|
865
865
|
}
|
|
866
866
|
}
|
|
867
867
|
return null;
|
|
@@ -879,7 +879,7 @@ export class TypeInferrer {
|
|
|
879
879
|
if (registrationHandler) {
|
|
880
880
|
const requestType = this.requestContractFromRegistration(this.unwrapExpressionNode(located), registrationHandler);
|
|
881
881
|
if (requestType) {
|
|
882
|
-
return this.
|
|
882
|
+
return this.declaredRequestInferredType(request, requestType, this.getNodeLocation(registrationHandler));
|
|
883
883
|
}
|
|
884
884
|
// A genuinely payload-less handler (no typed request read): do NOT fall
|
|
885
885
|
// through to read the registration call's return type, which would emit a
|
|
@@ -912,7 +912,7 @@ export class TypeInferrer {
|
|
|
912
912
|
if (declared) {
|
|
913
913
|
this.log(`Route registration at ${request.file_path}:${request.line_number} declares its ` +
|
|
914
914
|
'request contract; using the declaration over the located expression');
|
|
915
|
-
return this.
|
|
915
|
+
return this.declaredRequestInferredType(request, declared, this.getNodeLocation(declaredAt.handler));
|
|
916
916
|
}
|
|
917
917
|
}
|
|
918
918
|
// A text locator (Gemini `expression_text` + `expression_line`, as opposed
|
|
@@ -966,6 +966,21 @@ export class TypeInferrer {
|
|
|
966
966
|
}
|
|
967
967
|
}
|
|
968
968
|
}
|
|
969
|
+
// carrick#1101: the locator landed on a validated read — a call inside a
|
|
970
|
+
// registered handler whose type IS the parsed output of a body schema that
|
|
971
|
+
// the enclosing registration declares. That value is what the handler
|
|
972
|
+
// receives, not what a caller sends, so publish the schema's input. Only a
|
|
973
|
+
// direct call is read this way: a value that went through a serialiser or
|
|
974
|
+
// a variable hop is left to the path below, so a consumer body forwarded
|
|
975
|
+
// from inside a handler keeps its own type.
|
|
976
|
+
if (node === unwrapped && Node.isCallExpression(node)) {
|
|
977
|
+
const validatedRead = this.validatedReadRequestContract(node);
|
|
978
|
+
if (validatedRead) {
|
|
979
|
+
this.log(`Request locator at ${request.file_path}:${request.line_number} is a validated read ` +
|
|
980
|
+
"of its registration's body schema; publishing the schema's input");
|
|
981
|
+
return this.declaredRequestInferredType(request, validatedRead, this.getNodeLocation(node));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
969
984
|
const payloadType = node.getType();
|
|
970
985
|
let typeString = typeText(payloadType, node);
|
|
971
986
|
let isExplicit = false;
|
|
@@ -3126,12 +3141,13 @@ export class TypeInferrer {
|
|
|
3126
3141
|
// of that parameter's type;
|
|
3127
3142
|
// (b) a validation-schema object passed alongside the handler — `{ schema:
|
|
3128
3143
|
// { body: ref('CreateWidget'), response: { 200: ref('Widget') } } }` —
|
|
3129
|
-
// whose entries reference schema VALUES that
|
|
3130
|
-
//
|
|
3144
|
+
// whose entries reference schema VALUES that declare their input and
|
|
3145
|
+
// output types.
|
|
3131
3146
|
//
|
|
3132
3147
|
// Both are read structurally: (a) is "the `body` member of a parameter's
|
|
3133
3148
|
// type", (b) is "a `schema` property on a registration argument, whose
|
|
3134
|
-
// entries resolve to a value
|
|
3149
|
+
// entries resolve to a value that declares its types" (the body reads the
|
|
3150
|
+
// input, the response the output; carrick#1101). No
|
|
3135
3151
|
// framework, library, or method-name list is involved — a framework whose
|
|
3136
3152
|
// request object exposes `body` and whose route options carry `schema` is
|
|
3137
3153
|
// read the same way regardless of which one it is.
|
|
@@ -3160,13 +3176,13 @@ export class TypeInferrer {
|
|
|
3160
3176
|
* back to following the handler's return.
|
|
3161
3177
|
*/
|
|
3162
3178
|
declaredResponseInferredType(request, registration) {
|
|
3163
|
-
const declared = this.
|
|
3179
|
+
const declared = this.routeSchemaContract(registration, 'response');
|
|
3164
3180
|
if (!declared) {
|
|
3165
3181
|
return null;
|
|
3166
3182
|
}
|
|
3167
3183
|
this.log(`Route registration at ${request.file_path}:${request.line_number} declares its ` +
|
|
3168
3184
|
'response schema; using the declared contract');
|
|
3169
|
-
return this.createInferredType(request, declared, true, this.getNodeLocation(registration), undefined, undefined);
|
|
3185
|
+
return this.createInferredType(request, declared.text, true, this.getNodeLocation(registration), undefined, undefined);
|
|
3170
3186
|
}
|
|
3171
3187
|
/**
|
|
3172
3188
|
* The request contract of a route registration, in anchor order: the
|
|
@@ -3176,8 +3192,23 @@ export class TypeInferrer {
|
|
|
3176
3192
|
* the route declares its request nowhere we can read.
|
|
3177
3193
|
*/
|
|
3178
3194
|
requestContractFromRegistration(registration, handler) {
|
|
3179
|
-
|
|
3180
|
-
|
|
3195
|
+
const declared = this.declaredRequestContract(registration, handler);
|
|
3196
|
+
if (declared) {
|
|
3197
|
+
return declared;
|
|
3198
|
+
}
|
|
3199
|
+
const read = this.inferRequestReadFromHandler(handler);
|
|
3200
|
+
return read ? { text: read } : null;
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* An explicit request `InferredType` for a contract the route declares,
|
|
3204
|
+
* carrying the provenance the reading recorded.
|
|
3205
|
+
*/
|
|
3206
|
+
declaredRequestInferredType(request, contract, location) {
|
|
3207
|
+
const inferred = this.createInferredType(request, contract.text, true, location, undefined, undefined);
|
|
3208
|
+
if (contract.provenance && contract.provenance.length > 0) {
|
|
3209
|
+
inferred.any_provenance = contract.provenance;
|
|
3210
|
+
}
|
|
3211
|
+
return inferred;
|
|
3181
3212
|
}
|
|
3182
3213
|
/**
|
|
3183
3214
|
* The request contract a route DECLARES, in anchor order: the handler's
|
|
@@ -3191,9 +3222,12 @@ export class TypeInferrer {
|
|
|
3191
3222
|
* own locator authoritative when a route declares nothing.
|
|
3192
3223
|
*/
|
|
3193
3224
|
declaredRequestContract(registration, handler) {
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3225
|
+
const annotated = this.requestBodyFromHandlerParams(handler);
|
|
3226
|
+
if (annotated) {
|
|
3227
|
+
return { text: annotated };
|
|
3228
|
+
}
|
|
3229
|
+
return (this.routeSchemaContract(registration, 'body') ??
|
|
3230
|
+
this.validatedBodyContract(registration));
|
|
3197
3231
|
}
|
|
3198
3232
|
/**
|
|
3199
3233
|
* Anchor (a): the request contract declared on the handler's own signature.
|
|
@@ -3246,8 +3280,9 @@ export class TypeInferrer {
|
|
|
3246
3280
|
* The shape is a CALL among the registration's arguments whose own arguments
|
|
3247
3281
|
* are a request part and a schema value. Neither the middleware's name nor
|
|
3248
3282
|
* the schema library is checked: the part is read off the string literal, and
|
|
3249
|
-
* the schema is whatever
|
|
3250
|
-
*
|
|
3283
|
+
* the schema is whatever declares its types (`schemaContract`) — the same
|
|
3284
|
+
* test anchor (b) already applies to a `schema: { body: … }` object. The
|
|
3285
|
+
* contract is the schema's INPUT, what a caller sends (carrick#1101).
|
|
3251
3286
|
*
|
|
3252
3287
|
* `REQUEST_BODY_PARTS` is HTTP vocabulary for how a body is carried, not a
|
|
3253
3288
|
* framework list. A middleware bound to any other part (`query`, `param`,
|
|
@@ -3255,10 +3290,26 @@ export class TypeInferrer {
|
|
|
3255
3290
|
* at all is not read: publishing a query schema as the request contract would
|
|
3256
3291
|
* be the same confident-and-wrong answer this anchor exists to remove.
|
|
3257
3292
|
*/
|
|
3258
|
-
|
|
3293
|
+
validatedBodyContract(registration) {
|
|
3294
|
+
for (const candidate of this.middlewareBodySchemaValues(registration)) {
|
|
3295
|
+
const declared = this.schemaContract(candidate, 'input');
|
|
3296
|
+
if (declared) {
|
|
3297
|
+
return declared;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
return null;
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* The values a registration's validator middleware binds to a body part, in
|
|
3304
|
+
* argument order: every non-literal argument of a middleware call that names
|
|
3305
|
+
* a `REQUEST_BODY_PARTS` part. Whether each one IS a schema is the reader's
|
|
3306
|
+
* question (`schemaContract`), not this walk's.
|
|
3307
|
+
*/
|
|
3308
|
+
middlewareBodySchemaValues(registration) {
|
|
3259
3309
|
if (!Node.isCallExpression(registration)) {
|
|
3260
|
-
return
|
|
3310
|
+
return [];
|
|
3261
3311
|
}
|
|
3312
|
+
const values = [];
|
|
3262
3313
|
for (const argument of registration.getArguments()) {
|
|
3263
3314
|
const middleware = this.unwrapExpressionNode(argument);
|
|
3264
3315
|
if (!Node.isCallExpression(middleware)) {
|
|
@@ -3267,21 +3318,58 @@ export class TypeInferrer {
|
|
|
3267
3318
|
const middlewareArgs = middleware
|
|
3268
3319
|
.getArguments()
|
|
3269
3320
|
.map((arg) => this.unwrapExpressionNode(arg));
|
|
3270
|
-
const
|
|
3271
|
-
|
|
3321
|
+
const bindsBody = middlewareArgs.some((arg) => Node.isStringLiteral(arg) &&
|
|
3322
|
+
REQUEST_BODY_PARTS.has(arg.getLiteralValue().toLowerCase()));
|
|
3323
|
+
if (!bindsBody) {
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
3326
|
+
values.push(...middlewareArgs.filter((arg) => !Node.isStringLiteral(arg)));
|
|
3327
|
+
}
|
|
3328
|
+
return values;
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* The request contract behind a VALIDATED READ: a call inside a registered
|
|
3332
|
+
* handler whose type is exactly the parsed output of a body schema the
|
|
3333
|
+
* enclosing registration declares (carrick#1101).
|
|
3334
|
+
*
|
|
3335
|
+
* The read's own type is what validation hands the handler, the schema's
|
|
3336
|
+
* output, so a locator that lands on it would publish the output as the
|
|
3337
|
+
* request. The registration is found by walking the read's ancestors to each
|
|
3338
|
+
* call or registry object whose handler contains the read and which declares
|
|
3339
|
+
* a body schema (anchor (b) or (b2)). The match is on the printed type: only
|
|
3340
|
+
* a read whose structural text equals that schema's output is substituted, so
|
|
3341
|
+
* any other expression in the handler (a query read, a payload of a different
|
|
3342
|
+
* shape) keeps its own type.
|
|
3343
|
+
*/
|
|
3344
|
+
validatedReadRequestContract(read) {
|
|
3345
|
+
let readText;
|
|
3346
|
+
for (const ancestor of read.getAncestors()) {
|
|
3347
|
+
if (!Node.isCallExpression(ancestor) &&
|
|
3348
|
+
!Node.isObjectLiteralExpression(ancestor)) {
|
|
3272
3349
|
continue;
|
|
3273
3350
|
}
|
|
3274
|
-
|
|
3351
|
+
const handler = this.registrationHandlerAt(ancestor);
|
|
3352
|
+
if (!handler ||
|
|
3353
|
+
read.getStart() < handler.getStart() ||
|
|
3354
|
+
read.getEnd() > handler.getEnd()) {
|
|
3275
3355
|
continue;
|
|
3276
3356
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3357
|
+
const schemaValues = [
|
|
3358
|
+
...this.routeSchemaEntryValues(ancestor, 'body'),
|
|
3359
|
+
...this.middlewareBodySchemaValues(ancestor),
|
|
3360
|
+
];
|
|
3361
|
+
for (const value of schemaValues) {
|
|
3362
|
+
const output = this.schemaContract(value, 'output');
|
|
3363
|
+
if (!output) {
|
|
3279
3364
|
continue;
|
|
3280
3365
|
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3366
|
+
if (readText === undefined) {
|
|
3367
|
+
readText = this.structuralTextFromType(read.getType(), read);
|
|
3368
|
+
}
|
|
3369
|
+
if (readText !== output.text) {
|
|
3370
|
+
continue;
|
|
3284
3371
|
}
|
|
3372
|
+
return this.schemaContract(value, 'input');
|
|
3285
3373
|
}
|
|
3286
3374
|
}
|
|
3287
3375
|
return null;
|
|
@@ -3291,29 +3379,43 @@ export class TypeInferrer {
|
|
|
3291
3379
|
*
|
|
3292
3380
|
* `part` is `'body'` for the request contract and `'response'` for the
|
|
3293
3381
|
* response contract; a `response` entry keyed by status code resolves to its
|
|
3294
|
-
* success entry.
|
|
3295
|
-
*
|
|
3296
|
-
* when the
|
|
3297
|
-
*
|
|
3298
|
-
*
|
|
3382
|
+
* success entry. The body reads the schema's input and the response its
|
|
3383
|
+
* output (carrick#1101). Returns null when the registration carries no
|
|
3384
|
+
* schema, when the entry references something whose declared types cannot be
|
|
3385
|
+
* resolved, or when the schema is a plain JSON-schema literal (whose own
|
|
3386
|
+
* object type is the JSON-Schema document, not the payload — emitting that
|
|
3387
|
+
* would be worse than abstaining).
|
|
3388
|
+
*/
|
|
3389
|
+
routeSchemaContract(registration, part) {
|
|
3390
|
+
const [value] = this.routeSchemaEntryValues(registration, part);
|
|
3391
|
+
if (!value) {
|
|
3392
|
+
return null;
|
|
3393
|
+
}
|
|
3394
|
+
return this.schemaContract(value, part === 'body' ? 'input' : 'output');
|
|
3395
|
+
}
|
|
3396
|
+
/**
|
|
3397
|
+
* The value a route `schema` object declares for `part`, as a zero- or
|
|
3398
|
+
* one-element list: the `body` entry itself, or the success entry of a
|
|
3399
|
+
* status-keyed `response` map.
|
|
3299
3400
|
*/
|
|
3300
|
-
|
|
3401
|
+
routeSchemaEntryValues(registration, part) {
|
|
3301
3402
|
const schemaObject = this.routeSchemaObject(registration);
|
|
3302
3403
|
if (!schemaObject) {
|
|
3303
|
-
return
|
|
3404
|
+
return [];
|
|
3304
3405
|
}
|
|
3305
3406
|
const entry = schemaObject.getProperty(part);
|
|
3306
3407
|
if (!entry || !Node.isPropertyAssignment(entry)) {
|
|
3307
|
-
return
|
|
3408
|
+
return [];
|
|
3308
3409
|
}
|
|
3309
3410
|
const initializer = entry.getInitializer();
|
|
3310
3411
|
if (!initializer) {
|
|
3311
|
-
return
|
|
3412
|
+
return [];
|
|
3312
3413
|
}
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3414
|
+
return [
|
|
3415
|
+
part === 'response'
|
|
3416
|
+
? (this.successStatusEntry(initializer) ?? initializer)
|
|
3417
|
+
: initializer,
|
|
3418
|
+
];
|
|
3317
3419
|
}
|
|
3318
3420
|
/**
|
|
3319
3421
|
* The `schema` object literal carried by a route registration: scan the
|
|
@@ -3379,17 +3481,36 @@ export class TypeInferrer {
|
|
|
3379
3481
|
return best?.node;
|
|
3380
3482
|
}
|
|
3381
3483
|
/**
|
|
3382
|
-
* The payload type a schema entry declares
|
|
3484
|
+
* The payload type a schema entry declares, read in `direction`.
|
|
3383
3485
|
*
|
|
3384
3486
|
* The entry is either a REFERENCE call — `ref('CreateWidget')`, a
|
|
3385
3487
|
* name-to-schema indirection whose registry is the argument the ref function
|
|
3386
|
-
* was built from — or the schema value itself.
|
|
3387
|
-
*
|
|
3388
|
-
*
|
|
3389
|
-
*
|
|
3390
|
-
* and
|
|
3488
|
+
* was built from — or the schema value itself. A value that declares no type
|
|
3489
|
+
* in either direction is not a schema and yields null.
|
|
3490
|
+
*
|
|
3491
|
+
* `output` is the parsed value (`schemaOutputType`). `input` is what a caller
|
|
3492
|
+
* sends (`schemaInputType`), and a schema that declares no input member reads
|
|
3493
|
+
* its output instead, which is the whole of what is knowable about it.
|
|
3494
|
+
*
|
|
3495
|
+
* The input is decided member by member (carrick#1105). A member whose input
|
|
3496
|
+
* is `any`/`unknown` where the output is concrete is what a coercion declares
|
|
3497
|
+
* (it accepts any value and parses it into, say, a number). A top type in a
|
|
3498
|
+
* published contract disqualifies the whole row downstream, so THAT member
|
|
3499
|
+
* prints its output type, keeps the input key's optionality, and is recorded
|
|
3500
|
+
* as `coerced_input` provenance. Every other member keeps its input, so a
|
|
3501
|
+
* defaulted key beside a coerced one stays optional to send.
|
|
3502
|
+
*
|
|
3503
|
+
* Limits, each logged where it bites:
|
|
3504
|
+
* - the position walk is bounded (`walkTypePositions`), so a top type below
|
|
3505
|
+
* the bound is not compared and prints as the input declares it;
|
|
3506
|
+
* - a coerced position that cannot be substituted (its output is absent or
|
|
3507
|
+
* differs across union branches, or the printer cannot reach it inside a
|
|
3508
|
+
* tuple, behind a cycle or past the expansion depth) makes the row fall
|
|
3509
|
+
* back to the whole parsed output with every coerced member labelled. That
|
|
3510
|
+
* was the answer before the per-member reading, and it never publishes an
|
|
3511
|
+
* `unknown`.
|
|
3391
3512
|
*/
|
|
3392
|
-
|
|
3513
|
+
schemaContract(value, direction) {
|
|
3393
3514
|
const node = this.unwrapExpressionNode(value);
|
|
3394
3515
|
let schemaType;
|
|
3395
3516
|
if (Node.isCallExpression(node)) {
|
|
@@ -3403,13 +3524,201 @@ export class TypeInferrer {
|
|
|
3403
3524
|
return null;
|
|
3404
3525
|
}
|
|
3405
3526
|
}
|
|
3527
|
+
const location = this.getNodeLocation(node);
|
|
3528
|
+
const where = `${location.file_path}:${location.start_line}`;
|
|
3406
3529
|
const output = this.schemaOutputType(schemaType, node);
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3530
|
+
const input = direction === 'input' ? this.schemaInputType(schemaType, node) : undefined;
|
|
3531
|
+
if (!output && !input) {
|
|
3532
|
+
this.log(`Route schema entry at ${where} declares no resolvable type (schema library ` +
|
|
3533
|
+
'types unavailable, or a plain JSON-Schema literal); leaving unresolved');
|
|
3534
|
+
return null;
|
|
3535
|
+
}
|
|
3536
|
+
if (direction === 'output' || !input) {
|
|
3537
|
+
if (direction === 'input') {
|
|
3538
|
+
this.log(`Route schema entry at ${where} declares no input type; publishing its parsed output`);
|
|
3539
|
+
}
|
|
3540
|
+
const text = output ? this.structuralTextFromType(output, node) : null;
|
|
3541
|
+
return text ? { text } : null;
|
|
3542
|
+
}
|
|
3543
|
+
const inputTop = this.topTypePositions(input, node, where);
|
|
3544
|
+
const outputTop = output && inputTop.size > 0 ? this.topTypePositions(output, node, where) : undefined;
|
|
3545
|
+
const coerced = outputTop
|
|
3546
|
+
? [...inputTop.entries()]
|
|
3547
|
+
.filter(([position]) => !outputTop.has(position))
|
|
3548
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
3549
|
+
: [];
|
|
3550
|
+
if (!output || coerced.length === 0) {
|
|
3551
|
+
const text = this.structuralTextFromType(input, node);
|
|
3552
|
+
return text ? { text } : null;
|
|
3553
|
+
}
|
|
3554
|
+
const positions = coerced.map(([position]) => position || '<root>').join(', ');
|
|
3555
|
+
const provenance = coerced.map(([position, kind]) => ({
|
|
3556
|
+
path: position,
|
|
3557
|
+
kind,
|
|
3558
|
+
reason: 'coerced_input',
|
|
3559
|
+
detail: `the schema accepts any value here and coerces it, so the published type ` +
|
|
3560
|
+
`is what parsing produces, not a limit on what a caller may send`,
|
|
3561
|
+
}));
|
|
3562
|
+
const text = this.inputWithCoercedMembers(input, output, coerced.map(([position]) => position), node, where);
|
|
3563
|
+
if (text) {
|
|
3564
|
+
this.log(`Route schema entry at ${where} accepts any value at ${positions} on input; ` +
|
|
3565
|
+
'publishing the parsed type there and the input everywhere else');
|
|
3566
|
+
return { text, provenance };
|
|
3567
|
+
}
|
|
3568
|
+
const whole = this.structuralTextFromType(output, node);
|
|
3569
|
+
if (!whole) {
|
|
3410
3570
|
return null;
|
|
3411
3571
|
}
|
|
3412
|
-
|
|
3572
|
+
this.log(`Route schema entry at ${where} accepts any value at ${positions} on input, and not ` +
|
|
3573
|
+
'every one could be substituted member by member; publishing its whole parsed output');
|
|
3574
|
+
return { text: whole, provenance };
|
|
3575
|
+
}
|
|
3576
|
+
/**
|
|
3577
|
+
* Print a schema's input with each coerced position replaced by the output's
|
|
3578
|
+
* type at that position (carrick#1105). Null when any position cannot be
|
|
3579
|
+
* substituted: its output is missing or differs across union branches, or
|
|
3580
|
+
* the printer never reached it. A partial substitution would publish an
|
|
3581
|
+
* `unknown`, so it is not an answer.
|
|
3582
|
+
*/
|
|
3583
|
+
inputWithCoercedMembers(input, output, coerced, at, where) {
|
|
3584
|
+
const wanted = new Set(coerced);
|
|
3585
|
+
const types = new Map();
|
|
3586
|
+
const ambiguous = new Set();
|
|
3587
|
+
this.walkTypePositions(output, at, where, (type, position, unionPart) => {
|
|
3588
|
+
// A union's parts share its position; the union itself is the type there.
|
|
3589
|
+
if (wanted.has(position) && !unionPart) {
|
|
3590
|
+
const seen = types.get(position);
|
|
3591
|
+
if (!seen) {
|
|
3592
|
+
types.set(position, type);
|
|
3593
|
+
}
|
|
3594
|
+
else if (seen.compilerType !== type.compilerType) {
|
|
3595
|
+
ambiguous.add(position);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
return true;
|
|
3599
|
+
});
|
|
3600
|
+
for (const position of ambiguous) {
|
|
3601
|
+
types.delete(position);
|
|
3602
|
+
}
|
|
3603
|
+
const missing = coerced.filter((position) => !types.has(position));
|
|
3604
|
+
if (missing.length > 0) {
|
|
3605
|
+
this.log(`Route schema entry at ${where}: no single parsed type at ` +
|
|
3606
|
+
`${missing.map((position) => position || '<root>').join(', ')} to substitute ` +
|
|
3607
|
+
'for the coerced input');
|
|
3608
|
+
return null;
|
|
3609
|
+
}
|
|
3610
|
+
const root = types.get('');
|
|
3611
|
+
if (root) {
|
|
3612
|
+
return this.structuralTextFromType(root, at);
|
|
3613
|
+
}
|
|
3614
|
+
const overrides = { types, applied: new Set(), at };
|
|
3615
|
+
let text;
|
|
3616
|
+
try {
|
|
3617
|
+
text = expandTypeStructural(this.unwrapPromiseType(input), new Set(), 0, overrides);
|
|
3618
|
+
}
|
|
3619
|
+
catch {
|
|
3620
|
+
return null;
|
|
3621
|
+
}
|
|
3622
|
+
const unreached = coerced.filter((position) => !overrides.applied.has(position));
|
|
3623
|
+
if (unreached.length > 0) {
|
|
3624
|
+
this.log(`Route schema entry at ${where}: the printer did not reach coerced ` +
|
|
3625
|
+
`${unreached.join(', ')} (tuple, cycle or depth bound)`);
|
|
3626
|
+
return null;
|
|
3627
|
+
}
|
|
3628
|
+
return this.isUselessType(text) ? null : text;
|
|
3629
|
+
}
|
|
3630
|
+
/**
|
|
3631
|
+
* Member positions inside `root` that are `any` or `unknown`, keyed by the
|
|
3632
|
+
* member-path notation the provenance entries use (`''` for the root,
|
|
3633
|
+
* `sub.field`, `items<0>` for an array element). Union and intersection
|
|
3634
|
+
* members share their parent's position, so `unknown | undefined` on an
|
|
3635
|
+
* optional key reads as `unknown` there.
|
|
3636
|
+
*/
|
|
3637
|
+
topTypePositions(root, at, where) {
|
|
3638
|
+
const found = new Map();
|
|
3639
|
+
this.walkTypePositions(root, at, where, (type, position) => {
|
|
3640
|
+
if (type.isAny() || type.isUnknown()) {
|
|
3641
|
+
found.set(position, type.isAny() ? 'any' : 'unknown');
|
|
3642
|
+
return false;
|
|
3643
|
+
}
|
|
3644
|
+
return true;
|
|
3645
|
+
});
|
|
3646
|
+
return found;
|
|
3647
|
+
}
|
|
3648
|
+
/**
|
|
3649
|
+
* Visit every member position of `root`, in the notation `topTypePositions`
|
|
3650
|
+
* documents. `visit` returns false to stop descending below a position;
|
|
3651
|
+
* `unionPart` is true when the type is a union or intersection part visited
|
|
3652
|
+
* at its parent's position. Callables are not descended into.
|
|
3653
|
+
*
|
|
3654
|
+
* Bounded and cycle-safe. It only has to tell a schema's input apart from its
|
|
3655
|
+
* output, so a subtree past the bound is not compared, and that is logged:
|
|
3656
|
+
* a coercion below the bound prints as its input declares it.
|
|
3657
|
+
*/
|
|
3658
|
+
walkTypePositions(root, at, where, visit) {
|
|
3659
|
+
const MAX_DEPTH = 8;
|
|
3660
|
+
const MAX_VISITED = 512;
|
|
3661
|
+
const onPath = new Set();
|
|
3662
|
+
let visited = 0;
|
|
3663
|
+
let bounded = false;
|
|
3664
|
+
const walk = (type, position, depth, unionPart) => {
|
|
3665
|
+
if (depth > MAX_DEPTH || visited > MAX_VISITED) {
|
|
3666
|
+
bounded = true;
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
if (!visit(type, position, unionPart)) {
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3672
|
+
const compilerType = type.compilerType;
|
|
3673
|
+
if (onPath.has(compilerType)) {
|
|
3674
|
+
return;
|
|
3675
|
+
}
|
|
3676
|
+
onPath.add(compilerType);
|
|
3677
|
+
visited += 1;
|
|
3678
|
+
try {
|
|
3679
|
+
const parts = type.isUnion()
|
|
3680
|
+
? type.getUnionTypes()
|
|
3681
|
+
: type.isIntersection()
|
|
3682
|
+
? type.getIntersectionTypes()
|
|
3683
|
+
: undefined;
|
|
3684
|
+
if (parts) {
|
|
3685
|
+
for (const part of parts) {
|
|
3686
|
+
walk(part, position, depth + 1, true);
|
|
3687
|
+
}
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
if (type.isArray()) {
|
|
3691
|
+
const element = type.getArrayElementType();
|
|
3692
|
+
if (element) {
|
|
3693
|
+
walk(element, `${position}<0>`, depth + 1, false);
|
|
3694
|
+
}
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
if (!type.isObject() || this.isCallableType(type)) {
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
for (const property of type.getProperties()) {
|
|
3701
|
+
let propertyType;
|
|
3702
|
+
try {
|
|
3703
|
+
propertyType = property.getTypeAtLocation(at);
|
|
3704
|
+
}
|
|
3705
|
+
catch {
|
|
3706
|
+
continue;
|
|
3707
|
+
}
|
|
3708
|
+
const name = property.getName();
|
|
3709
|
+
walk(propertyType, position === '' ? name : `${position}.${name}`, depth + 1, false);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
finally {
|
|
3713
|
+
onPath.delete(compilerType);
|
|
3714
|
+
}
|
|
3715
|
+
};
|
|
3716
|
+
walk(root, '', 0, false);
|
|
3717
|
+
if (bounded) {
|
|
3718
|
+
this.log(`Schema type at ${where} is deeper or wider than the position walk bound ` +
|
|
3719
|
+
`(depth ${MAX_DEPTH}, ${MAX_VISITED} members); members past it are not ` +
|
|
3720
|
+
'compared for coercion');
|
|
3721
|
+
}
|
|
3413
3722
|
}
|
|
3414
3723
|
/**
|
|
3415
3724
|
* Follow a schema REFERENCE call (`ref('CreateWidget')`) to the schema value
|
|
@@ -3479,8 +3788,9 @@ export class TypeInferrer {
|
|
|
3479
3788
|
}
|
|
3480
3789
|
/**
|
|
3481
3790
|
* The parsed output type of a schema value: the return type of its `parse`
|
|
3482
|
-
* method, else a declared `_output` member
|
|
3483
|
-
*
|
|
3791
|
+
* method, else a declared `_output` member, else the Standard Schema member
|
|
3792
|
+
* `~standard.types.output` (a library that exposes nothing else). Returns
|
|
3793
|
+
* undefined when none carries a usable type — including when the schema library's own types are
|
|
3484
3794
|
* unavailable (an uninstalled dependency resolves the schema to `any`), which
|
|
3485
3795
|
* must abstain rather than publish `any` as a contract.
|
|
3486
3796
|
*/
|
|
@@ -3507,13 +3817,62 @@ export class TypeInferrer {
|
|
|
3507
3817
|
const outputSymbol = schemaType.getProperty('_output');
|
|
3508
3818
|
if (outputSymbol) {
|
|
3509
3819
|
try {
|
|
3510
|
-
|
|
3820
|
+
const declared = usable(outputSymbol.getTypeAtLocation(at));
|
|
3821
|
+
if (declared) {
|
|
3822
|
+
return declared;
|
|
3823
|
+
}
|
|
3511
3824
|
}
|
|
3512
3825
|
catch {
|
|
3513
|
-
|
|
3826
|
+
// fall through to the Standard Schema member
|
|
3514
3827
|
}
|
|
3515
3828
|
}
|
|
3516
|
-
return
|
|
3829
|
+
return usable(this.standardSchemaType(schemaType, at, 'output'));
|
|
3830
|
+
}
|
|
3831
|
+
/**
|
|
3832
|
+
* The input type of a schema value, what a caller may send (carrick#1101):
|
|
3833
|
+
* the Standard Schema member `~standard.types.input`, else a declared
|
|
3834
|
+
* `_input` member. Undefined when the schema declares neither.
|
|
3835
|
+
*
|
|
3836
|
+
* Unlike the output, an input of `unknown` is kept: it is a declared fact
|
|
3837
|
+
* (a coercion accepts any value), and `schemaContract` decides what to
|
|
3838
|
+
* publish for it. An `any` that is really an unresolved library still reads
|
|
3839
|
+
* as no input, because an unresolved schema type has no members to read.
|
|
3840
|
+
*/
|
|
3841
|
+
schemaInputType(schemaType, at) {
|
|
3842
|
+
const standard = this.standardSchemaType(schemaType, at, 'input');
|
|
3843
|
+
if (standard) {
|
|
3844
|
+
return standard;
|
|
3845
|
+
}
|
|
3846
|
+
const inputSymbol = schemaType.getProperty('_input');
|
|
3847
|
+
if (!inputSymbol) {
|
|
3848
|
+
return undefined;
|
|
3849
|
+
}
|
|
3850
|
+
try {
|
|
3851
|
+
return inputSymbol.getTypeAtLocation(at);
|
|
3852
|
+
}
|
|
3853
|
+
catch {
|
|
3854
|
+
return undefined;
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
/**
|
|
3858
|
+
* `~standard.types.<side>` on a schema value: the Standard Schema
|
|
3859
|
+
* specification's library-neutral statement of a schema's input and output
|
|
3860
|
+
* types. `types` is declared optional, so its `undefined` is stripped before
|
|
3861
|
+
* the side is read.
|
|
3862
|
+
*/
|
|
3863
|
+
standardSchemaType(schemaType, at, side) {
|
|
3864
|
+
try {
|
|
3865
|
+
const standard = schemaType.getProperty('~standard');
|
|
3866
|
+
const types = standard?.getTypeAtLocation(at).getProperty('types');
|
|
3867
|
+
const member = types
|
|
3868
|
+
?.getTypeAtLocation(at)
|
|
3869
|
+
.getNonNullableType()
|
|
3870
|
+
.getProperty(side);
|
|
3871
|
+
return member?.getTypeAtLocation(at);
|
|
3872
|
+
}
|
|
3873
|
+
catch {
|
|
3874
|
+
return undefined;
|
|
3875
|
+
}
|
|
3517
3876
|
}
|
|
3518
3877
|
/**
|
|
3519
3878
|
* Find a node by matching expression text near a target line.
|
|
@@ -28,22 +28,51 @@
|
|
|
28
28
|
* `type-inferrer.ts` (consumer-side inference), so both paths emit the same
|
|
29
29
|
* structural form rather than a dangling name.
|
|
30
30
|
*/
|
|
31
|
-
import { type Type } from 'ts-morph';
|
|
31
|
+
import { type Node, type Type } from 'ts-morph';
|
|
32
32
|
/**
|
|
33
33
|
* Bound on the structural-expansion recursion. Deep enough for every realistic
|
|
34
34
|
* request/response shape; a backstop against pathological/recursive types the
|
|
35
35
|
* cycle set somehow misses.
|
|
36
36
|
*/
|
|
37
37
|
export declare const MAX_EXPANSION_DEPTH = 12;
|
|
38
|
+
/**
|
|
39
|
+
* Types to print at named member positions instead of the walked type
|
|
40
|
+
* (carrick#1105).
|
|
41
|
+
*
|
|
42
|
+
* A validation schema's request contract is its INPUT, except at a member
|
|
43
|
+
* whose input is `unknown` and whose output is concrete (a coercion): that
|
|
44
|
+
* member prints the OUTPUT type. The substitution is per member, so it has to
|
|
45
|
+
* happen inside the walk, where the member is printed.
|
|
46
|
+
*
|
|
47
|
+
* Positions use the member-path notation of the provenance entries: `''` for
|
|
48
|
+
* the root, `sub.field` for a property (keyed by symbol name), `items<0>` for
|
|
49
|
+
* an array element; union and intersection members share their parent's
|
|
50
|
+
* position. The walk records every position it printed an override at in
|
|
51
|
+
* `applied`, so a caller can tell a substitution the walk could not reach (a
|
|
52
|
+
* tuple, a cycle, the depth backstop) from one it made.
|
|
53
|
+
*
|
|
54
|
+
* `at` is the node the member types are read at while walking toward an
|
|
55
|
+
* override. A library's inferred object type is a mapped type whose member
|
|
56
|
+
* declaration sits inside a generic, where the member reads as `any`; read at
|
|
57
|
+
* the schema's own node it is the instantiated type, the same read the
|
|
58
|
+
* caller's position walk makes.
|
|
59
|
+
*/
|
|
60
|
+
export interface MemberOverrides {
|
|
61
|
+
readonly types: ReadonlyMap<string, Type>;
|
|
62
|
+
readonly applied: Set<string>;
|
|
63
|
+
readonly at: Node;
|
|
64
|
+
}
|
|
38
65
|
/**
|
|
39
66
|
* Recursively render a `Type` as fully-inlined structural text.
|
|
40
67
|
*
|
|
41
68
|
* Named object/interface types are expanded to their member structure;
|
|
42
69
|
* primitives, literals, library types (`Date`, `Promise`, tuples, …) and
|
|
43
70
|
* functions stay by name. The `seen` set (object type ids on the current
|
|
44
|
-
* branch) breaks reference cycles; `depth` is a hard backstop.
|
|
71
|
+
* branch) breaks reference cycles; `depth` is a hard backstop. `overrides`
|
|
72
|
+
* substitutes a type at named member positions (`MemberOverrides`); without
|
|
73
|
+
* it the print is unchanged.
|
|
45
74
|
*/
|
|
46
|
-
export declare function expandTypeStructural(type: Type, seen?: Set<number>, depth?: number): string;
|
|
75
|
+
export declare function expandTypeStructural(type: Type, seen?: Set<number>, depth?: number, overrides?: MemberOverrides): string;
|
|
47
76
|
/**
|
|
48
77
|
* Non-expanded text for a type. Passes `undefined` as the enclosing node so
|
|
49
78
|
* the compiler can't throw on an invalid node context (tuples and some
|