Skip to content

Commit

Permalink
cleanup macros
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuawright11 committed Jun 23, 2024
1 parent b1ec1aa commit 05be476
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 303 deletions.
11 changes: 10 additions & 1 deletion AlchemyPlugin/Sources/AlchemyPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ import SwiftSyntaxMacros
@main
struct AlchemyPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [

// MARK: Jobs

JobMacro.self,

// MARK: Rune

ModelMacro.self,
IDMacro.self,
RelationshipMacro.self,

// MARK: Routing

ApplicationMacro.self,
ControllerMacro.self,
HTTPMethodMacro.self,
RelationshipMacro.self,
]
}

Expand Down
83 changes: 83 additions & 0 deletions AlchemyPlugin/Sources/Helpers/Endpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import SwiftSyntax

struct Endpoint {
/// Attributes to be applied to this endpoint. These take precedence
/// over attributes at the API scope.
let method: String
let path: String
let pathParameters: [String]
let options: String?
/// The name of the function defining this endpoint.
let name: String
let parameters: [EndpointParameter]
let isAsync: Bool
let isThrows: Bool
let responseType: String?
}

extension Endpoint {
static func parse(_ function: FunctionDeclSyntax) throws -> Endpoint? {
guard let (method, path, pathParameters, options) = parseMethodAndPath(function) else {
return nil
}

return Endpoint(
method: method,
path: path,
pathParameters: pathParameters,
options: options,
name: function.functionName,
parameters: function.parameters.compactMap {
EndpointParameter($0, httpMethod: method, pathParameters: pathParameters)
},
isAsync: function.isAsync,
isThrows: function.isThrows,
responseType: function.returnType
)
}

private static func parseMethodAndPath(
_ function: FunctionDeclSyntax
) -> (method: String, path: String, pathParameters: [String], options: String?)? {
var method, path, options: String?
for attribute in function.functionAttributes {
if case let .argumentList(list) = attribute.arguments {
let name = attribute.attributeName.trimmedDescription
switch name {
case "GET", "DELETE", "PATCH", "POST", "PUT", "OPTIONS", "HEAD", "TRACE", "CONNECT":
method = name
path = list.first?.expression.description.withoutQuotes
options = list.dropFirst().first?.expression.description.withoutQuotes
case "HTTP":
method = list.first.map { "RAW(value: \($0.expression.description))" }
path = list.dropFirst().first?.expression.description.withoutQuotes
options = list.dropFirst().dropFirst().first?.expression.description.withoutQuotes
default:
continue
}
}
}

guard let method, let path else {
return nil
}

return (method, path, path.pathParameters, options)
}
}

extension String {
fileprivate var pathParameters: [String] {
components(separatedBy: "/").compactMap(\.extractParameter)
}

private var extractParameter: String? {
if hasPrefix(":") {
String(dropFirst())
} else if hasPrefix("{") && hasSuffix("}") {
String(dropFirst().dropLast())
} else {
nil
}
}
}
22 changes: 22 additions & 0 deletions AlchemyPlugin/Sources/Helpers/EndpointGroup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation
import SwiftSyntax

struct EndpointGroup {
/// The name of the type defining the API.
let name: String
/// Attributes to be applied to every endpoint of this API.
let endpoints: [Endpoint]
}

extension EndpointGroup {
static func parse(_ decl: some DeclSyntaxProtocol) throws -> EndpointGroup {
guard let type = decl.as(StructDeclSyntax.self) else {
throw AlchemyMacroError("@Routes must be applied to structs for now")
}

return EndpointGroup(
name: type.name.text,
endpoints: try type.functions.compactMap( { try Endpoint.parse($0) })
)
}
}
41 changes: 41 additions & 0 deletions AlchemyPlugin/Sources/Helpers/EndpointParameter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftSyntax

/// Parsed from function parameters; indicates parts of the request.
struct EndpointParameter {
enum Kind {
case body
case field
case query
case header
case path
}

let label: String?
let name: String
let type: String
let kind: Kind
let validation: String?

init(_ parameter: FunctionParameterSyntax, httpMethod: String, pathParameters: [String]) {
self.label = parameter.label
self.name = parameter.name
self.type = parameter.typeName
self.validation = parameter.parameterAttributes
.first { $0.name == "Validate" }
.map { $0.trimmedDescription }

let attributeNames = parameter.parameterAttributes.map(\.name)
self.kind =
if attributeNames.contains("Path") { .path }
else if attributeNames.contains("Body") { .body }
else if attributeNames.contains("Header") { .header }
else if attributeNames.contains("Field") { .field }
else if attributeNames.contains("URLQuery") { .query }
// if name matches a path param, infer this belongs in path
else if pathParameters.contains(name) { .path }
// if method is GET, HEAD, DELETE, infer query
else if ["GET", "HEAD", "DELETE"].contains(httpMethod) { .query }
// otherwise infer it's a body field
else { .field }
}
}
Loading

0 comments on commit 05be476

Please sign in to comment.