From 296de1cf546dca587b966f38398898ee128863d6 Mon Sep 17 00:00:00 2001 From: Joshua Sosso Date: Tue, 15 Oct 2024 21:23:51 -0500 Subject: [PATCH] escape dictionary keys when serializing add tests to ensure affected characters are properly serialized when encoding json record structures --- .vscode/extensions.json | 3 +- .../src/main/kotlin/ExampleClient.kt | 6 ++-- languages/kotlin/kotlin-codegen/src/map.ts | 4 +-- .../src/example_client.rs | 6 ++-- languages/rust/rust-codegen/src/record.ts | 4 +-- .../SwiftCodegenReference.swift | 6 ++-- languages/swift/swift-codegen/src/record.ts | 2 +- .../src/referenceClient.ts | 6 ++-- languages/ts/ts-codegen/src/_index.test.ts | 1 + languages/ts/ts-codegen/src/record.ts | 4 +-- tests/clients/dart/test/test_client_test.dart | 1 + tests/clients/kotlin/src/main/kotlin/Main.kt | 2 +- .../kotlin/src/main/kotlin/TestClient.rpc.kt | 10 +++--- tests/clients/rust/src/main.rs | 1 + tests/clients/rust/src/test_client.g.rs | 10 +++--- .../clients/swift/Sources/TestClient.g.swift | 10 +++--- .../clients/swift/Tests/TestClientTests.swift | 2 +- tests/clients/ts/testClient.rpc.ts | 10 +++--- tests/clients/ts/testClient.test.ts | 1 + tooling/schema/src/compiler/serialize.ts | 33 +++++++++++++++++-- tooling/schema/src/lib/record.test.ts | 8 ++++- tooling/schema/src/lib/record.ts | 2 +- tooling/schema/src/testSuites.ts | 12 ++++++- 23 files changed, 97 insertions(+), 47 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 8d3a12ba..b28293fe 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,6 +7,7 @@ "dart-code.dart-code", "dart-code.flutter", "rust-lang.rust-analyzer", - "tamasfe.even-better-toml" + "tamasfe.even-better-toml", + "vadimcn.vscode-lldb" ] } diff --git a/languages/kotlin/kotlin-codegen-reference/src/main/kotlin/ExampleClient.kt b/languages/kotlin/kotlin-codegen-reference/src/main/kotlin/ExampleClient.kt index b32f8003..73b72c12 100644 --- a/languages/kotlin/kotlin-codegen-reference/src/main/kotlin/ExampleClient.kt +++ b/languages/kotlin/kotlin-codegen-reference/src/main/kotlin/ExampleClient.kt @@ -553,7 +553,7 @@ data class ObjectWithEveryType( if (__index != 0) { output += "," } - output += "\"${__entry.key}\":" + output += "${buildString { printQuoted(__entry.key) }}:" output += __entry.value } output += "}" @@ -1173,7 +1173,7 @@ data class ObjectWithOptionalFields( if (__index != 0) { output += "," } - output += "\"${__entry.key}\":" + output += "${buildString { printQuoted(__entry.key) }}:" output += __entry.value } output += "}" @@ -1497,7 +1497,7 @@ data class ObjectWithNullableFields( if (__index != 0) { output += "," } - output += "\"${__entry.key}\":" + output += "${buildString { printQuoted(__entry.key) }}:" output += __entry.value } output += "}" diff --git a/languages/kotlin/kotlin-codegen/src/map.ts b/languages/kotlin/kotlin-codegen/src/map.ts index c2a0555d..0c53637e 100644 --- a/languages/kotlin/kotlin-codegen/src/map.ts +++ b/languages/kotlin/kotlin-codegen/src/map.ts @@ -50,7 +50,7 @@ export function kotlinMapFromSchema( if (__index != 0) { ${target} += "," } - ${target} += "\\"\${__entry.key}\\":" + ${target} += "\${buildString { printQuoted(__entry.key) }}:" ${subType.toJson("__entry.value", target)} } ${target} += "}" @@ -61,7 +61,7 @@ export function kotlinMapFromSchema( if (__index != 0) { ${target} += "," } - ${target} += "\\"\${__entry.key}\\":" + ${target} += "\${buildString { printQuoted(__entry.key) }}:" ${subType.toJson("__entry.value", target)} } ${target} += "}"`; diff --git a/languages/rust/rust-codegen-reference/src/example_client.rs b/languages/rust/rust-codegen-reference/src/example_client.rs index 06dbeb3c..b68758b0 100644 --- a/languages/rust/rust-codegen-reference/src/example_client.rs +++ b/languages/rust/rust-codegen-reference/src/example_client.rs @@ -595,7 +595,7 @@ impl ArriModel for ObjectWithEveryType { if _index_ != 0 { _json_output_.push(','); } - _json_output_.push_str(format!("\"{}\":", _key_).as_str()); + _json_output_.push_str(format!("{}:", serialize_string(_key_)).as_str()); _json_output_.push_str(_value_.to_string().as_str()); } _json_output_.push('}'); @@ -1247,7 +1247,7 @@ impl ArriModel for ObjectWithOptionalFields { if _index_ != 0 { _json_output_.push(','); } - _json_output_.push_str(format!("\"{}\":", _key_).as_str()); + _json_output_.push_str(format!("{}:", serialize_string(_key_)).as_str()); _json_output_.push_str(_value_.to_string().as_str()); } _json_output_.push('}'); @@ -1796,7 +1796,7 @@ impl ArriModel for ObjectWithNullableFields { if _index_ != 0 { _json_output_.push(','); } - _json_output_.push_str(format!("\"{}\":", _key_).as_str()); + _json_output_.push_str(format!("{}:", serialize_string(_key_)).as_str()); _json_output_.push_str(_value_.to_string().as_str()); } _json_output_.push('}'); diff --git a/languages/rust/rust-codegen/src/record.ts b/languages/rust/rust-codegen/src/record.ts index 904aed91..3a459ab1 100644 --- a/languages/rust/rust-codegen/src/record.ts +++ b/languages/rust/rust-codegen/src/record.ts @@ -67,7 +67,7 @@ export default function rustRecordFromSchema( if _index_ != 0 { ${target}.push(','); } - ${target}.push_str(format!("\\"{}\\":", _key_).as_str()); + ${target}.push_str(format!("{}:", serialize_string(_key_)).as_str()); match _value_ { Some(value_val) => { ${innerType.toJsonTemplate("value_val", target)}; @@ -84,7 +84,7 @@ export default function rustRecordFromSchema( if _index_ != 0 { ${target}.push(','); } - ${target}.push_str(format!("\\"{}\\":", _key_).as_str()); + ${target}.push_str(format!("{}:", serialize_string(_key_)).as_str()); ${innerType.toJsonTemplate(`_value_`, target)}; } ${target}.push('}')`; diff --git a/languages/swift/swift-codegen-reference/Sources/SwiftCodegenReference/SwiftCodegenReference.swift b/languages/swift/swift-codegen-reference/Sources/SwiftCodegenReference/SwiftCodegenReference.swift index baecb582..046906e1 100644 --- a/languages/swift/swift-codegen-reference/Sources/SwiftCodegenReference/SwiftCodegenReference.swift +++ b/languages/swift/swift-codegen-reference/Sources/SwiftCodegenReference/SwiftCodegenReference.swift @@ -440,7 +440,7 @@ public struct ObjectWithEveryType: ArriClientModel { if __index > 0 { __json += "," } - __json += "\"\(__key)\":" + __json += "\(serializeString(input: __key)):" __json += "\(__value)" } __json += "}" @@ -1094,7 +1094,7 @@ public struct ObjectWithOptionalFields: ArriClientModel { if __index > 0 { __json += "," } - __json += "\"\(__key)\":" + __json += "\(serializeString(input: __key)):" __json += "\(__value)" } __json += "}" @@ -1471,7 +1471,7 @@ public struct ObjectWithNullableFields: ArriClientModel { if __index > 0 { __json += "," } - __json += "\"\(__key)\":" + __json += "\(serializeString(input: __key)):" __json += "\(__value)" } __json += "}" diff --git a/languages/swift/swift-codegen/src/record.ts b/languages/swift/swift-codegen/src/record.ts index 6d1f8bc2..b1029012 100644 --- a/languages/swift/swift-codegen/src/record.ts +++ b/languages/swift/swift-codegen/src/record.ts @@ -57,7 +57,7 @@ ${mainContent} if __index > 0 { ${target} += "," } - ${target} += "\\"\\(__key)\\":" + ${target} += "\\(serializeString(input: __key)):" ${subType.toJsonTemplate("__value", target)} } ${target} += "}"`; diff --git a/languages/ts/ts-codegen-reference/src/referenceClient.ts b/languages/ts/ts-codegen-reference/src/referenceClient.ts index 36b13449..e0b21615 100644 --- a/languages/ts/ts-codegen-reference/src/referenceClient.ts +++ b/languages/ts/ts-codegen-reference/src/referenceClient.ts @@ -683,7 +683,7 @@ export const $$ObjectWithEveryType: ArriModelValidator = { if (_recordPropertyCount !== 0) { json += ","; } - json += `"${_key}":`; + json += `${serializeString(_key)}:`; json += `${_value}`; _recordPropertyCount++; } @@ -1455,7 +1455,7 @@ export const $$ObjectWithOptionalFields: ArriModelValidator main() async { record: { "A": BigInt.from(1), "B": BigInt.from(0), + "\"C\"\t": BigInt.from(1), }, discriminator: ObjectWithEveryTypeDiscriminatorA(title: "Hello World"), nestedObject: ObjectWithEveryTypeNestedObject( diff --git a/tests/clients/kotlin/src/main/kotlin/Main.kt b/tests/clients/kotlin/src/main/kotlin/Main.kt index fb191c75..3bcd5082 100644 --- a/tests/clients/kotlin/src/main/kotlin/Main.kt +++ b/tests/clients/kotlin/src/main/kotlin/Main.kt @@ -64,7 +64,7 @@ fun main() { ) ) ), - record = mutableMapOf(Pair("01", 1UL), Pair("02", 0UL)), + record = mutableMapOf(Pair("01", 1UL), Pair("02", 0UL), Pair("\"03\"\t", 1UL)), nestedObject = ObjectWithEveryTypeNestedObject( id = "d1", timestamp = targetDate, data = ObjectWithEveryTypeNestedObjectData( id = "d2", timestamp = targetDate, data = ObjectWithEveryTypeNestedObjectDataData( diff --git a/tests/clients/kotlin/src/main/kotlin/TestClient.rpc.kt b/tests/clients/kotlin/src/main/kotlin/TestClient.rpc.kt index ad2f6e9a..b5827d59 100644 --- a/tests/clients/kotlin/src/main/kotlin/TestClient.rpc.kt +++ b/tests/clients/kotlin/src/main/kotlin/TestClient.rpc.kt @@ -908,7 +908,7 @@ output += "{" if (__index != 0) { output += "," } - output += "\"${__entry.key}\":" + output += "${buildString { printQuoted(__entry.key) }}:" output += "\"${__entry.value}\"" } output += "}" @@ -1840,7 +1840,7 @@ if (record == null) { if (__index != 0) { output += "," } - output += "\"${__entry.key}\":" + output += "${buildString { printQuoted(__entry.key) }}:" output += when (__entry.value) { is ULong -> "\"${__entry.value}\"" else -> "null" @@ -2899,7 +2899,7 @@ if (record != null) { if (__index != 0) { output += "," } - output += "\"${__entry.key}\":" + output += "${buildString { printQuoted(__entry.key) }}:" output += "\"${__entry.value}\"" } output += "}" @@ -5512,7 +5512,7 @@ output += "{" if (__index != 0) { output += "," } - output += "\"${__entry.key}\":" + output += "${buildString { printQuoted(__entry.key) }}:" output += __entry.value.toJson() } output += "}" @@ -5522,7 +5522,7 @@ output += "{" if (__index != 0) { output += "," } - output += "\"${__entry.key}\":" + output += "${buildString { printQuoted(__entry.key) }}:" output += JsonInstance.encodeToString(__entry.value) } output += "}" diff --git a/tests/clients/rust/src/main.rs b/tests/clients/rust/src/main.rs index ffc311a2..cf71e348 100644 --- a/tests/clients/rust/src/main.rs +++ b/tests/clients/rust/src/main.rs @@ -58,6 +58,7 @@ mod tests { let mut record = BTreeMap::::new(); record.insert("A".to_string(), 1); record.insert("B".to_string(), 0); + record.insert("\"C\"\t".to_string(), 1); let mut input = ObjectWithEveryType { any: serde_json::Value::String("hello world".to_string()), boolean: true, diff --git a/tests/clients/rust/src/test_client.g.rs b/tests/clients/rust/src/test_client.g.rs index b2b218e8..d93207b3 100644 --- a/tests/clients/rust/src/test_client.g.rs +++ b/tests/clients/rust/src/test_client.g.rs @@ -969,7 +969,7 @@ impl ArriModel for ObjectWithEveryType { if _index_ != 0 { _json_output_.push(','); } - _json_output_.push_str(format!("\"{}\":", _key_).as_str()); + _json_output_.push_str(format!("{}:", serialize_string(_key_)).as_str()); _json_output_.push_str(format!("\"{}\"", _value_).as_str()); } _json_output_.push('}'); @@ -1964,7 +1964,7 @@ impl ArriModel for ObjectWithEveryNullableType { if _index_ != 0 { _json_output_.push(','); } - _json_output_.push_str(format!("\"{}\":", _key_).as_str()); + _json_output_.push_str(format!("{}:", serialize_string(_key_)).as_str()); match _value_ { Some(value_val) => { _json_output_.push_str(format!("\"{}\"", value_val).as_str()); @@ -3365,7 +3365,7 @@ impl ArriModel for ObjectWithEveryOptionalType { if _index_ != 0 { _json_output_.push(','); } - _json_output_.push_str(format!("\"{}\":", _key_).as_str()); + _json_output_.push_str(format!("{}:", serialize_string(_key_)).as_str()); _json_output_.push_str(format!("\"{}\"", _value_).as_str()); } _json_output_.push('}'); @@ -5514,7 +5514,7 @@ impl ArriModel for UsersWatchUserResponse { if _index_ != 0 { _json_output_.push(','); } - _json_output_.push_str(format!("\"{}\":", _key_).as_str()); + _json_output_.push_str(format!("{}:", serialize_string(_key_)).as_str()); _json_output_.push_str(_value_.to_json_string().as_str()); } _json_output_.push('}'); @@ -5524,7 +5524,7 @@ impl ArriModel for UsersWatchUserResponse { if _index_ != 0 { _json_output_.push(','); } - _json_output_.push_str(format!("\"{}\":", _key_).as_str()); + _json_output_.push_str(format!("{}:", serialize_string(_key_)).as_str()); _json_output_.push_str( serde_json::to_string(_value_) .unwrap_or("null".to_string()) diff --git a/tests/clients/swift/Sources/TestClient.g.swift b/tests/clients/swift/Sources/TestClient.g.swift index 67e0b313..e49cdba7 100644 --- a/tests/clients/swift/Sources/TestClient.g.swift +++ b/tests/clients/swift/Sources/TestClient.g.swift @@ -705,7 +705,7 @@ public struct ObjectWithEveryType: ArriClientModel { if __index > 0 { __json += "," } - __json += "\"\(__key)\":" + __json += "\(serializeString(input: __key)):" __json += "\"\(__value)\"" } __json += "}" @@ -1642,7 +1642,7 @@ if self.record != nil { if __index > 0 { __json += "," } - __json += "\"\(__key)\":" + __json += "\(serializeString(input: __key)):" if __value != nil { __json += "\"\(__value!)\"" } else { @@ -2852,7 +2852,7 @@ __numKeys += 1 if __index > 0 { __json += "," } - __json += "\"\(__key)\":" + __json += "\(serializeString(input: __key)):" __json += "\"\(__value)\"" } __json += "}" @@ -5509,7 +5509,7 @@ public struct UsersWatchUserResponse: ArriClientModel { if __index > 0 { __json += "," } - __json += "\"\(__key)\":" + __json += "\(serializeString(input: __key)):" __json += __value.toJSONString() } __json += "}" @@ -5519,7 +5519,7 @@ public struct UsersWatchUserResponse: ArriClientModel { if __index > 0 { __json += "," } - __json += "\"\(__key)\":" + __json += "\(serializeString(input: __key)):" __json += serializeAny(input: __value) } __json += "}" diff --git a/tests/clients/swift/Tests/TestClientTests.swift b/tests/clients/swift/Tests/TestClientTests.swift index 3b207c29..c64961b3 100644 --- a/tests/clients/swift/Tests/TestClientTests.swift +++ b/tests/clients/swift/Tests/TestClientTests.swift @@ -43,7 +43,7 @@ final class TestSwiftClientTests: XCTestCase { boolean: true, timestamp: testDate ), - record: Dictionary(dictionaryLiteral: ("A", 1), ("B", 0)), + record: Dictionary(dictionaryLiteral: ("A", 1), ("B", 0), ("\"C\"\t", 0)), discriminator: ObjectWithEveryTypeDiscriminator.b( ObjectWithEveryTypeDiscriminatorB( title: "this is a title", diff --git a/tests/clients/ts/testClient.rpc.ts b/tests/clients/ts/testClient.rpc.ts index 522e4c05..6d6d5ee1 100644 --- a/tests/clients/ts/testClient.rpc.ts +++ b/tests/clients/ts/testClient.rpc.ts @@ -1001,7 +1001,7 @@ export const $$ObjectWithEveryType: ArriModelValidator = { if (_recordPropertyCount !== 0) { json += ","; } - json += `"${_key}":`; + json += `${serializeString(_key)}:`; json += `"${_value}"`; _recordPropertyCount++; } @@ -2140,7 +2140,7 @@ export const $$ObjectWithEveryNullableType: ArriModelValidator= 0xd800 && __point__ <= 0xdfff)) { + ${input.targetVal} += JSON.stringify(key); + __finished__ = true; + break; + } + if (__point__ === 0x22 || __point__ === 0x5c) { + __last__ === -1 && (__last__ = 0); + __result__ += key.slice(__last__, i) + '\\\\'; + __last__ = i; + } + } + if(!__finished__) { + if (__last__ === -1) { + ${input.targetVal} += \`"\${key}"\`; + } else { + ${input.targetVal} += \`"\${__result__}\${key.slice(__last__)}"\`; + } + } + } else if (key.length < 5000 && !STR_ESCAPE.test(key)) { + ${input.targetVal} += \`"\${key}"\`; } else { - ${input.targetVal} += \`"\${key}":\`; + ${input.targetVal} += JSON.stringify(key); } + ${input.targetVal} += ":"; ${innerTemplate} }`); templateParts.push(`${input.targetVal} += '}';`); diff --git a/tooling/schema/src/lib/record.test.ts b/tooling/schema/src/lib/record.test.ts index bf311641..6b2364a0 100644 --- a/tooling/schema/src/lib/record.test.ts +++ b/tooling/schema/src/lib/record.test.ts @@ -29,7 +29,13 @@ test("Type Inference", () => { describe("Parsing", () => { it("accepts good input", () => { expect(a.safeParse(NumberRecordSchema, { "1": 1, "2": 2 }).success); - expect(a.safeParse(StringRecordSchema, { "1": "1", "2": "2" }).success); + expect( + a.safeParse(StringRecordSchema, { + "1": "1", + "2": "2", + [`A song titled "Song"`]: `A song titled "Song"`, + }).success, + ); expect( a.safeParse(ObjectRecordSchema, { a: { id: "12345", type: "notification" }, diff --git a/tooling/schema/src/lib/record.ts b/tooling/schema/src/lib/record.ts index 6895c6d1..175b1ec7 100644 --- a/tooling/schema/src/lib/record.ts +++ b/tooling/schema/src/lib/record.ts @@ -63,7 +63,7 @@ export function record>( for (const key of Object.keys(input)) { const val = input[key]; strParts.push( - `"${key}":${schema.metadata[ + `${JSON.stringify(key)}:${schema.metadata[ SCHEMA_METADATA ].serialize(val, { instancePath: `${context.instancePath}/${key}`, diff --git a/tooling/schema/src/testSuites.ts b/tooling/schema/src/testSuites.ts index 397e9e87..61596364 100644 --- a/tooling/schema/src/testSuites.ts +++ b/tooling/schema/src/testSuites.ts @@ -543,7 +543,7 @@ export const validationTestSuites: Record< }, "record with boolean values": { schema: a.record(a.boolean()), - goodInputs: [{ a: true, b: false }, {}], + goodInputs: [{ a: true, b: false, [`"C"`]: true }, {}], badInputs: [{ a: true, b: true, c: "true" }, { a: "null" }, null], }, "record with objects": { @@ -1330,4 +1330,14 @@ Sed in commodo libero. Vestibulum sit amet convallis libero. Aenean tincidunt so }, ], }, + "record with quoted keys": { + schema: a.record(a.boolean()), + inputs: [ + { + A: true, + '"B"': false, + "\tC": true, + }, + ], + }, };