diff --git a/.changeset/curly-mice-call.md b/.changeset/curly-mice-call.md new file mode 100644 index 000000000..35746e1f3 --- /dev/null +++ b/.changeset/curly-mice-call.md @@ -0,0 +1,5 @@ +--- +"openapi-typescript": patch +--- + +Wrap nested readonly types in parentheses, allowing for nested immutable arrays diff --git a/packages/openapi-typescript/examples/digital-ocean-api.ts b/packages/openapi-typescript/examples/digital-ocean-api.ts index ffc47064e..947a9aad5 100644 --- a/packages/openapi-typescript/examples/digital-ocean-api.ts +++ b/packages/openapi-typescript/examples/digital-ocean-api.ts @@ -5522,10 +5522,10 @@ export interface external { * "doadmin" * ] */ - db_names?: readonly string[] | null; + db_names?: (readonly string[]) | null; connection?: external["resources/databases/models/database_connection.yml"]; private_connection?: external["resources/databases/models/database_connection.yml"]; - users?: readonly external["resources/databases/models/database_user.yml"][] | null; + users?: (readonly external["resources/databases/models/database_user.yml"][]) | null; maintenance_window?: external["resources/databases/models/database_maintenance_window.yml"]; /** * Format: uuid @@ -15392,7 +15392,7 @@ export interface external { * @description An array containing the IDs of the Droplets the volume is attached to. Note that at this time, a volume can only be attached to a single Droplet. * @example [] */ - droplet_ids?: readonly number[] | null; + droplet_ids?: (readonly number[]) | null; /** * @description A human-readable name for the block storage volume. Must be lowercase and be composed only of numbers, letters and "-", up to a limit of 64 characters. The name must begin with a letter. * @example example diff --git a/packages/openapi-typescript/examples/github-api-next.ts b/packages/openapi-typescript/examples/github-api-next.ts index 2be7df017..b56a99e87 100644 --- a/packages/openapi-typescript/examples/github-api-next.ts +++ b/packages/openapi-typescript/examples/github-api-next.ts @@ -10744,7 +10744,7 @@ export interface components { /** @description The package version that resolve the vulnerability. */ first_patched_version: string | null; /** @description The functions in the package that are affected by the vulnerability. */ - vulnerable_functions: readonly string[] | null; + vulnerable_functions: (readonly string[]) | null; })[]) | null; cvss: ({ /** @description The CVSS vector. */ @@ -10758,10 +10758,10 @@ export interface components { /** @description The name of the CWE. */ name: string; }[] | null; - credits: readonly { + credits: (readonly { user: components["schemas"]["simple-user"]; type: components["schemas"]["security-advisory-credit-types"]; - }[] | null; + }[]) | null; }; /** * Basic Error @@ -14434,12 +14434,12 @@ export interface components { /** @description The CVSS score. */ score: number | null; }) | null; - cwes: readonly { + cwes: (readonly { /** @description The Common Weakness Enumeration (CWE) identifier. */ cwe_id: string; /** @description The name of the CWE. */ name: string; - }[] | null; + }[]) | null; /** @description A list of only the CWE IDs. */ cwe_ids: string[] | null; credits: { @@ -14447,7 +14447,7 @@ export interface components { login?: string; type?: components["schemas"]["security-advisory-credit-types"]; }[] | null; - credits_detailed: readonly components["schemas"]["repository-advisory-credit"][] | null; + credits_detailed: (readonly components["schemas"]["repository-advisory-credit"][]) | null; /** @description A list of users that collaborate on the advisory. */ collaborating_users: components["schemas"]["simple-user"][] | null; /** @description A list of teams that collaborate on the advisory. */ @@ -16518,7 +16518,7 @@ export interface components { */ analyses_url?: string | null; /** @description Any errors that ocurred during processing of the delivery. */ - errors?: readonly string[] | null; + errors?: (readonly string[]) | null; }; /** * CODEOWNERS errors diff --git a/packages/openapi-typescript/examples/github-api.ts b/packages/openapi-typescript/examples/github-api.ts index f754de807..936ff82cd 100644 --- a/packages/openapi-typescript/examples/github-api.ts +++ b/packages/openapi-typescript/examples/github-api.ts @@ -8251,7 +8251,7 @@ export interface components { /** @description The package version that resolve the vulnerability. */ first_patched_version: string | null; /** @description The functions in the package that are affected by the vulnerability. */ - vulnerable_functions: readonly string[] | null; + vulnerable_functions: (readonly string[]) | null; })[]) | null; cvss: ({ /** @description The CVSS vector. */ @@ -8265,10 +8265,10 @@ export interface components { /** @description The name of the CWE. */ name: string; }[] | null; - credits: readonly { + credits: (readonly { user: components["schemas"]["simple-user"]; type: components["schemas"]["security-advisory-credit-types"]; - }[] | null; + }[]) | null; }; /** * Basic Error @@ -13572,12 +13572,12 @@ export interface components { /** @description The CVSS score. */ score: number | null; }) | null; - cwes: readonly { + cwes: (readonly { /** @description The Common Weakness Enumeration (CWE) identifier. */ cwe_id: string; /** @description The name of the CWE. */ name: string; - }[] | null; + }[]) | null; /** @description A list of only the CWE IDs. */ cwe_ids: string[] | null; credits: { @@ -13585,7 +13585,7 @@ export interface components { login?: string; type?: components["schemas"]["security-advisory-credit-types"]; }[] | null; - credits_detailed: readonly components["schemas"]["repository-advisory-credit"][] | null; + credits_detailed: (readonly components["schemas"]["repository-advisory-credit"][]) | null; /** @description A list of users that collaborate on the advisory. */ collaborating_users: components["schemas"]["simple-user"][] | null; /** @description A list of teams that collaborate on the advisory. */ @@ -17232,7 +17232,7 @@ export interface components { */ analyses_url?: string | null; /** @description Any errors that ocurred during processing of the delivery. */ - errors?: readonly string[] | null; + errors?: (readonly string[]) | null; }; /** * CODEOWNERS errors diff --git a/packages/openapi-typescript/src/utils.ts b/packages/openapi-typescript/src/utils.ts index 5edf106f6..bc4220e38 100644 --- a/packages/openapi-typescript/src/utils.ts +++ b/packages/openapi-typescript/src/utils.ts @@ -32,6 +32,7 @@ const TILDE_RE = /~/g; const FS_RE = /\//g; export const TS_INDEX_RE = /\[("(\\"|[^"])+"|'(\\'|[^'])+')]/g; // splits apart TS indexes (and allows for escaped quotes) const TS_UNION_INTERSECTION_RE = /[&|]/; +const TS_READONLY_RE = /^readonly\s+/; const JS_OBJ_KEY = /^(\d+|[A-Za-z_$][A-Za-z0-9_$]*)$/; /** Walk through any JSON-serializable object */ @@ -168,9 +169,9 @@ export function encodeRef(ref: string): string { return ref.replace(TILDE_RE, "~0").replace(FS_RE, "~1"); } -/** if the type has & or | we should parenthesise it for safety */ +/** add parenthesis around union, intersection (| and &) and readonly types */ function parenthesise(type: string) { - return TS_UNION_INTERSECTION_RE.test(type) ? `(${type})` : type; + return TS_UNION_INTERSECTION_RE.test(type) || TS_READONLY_RE.test(type) ? `(${type})` : type; } /** T[] */ diff --git a/packages/openapi-typescript/test/schema-object.test.ts b/packages/openapi-typescript/test/schema-object.test.ts index 161108523..a9cc89b38 100644 --- a/packages/openapi-typescript/test/schema-object.test.ts +++ b/packages/openapi-typescript/test/schema-object.test.ts @@ -790,11 +790,29 @@ describe("Schema Object", () => { ctx: { ...options.ctx, immutableTypes: true }, }); expect(generated).toBe(`{ - readonly array?: readonly { + readonly array?: (readonly { [key: string]: unknown; - }[] | null; + }[]) | null; }`); }); + + test("readonly arrays", () => { + const schema: SchemaObject = { + type: "array", + items: { + type: "array", + items: { + type: "string", + }, + }, + }; + + const generated = transformSchemaObject(schema, { + ...options, + ctx: { ...options.ctx, immutableTypes: true }, + }); + expect(generated).toBe(`readonly (readonly string[])[]`); + }); }); });