genoc 0.4.2 → 0.4.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/README.md +2 -1
- package/dist/analyzer/analyze.js +70 -1
- package/dist/analyzer/path-analyzer.d.ts +18 -0
- package/dist/analyzer/path-analyzer.js +1 -0
- package/dist/generator/client-generator.js +1 -1
- package/dist/generator/method-generator.js +12 -1
- package/dist/utils/operation-naming.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Generated code depends only on the tiny `genoc/runtime` module. Full type safety
|
|
|
12
12
|
|
|
13
13
|
- Full OpenAPI 3.0 and 3.1 specification support with automatic version detection
|
|
14
14
|
- End-to-end type safety: requests, responses, and errors are fully typed
|
|
15
|
+
- Typed path parameters: values are typed from their schema and accept `T | string` — pass a pre-formatted string for custom wire formatting; serialization is inline `String()`
|
|
15
16
|
- Works with any HTTP client: plug in fetch, axios, or anything else
|
|
16
17
|
- Shared runtime contract: `genoc/runtime` exports the `Requester` type and response and error classes, so one requester implementation works with every generated client
|
|
17
18
|
- Error types with per-status-code narrowing and type guards
|
|
@@ -115,7 +116,7 @@ For the full example of catching and narrowing errors, see [Error handling](./do
|
|
|
115
116
|
## Documentation
|
|
116
117
|
|
|
117
118
|
- [Configuration](./docs/configuration.md): CLI flags, `.genocrc` config files, method naming strategies, and proxy support
|
|
118
|
-
- [Usage](./docs/usage.md): the `Requester` contract, shared runtime, binary and file responses, and JSDoc output
|
|
119
|
+
- [Usage](./docs/usage.md): the `Requester` contract, `createClient` options, shared runtime, binary and file responses, and JSDoc output
|
|
119
120
|
- [Error handling](./docs/error-handling.md): typed errors, `isDefinedError`, and the catching example
|
|
120
121
|
- [OpenAPI 3.0 support](./docs/openapi-3.0-support.md): data types, schema keywords, parameters, bodies, uploads, responses, errors, `$ref`, components, security, servers, and operations
|
|
121
122
|
- [OpenAPI 3.1 support](./docs/openapi-3.1-support.md): all 3.0 features plus webhooks and JSON Schema 2020-12 alignment, with a diff against 3.0
|
package/dist/analyzer/analyze.js
CHANGED
|
@@ -41,6 +41,34 @@ function attachTypeJsDoc(jsDoc, definition) {
|
|
|
41
41
|
function indentJsDocBlock(jsDoc) {
|
|
42
42
|
return jsDoc.split('\n').map((line) => `${INDENT_UNIT}${line}`);
|
|
43
43
|
}
|
|
44
|
+
/** Schema used for parameters declared without a `schema` (typed as `string`). */
|
|
45
|
+
function defaultedParamSchema(param) {
|
|
46
|
+
return param.schema ?? { type: 'string' };
|
|
47
|
+
}
|
|
48
|
+
const STRING_SERIALIZABLE_MEMBER = /^(string|number|boolean|null|true|false|'[^']*'|-?\d+(?:\.\d+)?)$/;
|
|
49
|
+
/**
|
|
50
|
+
* Whether a finished path-param type can be serialized inline with `String()`:
|
|
51
|
+
* every union member must be a primitive with a defined string form, or a
|
|
52
|
+
* string/number literal (literal unions like `'fast' | 'slow'` are kept).
|
|
53
|
+
* Named component types count when their own mapped definition resolves
|
|
54
|
+
* (transitively, cycle-safe) to such members — so `$ref` path params to
|
|
55
|
+
* primitive aliases keep their named type. Anything else — objects, arrays,
|
|
56
|
+
* named non-primitive `$ref` types — falls back to a `string` signature (the
|
|
57
|
+
* pre-typed-path-params behavior), because there is no defined string
|
|
58
|
+
* serialization for such values.
|
|
59
|
+
*/
|
|
60
|
+
function isInlineSerializableType(tsType, namedMappedTypes, seen = new Set()) {
|
|
61
|
+
return tsType.split('|').every((member) => {
|
|
62
|
+
const token = member.trim();
|
|
63
|
+
if (STRING_SERIALIZABLE_MEMBER.test(token))
|
|
64
|
+
return true;
|
|
65
|
+
const namedDefinition = namedMappedTypes.get(token);
|
|
66
|
+
if (namedDefinition === undefined || seen.has(token))
|
|
67
|
+
return false;
|
|
68
|
+
const nextSeen = new Set(seen).add(token);
|
|
69
|
+
return isInlineSerializableType(namedDefinition, namedMappedTypes, nextSeen);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
44
72
|
/**
|
|
45
73
|
* Build the JSDoc block for a query/header parameter property using the
|
|
46
74
|
* pinned parameter merge rule: parameter-level description / deprecated /
|
|
@@ -62,7 +90,7 @@ function buildParamPropertyJsDoc(param, schema) {
|
|
|
62
90
|
function buildParamTypeBody(params, mapper) {
|
|
63
91
|
const lines = [];
|
|
64
92
|
for (const param of params) {
|
|
65
|
-
const paramSchema = param
|
|
93
|
+
const paramSchema = defaultedParamSchema(param);
|
|
66
94
|
const result = mapper.mapSchema(paramSchema);
|
|
67
95
|
const optional = param.required ? '' : '?';
|
|
68
96
|
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(param.name) ? param.name : `"${param.name}"`;
|
|
@@ -301,10 +329,14 @@ export function analyze(doc, opts = {}) {
|
|
|
301
329
|
});
|
|
302
330
|
// Section 1: Schema types
|
|
303
331
|
const schemaEntries = [];
|
|
332
|
+
// Mapped definition text per renamed component name — lets the path-param
|
|
333
|
+
// inline-serializability check resolve named types to their primitives.
|
|
334
|
+
const namedMappedTypes = new Map();
|
|
304
335
|
if (doc.components?.schemas) {
|
|
305
336
|
for (const [name, schema] of Object.entries(doc.components.schemas)) {
|
|
306
337
|
const renamedName = renameMap.get(name) ?? sanitizeTypeName(name);
|
|
307
338
|
const result = mapper.mapSchema(schema, renamedName);
|
|
339
|
+
namedMappedTypes.set(renamedName, result.tsType);
|
|
308
340
|
const resolved = resolver.resolve(schema);
|
|
309
341
|
const definition = `export type ${renamedName} = ${result.tsType};`;
|
|
310
342
|
schemaEntries.push({
|
|
@@ -337,6 +369,43 @@ export function analyze(doc, opts = {}) {
|
|
|
337
369
|
const { operations: analyzedOperations, requestBodySchemas } = analyzePathsDetailed(doc, resolver, strategy);
|
|
338
370
|
const operations = [];
|
|
339
371
|
for (const op of analyzedOperations) {
|
|
372
|
+
// Path params render as flat method arguments, so each carries its
|
|
373
|
+
// finished signature type. A raw `$ref` schema maps to the named contract
|
|
374
|
+
// type (the same raw-schema travel pattern as request bodies and
|
|
375
|
+
// responses — the contracts renderer emits the named type from the
|
|
376
|
+
// component table); the mapper's import facts feed the client file's
|
|
377
|
+
// `import type` list. Inline schemas map the resolved schema with
|
|
378
|
+
// `format` stripped: path params are typed by their base primitive
|
|
379
|
+
// (boolean, number, literal unions) and format brands don't flow into
|
|
380
|
+
// signatures.
|
|
381
|
+
const pathParamTypeNames = new Set();
|
|
382
|
+
for (const param of op.pathParams) {
|
|
383
|
+
const raw = param.rawSchema;
|
|
384
|
+
if (raw !== undefined && '$ref' in raw) {
|
|
385
|
+
const result = mapper.mapSchema(raw);
|
|
386
|
+
if (isInlineSerializableType(result.tsType, namedMappedTypes)) {
|
|
387
|
+
param.finishedType = result.tsType;
|
|
388
|
+
for (const name of result.imports) {
|
|
389
|
+
pathParamTypeNames.add(name);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
// Inline-serialization constraint: a `$ref` to an object/array
|
|
394
|
+
// schema (or any non-primitive named type) has no defined string
|
|
395
|
+
// form, so the parameter falls back to `string` and the named
|
|
396
|
+
// type is not imported into the client file.
|
|
397
|
+
param.finishedType = 'string';
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const schema = defaultedParamSchema(param);
|
|
402
|
+
const tracerSchema = schema.format === undefined ? schema : { ...schema, format: undefined };
|
|
403
|
+
const mapped = mapper.mapSchema(tracerSchema).tsType;
|
|
404
|
+
param.finishedType = isInlineSerializableType(mapped, namedMappedTypes) ? mapped : 'string';
|
|
405
|
+
}
|
|
406
|
+
if (pathParamTypeNames.size > 0) {
|
|
407
|
+
op.pathParamTypeNames = [...pathParamTypeNames];
|
|
408
|
+
}
|
|
340
409
|
operations.push({
|
|
341
410
|
...op,
|
|
342
411
|
contractsLines: [],
|
|
@@ -7,9 +7,21 @@ export interface AnalyzedParameter {
|
|
|
7
7
|
required: boolean;
|
|
8
8
|
/** Resolved param schema — analyzer-internal translation input for `analyze()`; renderers must not read it. */
|
|
9
9
|
schema: SchemaObject | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Raw (possibly `$ref`) param schema — analyzer-internal translation input
|
|
12
|
+
* for `analyze()`; renderers must not read it. Kept beside the resolved
|
|
13
|
+
* schema so `$ref` sites translate to the named contract type (the same
|
|
14
|
+
* raw-schema travel pattern as request bodies and responses).
|
|
15
|
+
*/
|
|
16
|
+
rawSchema: SchemaObject | ReferenceObject | undefined;
|
|
10
17
|
description?: string;
|
|
11
18
|
deprecated?: boolean;
|
|
12
19
|
example?: unknown;
|
|
20
|
+
/**
|
|
21
|
+
* Finished (real-mapper) TS type text for the method signature — set by
|
|
22
|
+
* `analyze()` for path params, the only params rendered as flat args.
|
|
23
|
+
*/
|
|
24
|
+
finishedType?: string;
|
|
13
25
|
}
|
|
14
26
|
export interface AnalyzedRequestBody {
|
|
15
27
|
required: boolean;
|
|
@@ -59,6 +71,12 @@ export interface AnalyzedOperation {
|
|
|
59
71
|
queryParams: AnalyzedParameter[];
|
|
60
72
|
headerParams: AnalyzedParameter[];
|
|
61
73
|
cookieParams: AnalyzedParameter[];
|
|
74
|
+
/**
|
|
75
|
+
* Named contract types referenced by path-param finished types (`$ref`
|
|
76
|
+
* path params). Set by `analyze()` from the mapper's import facts; drives
|
|
77
|
+
* the client file's `import type` list for method signatures.
|
|
78
|
+
*/
|
|
79
|
+
pathParamTypeNames?: string[];
|
|
62
80
|
requestBody: AnalyzedRequestBody | undefined;
|
|
63
81
|
responses: AnalyzedResponse[];
|
|
64
82
|
}
|
|
@@ -17,7 +17,7 @@ function buildClientMethodBody(op) {
|
|
|
17
17
|
const emissions = operationEmissions(op);
|
|
18
18
|
let urlTemplate = op.path;
|
|
19
19
|
for (const param of op.pathParams) {
|
|
20
|
-
urlTemplate = urlTemplate.replace(`{${param.name}}`, `\${encodeURIComponent(${param.name})}`);
|
|
20
|
+
urlTemplate = urlTemplate.replace(`{${param.name}}`, `\${encodeURIComponent(String(${param.name}))}`);
|
|
21
21
|
}
|
|
22
22
|
const urlExpr = `\`${urlTemplate}\``;
|
|
23
23
|
const errorCheckLines = [];
|
|
@@ -25,7 +25,18 @@ function buildParameters(op) {
|
|
|
25
25
|
const emissions = operationEmissions(op);
|
|
26
26
|
const specs = [];
|
|
27
27
|
for (const param of op.pathParams) {
|
|
28
|
-
|
|
28
|
+
// Finished signature type text arrives from the analyzer's translation
|
|
29
|
+
// layer; `?? 'string'` only guards hand-built `AnalyzedOperation` inputs.
|
|
30
|
+
const finished = param.finishedType ?? 'string';
|
|
31
|
+
// `T | string` is the caller escape hatch for custom wire formatting:
|
|
32
|
+
// pass a pre-formatted string instead of the schema type. When T is
|
|
33
|
+
// already `string` the union is skipped, and string-literal unions
|
|
34
|
+
// deliberately widen to `string` (flexibility over strictness here).
|
|
35
|
+
specs.push({
|
|
36
|
+
name: param.name,
|
|
37
|
+
type: finished === 'string' ? 'string' : `${finished} | string`,
|
|
38
|
+
isOptional: false,
|
|
39
|
+
});
|
|
29
40
|
}
|
|
30
41
|
if (emissions.query !== undefined) {
|
|
31
42
|
const allOptional = op.queryParams.every((p) => !p.required);
|
|
@@ -151,6 +151,11 @@ export function clientImportedNames(op) {
|
|
|
151
151
|
names.push(emissions.headers);
|
|
152
152
|
if (emissions.body !== undefined)
|
|
153
153
|
names.push(emissions.body);
|
|
154
|
+
// `$ref` path params reference named contract types directly in the method
|
|
155
|
+
// signature, so the client file must import them (fact collected by
|
|
156
|
+
// `analyze()` from the mapper's import list).
|
|
157
|
+
if (op.pathParamTypeNames !== undefined)
|
|
158
|
+
names.push(...op.pathParamTypeNames);
|
|
154
159
|
if (emissions.response !== undefined) {
|
|
155
160
|
const successType = getSuccessType(op);
|
|
156
161
|
if (successType !== 'void' && successType !== 'unknown' && /^[A-Z]/.test(successType)) {
|