Skip to content

Commit

Permalink
0.9.0 - bug fixes and verbose function added
Browse files Browse the repository at this point in the history
  • Loading branch information
Maciej Burdzicki committed Dec 11, 2024
1 parent 888b851 commit 45ec6d8
Show file tree
Hide file tree
Showing 9 changed files with 70 additions and 97 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Meet **Snowdrop** - type-safe, easy to use framework powered by Swift Macros cre
- [Interceptors](#interceptors)
- [Mockable](#mockable)
- [JSON Injection](#json-injection)
- [Verbose](#verbose)
- [Acknowledgements](#acknowledgements)

## Installation
Expand Down Expand Up @@ -219,11 +220,11 @@ func testEmptyArrayResult() async throws {

### JSON Injection

If you'd like to test your service against mocked JSONs, you can easily do it. Just make sure you got your JSON mock somewhere in your project files, then instantiate your service with `testMode` flag set to `true` and determine for which request your mock should be injected like in the example below.
If you'd like to test your service against mocked JSONs, you can easily do it. Just make sure you got your JSON mock somewhere in your project files, then instantiate your service and determine for which request your mock should be injected like in the example below.

```Swift
func testJSONMockInjectsion() async throws {
let service = MyEndpointService(baseUrl: someBaseURL, testMode: true)
let service = MyEndpointService(baseUrl: someBaseURL)
service.testJSONDictionary = ["users/123/info": "MyJSONMock"]

let result = try await service.getUserInfo(id: 123)
Expand All @@ -232,6 +233,14 @@ func testJSONMockInjectsion() async throws {
}
```

### Verbose

If you'd like to get see logs from Snowdrop, use `verbose` flag when creating new instance of your service.

```Swift
let service = MyEndpointService(baseUrl: URL(string: "https://my-endpoint.com")!, verbose: true)
```

## Acknowledgements

Retrofit was an inspiration for Snowdrop.
54 changes: 23 additions & 31 deletions Sources/Snowdrop/Core/SnowdropCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,27 @@
//

import Foundation
import OSLog

// MARK: Request perform methods

public extension Snowdrop.Core {
private var logger: Logger { Logger() }

@discardableResult
func performRequest(
_ request: URLRequest,
baseUrl: URL,
pinning: PinningMode?,
urlsExcludedFromPinning: [String],
requestBlocks: [String: RequestHandler],
responseBlocks: [String: ResponseHandler],
testJSONDictionary: [String: String]?
service: Service
) async throws -> (Data?, HTTPURLResponse) {
let session = getSession(pinningMode: pinning, urlsExcludedFromPinning: urlsExcludedFromPinning)
let session = getSession(pinningMode: service.pinningMode, urlsExcludedFromPinning: service.urlsExcludedFromPinning)
var data: Data?
var urlResponse: URLResponse?

var finalRequest = request
applyRequestBlocks(requestBlocks, for: &finalRequest)
applyRequestBlocks(service.requestBlocks, for: &finalRequest)

do {
(data, urlResponse) = try await executeRequest(baseUrl: baseUrl, session: session, request: finalRequest, testJSONDictionary: testJSONDictionary)
session.finishTasksAndInvalidate()
(data, urlResponse) = try await executeRequest(finalRequest, session: session, service: service)
} catch {
throw handleError(error as NSError)
}
Expand All @@ -38,36 +35,24 @@ public extension Snowdrop.Core {
try handleNon200Code(from: response, data: data)
guard var finalData = data else { return (data, response) }

applyResponseBlocks(responseBlocks, forData: &finalData, response: &response)
applyResponseBlocks(service.responseBlocks, forData: &finalData, response: &response)
log(level: .info, message: "Request finished. Response:\n\(String(data: finalData, encoding: .utf8) ?? "")", execute: service.verbose)
return (finalData, response)
}

func performRequestAndDecode<T: Codable>(
_ request: URLRequest,
baseUrl: URL,
decoder: JSONDecoder,
pinning: PinningMode?,
urlsExcludedFromPinning: [String],
requestBlocks: [String: RequestHandler],
responseBlocks: [String: ResponseHandler],
testJSONDictionary: [String: String]?
service: Service
) async throws -> T {
let (data, _) = try await performRequest(
request,
baseUrl: baseUrl,
pinning: pinning,
urlsExcludedFromPinning: urlsExcludedFromPinning,
requestBlocks: requestBlocks,
responseBlocks: responseBlocks,
testJSONDictionary: testJSONDictionary
)
let (data, _) = try await performRequest(request, service: service)

guard let unwrappedData = data else { throw SnowdropError(type: .unexpectedResponse) }

do {
let decodedData = try decoder.decode(T.self, from: unwrappedData)
let decodedData = try service.decoder.decode(T.self, from: unwrappedData)
return decodedData
} catch {
log(level: .error, message: "Response decoding failed.", execute: service.verbose)
throw SnowdropError(
type: .failedToMapResponse,
details: .init(
Expand All @@ -80,14 +65,14 @@ public extension Snowdrop.Core {
}
}

func executeRequest(baseUrl: URL, session: URLSession, request: URLRequest, testJSONDictionary: [String: String]?) async throws -> (Data?, URLResponse?) {
func executeRequest(_ request: URLRequest, session: URLSession, service: Service) async throws -> (Data?, URLResponse?) {
var data: Data?
var urlResponse: URLResponse?
let jsonPaths = [".json", ".JSON"].reduce([]) { $0 + Bundle.main.paths(forResourcesOfType: $1, inDirectory: nil) }

if let testJSONDictionary,
if let testJSONDictionary = service.testJSONDictionary,
let requestUrl = request.url,
let key = testJSONDictionary.keys.first(where: { baseUrl.appendingPathComponent($0).absoluteString == requestUrl.absoluteString }),
let key = testJSONDictionary.keys.first(where: { service.baseUrl.appendingPathComponent($0).absoluteString == requestUrl.absoluteString }),
let jsonName = testJSONDictionary[key],
let jsonPath = jsonPaths.first(where: { $0.hasSuffix(jsonName + ".json") || $0.hasSuffix(jsonName + ".JSON") }),
let jsonData = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) {
Expand All @@ -98,9 +83,11 @@ public extension Snowdrop.Core {
}

do {
log(level: .info, message: "Executing request \(request.url?.absoluteString ?? "unknown").", execute: service.verbose)
(data, urlResponse) = try await session.data(for: request)
session.finishTasksAndInvalidate()
} catch {
log(level: .error, message: "Request failed\n\(handleError(error as NSError))", execute: service.verbose)
throw handleError(error as NSError)
}

Expand Down Expand Up @@ -237,6 +224,11 @@ private extension Snowdrop.Core {
return $0 + ["\($1.key)=\(value)"]
}
}

func log(level: OSLogType, message: String, execute: Bool) {
guard execute else { return }
logger.log(level: level, "[Snowdrop] \(message)")
}
}

fileprivate extension Collection where Element == String {
Expand Down
3 changes: 2 additions & 1 deletion Sources/Snowdrop/Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ public protocol Service {
var decoder: JSONDecoder { get set }
var pinningMode: PinningMode? { get set }
var urlsExcludedFromPinning: [String] { get set }
var verbose: Bool { get }

init(baseUrl: URL,
pinningMode: PinningMode?,
urlsExcludedFromPinning: [String],
decoder: JSONDecoder,
testMode: Bool
verbose: Bool
)

func addBeforeSendingBlock(for path: String?, _ block: @escaping RequestHandler)
Expand Down
6 changes: 3 additions & 3 deletions Sources/SnowdropMacros/ClassBuilder/ClassBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,20 @@ struct ClassBuilder {
\(raw: accessModifier)var decoder: JSONDecoder
\(raw: accessModifier)var pinningMode: PinningMode?
\(raw: accessModifier)var urlsExcludedFromPinning: [String]
private let testMode: Bool
\(raw: accessModifier)let verbose: Bool
\(raw: accessModifier)required init(
baseUrl: URL,
pinningMode: PinningMode? = nil,
urlsExcludedFromPinning: [String] = [],
decoder: JSONDecoder = .init(),
testMode: Bool = false
verbose: Bool = false
) {
self.baseUrl = baseUrl
self.pinningMode = pinningMode
self.urlsExcludedFromPinning = urlsExcludedFromPinning
self.decoder = decoder
self.testMode = testMode
self.verbose = verbose
}
\(raw: ClassBuilder.buildBeforeSendingBlockFunc(for: type, accessModifier: accessModifier))
Expand Down
21 changes: 2 additions & 19 deletions Sources/SnowdropMacros/Macros/Service/ServiceRequestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,11 @@ struct ServiceRequestBuilder: ClassMethodBodyBuilderProtocol {
if let _ = details.returnType {
requestImpl += """
return try\(details.doesThrow ? "" : "?") await Snowdrop.core.performRequestAndDecode(
request,
baseUrl: baseUrl,
decoder: decoder,
pinning: pinningMode,
urlsExcludedFromPinning: urlsExcludedFromPinning,
requestBlocks: requestBlocks,
responseBlocks: responseBlocks,
testJSONDictionary: testMode ? testJSONDictionary : nil
)
return try\(details.doesThrow ? "" : "?") await Snowdrop.core.performRequestAndDecode(request, service: self)
"""
} else {
requestImpl += """
_ = try\(details.doesThrow ? "" : "?") await Snowdrop.core.performRequest(
request,
baseUrl: baseUrl,
pinning: pinningMode,
urlsExcludedFromPinning: urlsExcludedFromPinning,
requestBlocks: requestBlocks,
responseBlocks: responseBlocks,
testJSONDictionary: testMode ? testJSONDictionary : nil
)
_ = try\(details.doesThrow ? "" : "?") await Snowdrop.core.performRequest(request, service: self)
"""
}

Expand Down
12 changes: 9 additions & 3 deletions Sources/SnowdropMacros/Utilities/PathVariableFinder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct PathVariableFinder {
private let shortCollectionPattern = #"[ ]{0,1}=[ ]{0,1}\[[a-zA-Z0-9\\.\\@\" \\/\[\]\:\(\)\!]*\]"#
private let shortTupleLikePattern = #"[ ]{0,1}=[ ]{0,1}\([a-zA-Z0-9\\.\\@\" \,\\/\[\]\:\(\)\!]*\)"#

private let simpleRegex = try? NSRegularExpression(pattern: #"\{[a-z]+[a-zA-Z0-9]+\}"#)
private let numericVarRegex = try? NSRegularExpression(pattern: #"\{[a-z]+[a-zA-Z0-9]+[ ]{0,1}=[ ]{0,1}[0-9a-zA-Z\\.]*\}"#)
private let stringRegex = try? NSRegularExpression(pattern: #"\{[a-z]+[a-zA-Z0-9]+[ ]{0,1}=[ ]{0,1}\"[a-zA-Z0-9\\.\\@]*\"\}"#)
private let instanceRegex = try? NSRegularExpression(pattern: #"\{[a-z]+[a-zA-Z0-9]+[ ]{0,1}=[ ]{0,1}[a-zA-Z0-9\\.]+\([a-zA-Z0-9\\.\\@\" \\/\[\]\:\(\)\!]+\)[\!]{0,1}\}"#)
Expand Down Expand Up @@ -47,12 +48,13 @@ struct PathVariableFinder {
guard let url else { return "" }
var outcome = url

let regexes = [
let regexes: [(String?, NSRegularExpression?)] = [
(shortNumericVarPattern, numericVarRegex),
(shortStringPattern, stringRegex),
(shortInstancePattern, instanceRegex),
(shortCollectionPattern, collectionRegex),
(shortTupleLikePattern, tupleLikeRegex)
(shortTupleLikePattern, tupleLikeRegex),
(nil, simpleRegex)
]

regexes.forEach { shortRegex, regex in
Expand All @@ -70,7 +72,11 @@ struct PathVariableFinder {
outcome = outcome
.replacingOccurrences(of: "}", with: ")", range: range)
.replacingOccurrences(of: "{", with: "\\(", range: range)
.replacingOccurrences(of: shortRegex, with: "", options: .regularExpression, range: range)

if let shortRegex {
outcome = outcome
.replacingOccurrences(of: shortRegex, with: "", options: .regularExpression, range: range)
}
}
}

Expand Down
46 changes: 14 additions & 32 deletions Tests/SnowdropMacrosTests/SnowdropMacrosTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ final class SnowdropMacrosTests: XCTestCase {
"""
@Service
protocol TestEndpoint {
@GET(url: "/posts/{id=2}/comments")
@GET(url: "/posts/{id}/comments")
@Headers(["Content-Type": "application/json"])
@Body("model")
func getPosts(for id: Int, model: Model) async throws -> Post
Expand All @@ -40,20 +40,20 @@ final class SnowdropMacrosTests: XCTestCase {
var decoder: JSONDecoder
var pinningMode: PinningMode?
var urlsExcludedFromPinning: [String]
private let testMode: Bool
let verbose: Bool
required init(
baseUrl: URL,
pinningMode: PinningMode? = nil,
urlsExcludedFromPinning: [String] = [],
decoder: JSONDecoder = .init(),
testMode: Bool = false
verbose: Bool = false
) {
self.baseUrl = baseUrl
self.pinningMode = pinningMode
self.urlsExcludedFromPinning = urlsExcludedFromPinning
self.decoder = decoder
self.testMode = testMode
self.verbose = verbose
}
func addBeforeSendingBlock(for path: String? = nil, _ block: @escaping RequestHandler) {
Expand All @@ -80,12 +80,12 @@ final class SnowdropMacrosTests: XCTestCase {
responseBlocks[key] = block
}
func getPosts(for id: Int = 2, model: Model) async throws -> Post {
func getPosts(for id: Int, model: Model) async throws -> Post {
let _queryItems: [QueryItem] = []
return try await getPosts(for: id, model: model, _queryItems: _queryItems)
}
func getPosts(for id: Int = 2, model: Model, _queryItems: [QueryItem]) async throws -> Post {
func getPosts(for id: Int, model: Model, _queryItems: [QueryItem]) async throws -> Post {
let url = baseUrl.appendingPathComponent("/posts/\\(id)/comments")
let headers: [String: Any] = ["Content-Type": "application/json"]
Expand All @@ -100,16 +100,7 @@ final class SnowdropMacrosTests: XCTestCase {
request.httpBody = data
return try await Snowdrop.core.performRequestAndDecode(
request,
baseUrl: baseUrl,
decoder: decoder,
pinning: pinningMode,
urlsExcludedFromPinning: urlsExcludedFromPinning,
requestBlocks: requestBlocks,
responseBlocks: responseBlocks,
testJSONDictionary: testMode ? testJSONDictionary : nil
)
return try await Snowdrop.core.performRequestAndDecode(request, service: self)
}
private func prepareBasicRequest(url: URL, method: String, queryItems: [QueryItem], headers: [String: Any]) -> URLRequest {
Expand Down Expand Up @@ -172,20 +163,20 @@ final class SnowdropMacrosTests: XCTestCase {
public var decoder: JSONDecoder
public var pinningMode: PinningMode?
public var urlsExcludedFromPinning: [String]
private let testMode: Bool
public let verbose: Bool
public required init(
baseUrl: URL,
pinningMode: PinningMode? = nil,
urlsExcludedFromPinning: [String] = [],
decoder: JSONDecoder = .init(),
testMode: Bool = false
verbose: Bool = false
) {
self.baseUrl = baseUrl
self.pinningMode = pinningMode
self.urlsExcludedFromPinning = urlsExcludedFromPinning
self.decoder = decoder
self.testMode = testMode
self.verbose = verbose
}
public func addBeforeSendingBlock(for path: String? = nil, _ block: @escaping RequestHandler) {
Expand Down Expand Up @@ -232,16 +223,7 @@ final class SnowdropMacrosTests: XCTestCase {
request.httpBody = Snowdrop.core.dataWithBoundary(file, payloadDescription: _payloadDescription)
return try await Snowdrop.core.performRequestAndDecode(
request,
baseUrl: baseUrl,
decoder: decoder,
pinning: pinningMode,
urlsExcludedFromPinning: urlsExcludedFromPinning,
requestBlocks: requestBlocks,
responseBlocks: responseBlocks,
testJSONDictionary: testMode ? testJSONDictionary : nil
)
return try await Snowdrop.core.performRequestAndDecode(request, service: self)
}
private func prepareBasicRequest(url: URL, method: String, queryItems: [QueryItem], headers: [String: Any]) -> URLRequest {
Expand Down Expand Up @@ -304,20 +286,20 @@ final class SnowdropMacrosTests: XCTestCase {
public var decoder: JSONDecoder
public var pinningMode: PinningMode?
public var urlsExcludedFromPinning: [String]
private let testMode: Bool
public let verbose: Bool
public required init(
baseUrl: URL,
pinningMode: PinningMode? = nil,
urlsExcludedFromPinning: [String] = [],
decoder: JSONDecoder = .init(),
testMode: Bool = false
verbose: Bool = false
) {
self.baseUrl = baseUrl
self.pinningMode = pinningMode
self.urlsExcludedFromPinning = urlsExcludedFromPinning
self.decoder = decoder
self.testMode = testMode
self.verbose = verbose
}
public func addBeforeSendingBlock(for path: String? = nil, _ block: @escaping RequestHandler) {
Expand Down
Loading

0 comments on commit 45ec6d8

Please sign in to comment.