Skip to content

Commit 340fc80

Browse files
committed
Merge branch 'mixpanel'
2 parents 3f1568f + 00ea976 commit 340fc80

File tree

9 files changed

+343
-59
lines changed

9 files changed

+343
-59
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ DerivedData
1616
*.hmap
1717
*.ipa
1818
*.xcuserstate
19+
*.xcscmblueprint
1920

2021
# CocoaPods
2122
#

Mockingjay.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
2746CDCB1A702F7800719B66 /* Mockingjay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2746CDBF1A702F7800719B66 /* Mockingjay.framework */; };
2222
2746CDD21A702F7800719B66 /* MockingjayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746CDD11A702F7800719B66 /* MockingjayTests.swift */; };
2323
27703A631CE2560600194732 /* MockingjayURLSessionConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 27703A621CE2560600194732 /* MockingjayURLSessionConfiguration.m */; };
24+
444EA6091C5261DE000C3A9F /* MockingjayAsyncProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444EA6081C5261DE000C3A9F /* MockingjayAsyncProtocolTests.swift */; };
25+
444EA60C1C52666D000C3A9F /* TestAudio.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 444EA60B1C52666D000C3A9F /* TestAudio.m4a */; };
2426
A1E3C5701AA4EA130069C998 /* XCTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2743679B1AA28D4D0030C97B /* XCTest.swift */; };
2527
/* End PBXBuildFile section */
2628

@@ -77,6 +79,8 @@
7779
2746CDDD1A702FC100719B66 /* UniversalFramework_Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UniversalFramework_Framework.xcconfig; sourceTree = "<group>"; };
7880
2746CDDE1A702FC100719B66 /* UniversalFramework_Test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UniversalFramework_Test.xcconfig; sourceTree = "<group>"; };
7981
27703A621CE2560600194732 /* MockingjayURLSessionConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MockingjayURLSessionConfiguration.m; sourceTree = "<group>"; };
82+
444EA6081C5261DE000C3A9F /* MockingjayAsyncProtocolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockingjayAsyncProtocolTests.swift; sourceTree = "<group>"; };
83+
444EA60B1C52666D000C3A9F /* TestAudio.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestAudio.m4a; sourceTree = "<group>"; };
8084
/* End PBXFileReference section */
8185

8286
/* Begin PBXFrameworksBuildPhase section */
@@ -159,9 +163,11 @@
159163
children = (
160164
2746CDD11A702F7800719B66 /* MockingjayTests.swift */,
161165
274367951AA27B170030C97B /* MockingjayProtocolTests.swift */,
166+
444EA6081C5261DE000C3A9F /* MockingjayAsyncProtocolTests.swift */,
162167
274367991AA28B0D0030C97B /* MatcherTests.swift */,
163168
274367B11AA29E620030C97B /* BuildersTests.swift */,
164169
2705946A1C4FA7A6002A3AA9 /* MockingjayXCTestTests.swift */,
170+
444EA60D1C526673000C3A9F /* Resources */,
165171
2746CDCF1A702F7800719B66 /* Supporting Files */,
166172
);
167173
path = MockingjayTests;
@@ -185,6 +191,14 @@
185191
path = Configurations;
186192
sourceTree = "<group>";
187193
};
194+
444EA60D1C526673000C3A9F /* Resources */ = {
195+
isa = PBXGroup;
196+
children = (
197+
444EA60B1C52666D000C3A9F /* TestAudio.m4a */,
198+
);
199+
name = Resources;
200+
sourceTree = "<group>";
201+
};
188202
/* End PBXGroup section */
189203

190204
/* Begin PBXHeadersBuildPhase section */
@@ -308,6 +322,7 @@
308322
isa = PBXResourcesBuildPhase;
309323
buildActionMask = 2147483647;
310324
files = (
325+
444EA60C1C52666D000C3A9F /* TestAudio.m4a in Resources */,
311326
);
312327
runOnlyForDeploymentPostprocessing = 0;
313328
};
@@ -334,6 +349,7 @@
334349
files = (
335350
2743679A1AA28B0D0030C97B /* MatcherTests.swift in Sources */,
336351
274367961AA27B170030C97B /* MockingjayProtocolTests.swift in Sources */,
352+
444EA6091C5261DE000C3A9F /* MockingjayAsyncProtocolTests.swift in Sources */,
337353
274367B21AA29E620030C97B /* BuildersTests.swift in Sources */,
338354
2705946B1C4FA7A6002A3AA9 /* MockingjayXCTestTests.swift in Sources */,
339355
2746CDD21A702F7800719B66 /* MockingjayTests.swift in Sources */,

Mockingjay/Builders.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@ public func failure(error:NSError) -> (request:NSURLRequest) -> Response {
1515
return { _ in return .Failure(error) }
1616
}
1717

18-
public func http(status:Int = 200, headers:[String:String]? = nil, data:NSData? = nil) -> (request:NSURLRequest) -> Response {
19-
18+
public func http(status:Int = 200, headers:[String:String]? = nil, download:Download=nil) -> (request:NSURLRequest) -> Response {
2019
return { (request:NSURLRequest) in
2120
if let response = NSHTTPURLResponse(URL: request.URL!, statusCode: status, HTTPVersion: nil, headerFields: headers) {
22-
return Response.Success(response, data)
21+
return Response.Success(response, download)
2322
}
24-
23+
2524
return .Failure(NSError(domain: NSInternalInconsistencyException, code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to construct response for stub."]))
2625
}
2726
}
@@ -44,6 +43,6 @@ public func jsonData(data: NSData, status: Int = 200, headers: [String:String]?
4443
headers["Content-Type"] = "application/json; charset=utf-8"
4544
}
4645

47-
return http(status, headers: headers, data: data)(request:request)
46+
return http(status, headers: headers, download: .Content(data))(request:request)
4847
}
4948
}

Mockingjay/Mockingjay.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,43 @@
88

99
import Foundation
1010

11+
public enum Download: NilLiteralConvertible, Equatable {
12+
public init(nilLiteral: ()) {
13+
self = .NoContent
14+
}
15+
16+
//Simulate download in one step
17+
case Content(NSData)
18+
//Simulate download as byte stream
19+
case StreamContent(data:NSData, inChunksOf:Int)
20+
//Simulate empty download
21+
case NoContent
22+
}
23+
24+
public func ==(lhs:Download, rhs:Download) -> Bool {
25+
switch(lhs, rhs) {
26+
case let (.Content(lhsData), .Content(rhsData)):
27+
return lhsData.isEqualToData(rhsData)
28+
case let (.StreamContent(data:lhsData, inChunksOf:lhsBytes), .StreamContent(data:rhsData, inChunksOf:rhsBytes)):
29+
return lhsData.isEqualToData(rhsData) && lhsBytes == rhsBytes
30+
case (.NoContent, .NoContent):
31+
return true
32+
default:
33+
return false
34+
}
35+
}
36+
1137
public enum Response : Equatable {
12-
case Success(NSURLResponse, NSData?)
38+
case Success(NSURLResponse, Download)
1339
case Failure(NSError)
1440
}
1541

1642
public func ==(lhs:Response, rhs:Response) -> Bool {
1743
switch (lhs, rhs) {
1844
case let (.Failure(lhsError), .Failure(rhsError)):
1945
return lhsError == rhsError
20-
case let (.Success(lhsResponse, lhsData), .Success(rhsResponse, rhsData)):
21-
return lhsResponse == rhsResponse && lhsData == rhsData
46+
case let (.Success(lhsResponse, lhsDownload), .Success(rhsResponse, rhsDownload)):
47+
return lhsResponse == rhsResponse && lhsDownload == rhsDownload
2248
default:
2349
return false
2450
}

Mockingjay/MockingjayProtocol.swift

Lines changed: 113 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public struct Stub : Equatable {
1414
let matcher:Matcher
1515
let builder:Builder
1616
let uuid:NSUUID
17-
17+
1818
init(matcher:Matcher, builder:Builder) {
1919
self.matcher = matcher
2020
self.builder = builder
@@ -30,36 +30,38 @@ var stubs = [Stub]()
3030

3131
public class MockingjayProtocol : NSURLProtocol {
3232
// MARK: Stubs
33-
33+
private var enableDownloading = true
34+
private let operationQueue = NSOperationQueue()
35+
3436
class func addStub(stub:Stub) -> Stub {
3537
stubs.append(stub)
36-
38+
3739
var token: dispatch_once_t = 0
3840
dispatch_once(&token) {
3941
NSURLProtocol.registerClass(self)
4042
return
4143
}
42-
44+
4345
return stub
4446
}
45-
47+
4648
/// Register a matcher and a builder as a new stub
4749
public class func addStub(matcher:Matcher, builder:Builder) -> Stub {
4850
return addStub(Stub(matcher: matcher, builder: builder))
4951
}
50-
52+
5153
/// Unregister the given stub
5254
public class func removeStub(stub:Stub) {
5355
if let index = stubs.indexOf(stub) {
5456
stubs.removeAtIndex(index)
5557
}
5658
}
57-
59+
5860
/// Remove all registered stubs
5961
public class func removeAllStubs() {
6062
stubs.removeAll(keepCapacity: false)
6163
}
62-
64+
6365
/// Finds the appropriate stub for a request
6466
/// This method searches backwards though the registered requests
6567
/// to find the last registered stub that handles the request.
@@ -69,40 +71,130 @@ public class MockingjayProtocol : NSURLProtocol {
6971
return stub
7072
}
7173
}
72-
74+
7375
return nil
7476
}
75-
77+
7678
// MARK: NSURLProtocol
77-
79+
7880
/// Returns whether there is a registered stub handler for the given request.
7981
override public class func canInitWithRequest(request:NSURLRequest) -> Bool {
8082
return stubForRequest(request) != nil
8183
}
82-
84+
8385
override public class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
8486
return request
8587
}
86-
88+
8789
override public func startLoading() {
8890
if let stub = MockingjayProtocol.stubForRequest(request) {
8991
switch stub.builder(request) {
9092
case .Failure(let error):
9193
client?.URLProtocol(self, didFailWithError: error)
92-
case .Success(let response, let data):
93-
client?.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
94-
95-
if let data = data {
94+
case .Success(var response, let download):
95+
let headers = self.request.allHTTPHeaderFields
96+
97+
switch(download) {
98+
case .Content(var data):
99+
applyRangeFromHTTPHeaders(headers, toData: &data, andUpdateResponse: &response)
96100
client?.URLProtocol(self, didLoadData: data)
101+
client?.URLProtocolDidFinishLoading(self)
102+
case .StreamContent(data: var data, inChunksOf: let bytes):
103+
applyRangeFromHTTPHeaders(headers, toData: &data, andUpdateResponse: &response)
104+
self.download(data, inChunksOfBytes: bytes)
105+
return
106+
case .NoContent:
107+
client?.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
108+
client?.URLProtocolDidFinishLoading(self)
97109
}
98-
99-
client?.URLProtocolDidFinishLoading(self)
100110
}
101111
} else {
102112
let error = NSError(domain: NSInternalInconsistencyException, code: 0, userInfo: [ NSLocalizedDescriptionKey: "Handling request without a matching stub." ])
103113
client?.URLProtocol(self, didFailWithError: error)
104114
}
105115
}
106-
107-
override public func stopLoading() {}
116+
117+
override public func stopLoading() {
118+
self.enableDownloading = false
119+
self.operationQueue.cancelAllOperations()
120+
}
121+
122+
// MARK: Private Methods
123+
124+
private func download(data:NSData?, inChunksOfBytes bytes:Int) {
125+
guard let data = data else {
126+
client?.URLProtocolDidFinishLoading(self)
127+
return
128+
}
129+
self.operationQueue.maxConcurrentOperationCount = 1
130+
self.operationQueue.addOperationWithBlock { () -> Void in
131+
self.download(data, fromOffset: 0, withMaxLength: bytes)
132+
}
133+
}
134+
135+
136+
private func download(data:NSData, fromOffset offset:Int, withMaxLength maxLength:Int) {
137+
guard let queue = NSOperationQueue.currentQueue() else {
138+
return
139+
}
140+
guard (offset < data.length) else {
141+
client?.URLProtocolDidFinishLoading(self)
142+
return
143+
}
144+
let length = min(data.length - offset, maxLength)
145+
146+
queue.addOperationWithBlock { () -> Void in
147+
guard self.enableDownloading else {
148+
self.enableDownloading = true
149+
return
150+
}
151+
152+
let subdata = data.subdataWithRange(NSMakeRange(offset, length))
153+
self.client?.URLProtocol(self, didLoadData: subdata)
154+
NSThread.sleepForTimeInterval(0.01)
155+
self.download(data, fromOffset: offset + length, withMaxLength: maxLength)
156+
}
157+
}
158+
159+
private func extractRangeFromHTTPHeaders(headers:[String : String]?) -> NSRange? {
160+
guard let rangeStr = headers?["Range"] else {
161+
return nil
162+
}
163+
let range = rangeStr.componentsSeparatedByString("=")[1].componentsSeparatedByString("-").map({ (str) -> Int in
164+
Int(str)!
165+
})
166+
let loc = range[0]
167+
let length = range[1] - loc + 1
168+
return NSMakeRange(loc, length)
169+
}
170+
171+
private func applyRangeFromHTTPHeaders(
172+
headers:[String : String]?,
173+
inout toData data:NSData,
174+
inout andUpdateResponse response:NSURLResponse) {
175+
guard let range = extractRangeFromHTTPHeaders(headers) else {
176+
client?.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
177+
return
178+
}
179+
let fullLength = data.length
180+
data = data.subdataWithRange(range)
181+
182+
//Attach new headers to response
183+
if let r = response as? NSHTTPURLResponse {
184+
var header = r.allHeaderFields as! [String:String]
185+
header["Content-Length"] = String(data.length)
186+
header["Content-Range"] = String(range.httpRangeStringWithFullLength(fullLength))
187+
response = NSHTTPURLResponse(URL: r.URL!, statusCode: r.statusCode, HTTPVersion: nil, headerFields: header)!
188+
}
189+
190+
client?.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
191+
}
192+
108193
}
194+
195+
extension NSRange {
196+
func httpRangeStringWithFullLength(fullLength:Int) -> String {
197+
let endLoc = self.location + self.length - 1
198+
return "bytes " + String(self.location) + "-" + String(endLoc) + "/" + String(fullLength)
199+
}
200+
}

0 commit comments

Comments
 (0)