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
100 changes: 90 additions & 10 deletions Sources/AnyLanguageModelMacros/GenerableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro {

return [
generateRawContentProperty(),
generateMemberwiseInit(properties: properties),
generateInitFromGeneratedContent(structName: structName, properties: properties),
generateGeneratedContentProperty(
structName: structName,
Expand Down Expand Up @@ -69,7 +70,10 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro {
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let nonisolatedModifier = DeclModifierSyntax(name: .keyword(.nonisolated))

let extensionDecl = ExtensionDeclSyntax(
modifiers: DeclModifierListSyntax([nonisolatedModifier]),
extendedType: type,
inheritanceClause: InheritanceClauseSyntax(
inheritedTypes: InheritedTypeListSyntax([
Expand Down Expand Up @@ -247,6 +251,91 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro {
)
}

private static func generateMemberwiseInit(properties: [PropertyInfo]) -> DeclSyntax {
if properties.isEmpty {
return DeclSyntax(
stringLiteral: """
nonisolated public init() {
self._rawGeneratedContent = GeneratedContent(kind: .structure(properties: [:], orderedKeys: []))
}
"""
)
}

let parameters = properties.map { prop in
"\(prop.name): \(prop.type)"
}.joined(separator: ", ")

let assignments = properties.map { prop in
"self.\(prop.name) = \(prop.name)"
}.joined(separator: "\n ")

let propertyConversions = properties.map { prop in
let propName = prop.name
let propType = prop.type

if propType.hasSuffix("?") {
let baseType = String(propType.dropLast())
if baseType == "String" {
return
"properties[\"\(propName)\"] = \(propName).map { GeneratedContent($0) } ?? GeneratedContent(kind: .null)"
} else if baseType == "Int" || baseType == "Double" || baseType == "Float"
|| baseType == "Bool" || baseType == "Decimal"
{
return
"properties[\"\(propName)\"] = \(propName).map { $0.generatedContent } ?? GeneratedContent(kind: .null)"
} else if isDictionaryType(baseType) {
return
"properties[\"\(propName)\"] = \(propName).map { $0.generatedContent } ?? GeneratedContent(kind: .null)"
} else if baseType.hasPrefix("[") && baseType.hasSuffix("]") {
return
"properties[\"\(propName)\"] = \(propName).map { GeneratedContent(elements: $0) } ?? GeneratedContent(kind: .null)"
} else {
return """
if let value = \(propName) {
properties["\(propName)"] = value.generatedContent
} else {
properties["\(propName)"] = GeneratedContent(kind: .null)
}
"""
}
} else if isDictionaryType(propType) {
return "properties[\"\(propName)\"] = \(propName).generatedContent"
} else if propType.hasPrefix("[") && propType.hasSuffix("]") {
return "properties[\"\(propName)\"] = GeneratedContent(elements: \(propName))"
} else {
switch propType {
case "String":
return "properties[\"\(propName)\"] = GeneratedContent(\(propName))"
case "Int", "Double", "Float", "Bool", "Decimal":
return "properties[\"\(propName)\"] = \(propName).generatedContent"
default:
return "properties[\"\(propName)\"] = \(propName).generatedContent"
}
}
}.joined(separator: "\n ")

let orderedKeys = properties.map { "\"\($0.name)\"" }.joined(separator: ", ")

return DeclSyntax(
stringLiteral: """
nonisolated public init(\(parameters)) {
\(assignments)

var properties: [String: GeneratedContent] = [:]
\(propertyConversions)

self._rawGeneratedContent = GeneratedContent(
kind: .structure(
properties: properties,
orderedKeys: [\(orderedKeys)]
)
)
}
"""
)
}

private static func generateInitFromGeneratedContent(
structName: String,
properties: [PropertyInfo]
Expand Down Expand Up @@ -457,16 +546,7 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro {
} else if isDictionaryType(propType) {
return "properties[\"\(propName)\"] = \(propName).generatedContent"
} else if propType.hasPrefix("[") && propType.hasSuffix("]") {
let elementType = String(propType.dropFirst().dropLast())
if elementType == "String" {
return "properties[\"\(propName)\"] = GeneratedContent(elements: \(propName))"
} else if elementType == "Int" || elementType == "Double" || elementType == "Bool"
|| elementType == "Float" || elementType == "Decimal"
{
return "properties[\"\(propName)\"] = GeneratedContent(elements: \(propName))"
} else {
return "properties[\"\(propName)\"] = GeneratedContent(elements: \(propName))"
}
return "properties[\"\(propName)\"] = GeneratedContent(elements: \(propName))"
} else {
switch propType {
case "String":
Expand Down
48 changes: 43 additions & 5 deletions Tests/AnyLanguageModelTests/GenerableMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,19 @@ private struct TestStructWithNewlines {
var field: String
}

@Generable
struct TestArguments {
@Guide(description: "A name field")
var name: String

@Guide(description: "An age field")
var age: Int
}

@Suite("Generable Macro")
struct GenerableMacroTests {
@Test func multilineGuideDescription() async throws {
@Test("@Guide description with multiline string")
func multilineGuideDescription() async throws {
let schema = TestStructWithMultilineDescription.generationSchema
let encoder = JSONEncoder()
let jsonData = try encoder.encode(schema)
Expand All @@ -41,7 +51,8 @@ struct GenerableMacroTests {
#expect(decodedSchema.debugDescription.contains("object"))
}

@Test func guideDescriptionWithSpecialCharacters() async throws {
@Test("@Guide description with special characters")
func guideDescriptionWithSpecialCharacters() async throws {
let schema = TestStructWithSpecialCharacters.generationSchema
let encoder = JSONEncoder()
let jsonData = try encoder.encode(schema)
Expand All @@ -57,7 +68,8 @@ struct GenerableMacroTests {
#expect(decodedSchema.debugDescription.contains("object"))
}

@Test func guideDescriptionWithNewlines() async throws {
@Test("@Guide description with newlines")
func guideDescriptionWithNewlines() async throws {
let schema = TestStructWithNewlines.generationSchema
let encoder = JSONEncoder()
let jsonData = try encoder.encode(schema)
Expand All @@ -78,9 +90,9 @@ struct GenerableMacroTests {
var field: String
}

/// Test to verify @Generable works correctly with MainActor isolation.
@MainActor
@Test func mainActorIsolation() async throws {
@Test("@MainActor isolation")
func mainActorIsolation() async throws {
let generatedContent = GeneratedContent(properties: [
"field": "test value"
])
Expand All @@ -97,4 +109,30 @@ struct GenerableMacroTests {
let partiallyGenerated = instance.asPartiallyGenerated()
#expect(partiallyGenerated.field == "test value")
}

@Test("Memberwise initializer")
func memberwiseInit() throws {
// This is the natural Swift way to create instances
let args = TestArguments(name: "Alice", age: 30)

#expect(args.name == "Alice")
#expect(args.age == 30)

// The generatedContent should also be properly populated
let content = args.generatedContent
#expect(content.jsonString.contains("Alice"))
#expect(content.jsonString.contains("30"))
}

@Test("Create instance from GeneratedContent")
func fromGeneratedContent() throws {
let content = GeneratedContent(properties: [
"name": GeneratedContent("Bob"),
"age": GeneratedContent(kind: .number(25)),
])

let args = try TestArguments(content)
#expect(args.name == "Bob")
#expect(args.age == 25)
}
}