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/upgrade-redocly-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": patch
---

Upgrade @redocly/openapi-core from v1 to v2, adding support for OpenAPI 3.2 document validation
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
20 changes: 13 additions & 7 deletions packages/openapi-typescript/bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,24 +196,30 @@ 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 hasRedoclyApis = Object.keys(redocly?.resolvedConfig?.apis ?? {}).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(redocly.resolvedConfig.apis).map(async ([name, api]) => {
let configRoot = CWD;

const config = { ...flags, redocly };
if (redocly.configFile) {
if (redocly.configPath) {
// 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(redocly.configPath)
? new URL(`file://${redocly.configPath}`)
: new URL(redocly.configPath, `file://${process.cwd()}/`);
}
if (!api[REDOC_CONFIG_KEY]?.output) {
errorAndExit(
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
{ 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;
}
13 changes: 13 additions & 0 deletions packages/openapi-typescript/src/lib/ref-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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 [uri, fragment = ""] = ref.split("#", 2);
return {
uri: uri || null,
pointer: fragment
.split("/")
.filter(Boolean)
.map((segment) => unescapePointerFragment(segment)),
};
}
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
7 changes: 4 additions & 3 deletions packages/openapi-typescript/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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";
import type { DiscriminatorObject, OpenAPI3, OpenAPITSOptions, ReferenceObject, SchemaObject } from "../types.js";
import { parseRef } from "./ref-utils.js";
import { tsLiteral, tsModifiers, tsPropertyIndex } from "./ts.js";

if (!supportsColor.stdout || supportsColor.stdout.hasBasic === false) {
Expand Down Expand Up @@ -55,10 +56,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
@@ -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
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
16 changes: 8 additions & 8 deletions packages/openapi-typescript/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe("openapiTS", () => {
"$refs > basic",
{
given: {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "Test", version: "1.0" },
components: {
schemas: {
Expand Down Expand Up @@ -77,7 +77,7 @@ export type operations = Record<string, never>;`,
"$refs > arbitrary $refs are respected",
{
given: {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "Test", version: "1.0" },
components: {
schemas: {
Expand Down Expand Up @@ -337,7 +337,7 @@ export type operations = Record<string, never>;`,
"parameters > operations get correct params",
{
given: {
openapi: "3.0",
openapi: "3.0.0",
info: { title: "Test", version: "1.0" },
paths: {
"/post/{id}": {
Expand Down Expand Up @@ -457,7 +457,7 @@ export interface operations {
"examples > skipped",
{
given: {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "Test", version: "1.0" },
components: {
schemas: {
Expand Down Expand Up @@ -505,7 +505,7 @@ export type operations = Record<string, never>;`,
"operations > # character is parsed correctly",
{
given: {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "Test", version: "1.0" },
paths: {
"/accounts": {
Expand Down Expand Up @@ -622,7 +622,7 @@ export type operations = Record<string, never>;`,
"TypeScript > WithRequired type helper",
{
given: {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "Test", version: "1.0" },
components: {
schemas: {
Expand Down Expand Up @@ -691,7 +691,7 @@ export type operations = Record<string, never>;`,
"inject option",
{
given: {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "Test", version: "1.0" },
},
want: `type Foo = string;
Expand Down Expand Up @@ -1130,7 +1130,7 @@ export type operations = Record<string, never>;`,

test("does not mutate original reference", async () => {
const schema: OpenAPI3 = {
openapi: "3.1",
openapi: "3.1.0",
info: { title: "test", version: "1.0" },
components: {
schemas: {
Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-typescript/test/invalid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe("Invalid schemas", () => {

await expect(() =>
openapiTS({
openapi: "3.1",
openapi: "3.1.0",
info: { title: "test", version: "1.0" },
components: {
schemas: {
Expand Down
Loading
Loading