Skip to content

Commit ccbf277

Browse files
committed
Add support for extracting recorded requests
1 parent d4551da commit ccbf277

File tree

4 files changed

+310
-52
lines changed

4 files changed

+310
-52
lines changed

MockDuck/Sources/MockBundle.swift

+113-43
Original file line numberDiff line numberDiff line change
@@ -29,59 +29,108 @@ final class MockBundle {
2929
/// - Returns: The MockRequestResponse, if it can be loaded
3030
func loadResponse(for requestResponse: MockRequestResponse) -> Bool {
3131
guard let fileName = requestResponse.fileName(for: .request) else { return false }
32-
33-
var targetURL: URL?
34-
var targetLoadingURL: URL?
3532
let request = requestResponse.request
3633

34+
var loadedPath: String?
35+
var loadedResponse: MockResponse?
3736
if let response = checkRequestHandlers(for: request) {
38-
requestResponse.responseWrapper = response
39-
return true
40-
} else if
41-
let inputURL = loadingURL?.appendingPathComponent(fileName),
42-
FileManager.default.fileExists(atPath: inputURL.path)
43-
{
44-
os_log("Loading request %@ from: %@", log: MockDuck.log, type: .debug, "\(request)", inputURL.path)
45-
targetURL = inputURL
46-
targetLoadingURL = loadingURL
47-
} else if
48-
let inputURL = recordingURL?.appendingPathComponent(fileName),
49-
FileManager.default.fileExists(atPath: inputURL.path)
50-
{
51-
os_log("Loading request %@ from: %@", log: MockDuck.log, type: .debug, "\(request)", inputURL.path)
52-
targetURL = inputURL
53-
targetLoadingURL = recordingURL
37+
loadedResponse = response
38+
} else if let response = loadResponseFile(relativePath: fileName, baseURL: loadingURL) {
39+
loadedPath = loadingURL?.path ?? "" + fileName
40+
loadedResponse = response.responseWrapper
41+
} else if let response = loadResponseFile(relativePath: fileName, baseURL: recordingURL) {
42+
loadedPath = recordingURL?.path ?? "" + fileName
43+
loadedResponse = response.responseWrapper
5444
} else {
5545
os_log("Request %@ not found on disk. Expected file name: %@", log: MockDuck.log, type: .debug, "\(request)", fileName)
5646
}
57-
58-
if
59-
let targetURL = targetURL,
60-
let targetLoadingURL = targetLoadingURL
61-
{
62-
let decoder = JSONDecoder()
63-
64-
do {
65-
let data = try Data(contentsOf: targetURL)
66-
67-
let loaded = try decoder.decode(MockRequestResponse.self, from: data)
68-
requestResponse.responseWrapper = loaded.responseWrapper
69-
70-
// Load the response data if the format is supported.
71-
// This should be the same filename with a different extension.
72-
if let dataFileName = requestResponse.fileName(for: .responseData) {
73-
let dataURL = targetLoadingURL.appendingPathComponent(dataFileName)
74-
requestResponse.responseData = try Data(contentsOf: dataURL)
75-
}
76-
77-
return true
78-
} catch {
79-
os_log("Error decoding JSON: %@", log: MockDuck.log, type: .error, "\(error)")
47+
48+
if let response = loadedResponse {
49+
requestResponse.responseWrapper = response
50+
if let path = loadedPath {
51+
os_log("Loading request %@ from: %@",
52+
log: MockDuck.log,
53+
type: .debug,
54+
"\(request)",
55+
path)
8056
}
57+
return true
8158
}
82-
8359
return false
8460
}
61+
62+
/// Takes a URL and attempts to parse the file at that location into a MockRequestResponse
63+
/// If the file doesn't exist, or isn't in the expected MockDuck format, nil is returned
64+
///
65+
/// - Parameter targetURL: URL that should be loaded from file
66+
/// - Returns: MockRequestResponse if the request exists at that URL
67+
func loadResponseFile(relativePath: String, baseURL: URL?) -> MockRequestResponse? {
68+
guard let baseURL = baseURL else { return nil }
69+
let targetURL = baseURL.appendingPathComponent(relativePath)
70+
guard FileManager.default.fileExists(atPath: targetURL.path) else { return nil}
71+
72+
let decoder = JSONDecoder()
73+
74+
do {
75+
let data = try Data(contentsOf: targetURL)
76+
77+
let response = try decoder.decode(MockRequestResponse.self, from: data)
78+
79+
// Load the response data if the format is supported.
80+
// This should be the same filename with a different extension.
81+
if let dataFileName = response.fileName(for: .responseData) {
82+
let dataURL = baseURL.appendingPathComponent(dataFileName)
83+
response.responseData = try? Data(contentsOf: dataURL)
84+
}
85+
86+
return response
87+
} catch {
88+
os_log("Error decoding JSON: %@", log: MockDuck.log, type: .error, "\(error)")
89+
}
90+
return nil
91+
}
92+
93+
/// Takes a passed in hostname and returns all the recorded mocks for that URL.
94+
/// If an empty string is passed in, all recordings will be returned.
95+
///
96+
/// - Parameter hostname: String representing the hostname to load requests from.
97+
/// - Returns: An array of MockRequestResponse for each request under that domain
98+
func getResponses(for hostname: String) -> [MockRequestResponse] {
99+
guard let recordingURL = recordingURL else { return [] }
100+
101+
let baseURL = recordingURL.resolvingSymlinksInPath()
102+
var responses = [MockRequestResponse]()
103+
let targetURL = baseURL.appendingPathComponent(hostname)
104+
105+
let results = FileManager.default.enumerator(
106+
at: targetURL,
107+
includingPropertiesForKeys: [.isDirectoryKey],
108+
options: [])
109+
110+
if let results = results {
111+
for case let item as URL in results {
112+
var isDir = ObjCBool(false)
113+
let itemURL = item.resolvingSymlinksInPath()
114+
115+
/// Check if the item:
116+
/// 1) isn't a directory
117+
/// 2) doesn't end in '-response' (a sidecar file)
118+
/// If so, load it using loadResponseFile so any associated
119+
/// '-response' file is also loaded with the repsonse.
120+
if
121+
FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDir),
122+
!isDir.boolValue,
123+
!itemURL.lastPathComponent.contains("-response"),
124+
let relativePath = itemURL.pathRelative(to: baseURL),
125+
let response = loadResponseFile(relativePath: relativePath, baseURL: recordingURL)
126+
{
127+
responses.append(response)
128+
}
129+
}
130+
}
131+
132+
return responses
133+
}
85134

86135
/// If recording is enabled, this method saves the request to the filesystem. If the request
87136
/// body or the response data are of a certain type 'jpg/png/gif/json', the request is saved
@@ -169,3 +218,24 @@ final class MockBundle {
169218
}
170219
}
171220
}
221+
222+
223+
extension URL {
224+
func pathRelative(to url: URL) -> String? {
225+
guard
226+
host == url.host,
227+
scheme == url.scheme
228+
else { return nil }
229+
230+
let components = self.standardized.pathComponents
231+
let baseComponents = url.standardized.pathComponents
232+
233+
if components.count < baseComponents.count { return nil }
234+
for (index, baseComponent) in baseComponents.enumerated() {
235+
let component = components[index]
236+
if component != baseComponent { return nil }
237+
}
238+
239+
return components[baseComponents.count..<components.count].joined(separator: "/")
240+
}
241+
}

MockDuck/Sources/MockDuck.swift

+12
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,18 @@ public final class MockDuck {
135135
public static func unregisterAllRequestHandlers() {
136136
mockBundle.unregisterAllRequestHandlers()
137137
}
138+
139+
// MARK: - Fetching Response Objects
140+
141+
/// Takes a passed in hostname and returns all the recorded mocks for that URL.
142+
/// If an empty string is passed in, all recordings will be returned.
143+
///
144+
/// - Parameter hostname: String representing the hostname to load requests from.
145+
/// - Returns: An array of MockRequestResponse for each request under that domain
146+
public static func getResponses(for hostname: String) -> [MockRequestResponse] {
147+
checkConfigureMockDuck()
148+
return mockBundle.getResponses(for: hostname)
149+
}
138150

139151
// MARK: - Internal Use Only
140152

MockDuck/Sources/MockRequestResponse.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Foundation
1010

1111
/// A basic container for holding a request, a response, and any associated data.
12-
final class MockRequestResponse: Codable, CustomDebugStringConvertible {
12+
public final class MockRequestResponse: Codable, CustomDebugStringConvertible {
1313

1414
enum MockFileTarget {
1515
case request
@@ -19,7 +19,7 @@ final class MockRequestResponse: Codable, CustomDebugStringConvertible {
1919

2020
// MARK: - Properties
2121

22-
var request: URLRequest {
22+
public var request: URLRequest {
2323
get {
2424
return requestWrapper.request
2525
}
@@ -28,11 +28,11 @@ final class MockRequestResponse: Codable, CustomDebugStringConvertible {
2828
}
2929
}
3030

31-
var response: URLResponse? {
31+
public var response: URLResponse? {
3232
return responseWrapper?.response
3333
}
3434

35-
var responseData: Data? {
35+
public var responseData: Data? {
3636
get {
3737
return responseWrapper?.responseData
3838
}
@@ -140,7 +140,7 @@ final class MockRequestResponse: Codable, CustomDebugStringConvertible {
140140
case responseWrapper = "response"
141141
}
142142

143-
init(from decoder: Decoder) throws {
143+
public init(from decoder: Decoder) throws {
144144
let container = try decoder.container(keyedBy: CodingKeys.self)
145145
requestWrapper = try container.decode(MockRequest.self, forKey: .requestWrapper)
146146
responseWrapper = try container.decodeIfPresent(MockResponse.self, forKey: .responseWrapper)

0 commit comments

Comments
 (0)