Skip to content

Commit 11b8e8f

Browse files
author
Jens Karlsson
committed
adds make-immutable configuration to force all types to be immutable
1 parent 64cb8bf commit 11b8e8f

File tree

11 files changed

+268
-31
lines changed

11 files changed

+268
-31
lines changed

index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ const generateCommand = defineCommand({
8686
description: "generate readonly properties",
8787
default: codeGenBaseConfig.addReadonly,
8888
},
89+
"make-immutable": {
90+
type: "boolean",
91+
description: "makes all properties and values readonly",
92+
default: codeGenBaseConfig.makeImmutable,
93+
},
8994
"another-array-type": {
9095
type: "boolean",
9196
description: "generate array types as Array<Type> (by default Type[])",

src/configuration.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const TsKeyword = {
3030
Enum: "enum",
3131
Interface: "interface",
3232
Array: "Array",
33+
ReadonlyArray: "ReadonlyArray",
34+
Readonly: "Readonly",
3335
Record: "Record",
3436
Intersection: "&",
3537
Union: "|",
@@ -55,6 +57,8 @@ export class CodeGenConfig {
5557
generateUnionEnums = false;
5658
/** CLI flag */
5759
addReadonly = false;
60+
/** CLI flag */
61+
makeImmutable = false;
5862
enumNamesAsValues = false;
5963
/** parsed swagger schema from getSwaggerObject() */
6064

@@ -225,12 +229,20 @@ export class CodeGenConfig {
225229
/**
226230
* $A[] or Array<$A>
227231
*/
228-
ArrayType: (content: unknown) => {
232+
ArrayType: ({ readonly, content }: Record<string, unknown>) => {
229233
if (this.anotherArrayType) {
230-
return this.Ts.TypeWithGeneric(this.Ts.Keyword.Array, [content]);
234+
return this.Ts.TypeWithGeneric(
235+
readonly ? this.Ts.Keyword.ReadonlyArray : this.Ts.Keyword.Array,
236+
[content],
237+
);
231238
}
232-
233-
return `${this.Ts.ExpressionGroup(content)}[]`;
239+
return lodash
240+
.compact([
241+
readonly && "readonly ",
242+
this.Ts.ExpressionGroup(content),
243+
"[]",
244+
])
245+
.join("");
234246
},
235247
/**
236248
* "$A"
@@ -265,8 +277,16 @@ export class CodeGenConfig {
265277
/**
266278
* Record<$A1, $A2>
267279
*/
268-
RecordType: (key: unknown, value: unknown) =>
269-
this.Ts.TypeWithGeneric(this.Ts.Keyword.Record, [key, value]),
280+
RecordType: ({ readonly, key, value }: Record<string, unknown>) => {
281+
const record = this.Ts.TypeWithGeneric(this.Ts.Keyword.Record, [
282+
key,
283+
value,
284+
]);
285+
if (readonly) {
286+
return this.Ts.TypeWithGeneric(this.Ts.Keyword.Readonly, [record]);
287+
}
288+
return record;
289+
},
270290
/**
271291
* readonly $key?:$value
272292
*/
@@ -277,8 +297,15 @@ export class CodeGenConfig {
277297
/**
278298
* [key: $A1]: $A2
279299
*/
280-
InterfaceDynamicField: (key: unknown, value: unknown) =>
281-
`[key: ${key}]: ${value}`,
300+
InterfaceDynamicField: ({
301+
readonly,
302+
303+
key,
304+
value,
305+
}: Record<string, unknown>) =>
306+
lodash
307+
.compact([readonly && "readonly ", `[key: ${key}]`, `: ${value}`])
308+
.join(""),
282309

283310
/**
284311
* EnumName.EnumKey
@@ -344,8 +371,11 @@ export class CodeGenConfig {
344371
/**
345372
* [$A1, $A2, ...$AN]
346373
*/
347-
Tuple: (values: unknown[]) => {
348-
return `[${values.join(", ")}]`;
374+
Tuple: ({
375+
readonly,
376+
values,
377+
}: Record<string, unknown> & { values: unknown[] }) => {
378+
return `${readonly ? "readonly " : ""}[${values.join(", ")}]`;
349379
},
350380
};
351381

src/schema-parser/base-schema-parsers/array.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { MonoSchemaParser } from "../mono-schema-parser.js";
44
export class ArraySchemaParser extends MonoSchemaParser {
55
override parse() {
66
let contentType;
7-
const { type, description, items } = this.schema || {};
7+
const { type, description, items, readOnly } = this.schema || {};
8+
9+
const readonly =
10+
(readOnly && this.config.addReadonly) || this.config.makeImmutable;
811

912
if (Array.isArray(items) && type === SCHEMA_TYPES.ARRAY) {
1013
const tupleContent = [];
@@ -15,12 +18,15 @@ export class ArraySchemaParser extends MonoSchemaParser {
1518
.getInlineParseContent(),
1619
);
1720
}
18-
contentType = this.config.Ts.Tuple(tupleContent);
21+
contentType = this.config.Ts.Tuple({
22+
readonly,
23+
values: tupleContent,
24+
});
1925
} else {
2026
const content = this.schemaParserFabric
2127
.createSchemaParser({ schema: items, schemaPath: this.schemaPath })
2228
.getInlineParseContent();
23-
contentType = this.config.Ts.ArrayType(content);
29+
contentType = this.config.Ts.ArrayType({ readonly, content });
2430
}
2531

2632
return {

src/schema-parser/base-schema-parsers/discriminator.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class DiscriminatorSchemaParser extends MonoSchemaParser {
5858
"schemas",
5959
this.typeName,
6060
]);
61-
const { discriminator } = this.schema;
61+
const { discriminator, readOnly } = this.schema;
6262
const mappingEntries = lodash.entries(discriminator.mapping);
6363
const ableToCreateMappingType =
6464
!skipMappingType &&
@@ -84,6 +84,9 @@ export class DiscriminatorSchemaParser extends MonoSchemaParser {
8484
const content = ts.IntersectionType([
8585
ts.ObjectWrapper(
8686
ts.TypeField({
87+
readonly:
88+
(readOnly && this.config.addReadonly) ||
89+
this.config.makeImmutable,
8790
key: ts.StringValue(discriminator.propertyName),
8891
value: "Key",
8992
}),
@@ -127,6 +130,9 @@ export class DiscriminatorSchemaParser extends MonoSchemaParser {
127130
ts.IntersectionType([
128131
ts.ObjectWrapper(
129132
ts.TypeField({
133+
readonly:
134+
(mappingSchema.readOnly && this.config.addReadonly) ||
135+
this.config.makeImmutable,
130136
key: discriminator.propertyName,
131137
value: mappingUsageKey,
132138
}),

src/schema-parser/base-schema-parsers/object.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ export class ObjectSchemaParser extends MonoSchemaParser {
7373
name: fieldName,
7474
value: fieldValue,
7575
field: this.config.Ts.TypeField({
76-
readonly: readOnly && this.config.addReadonly,
76+
readonly:
77+
(readOnly && this.config.addReadonly) || this.config.makeImmutable,
7778
optional: !required,
7879
key: fieldName,
7980
value: fieldValue,
@@ -101,10 +102,13 @@ export class ObjectSchemaParser extends MonoSchemaParser {
101102
$$raw: { additionalProperties },
102103
description: "",
103104
isRequired: false,
104-
field: this.config.Ts.InterfaceDynamicField(
105-
interfaceKeysContent,
106-
this.config.Ts.Keyword.Any,
107-
),
105+
field: this.config.Ts.InterfaceDynamicField({
106+
readonly:
107+
(additionalProperties.readOnly && this.config.addReadonly) ||
108+
this.config.makeImmutable,
109+
key: interfaceKeysContent,
110+
value: this.config.Ts.Keyword.Any,
111+
}),
108112
});
109113
}
110114

src/schema-parser/base-schema-parsers/primitive.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import { MonoSchemaParser } from "../mono-schema-parser.js";
44
export class PrimitiveSchemaParser extends MonoSchemaParser {
55
override parse() {
66
let contentType = null;
7-
const { additionalProperties, type, description, items } =
7+
const { additionalProperties, type, description, items, readOnly } =
88
this.schema || {};
99

10+
const readonly =
11+
(readOnly && this.config.addReadonly) || this.config.makeImmutable;
12+
1013
if (type === this.config.Ts.Keyword.Object && additionalProperties) {
1114
const propertyNamesSchema = this.schemaUtils.getSchemaPropertyNamesSchema(
1215
this.schema,
@@ -37,10 +40,11 @@ export class PrimitiveSchemaParser extends MonoSchemaParser {
3740
recordValuesContent = this.config.Ts.Keyword.Any;
3841
}
3942

40-
contentType = this.config.Ts.RecordType(
41-
recordKeysContent,
42-
recordValuesContent,
43-
);
43+
contentType = this.config.Ts.RecordType({
44+
readonly,
45+
key: recordKeysContent,
46+
value: recordValuesContent,
47+
});
4448
}
4549

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

5357
if (Array.isArray(items) && type === SCHEMA_TYPES.ARRAY) {
54-
contentType = this.config.Ts.Tuple(
55-
items.map((item) =>
58+
contentType = this.config.Ts.Tuple({
59+
readonly,
60+
values: items.map((item) =>
5661
this.schemaParserFabric
5762
.createSchemaParser({ schema: item, schemaPath: this.schemaPath })
5863
.getInlineParseContent(),
5964
),
60-
);
65+
});
6166
}
6267

6368
return {

src/schema-parser/schema-formatters.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,13 @@ export class SchemaFormatters {
8282
? this.config.Ts.ObjectWrapper(
8383
this.formatObjectContent(parsedSchema.content),
8484
)
85-
: this.config.Ts.RecordType(
86-
this.config.Ts.Keyword.String,
87-
this.config.Ts.Keyword.Any,
88-
),
85+
: this.config.Ts.RecordType({
86+
readonly:
87+
(parsedSchema.readOnly && this.config.addReadonly) ||
88+
this.config.makeImmutable,
89+
key: this.config.Ts.Keyword.String,
90+
value: this.config.Ts.Keyword.Any,
91+
}),
8992
),
9093
};
9194
},
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`basic > immutable 1`] = `
4+
"/* eslint-disable */
5+
/* tslint:disable */
6+
// @ts-nocheck
7+
/*
8+
* ---------------------------------------------------------------
9+
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
10+
* ## ##
11+
* ## AUTHOR: acacode ##
12+
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
13+
* ---------------------------------------------------------------
14+
*/
15+
16+
export interface Pet {
17+
/** @format int64 */
18+
readonly id: number;
19+
readonly name: string;
20+
readonly tag?: string;
21+
readonly list?: readonly string[];
22+
readonly obj?: Readonly<Record<string, any>>;
23+
readonly multiple?: string | number;
24+
}
25+
"
26+
`;
27+
28+
exports[`basic > immutable another-array-type 1`] = `
29+
"/* eslint-disable */
30+
/* tslint:disable */
31+
// @ts-nocheck
32+
/*
33+
* ---------------------------------------------------------------
34+
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
35+
* ## ##
36+
* ## AUTHOR: acacode ##
37+
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
38+
* ---------------------------------------------------------------
39+
*/
40+
41+
export interface Pet {
42+
/** @format int64 */
43+
readonly id: number;
44+
readonly name: string;
45+
readonly tag?: string;
46+
readonly list?: ReadonlyArray<string>;
47+
readonly obj?: Readonly<Record<string, any>>;
48+
readonly multiple?: string | number;
49+
}
50+
"
51+
`;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as fs from "node:fs/promises";
2+
import * as os from "node:os";
3+
import * as path from "node:path";
4+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
5+
import { generateApi } from "../../../src/index.js";
6+
7+
describe("basic", async () => {
8+
let tmpdir = "";
9+
10+
beforeAll(async () => {
11+
tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), "swagger-typescript-api"));
12+
});
13+
14+
afterAll(async () => {
15+
await fs.rm(tmpdir, { recursive: true });
16+
});
17+
18+
test("immutable", async () => {
19+
await generateApi({
20+
fileName: "schema",
21+
input: path.resolve(import.meta.dirname, "schema.json"),
22+
output: tmpdir,
23+
silent: true,
24+
makeImmutable: true,
25+
generateClient: false,
26+
});
27+
28+
const content = await fs.readFile(path.join(tmpdir, "schema.ts"), {
29+
encoding: "utf8",
30+
});
31+
32+
expect(content).toMatchSnapshot();
33+
});
34+
test("immutable another-array-type", async () => {
35+
await generateApi({
36+
fileName: "schema",
37+
input: path.resolve(import.meta.dirname, "schema.json"),
38+
output: tmpdir,
39+
silent: true,
40+
makeImmutable: true,
41+
generateClient: false,
42+
anotherArrayType: true,
43+
});
44+
45+
const content = await fs.readFile(path.join(tmpdir, "schema.ts"), {
46+
encoding: "utf8",
47+
});
48+
49+
expect(content).toMatchSnapshot();
50+
});
51+
});

0 commit comments

Comments
 (0)