-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b1ec1aa
commit 05be476
Showing
12 changed files
with
256 additions
and
303 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) }) | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} | ||
} |
Oops, something went wrong.