Skip to content
Merged
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
14 changes: 12 additions & 2 deletions packages/quicktype-core/src/language/Swift/SwiftRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,8 @@ export class SwiftRenderer extends ConvenienceRenderer {

const isClass = this._options.useClasses || this.isCycleBreakerType(c);
const structOrClass = isClass ? "class" : "struct";
const finalPrefix =
isClass && this._options.finalClasses ? "final " : "";

if (isClass && this._options.objcSupport) {
// [Michael Fey (@MrRooni), 2019-4-24] Swift 5 or greater, must come before the access declaration for the class.
Expand All @@ -508,6 +510,7 @@ export class SwiftRenderer extends ConvenienceRenderer {

this.emitBlockWithAccess(
[
finalPrefix,
structOrClass,
" ",
className,
Expand Down Expand Up @@ -1163,11 +1166,13 @@ encoder.dateEncodingStrategy = .formatted(formatter)`);
this.emitLine(
this.objcMembersDeclaration,
this.accessLevel,
this._options.finalClasses ? "final " : "",
"class JSONNull: NSObject, Codable {",
);
} else {
this.emitLine(
this.accessLevel,
this._options.finalClasses ? "final " : "",
"class JSONNull: Codable, Hashable {",
);
}
Expand Down Expand Up @@ -1214,7 +1219,7 @@ encoder.dateEncodingStrategy = .formatted(formatter)`);

if (this._needAny) {
this.ensureBlankLine();
this.emitMultiline(`class JSONCodingKey: CodingKey {
this.emitMultiline(`${this._options.finalClasses ? "final " : ""}class JSONCodingKey: CodingKey {
let key: String

required init?(intValue: Int) {
Expand All @@ -1239,10 +1244,15 @@ encoder.dateEncodingStrategy = .formatted(formatter)`);
this.emitLine(
this.objcMembersDeclaration,
this.accessLevel,
this._options.finalClasses ? "final " : "",
"class JSONAny: NSObject, Codable {",
);
} else {
this.emitLine(this.accessLevel, "class JSONAny: Codable {");
this.emitLine(
this.accessLevel,
this._options.finalClasses ? "final " : "",
"class JSONAny: Codable {",
);
}

this.ensureBlankLine();
Expand Down
5 changes: 5 additions & 0 deletions packages/quicktype-core/src/language/Swift/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export const swiftOptions = {
} as const,
"struct",
),
finalClasses: new BooleanOption(
"final-classes",
"Mark classes as final",
false,
),
mutableProperties: new BooleanOption(
"mutable-properties",
"Use var instead of let for object properties",
Expand Down
4 changes: 4 additions & 0 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,10 @@ export const SwiftLanguage: Language = {
quickTestRendererOptions: [
{ "support-linux": "false" },
{ "struct-or-class": "class" },
[
"simple-object.json",
{ "struct-or-class": "class", "final-classes": "true" },
],
{ density: "dense" },
{ density: "normal" },
{ "access-level": "internal" },
Expand Down
113 changes: 113 additions & 0 deletions test/unit/swift-final-classes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
InputData,
JSONSchemaInput,
type RendererOptions,
quicktype,
} from "../../packages/quicktype-core/src/index.js";
import { describe, expect, test } from "vitest";

async function renderSwift(
schema: object,
rendererOptions: RendererOptions = {},
): Promise<string> {
const schemaInput = new JSONSchemaInput(undefined);
await schemaInput.addSource({
name: "TopLevel",
schema: JSON.stringify(schema),
});

const inputData = new InputData();
inputData.addInput(schemaInput);

const result = await quicktype({
inputData,
lang: "swift",
rendererOptions,
});
return result.lines.join("\n");
}

function classDeclarations(output: string): string[] {
return output
.split("\n")
.filter((line) =>
/^\s*(?:(?:public|internal)\s+)?(?:final\s+)?class\s+/.test(line),
);
}

describe("Swift class generation", () => {
const modelSchema = {
type: "object",
properties: {
child: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
},
},
required: ["child"],
};
const recursiveSchema = {
$ref: "#/definitions/Node",
definitions: {
Node: {
type: "object",
properties: {
next: { $ref: "#/definitions/Node" },
value: {},
},
required: ["value"],
},
},
};

test("keeps classes open by default", async () => {
const modelOutput = await renderSwift(modelSchema, {
"struct-or-class": "class",
});
const recursiveOutput = await renderSwift(recursiveSchema);
const declarations = classDeclarations(
`${modelOutput}\n${recursiveOutput}`,
);

expect(declarations.length).toBeGreaterThan(0);
expect(
declarations.every((line) => !line.includes("final class")),
).toBe(true);
});

test("marks requested model classes as final when enabled", async () => {
const output = await renderSwift(modelSchema, {
"struct-or-class": "class",
"final-classes": "true",
});
const declarations = classDeclarations(output);

expect(declarations).toHaveLength(2);
expect(declarations).toEqual(
expect.arrayContaining([
expect.stringContaining("final class TopLevel"),
expect.stringContaining("final class Child"),
]),
);
});

test("marks cycle breakers and Codable helper classes as final", async () => {
const output = await renderSwift(recursiveSchema, {
"final-classes": "true",
});
const declarations = classDeclarations(output);

expect(declarations).toEqual(
expect.arrayContaining([
expect.stringContaining("final class TopLevel"),
expect.stringContaining("final class JSONNull"),
expect.stringContaining("final class JSONCodingKey"),
expect.stringContaining("final class JSONAny"),
]),
);
expect(declarations.every((line) => line.includes("final class"))).toBe(
true,
);
});
});
Loading