Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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