Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/add-itemschema-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": minor
---

Add OpenAPI 3.2 `itemSchema` support for SSE and streaming responses
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: "3.1"
openapi: "3.1.0"
info:
title: openapi-fetch
version: "1.0"
Expand Down
22 changes: 15 additions & 7 deletions packages/openapi-typescript/bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,24 +196,32 @@ async function main() {
}
const redocly = redocConfigPath
? await loadConfig({ configPath: redocConfigPath })
: await createConfig({}, { extends: ["minimal"] });
: await createConfig({
extends: ["minimal"],
rules: {
struct: "warn",
"no-server-trailing-slash": "warn",
},
});

// handle Redoc APIs
const hasRedoclyApis = Object.keys(redocly?.apis ?? {}).length > 0;
const redoclyApis = redocly?.resolvedConfig?.apis ?? redocly?.apis ?? {};
const hasRedoclyApis = Object.keys(redoclyApis).length > 0;
if (hasRedoclyApis) {
if (input) {
warn("APIs are specified both in Redocly Config and CLI argument. Only using Redocly config.");
}
await Promise.all(
Object.entries(redocly.apis).map(async ([name, api]) => {
Object.entries(redoclyApis).map(async ([name, api]) => {
let configRoot = CWD;

const config = { ...flags, redocly };
if (redocly.configFile) {
const configFile = redocly.configPath ?? redocly.configFile;
if (configFile) {
// note: this will be absolute if --redoc is passed; otherwise, relative
configRoot = path.isAbsolute(redocly.configFile)
? new URL(`file://${redocly.configFile}`)
: new URL(redocly.configFile, `file://${process.cwd()}/`);
configRoot = path.isAbsolute(configFile)
? new URL(`file://${configFile}`)
: new URL(configFile, `file://${process.cwd()}/`);
}
if (!api[REDOC_CONFIG_KEY]?.output) {
errorAndExit(
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
},
"scripts": {
"build": "unbuild",
"dev": "tsc -p tsconfig.build.json --watch",
"dev": "unbuild --stub",
"download:schemas": "vite-node ./scripts/download-schemas.ts",
"format": "biome format . --write",
"lint": "pnpm run lint:js && pnpm run lint:ts",
Expand All @@ -62,7 +62,7 @@
"typescript": "^5.x"
},
"dependencies": {
"@redocly/openapi-core": "^1.34.6",
"@redocly/openapi-core": "^2.21.1",
"ansi-colors": "^4.1.3",
"change-case": "^5.4.4",
"parse-json": "^8.3.0",
Expand Down
14 changes: 7 additions & 7 deletions packages/openapi-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ export default async function openapiTS(

const redoc =
options.redocly ??
(await createConfig(
{
rules: {
"operation-operationId-unique": { severity: "error" }, // throw error on duplicate operationIDs
},
(await createConfig({
extends: ["minimal"],
rules: {
"operation-operationId-unique": { severity: "error" }, // throw error on duplicate operationIDs
struct: "warn", // downgrade struct rule to warning to allow incomplete schemas
"no-server-trailing-slash": "warn", // downgrade to warning to handle real-world specs
},
{ extends: ["minimal"] },
));
}));

const schema = await validateAndBundle(source, {
redoc,
Expand Down
25 changes: 12 additions & 13 deletions packages/openapi-typescript/src/lib/redoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BaseResolver,
bundle,
type Document,
isPlainObject,
lintDocument,
makeDocumentFromString,
type NormalizedProblem,
Expand Down Expand Up @@ -123,19 +124,17 @@ export async function validateAndBundle(
debug("Parsed schema", "redoc", performance.now() - redocParseT);

// 1. check for OpenAPI 3 or greater
const openapiVersion = Number.parseFloat(document.parsed.openapi);
if (
document.parsed.swagger ||
!document.parsed.openapi ||
Number.isNaN(openapiVersion) ||
openapiVersion < 3 ||
openapiVersion >= 4
) {
if (document.parsed.swagger) {
if (!isPlainObject(document.parsed)) {
throw new Error("Unsupported schema format, expected `openapi: 3.x`");
}
const parsed = document.parsed;
const openapiVersion = Number.parseFloat(String(parsed.openapi ?? ""));
if (parsed.swagger || !parsed.openapi || Number.isNaN(openapiVersion) || openapiVersion < 3 || openapiVersion >= 4) {
if (parsed.swagger) {
throw new Error("Unsupported Swagger version: 2.x. Use OpenAPI 3.x instead.");
}
if (document.parsed.openapi || openapiVersion < 3 || openapiVersion >= 4) {
throw new Error(`Unsupported OpenAPI version: ${document.parsed.openapi}`);
if (parsed.openapi || openapiVersion < 3 || openapiVersion >= 4) {
throw new Error(`Unsupported OpenAPI version: ${parsed.openapi}`);
}
throw new Error("Unsupported schema format, expected `openapi: 3.x`");
}
Expand All @@ -144,7 +143,7 @@ export async function validateAndBundle(
const redocLintT = performance.now();
const problems = await lintDocument({
document,
config: options.redoc.styleguide,
config: options.redoc,
externalRefResolver: resolver,
});
_processProblems(problems, options);
Expand All @@ -160,5 +159,5 @@ export async function validateAndBundle(
_processProblems(bundled.problems, options);
debug("Bundled schema", "bundle", performance.now() - redocBundleT);

return bundled.bundle.parsed;
return bundled.bundle.parsed as OpenAPI3;
}
15 changes: 15 additions & 0 deletions packages/openapi-typescript/src/lib/ref-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { unescapePointerFragment } from "@redocly/openapi-core";

/** Parse a $ref string into its URI and JSON Pointer parts */
export function parseRef(ref: string): { uri: string | null; pointer: string[] } {
const hashIndex = ref.indexOf("#");
const uri = hashIndex === -1 ? ref : ref.slice(0, hashIndex);
const fragment = hashIndex === -1 ? "" : ref.slice(hashIndex + 1);
return {
uri: uri || null,
pointer: fragment
.split("/")
.map(unescapePointerFragment)
.filter((s) => s !== ""),
};
}
2 changes: 1 addition & 1 deletion packages/openapi-typescript/src/lib/ts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { OasRef, Referenced } from "@redocly/openapi-core";
import { parseRef } from "@redocly/openapi-core/lib/ref-utils.js";
import ts, { type LiteralTypeNode, type TypeLiteralNode } from "typescript";
import type { ParameterObject } from "../types.js";
import { parseRef } from "./ref-utils.js";

export const JS_PROPERTY_INDEX_RE = /^[A-Za-z_$][A-Za-z_$0-9]*$/;
export const JS_ENUM_INVALID_CHARS_RE = /[^A-Za-z_$0-9]+(.)?/g;
Expand Down
8 changes: 5 additions & 3 deletions packages/openapi-typescript/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { escapePointer, parseRef } from "@redocly/openapi-core/lib/ref-utils.js";
import { escapePointerFragment } from "@redocly/openapi-core";
import c from "ansi-colors";
import supportsColor from "supports-color";
import ts from "typescript";
Expand All @@ -16,6 +16,8 @@ const DEBUG_GROUPS: Record<string, c.StyleFunction | undefined> = {
ts: c.blueBright,
};

import { parseRef } from "./ref-utils.js";

export { c };

/** Given a discriminator object, get the property name */
Expand Down Expand Up @@ -55,10 +57,10 @@ export function createRef(parts: (number | string | undefined | null)[]): string
const maybeRef = parseRef(String(part)).pointer;
if (maybeRef.length) {
for (const refPart of maybeRef) {
pointer += `/${escapePointer(refPart)}`;
pointer += `/${escapePointerFragment(refPart)}`;
}
} else {
pointer += `/${escapePointer(part)}`;
pointer += `/${escapePointerFragment(part)}`;
}
}
return pointer;
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi-typescript/src/transform/header-object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { escapePointer } from "@redocly/openapi-core/lib/ref-utils.js";
import { escapePointerFragment } from "@redocly/openapi-core";
import ts from "typescript";
import { addJSDocComment, tsModifiers, tsPropertyIndex, UNKNOWN } from "../lib/ts.js";
import { getEntries } from "../lib/utils.js";
Expand All @@ -18,7 +18,7 @@ export default function transformHeaderObject(headerObject: HeaderObject, option
if (headerObject.content) {
const type: ts.TypeElement[] = [];
for (const [contentType, mediaTypeObject] of getEntries(headerObject.content ?? {}, options.ctx)) {
const nextPath = `${options.path ?? "#"}/${escapePointer(contentType)}`;
const nextPath = `${options.path ?? "#"}/${escapePointerFragment(contentType)}`;
const mediaType =
"$ref" in mediaTypeObject
? transformSchemaObject(mediaTypeObject, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ export default function transformMediaTypeObject(
mediaTypeObject: MediaTypeObject,
options: TransformNodeOptions,
): ts.TypeNode {
if (!mediaTypeObject.schema) {
const targetSchema = mediaTypeObject.itemSchema ?? mediaTypeObject.schema;
if (!targetSchema) {
return UNKNOWN;
}
return transformSchemaObject(mediaTypeObject.schema, options);
return transformSchemaObject(targetSchema, options);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { parseRef } from "@redocly/openapi-core/lib/ref-utils.js";
import ts from "typescript";
import { parseRef } from "../lib/ref-utils.js";
import {
addJSDocComment,
BOOLEAN,
Expand Down
2 changes: 2 additions & 0 deletions packages/openapi-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ export interface RequestBodyObject extends Extensable {
export interface MediaTypeObject extends Extensable {
/** The schema defining the content of the request, response, or parameter. */
schema?: SchemaObject | ReferenceObject;
/** OAS 3.2: The schema defining the content of individual items in a streaming response (e.g. text/event-stream). When present, takes precedence over schema for type generation. */
itemSchema?: SchemaObject | ReferenceObject;
/** Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The example field is mutually exclusive of the examples field. Furthermore, if referencing a schema which contains an example, the example value SHALL override the example provided by the schema. */
example?: any;
/** Examples of the media type. Each example object SHOULD match the media type and specified schema if present. The examples field is mutually exclusive of the example field. Furthermore, if referencing a schema which contains an example, the examples value SHALL override the example provided by the schema. */
Expand Down
8 changes: 4 additions & 4 deletions packages/openapi-typescript/test/discriminators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe("3.1 discriminators", () => {
"allOf > mapping",
{
given: {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "test", version: "1.0" },
components: {
schemas: {
Expand Down Expand Up @@ -120,7 +120,7 @@ export type operations = Record<string, never>;`,
"allOf > no mapping",
{
given: {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "test", version: "1.0" },
components: {
schemas: {
Expand Down Expand Up @@ -189,7 +189,7 @@ export type operations = Record<string, never>;`,
"allOf > inline inheritance",
{
given: {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "test", version: "1.0" },
components: {
schemas: {
Expand Down Expand Up @@ -237,7 +237,7 @@ export type operations = Record<string, never>;`,
"oneOf > implicit mapping",
{
given: {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "test", version: "1.0" },
components: {
schemas: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: "3.0"
openapi: "3.0.0"
info:
title: test
version: "1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: "3.0"
openapi: "3.0.0"
info:
title: test
version: "1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: "3.0"
openapi: "3.0.0"
info:
title: test
version: "1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: "3.0"
openapi: "3.0.0"
info:
title: Test
version: "1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: "3.0"
openapi: "3.0.0"
info:
title: test
version: "1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: "3.0"
openapi: "3.0.0"
info:
title: Test
version: "1.0"
Expand Down
Loading
Loading