Skip to content

Commit c7e86f1

Browse files
committed
review
1 parent f931ce6 commit c7e86f1

File tree

8 files changed

+90
-48
lines changed

8 files changed

+90
-48
lines changed

FirebaseAI/Sources/GenerationConfig.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,9 @@ public struct GenerationConfig: Sendable {
205205
}
206206

207207
/// Internal initializer to create a new config by overriding specific values of another.
208-
internal init(
209-
from base: GenerationConfig?,
210-
responseMIMEType: String,
211-
responseSchema: Schema
212-
) {
208+
init(from base: GenerationConfig?,
209+
responseMIMEType: String,
210+
responseSchema: Schema) {
213211
self.init(
214212
temperature: base?.temperature,
215213
topP: base?.topP,

FirebaseAI/Sources/GenerativeAIService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ struct GenerativeAIService {
2828

2929
let firebaseInfo: FirebaseInfo
3030

31-
private let urlSession: URLSession
31+
let urlSession: URLSession
3232

3333
init(firebaseInfo: FirebaseInfo, urlSession: URLSession) {
3434
self.firebaseInfo = firebaseInfo

FirebaseAI/Sources/GenerativeModel+GenerateObject.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,7 @@ public extension GenerativeModel {
3737
)
3838

3939
// Create a new model instance with the overridden config.
40-
let model = GenerativeModel(
41-
modelName: modelName,
42-
modelResourceName: modelResourceName,
43-
firebaseInfo: generativeAIService.firebaseInfo,
44-
apiConfig: apiConfig,
45-
generationConfig: newGenerationConfig,
46-
safetySettings: safetySettings,
47-
tools: tools,
48-
toolConfig: toolConfig,
49-
systemInstruction: systemInstruction,
50-
requestOptions: requestOptions
51-
)
40+
let model = GenerativeModel(copying: self, generationConfig: newGenerationConfig)
5241
let response = try await model.generateContent(prompt)
5342

5443
guard let text = response.text, let data = text.data(using: .utf8) else {

FirebaseAI/Sources/GenerativeModel.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ public final class GenerativeModel: Sendable {
118118
AILog.debug(code: .generativeModelInitialized, "Model \(modelResourceName) initialized.")
119119
}
120120

121+
/// Internal initializer to create a new model by copying an existing one with specific overrides.
122+
convenience init(copying other: GenerativeModel,
123+
generationConfig: GenerationConfig? = nil) {
124+
self.init(
125+
modelName: other.modelName,
126+
modelResourceName: other.modelResourceName,
127+
firebaseInfo: other.generativeAIService.firebaseInfo,
128+
apiConfig: other.apiConfig,
129+
generationConfig: generationConfig ?? other.generationConfig,
130+
safetySettings: other.safetySettings,
131+
tools: other.tools,
132+
toolConfig: other.toolConfig,
133+
systemInstruction: other.systemInstruction,
134+
requestOptions: other.requestOptions,
135+
urlSession: other.generativeAIService.urlSession
136+
)
137+
}
138+
121139
/// Generates content from String and/or image inputs, given to the model as a prompt, that are
122140
/// representable as one or more ``Part``s.
123141
///

FirebaseAI/Sources/Types/Public/Schema.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ public extension Schema {
515515
/// `userSchema.nullable()`.
516516
///
517517
/// - Returns: A new `Schema` instance with `nullable` set to `true`.
518-
func asNullable() -> Schema {
519-
return Schema(copying: self, nullable: true)
520-
}}
518+
func asNullable() -> Schema {
519+
return Schema(copying: self, nullable: true)
520+
}
521+
}

FirebaseAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,9 @@ struct GenerateContentIntegrationTests {
315315
"Caught GenerateContentError, but it was not a responseStoppedEarly error with maxTokens reason."
316316
)
317317
}
318-
} catch {}
318+
} catch {
319+
Issue.record("Function threw an unexpected error type: \(error)")
320+
}
319321
}
320322

321323
// A dessert with optional properties for testing nullable schema generation.

FirebaseAILogicMacros/Sources/FirebaseAILogicMacro/FirebaseGenerableMacro.swift

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,34 @@ public struct FirebaseGenerableMacro: MemberMacro, ExtensionMacro {
2424
-> [SwiftSyntax.DeclSyntax]
2525
where Declaration: SwiftSyntax.DeclGroupSyntax,
2626
Context: SwiftSyntaxMacros.MacroExpansionContext {
27-
let properties = declaration.memberBlock.members.compactMap {
28-
$0.decl.as(VariableDeclSyntax.self)
29-
}
27+
// 1. Get all variable declarations from the member block.
28+
let varDecls = declaration.memberBlock.members
29+
.compactMap { $0.decl.as(VariableDeclSyntax.self) }
3030

31-
let schemaProperties: [String] = try properties.map { property in
32-
let (name, type) = try property.toNameAndType()
33-
return try """
34-
"\(name)": \(schema(for: type))
35-
"""
31+
// 2. Process each declaration to extract schema properties from its bindings.
32+
let schemaProperties = try varDecls.flatMap { varDecl -> [String] in
33+
// For declarations like `let a, b: String`, the type annotation is on the last binding.
34+
let typeAnnotationFromDecl = varDecl.bindings.last?.typeAnnotation
35+
36+
return try varDecl.bindings.compactMap { binding -> String? in
37+
// 3. Filter out computed properties. Stored properties do not have a getter/setter block.
38+
guard binding.accessorBlock == nil else {
39+
return nil
40+
}
41+
42+
// 4. Get the property's name. Skip complex patterns like tuples.
43+
guard let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else {
44+
return nil
45+
}
46+
47+
// 5. Determine the property's type. It can be on the binding itself or on the declaration.
48+
guard let type = binding.typeAnnotation?.type ?? typeAnnotationFromDecl?.type else {
49+
throw MacroError.missingExplicitType(for: name)
50+
}
51+
52+
// 6. Generate the schema string for this property.
53+
return try "\"\(name)\": \(schema(for: type))"
54+
}
3655
}
3756

3857
return [
@@ -106,29 +125,16 @@ public struct FirebaseGenerableMacro: MemberMacro, ExtensionMacro {
106125
}
107126
}
108127

109-
private extension VariableDeclSyntax {
110-
func toNameAndType() throws -> (String, TypeSyntax) {
111-
guard let binding = bindings.first else {
112-
throw MacroError.unsupportedType(Syntax(self))
113-
}
114-
guard let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else {
115-
throw MacroError.unsupportedType(Syntax(self))
116-
}
117-
guard let type = binding.typeAnnotation?.type else {
118-
throw MacroError.unsupportedType(Syntax(self))
119-
}
120-
121-
return (name, type)
122-
}
123-
}
124-
125128
private enum MacroError: Error, CustomStringConvertible {
126129
case unsupportedType(Syntax)
130+
case missingExplicitType(for: String)
127131

128132
var description: String {
129133
switch self {
130-
case let .unsupportedType(type):
131-
return "Unsupported type: \(type)"
134+
case let .unsupportedType(syntax):
135+
return "Unsupported type syntax: \(syntax)"
136+
case let .missingExplicitType(name):
137+
return "Property '\(name)' must have an explicit type annotation to be used with @FirebaseGenerable."
132138
}
133139
}
134140
}

FirebaseAILogicMacros/Tests/FirebaseAILogicMacroTests/FirebaseAILogicMacroTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,34 @@ final class FirebaseGenerableMacroTests: XCTestCase {
4747
extension MyDessert: FirebaseAILogic.FirebaseGenerable {}
4848
"""
4949

50+
assertMacroExpansion(originalSource, expandedSource: expectedSource, macros: macros)
51+
func testExpansion_handlesMultiBindingAndComputedProperties() {
52+
let originalSource = """
53+
@FirebaseGenerable
54+
struct MyType: Decodable {
55+
let a, b: String
56+
let c: Int
57+
var isComputed: Bool { return true }
58+
}
59+
"""
60+
61+
let expectedSource = """
62+
struct MyType: Decodable {
63+
let a, b: String
64+
let c: Int
65+
var isComputed: Bool { return true }
66+
public static var firebaseGenerationSchema: FirebaseAILogic.Schema {
67+
.object(properties: [
68+
"a": FirebaseAILogic.Schema.string(),
69+
"b": FirebaseAILogic.Schema.string(),
70+
"c": FirebaseAILogic.Schema.integer()
71+
])
72+
}
73+
}
74+
75+
extension MyType: FirebaseAILogic.FirebaseGenerable {}
76+
"""
77+
5078
assertMacroExpansion(originalSource, expandedSource: expectedSource, macros: macros)
5179
}
5280
}

0 commit comments

Comments
 (0)