Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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/real-wombats-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"swagger-typescript-api": minor
---

Supports immutable types with --make-immutable or makeImmutable
6 changes: 6 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ const generateCommand = defineCommand({
description: "generate readonly properties",
default: codeGenBaseConfig.addReadonly,
},
"make-immutable": {
type: "boolean",
description: "makes all properties and values readonly",
default: codeGenBaseConfig.makeImmutable,
},
"another-array-type": {
type: "boolean",
description: "generate array types as Array<Type> (by default Type[])",
Expand Down Expand Up @@ -316,6 +321,7 @@ const generateCommand = defineCommand({
? HTTP_CLIENT.AXIOS
: HTTP_CLIENT.FETCH,
input: path.resolve(process.cwd(), args.path as string),
makeImmutable: args["make-immutable"],
modular: args.modular,
moduleNameFirstTag: args["module-name-first-tag"],
moduleNameIndex: +args["module-name-index"] || 0,
Expand Down
49 changes: 39 additions & 10 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const TsKeyword = {
Enum: "enum",
Interface: "interface",
Array: "Array",
ReadonlyArray: "ReadonlyArray",
Readonly: "Readonly",
Record: "Record",
Intersection: "&",
Union: "|",
Expand All @@ -55,6 +57,8 @@ export class CodeGenConfig {
generateUnionEnums = false;
/** CLI flag */
addReadonly = false;
/** CLI flag */
makeImmutable = false;
enumNamesAsValues = false;
/** parsed swagger schema from getSwaggerObject() */

Expand Down Expand Up @@ -225,12 +229,20 @@ export class CodeGenConfig {
/**
* $A[] or Array<$A>
*/
ArrayType: (content: unknown) => {
ArrayType: ({ readonly, content }: Record<string, unknown>) => {
if (this.anotherArrayType) {
return this.Ts.TypeWithGeneric(this.Ts.Keyword.Array, [content]);
return this.Ts.TypeWithGeneric(
readonly ? this.Ts.Keyword.ReadonlyArray : this.Ts.Keyword.Array,
[content],
);
}

return `${this.Ts.ExpressionGroup(content)}[]`;
return lodash
.compact([
readonly && "readonly ",
this.Ts.ExpressionGroup(content),
"[]",
])
.join("");
},
/**
* "$A"
Expand Down Expand Up @@ -265,8 +277,16 @@ export class CodeGenConfig {
/**
* Record<$A1, $A2>
*/
RecordType: (key: unknown, value: unknown) =>
this.Ts.TypeWithGeneric(this.Ts.Keyword.Record, [key, value]),
RecordType: ({ readonly, key, value }: Record<string, unknown>) => {
const record = this.Ts.TypeWithGeneric(this.Ts.Keyword.Record, [
key,
value,
]);
if (readonly) {
return this.Ts.TypeWithGeneric(this.Ts.Keyword.Readonly, [record]);
}
return record;
},
/**
* readonly $key?:$value
*/
Expand All @@ -277,8 +297,14 @@ export class CodeGenConfig {
/**
* [key: $A1]: $A2
*/
InterfaceDynamicField: (key: unknown, value: unknown) =>
`[key: ${key}]: ${value}`,
InterfaceDynamicField: ({
readonly,
key,
value,
}: Record<string, unknown>) =>
lodash
.compact([readonly && "readonly ", `[key: ${key}]`, `: ${value}`])
.join(""),

/**
* EnumName.EnumKey
Expand Down Expand Up @@ -344,8 +370,11 @@ export class CodeGenConfig {
/**
* [$A1, $A2, ...$AN]
*/
Tuple: (values: unknown[]) => {
return `[${values.join(", ")}]`;
Tuple: ({
readonly,
values,
}: Record<string, unknown> & { values: unknown[] }) => {
return `${readonly ? "readonly " : ""}[${values.join(", ")}]`;
},
};

Expand Down
12 changes: 9 additions & 3 deletions src/schema-parser/base-schema-parsers/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { MonoSchemaParser } from "../mono-schema-parser.js";
export class ArraySchemaParser extends MonoSchemaParser {
override parse() {
let contentType;
const { type, description, items } = this.schema || {};
const { type, description, items, readOnly } = this.schema || {};

const readonly =
(readOnly && this.config.addReadonly) || this.config.makeImmutable;

if (Array.isArray(items) && type === SCHEMA_TYPES.ARRAY) {
const tupleContent = [];
Expand All @@ -15,12 +18,15 @@ export class ArraySchemaParser extends MonoSchemaParser {
.getInlineParseContent(),
);
}
contentType = this.config.Ts.Tuple(tupleContent);
contentType = this.config.Ts.Tuple({
readonly,
values: tupleContent,
});
} else {
const content = this.schemaParserFabric
.createSchemaParser({ schema: items, schemaPath: this.schemaPath })
.getInlineParseContent();
contentType = this.config.Ts.ArrayType(content);
contentType = this.config.Ts.ArrayType({ readonly, content });
}

return {
Expand Down
8 changes: 7 additions & 1 deletion src/schema-parser/base-schema-parsers/discriminator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class DiscriminatorSchemaParser extends MonoSchemaParser {
"schemas",
this.typeName,
]);
const { discriminator } = this.schema;
const { discriminator, readOnly } = this.schema;
const mappingEntries = lodash.entries(discriminator.mapping);
const ableToCreateMappingType =
!skipMappingType &&
Expand All @@ -84,6 +84,9 @@ export class DiscriminatorSchemaParser extends MonoSchemaParser {
const content = ts.IntersectionType([
ts.ObjectWrapper(
ts.TypeField({
readonly:
(readOnly && this.config.addReadonly) ||
this.config.makeImmutable,
key: ts.StringValue(discriminator.propertyName),
value: "Key",
}),
Expand Down Expand Up @@ -127,6 +130,9 @@ export class DiscriminatorSchemaParser extends MonoSchemaParser {
ts.IntersectionType([
ts.ObjectWrapper(
ts.TypeField({
readonly:
(mappingSchema.readOnly && this.config.addReadonly) ||
this.config.makeImmutable,
key: discriminator.propertyName,
value: mappingUsageKey,
}),
Expand Down
14 changes: 9 additions & 5 deletions src/schema-parser/base-schema-parsers/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export class ObjectSchemaParser extends MonoSchemaParser {
name: fieldName,
value: fieldValue,
field: this.config.Ts.TypeField({
readonly: readOnly && this.config.addReadonly,
readonly:
(readOnly && this.config.addReadonly) || this.config.makeImmutable,
optional: !required,
key: fieldName,
value: fieldValue,
Expand Down Expand Up @@ -101,10 +102,13 @@ export class ObjectSchemaParser extends MonoSchemaParser {
$$raw: { additionalProperties },
description: "",
isRequired: false,
field: this.config.Ts.InterfaceDynamicField(
interfaceKeysContent,
this.config.Ts.Keyword.Any,
),
field: this.config.Ts.InterfaceDynamicField({
readonly:
(additionalProperties.readOnly && this.config.addReadonly) ||
this.config.makeImmutable,
key: interfaceKeysContent,
value: this.config.Ts.Keyword.Any,
}),
});
}

Expand Down
21 changes: 13 additions & 8 deletions src/schema-parser/base-schema-parsers/primitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { MonoSchemaParser } from "../mono-schema-parser.js";
export class PrimitiveSchemaParser extends MonoSchemaParser {
override parse() {
let contentType = null;
const { additionalProperties, type, description, items } =
const { additionalProperties, type, description, items, readOnly } =
this.schema || {};

const readonly =
(readOnly && this.config.addReadonly) || this.config.makeImmutable;

if (type === this.config.Ts.Keyword.Object && additionalProperties) {
const propertyNamesSchema = this.schemaUtils.getSchemaPropertyNamesSchema(
this.schema,
Expand Down Expand Up @@ -37,10 +40,11 @@ export class PrimitiveSchemaParser extends MonoSchemaParser {
recordValuesContent = this.config.Ts.Keyword.Any;
}

contentType = this.config.Ts.RecordType(
recordKeysContent,
recordValuesContent,
);
contentType = this.config.Ts.RecordType({
readonly,
key: recordKeysContent,
value: recordValuesContent,
});
}

if (Array.isArray(type) && type.length) {
Expand All @@ -51,13 +55,14 @@ export class PrimitiveSchemaParser extends MonoSchemaParser {
}

if (Array.isArray(items) && type === SCHEMA_TYPES.ARRAY) {
contentType = this.config.Ts.Tuple(
items.map((item) =>
contentType = this.config.Ts.Tuple({
readonly,
values: items.map((item) =>
this.schemaParserFabric
.createSchemaParser({ schema: item, schemaPath: this.schemaPath })
.getInlineParseContent(),
),
);
});
}

return {
Expand Down
11 changes: 7 additions & 4 deletions src/schema-parser/schema-formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,13 @@ export class SchemaFormatters {
? this.config.Ts.ObjectWrapper(
this.formatObjectContent(parsedSchema.content),
)
: this.config.Ts.RecordType(
this.config.Ts.Keyword.String,
this.config.Ts.Keyword.Any,
),
: this.config.Ts.RecordType({
readonly:
(parsedSchema.readOnly && this.config.addReadonly) ||
this.config.makeImmutable,
key: this.config.Ts.Keyword.String,
value: this.config.Ts.Keyword.Any,
}),
),
};
},
Expand Down
51 changes: 51 additions & 0 deletions tests/spec/readonly-always/__snapshots__/basic.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`basic > immutable 1`] = `
"/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/

export interface Pet {
/** @format int64 */
readonly id: number;
readonly name: string;
readonly tag?: string;
readonly list?: readonly string[];
readonly obj?: Readonly<Record<string, any>>;
readonly multiple?: string | number;
}
"
`;

exports[`basic > immutable another-array-type 1`] = `
"/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/

export interface Pet {
/** @format int64 */
readonly id: number;
readonly name: string;
readonly tag?: string;
readonly list?: ReadonlyArray<string>;
readonly obj?: Readonly<Record<string, any>>;
readonly multiple?: string | number;
}
"
`;
51 changes: 51 additions & 0 deletions tests/spec/readonly-always/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { generateApi } from "../../../src/index.js";

describe("basic", async () => {
let tmpdir = "";

beforeAll(async () => {
tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api"));
});

afterAll(async () => {
await fs.rm(tmpdir, { recursive: true });
});

test("immutable", async () => {
await generateApi({
fileName: "schema",
input: path.resolve(import.meta.dirname, "schema.json"),
output: tmpdir,
silent: true,
makeImmutable: true,
generateClient: false,
});

const content = await fs.readFile(path.join(tmpdir, "schema.ts"), {
encoding: "utf8",
});

expect(content).toMatchSnapshot();
});
test("immutable another-array-type", async () => {
await generateApi({
fileName: "schema",
input: path.resolve(import.meta.dirname, "schema.json"),
output: tmpdir,
silent: true,
makeImmutable: true,
generateClient: false,
anotherArrayType: true,
});

const content = await fs.readFile(path.join(tmpdir, "schema.ts"), {
encoding: "utf8",
});

expect(content).toMatchSnapshot();
});
});
Loading