diff --git a/Sources/AnyLanguageModelMacros/GenerableMacro.swift b/Sources/AnyLanguageModelMacros/GenerableMacro.swift index 61e44388..10252daa 100644 --- a/Sources/AnyLanguageModelMacros/GenerableMacro.swift +++ b/Sources/AnyLanguageModelMacros/GenerableMacro.swift @@ -538,10 +538,25 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { guidesArray = "[\(guides.joined(separator: ", "))]" } + // Escape the description string so it can be safely embedded in generated code. + // Multi-line strings need newlines converted to \n escape sequences, + // and special characters (backslashes, quotes) must be escaped. + let escapedDescription: String + if let desc = prop.guideDescription { + let escaped = + desc + .replacingOccurrences(of: "\\", with: "\\\\") // Escape backslashes first + .replacingOccurrences(of: "\"", with: "\\\"") // Escape quotes + .replacingOccurrences(of: "\n", with: "\\n") // Convert newlines to escape sequences + escapedDescription = "\"\(escaped)\"" + } else { + escapedDescription = "nil" + } + return """ GenerationSchema.Property( name: "\(prop.name)", - description: \(prop.guideDescription.map { "\"\($0)\"" } ?? "nil"), + description: \(escapedDescription), type: \(prop.type).self, guides: \(guidesArray) ) diff --git a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift new file mode 100644 index 00000000..4b6f5ac7 --- /dev/null +++ b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift @@ -0,0 +1,73 @@ +import Testing +import AnyLanguageModel +import Foundation + +@Generable +private struct TestStructWithMultilineDescription { + @Guide( + description: """ + This is a multi-line description. + It spans multiple lines. + """ + ) + var field: String +} + +@Generable +private struct TestStructWithSpecialCharacters { + @Guide(description: "A description with \"quotes\" and backslashes \\") + var field: String +} + +@Generable +private struct TestStructWithNewlines { + @Guide(description: "Line 1\nLine 2\nLine 3") + var field: String +} + +@Suite("Generable Macro") +struct GenerableMacroTests { + @Test func multilineGuideDescription() async throws { + let schema = TestStructWithMultilineDescription.generationSchema + let encoder = JSONEncoder() + let jsonData = try encoder.encode(schema) + + // Verify that the schema can be encoded without errors (no unterminated strings) + #expect(jsonData.count > 0) + + // Verify it can be decoded back + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(GenerationSchema.self, from: jsonData) + #expect(decodedSchema.debugDescription.contains("object")) + } + + @Test func guideDescriptionWithSpecialCharacters() async throws { + let schema = TestStructWithSpecialCharacters.generationSchema + let encoder = JSONEncoder() + let jsonData = try encoder.encode(schema) + let jsonString = String(data: jsonData, encoding: .utf8)! + + // Verify the special characters are escaped + #expect(jsonString.contains(#"\\\"quotes\\\""#)) + #expect(jsonString.contains(#"backslashes \\\\"#)) + + // Verify roundtrip encoding/decoding works + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(GenerationSchema.self, from: jsonData) + #expect(decodedSchema.debugDescription.contains("object")) + } + + @Test func guideDescriptionWithNewlines() async throws { + let schema = TestStructWithNewlines.generationSchema + let encoder = JSONEncoder() + let jsonData = try encoder.encode(schema) + + // Verify that the schema can be encoded without errors + #expect(jsonData.count > 0) + + // Verify roundtrip encoding/decoding works + let decoder = JSONDecoder() + let decodedSchema = try decoder.decode(GenerationSchema.self, from: jsonData) + #expect(decodedSchema.debugDescription.contains("object")) + } +}