Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,10 @@ Inferred from success response schemas. Details: [docs/envelope.md](docs/envelop

Set `unwrapResponseData: true` when the project's injected `HTTPFetch`
normalizes successful envelope bodies before returning `{ data }`. Every
operation whose success schema contains a `success` field then receives its
`data` payload type. Data-only objects in mixed specs remain raw. Success
envelopes without a `data` field receive the `null` type,
operation whose success schema is recognized as an API envelope then receives
its `data` payload type. Data-only objects and business payloads that also
contain `success` remain raw. Metadata-only envelopes without a `data` field
receive the `null` type,
matching clients that normalize an omitted payload to `null`.
The default remains envelope-preserving and is compatible with the bundled
Axios and Fetch adapters.
Expand Down
11 changes: 7 additions & 4 deletions docs/envelope.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

The generator inspects each operation’s success JSON schema and classifies the spec into one of three **envelope modes**. You do not set the mode in `source.ts`; it is inferred.

An object “looks like an envelope” when it has a `data`, `success`, or `message` field.
An object “looks like an envelope” when it has a `data` field, or when it has a
`success` field and every other field is envelope metadata (`error`, `message`,
`requestId`, or `timestamp`). A business payload such as
`{ success, deletedCount, requested }` remains raw.

## shared

Expand Down Expand Up @@ -41,9 +44,9 @@ Callers still return the HTTPFetch `{ data }` payload (the transport wrapper), n
## HTTP clients that unwrap envelopes

Set `unwrapResponseData: true` only when the injected `HTTPFetch` already
normalizes `{ success, data }` bodies. Responses containing `success` emit the
inner `data` payload type, or `null` when `data` is absent. Data-only objects
remain raw, matching clients that use `success` to distinguish an API envelope.
normalizes `{ success, data }` bodies. Recognized envelopes emit the inner
`data` payload type, or `null` when `data` is absent. Business payloads
containing `success` plus domain fields and data-only cursor objects remain raw.
Envelope objects composed through component references and `allOf` are
recognized without changing their source schemas.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openmirai/openapi-codegen",
"version": "0.1.6",
"version": "0.1.7",
"description": "Headless OpenAPI to TypeScript codegen CLI and HTTPFetch runtime",
"homepage": "https://github.com/openmirai/mirai-openapi-codegen#readme",
"bugs": {
Expand Down
5 changes: 3 additions & 2 deletions src/emitters/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
buildBaseResponseInterface,
isEnvelopeSchema,
matchesEnvelopeShape,
} from "../../envelope-guard/index";
import type { EnvelopeMode, EnvelopeShape } from "../../envelope-guard/index";
Expand Down Expand Up @@ -246,8 +247,8 @@ function renderResponseType(
: undefined;
const isSuccessEnvelope =
resolved.kind === "object" &&
resolved.properties !== undefined &&
resolved.properties.success !== undefined;
resolved.properties?.success !== undefined &&
isEnvelopeSchema(schema, options.source.components.schemas);
if (options.unwrapResponseData === true && isSuccessEnvelope) {
if (dataSchema === undefined) {
return `export type ${typeName}Response = null;`;
Expand Down
24 changes: 23 additions & 1 deletion src/envelope-guard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,31 @@
return actual !== undefined && fingerprint(actual) === fingerprint(expected);
}

const ENVELOPE_METADATA_FIELDS = new Set([
"error",
"message",
"requestId",
"success",
"timestamp",
]);

function looksLikeEnvelope(shape: EnvelopeShape): boolean {
const names = new Set(shape.fields.map((field) => field.name));
return names.has("data") || names.has("success") || names.has("message");
if (names.has("data")) {
return true;
}
if (!names.has("success")) {
return false;
}
return [...names].every((name) => ENVELOPE_METADATA_FIELDS.has(name));
}

export function isEnvelopeSchema(
schema: IRSchema,
components: Record<string, IRSchema>
): boolean {
const shape = extractEnvelopeShape(schema, components);
return shape !== undefined && looksLikeEnvelope(shape);
}

export function collectOperationEnvelopes(
Expand Down Expand Up @@ -232,10 +254,10 @@
if (plain === null) {
return undefined;
}
return parseBaseResponseBody(plain[1]!);

Check warning on line 257 in src/envelope-guard/index.ts

View workflow job for this annotation

GitHub Actions / Check repository

typescript(no-non-null-assertion)

Forbidden non-null assertion.
}

return parseBaseResponseBody(match[1]!);

Check warning on line 260 in src/envelope-guard/index.ts

View workflow job for this annotation

GitHub Actions / Check repository

typescript(no-non-null-assertion)

Forbidden non-null assertion.
}

function parseBaseResponseBody(body: string): UserBaseResponseShape {
Expand Down
21 changes: 21 additions & 0 deletions test/fixtures/specs/mixed-envelope.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,27 @@
}
}
},
"/api/acme/v3/batch-result": {
"delete": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": { "type": "boolean" },
"deletedCount": { "type": "integer" },
"requested": { "type": "integer" }
},
"required": ["success"]
}
}
}
}
}
}
},
"/api/acme/v3/composed": {
"get": {
"responses": {
Expand Down
11 changes: 11 additions & 0 deletions test/integration/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,17 @@ describe("integration: monolith generate", () => {
"export type POSTApiAcmeV3EmptyResponse = null;"
);

const rawSuccessPayloadType = readFileSync(
join(generatedDir, "types/api/acme/v3/batch-result/DELETE.d.ts"),
"utf8"
);
expect(rawSuccessPayloadType).toContain("deletedCount?: number");
expect(rawSuccessPayloadType).toContain("requested?: number");
expect(rawSuccessPayloadType).toContain("success: boolean");
expect(rawSuccessPayloadType).not.toContain(
"DELETEApiAcmeV3BatchResultResponse = null"
);

const composedEnvelopeType = readFileSync(
join(generatedDir, "types/api/acme/v3/composed/GET.d.ts"),
"utf8"
Expand Down
Loading