Skip to content

Commit

Permalink
feat: implement curl interceptor
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahKamara committed Mar 13, 2024
1 parent 5538cd0 commit 23d596f
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 117 deletions.
41 changes: 41 additions & 0 deletions PapyrusCore/Sources/Interceptors/CurlInterceptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Foundation

/// An `Interceptor` that logs requests based on a condition
public struct CurlLogger {
public enum Condition {
case always

/// only log when the request encountered an error
case onError
}

let loggingFn: (String) -> Void
let condition: Condition

/// An `Interceptor` that calls a loggingFn with a request based on a condition
/// - Parameters:
/// - condition: must be met for the logging function to be called
/// - loggingFn: a function that implements logging. defaults to `print()`
public init(when condition: Condition, using loggingFn: @escaping (String) -> Void = { print($0) }) {
self.condition = condition
self.loggingFn = loggingFn
}
}

extension CurlLogger: Interceptor {
public func intercept(req: any Request, next: (any Request) async throws -> any Response) async throws -> any Response {
if condition == .always {
loggingFn(req.curl(sortedHeaders: true))
}

do {
let res = try await next(req)
return res
} catch {
if condition == .onError {
loggingFn(req.curl(sortedHeaders: true))
}
throw error
}
}
}
117 changes: 0 additions & 117 deletions PapyrusCore/Tests/CurlConvertingTests.swift

This file was deleted.

224 changes: 224 additions & 0 deletions PapyrusCore/Tests/CurlTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import XCTest
@testable import PapyrusCore


final class CurlTests: XCTestCase {
func testConvertPath() throws {
let req = RequestBuilder(baseURL: "foo/", method: "bar", path: "baz")

let request = try TestRequest(from: req)

// Assert Multi Line
XCTAssertEqual(request.curl(sortedHeaders: true), """
curl 'foo/baz' \\
-X bar \\
-H 'Content-Length: 0' \\
-H 'Content-Type: application/json'
""")
}

func testConvertHeaders() async throws {
var req = RequestBuilder(baseURL: "foo/", method: "GET", path: "/baz")
req.addHeader("Hello", value: "There")
req.addHeader("High", value: "Ground")

let request = try TestRequest(from: req)

let normalizedCurl = request.curl(sortedHeaders: true)

XCTAssertEqual(normalizedCurl, """
curl 'foo/baz' \\
-X GET \\
-H 'Content-Length: 0' \\
-H 'Content-Type: application/json' \\
-H 'Hello: There' \\
-H 'High: Ground'
""")
}

func testConvertMultipart() throws {
var req = RequestBuilder(baseURL: "foo/", method: "bar", path: "/baz")
let encoder = MultipartEncoder(boundary: UUID().uuidString)
req.requestEncoder = encoder
req.addField("a", value: Part(data: Data("one".utf8), fileName: "one.txt", mimeType: "text/plain"))
req.addField("b", value: Part(data: Data("two".utf8)))

let request = try TestRequest(from: req)

XCTAssertEqual(request.curl(sortedHeaders: true), """
curl 'foo/baz' \\
-X bar \\
-H 'Content-Length: 266' \\
-H 'Content-Type: multipart/form-data; boundary=\(encoder.boundary)' \\
-d '--\(encoder.boundary)\r
Content-Disposition: form-data; name="a"; filename="one.txt"\r
Content-Type: text/plain\r
\r
one\r
--\(encoder.boundary)\r
Content-Disposition: form-data; name="b"\r
\r
two\r
--\(encoder.boundary)--\r
'
""")
}

func testConvertJSON() async throws {
var req = RequestBuilder(baseURL: "foo/", method: "bar", path: "/baz")
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
req.requestEncoder = encoder
req.addField("a", value: "one")
req.addField("b", value: "two")

let request = try TestRequest(from: req)

let s = """
curl 'foo/baz' \\
-X bar \\
-H 'Content-Length: 32' \\
-H 'Content-Type: application/json' \\
-d '{
"a" : "one",
"b" : "two"
}'
"""

XCTAssertEqual(request.curl(sortedHeaders: true), s)
}

func testConvertURLForm() async throws {
var req = RequestBuilder(baseURL: "foo/", method: "bar", path: "/baz")
req.requestEncoder = URLEncodedFormEncoder()
req.addField("a", value: "one")
req.addField("b", value: "two")

let request = try TestRequest(from: req)

let normalizedCurl = request.curl(sortedHeaders: true)
.replacingOccurrences(of: "b=two&a=one", with: "a=one&b=two")

XCTAssertEqual(normalizedCurl, """
curl 'foo/baz' \\
-X bar \\
-H 'Content-Length: 11' \\
-H 'Content-Type: application/x-www-form-urlencoded' \\
-d 'a=one&b=two'
""")
}

func testInterceptorAlways() async throws {
var req = RequestBuilder(baseURL: "foo/", method: "GET", path: "/baz")
req.addQuery("Hello", value: "There")
req.addHeader("High", value: "Ground")
let request = try TestRequest(from: req)

var message: String? = nil

let logger = CurlLogger(when: .always, using: {
message = $0
})

_ = try await logger.intercept(req: request) { req in
return TestResponse(request: req)
}

XCTAssertNotNil(message, "Logger did not output")

guard let message else { return }

XCTAssertEqual(message, """
curl 'foo/baz?Hello=There' \\
-X GET \\
-H 'Content-Length: 0' \\
-H 'Content-Type: application/json' \\
-H 'High: Ground'
""")
}

func testInterceptorOnError() async throws {
var req = RequestBuilder(baseURL: "foo/", method: "GET", path: "/baz")
req.addQuery("Hello", value: "There")
req.addHeader("High", value: "Ground")
let request = try TestRequest(from: req)

var message: String? = nil

let logger = CurlLogger(when: .onError, using: {
print("WHAT?")
message = $0
})

_ = try? await logger.intercept(req: request) { req in
throw PapyrusError("")
}

XCTAssertNotNil(message, "Logger did not output")
guard let message else { return }

XCTAssertEqual(message, """
curl 'foo/baz?Hello=There' \\
-X GET \\
-H 'Content-Length: 0' \\
-H 'Content-Type: application/json' \\
-H 'High: Ground'
""")
}

func testInterceptorOnErrorNoError() async throws {
var req = RequestBuilder(baseURL: "foo/", method: "GET", path: "/baz")
req.addQuery("Hello", value: "There")
req.addHeader("High", value: "Ground")

let request = try TestRequest(from: req)

let normalizedCurl = request.curl(sortedHeaders: true)

var message: String? = nil

let logger = CurlLogger(when: .onError, using: {
message = $0
})

_ = try await logger.intercept(req: request) { req in
return TestResponse(request: req)
}

XCTAssertNil(message, "Logger did output")
}
}

private struct TestResponse: Response {
var request: Request? = nil
var body: Data? = nil
var headers: [String : String]? = nil
var statusCode: Int? = nil
var error: Error? = nil
}

private struct TestRequest: Request {
var method: String
var url: URL?
var headers: [String : String]
var body: Data?

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

init(from builder: RequestBuilder) throws {
let url = try builder.fullURL()
let (body, headers) = try builder.bodyAndHeaders()

self.init(
method: builder.method,
url: url,
headers: headers,
body: body
)
}
}

0 comments on commit 23d596f

Please sign in to comment.