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
17 changes: 16 additions & 1 deletion Sources/AnyLanguageModelMacros/GenerableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down
73 changes: 73 additions & 0 deletions Tests/AnyLanguageModelTests/GenerableMacroTests.swift
Original file line number Diff line number Diff line change
@@ -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"))
}
}