Skip to content

Commit fa0494d

Browse files
committed
Generate through swift macro
1 parent 7b3b894 commit fa0494d

17 files changed

Lines changed: 499 additions & 240 deletions

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ let package = Package(
1919
),
2020
],
2121
dependencies: [
22-
.package(url: "https://github.com/apple/swift-syntax.git", from: "600.0.1"),
22+
.package(url: "https://github.com/apple/swift-syntax.git", from: "601.0.1"),
2323
.package(url: "https://github.com/apple/swift-collections", from: "1.1.4"),
2424
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
2525
],

Sources/Compiler/Compiler.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,39 @@ public struct Compiler {
4646
return diagnostics
4747
}
4848

49+
public mutating func compile(
50+
query: String,
51+
named name: String,
52+
inputType: String?,
53+
outputType: String?,
54+
namespace: Namespace
55+
) -> Diagnostics {
56+
var (stmts, diagnostics) = compile(
57+
source: query,
58+
validator: IsValidForQueries(),
59+
context: "queries"
60+
)
61+
62+
guard let stmt = stmts.first else {
63+
let loc = SourceLocation(range: query.startIndex..<query.endIndex, line: 0, column: 0)
64+
diagnostics.add(.init("Query has no statements", at: loc))
65+
self.diagnostics[namespace] = diagnostics
66+
return diagnostics
67+
}
68+
69+
let stmtWithDef = stmt.with(
70+
definition: Definition(
71+
name: name[...],
72+
input: inputType?[...],
73+
output: outputType?[...]
74+
)
75+
)
76+
77+
self.queries.append(stmtWithDef)
78+
self.diagnostics[namespace] = diagnostics
79+
return diagnostics
80+
}
81+
4982
mutating func compile<Validator>(
5083
source: String,
5184
validator: Validator,

Sources/Compiler/Diagnostic.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ public struct Diagnostic: Error {
4141
}
4242
}
4343

44+
extension Diagnostic: CustomStringConvertible {
45+
public var description: String {
46+
return message
47+
}
48+
}
49+
4450
extension Diagnostic {
4551
static func incorrectType(
4652
_ actual: TypeNameSyntax,

Sources/Compiler/Diagnostics.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,13 @@ public struct Diagnostics {
5656
}
5757

5858
extension Diagnostics: Sequence {
59-
public func makeIterator() -> some IteratorProtocol {
59+
public func makeIterator() -> [Diagnostic].Iterator {
6060
return elements.makeIterator()
6161
}
6262
}
63+
64+
extension Diagnostics: ExpressibleByArrayLiteral {
65+
public init(arrayLiteral elements: Diagnostic...) {
66+
self = Diagnostics(diagnostics: elements)
67+
}
68+
}

Sources/Compiler/Gen/Language.swift

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,6 @@ public protocol Language {
1313
/// Returns the Language builtin for the given SQL type
1414
static func builtinType(for type: Type) -> String
1515

16-
/// The query type name with the given cardinality, input and output
17-
static func queryType(
18-
for cardinality: Cardinality?,
19-
input: BuiltinOrGenerated?,
20-
output: BuiltinOrGenerated?
21-
) -> String
22-
2316
/// A file source code containing all of the generated tables, queries and migrations.
2417
static func file(
2518
databaseName: String,
@@ -48,18 +41,29 @@ extension Language {
4841
schema: Schema,
4942
options: GenerationOptions
5043
) throws -> String {
51-
let tables = schema.mapValues(model(for:))
52-
let queries = queries.map { query(for: $0, tables: tables) }
44+
let values = try assemble(queries: queries, schema: schema)
5345

5446
return try file(
5547
databaseName: databaseName,
5648
migrations: migrations,
57-
tables: Array(tables.values),
58-
queries: queries,
49+
tables: values.tables,
50+
queries: values.queries,
5951
options: options
6052
)
6153
}
6254

55+
public static func assemble(
56+
queries: [Statement],
57+
schema: Schema
58+
) throws -> (
59+
tables: [GeneratedModel],
60+
queries: [GeneratedQuery]
61+
) {
62+
let tables = schema.mapValues(model(for:))
63+
let queries = queries.map { query(for: $0, tables: tables) }
64+
return (Array(tables.values), queries)
65+
}
66+
6367
private static func query(
6468
for statement: Statement,
6569
tables: OrderedDictionary<Substring, GeneratedModel>
@@ -71,12 +75,6 @@ extension Language {
7175
let input = inputTypeIfNeeded(statement: statement, definition: definition)
7276
let output = outputTypeIfNeeded(statement: statement, definition: definition, tables: tables)
7377

74-
let type = queryType(
75-
for: statement.noOutput ? nil : statement.outputCardinality,
76-
input: input,
77-
output: output
78-
)
79-
8078
// Join the source segments together inserting the code to assemble the
8179
// question marks for any input.
8280
let sql = statement.sourceSegments.map { segment in
@@ -92,7 +90,6 @@ extension Language {
9290

9391
return GeneratedQuery(
9492
name: "\(definition.name)Query",
95-
type: type,
9693
input: input,
9794
output: output,
9895
outputCardinality: statement.outputCardinality,
@@ -260,7 +257,6 @@ public struct GeneratedField {
260257

261258
public struct GeneratedQuery {
262259
let name: String
263-
let type: String
264260
let input: BuiltinOrGenerated?
265261
let output: BuiltinOrGenerated?
266262
let outputCardinality: Cardinality

Sources/Compiler/Gen/SwiftLanguage.swift

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,37 @@ public struct SwiftLanguage: Language {
8282
return file.formatted().description
8383
}
8484

85+
public static func macro(
86+
databaseName: String,
87+
tables: [GeneratedModel],
88+
queries: [GeneratedQuery],
89+
options: GenerationOptions,
90+
addConnection: Bool
91+
) throws -> [DeclSyntax] {
92+
var decls: [DeclSyntax] = []
93+
94+
if addConnection {
95+
decls.append("let connection: any Feather.Connection")
96+
}
97+
98+
for table in tables {
99+
try decls.append(declaration(for: table, isOutput: true, options: options))
100+
}
101+
102+
// Always do this at the top level since it will automatically namespaced under the
103+
// struct that the macro is attached too.
104+
for query in queries {
105+
try decls.append(contentsOf: modelsFor(query: query, options: options))
106+
try decls.append(declaration(for: query, underscoreName: true, databaseName: databaseName, options: options))
107+
try decls.append(DeclSyntax(dbTypealiasFor(query: query, databaseName: databaseName, options: options)))
108+
try decls.append(DeclSyntax(typealiasFor(query: query, databaseName: databaseName, options: options)))
109+
}
110+
111+
// TODO: Generate extensions if this can be done.
112+
113+
return decls
114+
}
115+
85116
private static func modelsFor(
86117
query: GeneratedQuery,
87118
options: GenerationOptions
@@ -98,22 +129,7 @@ public struct SwiftLanguage: Language {
98129

99130
return decls
100131
}
101-
102-
public static func queryType(
103-
for cardinality: Cardinality?,
104-
input: BuiltinOrGenerated?,
105-
output: BuiltinOrGenerated?
106-
) -> String {
107-
let input = input?.description ?? "()"
108-
let output = output?.description ?? "()"
109-
110-
return switch cardinality {
111-
case .single: "FetchSingleQuery<\(input), \(output)>"
112-
case .many: "FetchManyQuery<\(input), \(output)>"
113-
default: "VoidQuery<\(input)>"
114-
}
115-
}
116-
132+
117133
private static func declaration(
118134
for migrations: [String],
119135
options: GenerationOptions
@@ -132,14 +148,16 @@ public struct SwiftLanguage: Language {
132148

133149
private static func declaration(
134150
for query: GeneratedQuery,
151+
underscoreName: Bool = false,
135152
databaseName: String,
136153
options: GenerationOptions
137154
) throws -> DeclSyntax {
138155
let inputTypeName = inputTypeName(for: query, databaseName: databaseName)
139156
let outputTypeName = outputTypeName(for: query, databaseName: databaseName)
140157
let queryTypeName = "AnyDatabaseQuery<\(inputTypeName), \(outputTypeName)>"
158+
let variableName = underscoreName ? "_\(query.name)" : query.name
141159

142-
let query = try VariableDeclSyntax("var \(raw: query.name): \(raw: queryTypeName)") {
160+
let query = try VariableDeclSyntax("var \(raw: variableName): \(raw: queryTypeName)") {
143161
FunctionCallExprSyntax(
144162
calledExpression: DeclReferenceExprSyntax(
145163
baseName: .identifier("AnyDatabaseQuery<\(inputTypeName), \(outputTypeName)>")
@@ -215,6 +233,20 @@ public struct SwiftLanguage: Language {
215233
)
216234
}
217235

236+
private static func dbTypealiasFor(
237+
query: GeneratedQuery,
238+
databaseName: String,
239+
options: GenerationOptions
240+
) throws -> TypeAliasDeclSyntax {
241+
let namespace = options.contains(.namespaceGeneratedModels)
242+
let inputTypeName = inputTypeName(for: query, namespaced: namespace, databaseName: databaseName)
243+
let outputTypeName = outputTypeName(for: query, namespaced: namespace, databaseName: databaseName)
244+
let name = query.name.capitalizedFirst.replacingOccurrences(of: "Query", with: "DatabaseQuery")
245+
return try TypeAliasDeclSyntax(
246+
"typealias \(raw: name) = AnyDatabaseQuery<\(raw: inputTypeName), \(raw: outputTypeName)>"
247+
)
248+
}
249+
218250
private static func inputExtension(
219251
for query: GeneratedQuery,
220252
input: GeneratedModel,

Sources/Feather/Macros.swift

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
2-
@freestanding(declaration, names: arbitrary)
3-
public macro schema(_ source: [String: String]) = #externalMacro(module: "FeatherMacros", type: "SchemaMacro")
4-
5-
@freestanding(expression)
6-
public macro query(_ source: String) = #externalMacro(module: "FeatherMacros", type: "QueryMacro")
7-
81
@attached(member, names: arbitrary)
9-
public macro Schema() = #externalMacro(module: "FeatherMacros", type: "DatabaseMacro")
10-
11-
public protocol Schema {
12-
static var queries: [String] { get }
13-
static var migrations: [String] { get }
14-
}
2+
@attached(extension, conformances: Database)
3+
public macro Database() = #externalMacro(module: "FeatherMacros", type: "DatabaseMacro")
4+
5+
@attached(accessor)
6+
public macro Query(
7+
_ source: String,
8+
inputName: String? = nil,
9+
oututName: String? = nil
10+
) = #externalMacro(module: "FeatherMacros", type: "QueryMacro")
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//
2+
// DatabaseMacro.swift
3+
// Feather
4+
//
5+
// Created by Wes Wickwire on 5/10/25.
6+
//
7+
8+
import Compiler
9+
import SwiftCompilerPlugin
10+
import SwiftDiagnostics
11+
import SwiftSyntax
12+
import SwiftSyntaxBuilder
13+
import SwiftSyntaxMacros
14+
15+
public struct DatabaseMacro {}
16+
17+
extension DatabaseMacro: MemberMacro {
18+
public static func expansion(
19+
of node: AttributeSyntax,
20+
providingMembersOf declaration: some DeclGroupSyntax,
21+
conformingTo protocols: [TypeSyntax],
22+
in context: some MacroExpansionContext
23+
) throws -> [DeclSyntax] {
24+
guard let structDecl = declaration.as(StructDeclSyntax.self) else {
25+
context.addDiagnostics(from: SyntaxError("@Database can only be applied to a struct"), node: declaration)
26+
return []
27+
}
28+
29+
let variables = declaration.memberBlock.variableDecls()
30+
31+
guard let migrations = variables["migrations"]?.asMigrationsArray(in: context) else {
32+
context.addDiagnostics(from: SyntaxError("Unable to resolve migrations"), node: node)
33+
return []
34+
}
35+
36+
var compiler = Compiler()
37+
38+
for (migration, expr) in migrations {
39+
for diag in compiler.compile(migration: migration, namespace: .global) {
40+
context.addDiagnostics(from: diag, node: expr)
41+
}
42+
}
43+
44+
for (name, variable) in variables {
45+
guard let queryMacro = variable.queryMacroInputsIfIsQuery(in: context) else { continue }
46+
47+
for diag in compiler.compile(
48+
query: queryMacro.source,
49+
named: name.removingQuerySuffix(),
50+
inputType: queryMacro.inputName,
51+
outputType: queryMacro.outputName,
52+
namespace: .global
53+
) {
54+
context.addDiagnostics(from: diag, node: variable)
55+
}
56+
}
57+
58+
let (generatedTables, generatedQueries) = try SwiftLanguage.assemble(
59+
queries: compiler.queries,
60+
schema: compiler.schema
61+
)
62+
63+
return try SwiftLanguage.macro(
64+
databaseName: structDecl.name.text,
65+
tables: generatedTables,
66+
queries: generatedQueries,
67+
options: [],
68+
addConnection: variables["connection"] == nil
69+
)
70+
}
71+
}
72+
73+
extension DatabaseMacro: ExtensionMacro {
74+
public static func expansion(
75+
of node: AttributeSyntax,
76+
attachedTo declaration: some DeclGroupSyntax,
77+
providingExtensionsOf type: some TypeSyntaxProtocol,
78+
conformingTo protocols: [TypeSyntax],
79+
in context: some MacroExpansionContext
80+
) throws -> [ExtensionDeclSyntax] {
81+
guard !protocols.isEmpty else { return [] }
82+
83+
let decl: DeclSyntax = """
84+
extension \(raw: type.trimmedDescription): Feather.Database {}
85+
"""
86+
return [decl.cast(ExtensionDeclSyntax.self)]
87+
}
88+
}

0 commit comments

Comments
 (0)