From e643d3fc63482f1099c5131c9ce7769b327ece87 Mon Sep 17 00:00:00 2001 From: Henry Su Date: Sat, 29 Aug 2026 20:55:40 -0500 Subject: [PATCH] fix(typegen): escape Swift string literals Signed-off-by: Henry Su --- src/server/templates/swift.ts | 33 +++++++++++- test/server/templates/swift.test.ts | 81 +++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 test/server/templates/swift.test.ts diff --git a/src/server/templates/swift.ts b/src/server/templates/swift.ts index 69aec816..bcfd0b8a 100644 --- a/src/server/templates/swift.ts +++ b/src/server/templates/swift.ts @@ -153,6 +153,36 @@ function generateProtocolConformances(protocols: string[]): string { return protocols.length === 0 ? '' : `: ${protocols.join(', ')}` } +function swiftStringLiteral(value: string): string { + let escaped = '' + for (const character of value) { + const codePoint = character.codePointAt(0)! + switch (character) { + case '\\': + escaped += '\\\\' + break + case '"': + escaped += '\\"' + break + case '\t': + escaped += '\\t' + break + case '\n': + escaped += '\\n' + break + case '\r': + escaped += '\\r' + break + default: + escaped += + codePoint < 0x20 || codePoint === 0x7f || codePoint === 0x2028 || codePoint === 0x2029 + ? `\\u{${codePoint.toString(16)}}` + : character + } + } + return `"${escaped}"` +} + function generateEnum( enum_: SwiftEnum, { accessControl, level }: SwiftGeneratorOptions & { level: number } @@ -160,7 +190,8 @@ function generateEnum( return [ `${ident(level)}${accessControl} enum ${enum_.formattedEnumName}${generateProtocolConformances(enum_.protocolConformances)} {`, ...enum_.cases.map( - (case_) => `${ident(level + 1)}case ${case_.formattedName} = "${case_.rawValue}"` + (case_) => + `${ident(level + 1)}case ${case_.formattedName} = ${swiftStringLiteral(case_.rawValue)}` ), `${ident(level)}}`, ] diff --git a/test/server/templates/swift.test.ts b/test/server/templates/swift.test.ts new file mode 100644 index 00000000..fa329399 --- /dev/null +++ b/test/server/templates/swift.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from 'vitest' + +import { apply } from '../../../src/server/templates/swift' +import type { GeneratorMetadata } from '../../../src/lib/generators' +import type { + PostgresColumn, + PostgresSchema, + PostgresTable, + PostgresType, +} from '../../../src/lib/types' + +const baseSchema: PostgresSchema = { + id: 1, + name: 'public', + owner: 'postgres', +} + +const baseTable = { + id: 1, + schema: 'public', + name: 'tickets', +} as unknown as Omit + +const baseColumn = (name: string): PostgresColumn => + ({ + table_id: 1, + name, + format: 'text', + is_identity: false, + is_generated: false, + is_nullable: false, + default_value: null, + }) as PostgresColumn + +const enumType = (enums: string[]): PostgresType => + ({ + id: 100, + name: 'status', + schema: 'public', + format: 'status', + enums, + attributes: [], + }) as PostgresType + +const buildMetadata = (overrides: Partial = {}) => ({ + schemas: [baseSchema], + tables: [baseTable], + foreignTables: [], + views: [], + materializedViews: [], + columns: [], + relationships: [], + functions: [], + types: [], + accessControl: 'internal' as const, + ...overrides, +}) + +describe('swift typegen string literal escaping', () => { + test('escapes enum raw values', async () => { + const result = await apply(buildMetadata({ types: [enumType(['say"hi', 'use\\path'])] })) + + expect(result).toContain('case sayHi = "say\\"hi"') + expect(result).toContain('case usePath = "use\\\\path"') + }) + + test('escapes coding key raw values', async () => { + const result = await apply(buildMetadata({ columns: [baseColumn('say"hi')] })) + + expect(result).toContain('case sayHi = "say\\"hi"') + }) + + test('escapes newlines and string interpolation markers', async () => { + const result = await apply( + buildMetadata({ types: [enumType(['line\nbreak', 'value\\(call)'])] }) + ) + + expect(result).toContain('case lineBreak = "line\\nbreak"') + expect(result).toContain('case valueCall = "value\\\\(call)"') + }) +})