Skip to content

Commit

Permalink
add basic @routes
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuawright11 committed May 24, 2024
1 parent 4171572 commit 79d1b8c
Show file tree
Hide file tree
Showing 17 changed files with 639 additions and 401 deletions.
35 changes: 34 additions & 1 deletion PapyrusCore/Sources/Interceptors/CurlInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public struct CurlLogger {
}

extension CurlLogger: Interceptor {
public func intercept(req: any Request, next: (any Request) async throws -> any Response) async throws -> any Response {
public func intercept(req: Request, next: Next) async throws -> Response {
if condition == .always {
logHandler(req.curl(sortedHeaders: true))
}
Expand All @@ -39,3 +39,36 @@ extension CurlLogger: Interceptor {
}
}
}

public extension Request {
/// Create a cURL command from this instance
///
/// - Parameter sortedHeaders: sort headers in output
/// - Returns: cURL Command
func curl(sortedHeaders: Bool = false) -> String {
let lineSeparator = " \\\n"
var components = [String]()

// Add URL on same line
if let url {
components.append("curl '\(url.absoluteString)'")
} else {
components.append("curl")
}

// Add method
components.append("-X \(method)")

// Add headers
let headerOptions = headers.map { "-H '\($0): \($1)'" }
components += sortedHeaders ? headerOptions.sorted() : headerOptions

// Add body
if let body {
let bodyString = String(data: body, encoding: .utf8) ?? ""
components.append("-d '\(bodyString)'")
}

return components.joined(separator: lineSeparator)
}
}
4 changes: 2 additions & 2 deletions PapyrusCore/Sources/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import Foundation
public macro API() = #externalMacro(module: "PapyrusPlugin", type: "APIMacro")

@attached(extension, names: named(useAPI))
@attached(peer, names: suffixed(Live), suffixed(Registry))
public macro APIRoutes() = #externalMacro(module: "PapyrusPlugin", type: "APIRoutesMacro")
@attached(peer, names: suffixed(Live), suffixed(Routes))
public macro Routes() = #externalMacro(module: "PapyrusPlugin", type: "RoutesMacro")

@attached(peer, names: suffixed(Mock))
public macro Mock() = #externalMacro(module: "PapyrusPlugin", type: "MockMacro")
Expand Down
8 changes: 7 additions & 1 deletion PapyrusCore/Sources/PapyrusError.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// A Papyrus related error.
public struct PapyrusError: Error {
public struct PapyrusError: Error, CustomDebugStringConvertible {
/// What went wrong.
public let message: String
/// Error related request.
Expand All @@ -17,4 +17,10 @@ public struct PapyrusError: Error {
self.request = request
self.response = response
}

// MARK: CustomDebugStringConvertible

public var debugDescription: String {
"PapyrusError: \(message)"
}
}
107 changes: 107 additions & 0 deletions PapyrusCore/Sources/PapyrusRouter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Foundation

public protocol PapyrusRouter {
func register(
method: String,
path: String,
action: @escaping (RouterRequest) async throws -> RouterResponse
)
}

public struct RouterRequest {
public let url: URL
public let method: String
public let headers: [String: String]
public let body: Data?

public init(url: URL, method: String, headers: [String : String], body: Data?) {
self.url = url
self.method = method
self.headers = headers
self.body = body
}

public func getQuery<L: LosslessStringConvertible>(_ name: String) throws -> L {
guard let parameters = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
throw PapyrusError("unable to parse url components for url `\(url)`")
}

guard let item = parameters.queryItems?.first(where: { $0.name == name }) else {
throw PapyrusError("no query item found for `\(name)`")
}

guard let string = item.value, let value = L(string) else {
throw PapyrusError("query `\(item.name)` was not convertible to `\(L.self)`")
}

return value
}

public func getBody<D: Decodable>(_ type: D.Type) throws -> D {
guard let body else {
throw PapyrusError("expected request body")
}

let decoder = JSONDecoder()
return try decoder.decode(type, from: body)
}

public func getHeader<L: LosslessStringConvertible>(_ name: String) throws -> L {
guard let string = headers[name] else {
throw PapyrusError("missing header `\(name)`")
}

guard let value = L(string) else {
throw PapyrusError("header `\(name)` was not convertible to `\(L.self)`")
}

return value
}

public func getParameter<L: LosslessStringConvertible>(_ name: String, path: String) throws -> L {
let templatePathComponents = path.components(separatedBy: "/")
let requestPathComponents = url.pathComponents
let parametersByName = [String: String](
zip(templatePathComponents, requestPathComponents)
.compactMap {
guard let parameter = $0.extractParameter else { return nil }
return (parameter, $1)
},
uniquingKeysWith: { a, _ in a }
)

guard let string = parametersByName[name] else {
throw PapyrusError("parameter `\(name)` not found")
}

guard let value = L(string) else {
throw PapyrusError("parameter `\(name)` was not convertible to `\(L.self)`")
}

return value
}
}

public struct RouterResponse {
public let status: Int
public let headers: [String: String]
public let body: Data?

public init(_ status: Int, headers: [String: String] = [:], body: Data? = nil) {
self.status = status
self.headers = headers
self.body = body
}
}

extension String {
fileprivate var extractParameter: String? {
if hasPrefix(":") {
String(dropFirst())
} else if hasPrefix("{") && hasSuffix("}") {
String(dropFirst().dropLast())
} else {
nil
}
}
}
33 changes: 0 additions & 33 deletions PapyrusCore/Sources/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,3 @@ public protocol Request {
var headers: [String: String] { get set }
var body: Data? { get set }
}

public extension Request {
/// Create a cURL command from this instance
///
/// - Parameter sortedHeaders: sort headers in output
/// - Returns: cURL Command
func curl(sortedHeaders: Bool = false) -> String {
let lineSeparator = " \\\n"
var components = [String]()

// Add URL on same line
if let url {
components.append("curl '\(url.absoluteString)'")
} else {
components.append("curl")
}

// Add method
components.append("-X \(method)")

// Add headers
let headerOptions = headers.map { "-H '\($0): \($1)'" }
components += sortedHeaders ? headerOptions.sorted() : headerOptions

// Add body
if let body {
let bodyString = String(data: body, encoding: .utf8) ?? ""
components.append("-d '\(bodyString)'")
}

return components.joined(separator: lineSeparator)
}
}
18 changes: 18 additions & 0 deletions PapyrusPlugin/Sources/Extensions/String+Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,26 @@ extension String {
"\"\(self)\""
}

var inParentheses: String {
"(\(self))"
}

// Need this since `capitalized` lowercases everything else.
var capitalizeFirst: String {
prefix(1).capitalized + dropFirst()
}

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

private var extractParameter: String? {
if hasPrefix(":") {
String(dropFirst())
} else if hasPrefix("{") && hasSuffix("}") {
String(dropFirst().dropLast())
} else {
nil
}
}
}
53 changes: 26 additions & 27 deletions PapyrusPlugin/Sources/Extensions/SwiftSyntax+Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ extension ProtocolDeclSyntax {
name.text
}

var access: String {
modifiers.first.map { "\($0.trimmedDescription) " } ?? ""
var access: String? {
modifiers.first?.trimmedDescription
}

var functions: [FunctionDeclSyntax] {
Expand All @@ -21,10 +21,6 @@ extension ProtocolDeclSyntax {
}

extension FunctionDeclSyntax {
enum ReturnType {
case tuple([(label: String?, type: String)])
case type(String)
}

// MARK: Function effects & attributes

Expand All @@ -51,31 +47,28 @@ extension FunctionDeclSyntax {

// MARK: Return Data

var returnResponseOnly: Bool {
if case .type("Response") = returnType {
return true
} else {
return false
}
var returnsResponse: Bool {
returnType == "Response"
}

var returnType: ReturnType? {
guard let type = signature.returnClause?.type else {
return nil
}
var returnType: String? {
signature.returnClause?.type.trimmedDescription
}

if let type = type.as(TupleTypeSyntax.self) {
return .tuple(
type.elements
.map { (label: $0.firstName?.text, type: $0.type.trimmedDescription) }
)
} else {
return .type(type.trimmedDescription)
var returnsVoid: Bool {
guard let returnType else {
return true
}

return returnType == "Void"
}
}

extension FunctionParameterSyntax {
var label: String? {
secondName != nil ? firstName.text : nil
}

var name: String {
(secondName ?? firstName).text
}
Expand All @@ -86,11 +79,17 @@ extension FunctionParameterSyntax {
}

extension AttributeSyntax {
var firstArgument: String? {
if case let .argumentList(list) = arguments {
return list.first?.expression.description.withoutQuotes
var name: String {
attributeName.trimmedDescription
}

var labeledArguments: [(label: String?, value: String)] {
guard case let .argumentList(list) = arguments else {
return []
}

return nil
return list.map {
($0.label?.text, $0.expression.description)
}
}
}

0 comments on commit 79d1b8c

Please sign in to comment.