Skip to content

Commit

Permalink
Implement NSURLProtocol.stopLoading() for delayed requests (#96)
Browse files Browse the repository at this point in the history
* implemented NSURLProtocol stopLoading method

* added IntelliJ AppCode meta file directory to git ignore

* using `indexOf` instead of swift array `filter` to reduce roundtrips

* Detailed the code documentation to make the internal NSURLProtocol (OS) logic better understandable

* Fixed spelling mistakes in code docs

* Router now remembers the 'to be canceled' URLs & added router unit tests for 'stopLoading'

* Move request cancelation to private method and fixed unit test for Router (avoid accessing private vars)

* Check that the url client does not deliver notifications when the router's request loading gets cancelled.

* Added stopLoading tests to cover the cancellation logic

* Cancel the request immediately

* Using simple flag on the KakapoServer instance to let the OS mark a server request be marked as cancelled, adopted tests accordingly,

* Removed code comments, which expose too much of internal implementation logic, reduced visibility of variables

* fixed spelling mistake in test description

* Removed error prone initialization of error response objects in test case.

* fixed duplicate URL request test
  • Loading branch information
leviathan authored and MP0w committed Aug 20, 2016
1 parent d8ee24c commit 28abbfa
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 58 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ xcuserdata
timeline.xctimeline
playground.xcworkspace

# AppCode
.idea
.idea/

# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
Expand All @@ -56,7 +60,7 @@ Carthage/Build

# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
Expand Down
2 changes: 1 addition & 1 deletion Source/JSONAPIError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import Foundation

/// A convenince error object that conform to JSON API
/// A convenience error object that conform to JSON API
public struct JSONAPIError: ResponseFieldsProvider {

/// An object containing references to the source of the error, optionally including any of the following members
Expand Down
4 changes: 2 additions & 2 deletions Source/JSONAPILinks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public enum JSONAPILink: CustomSerializable {
}
```
Appart from their own entity links, and in order to provide extra links for relationships, `User` must specify them for each relationship key:
Apart from their own entity links, and in order to provide extra links for relationships, `User` must specify them for each relationship key:
```swift
let cats = [Cat(id: "33", name: "Stancho", links: nil),
Expand Down Expand Up @@ -111,7 +111,7 @@ public enum JSONAPILink: CustomSerializable {
public protocol JSONAPILinkedEntity {
/// The related links, must use link-names as keys and links as values.
var links: [String : JSONAPILink]? { get }
/// The relationships links, an object containing relationsips can specify top level links for every relationship type. The object must provide a Dictionary where keys are the relationships types and values are dictionaries with link-names as keys and link as values.
/// The relationships links, an object containing relationships can specify top level links for every relationship type. The object must provide a Dictionary where keys are the relationships types and values are dictionaries with link-names as keys and link as values.
var relationshipsLinks: [String : [String : JSONAPILink]]? { get }
}

Expand Down
4 changes: 2 additions & 2 deletions Source/JSONAPISerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ public extension JSONAPIEntity {

// MARK: JSONAPIEntity

/// returns the lowercased class name as string by default
/// returns the lower-cased class name as string by default
static var type: String {
return String(self).lowercaseString
}
Expand Down Expand Up @@ -332,7 +332,7 @@ public extension JSONAPIEntity {
return data
}

/// returns the `included` relationsips field conforming to JSON API
/// returns the `included` relationships field conforming to JSON API
public func includedRelationships(includeChildren: Bool, keyTransformer: KeyTransformer?) -> [AnyObject]? {
let mirror = Mirror(reflecting: self)
let includedRelationships = mirror.children.flatMap { (label, value) -> [AnyObject] in
Expand Down
12 changes: 6 additions & 6 deletions Source/KakapoDB.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public protocol Storable {
An initializer that is used by `KakapoDB` to create objects to be stored in the db
- parameter id: The unique identifier provided by `KakapoDB`, objects shouldn't generate ids themselves. `KakapoDB` generate `Int` ids converted to String for better compatibilities with standards like JSONAPI, in case you need `Int` ids is safe to ssume that the conversion will always succeeed.
- parameter db: The db that is creating the object, can be used to generate other `Storable` objects, for example relationsips of the object: `myRelationship = db.create(MyRelationshipType)` or `myrelationship = db.insert { MyRelationshipType(id: $0, db: db) }`. The relationsip will also recieve the `db` instance to eventually initialize its relationships.
- parameter db: The db that is creating the object, can be used to generate other `Storable` objects, for example relationships of the object: `myRelationship = db.create(MyRelationshipType)` or `myrelationship = db.insert { MyRelationshipType(id: $0, db: db) }`. The relationsip will also recieve the `db` instance to eventually initialize its relationships.
- returns: A configured object stored in the db.
*/
Expand Down Expand Up @@ -93,7 +93,7 @@ public final class KakapoDB {
/**
Creates and inserts Storable objects based on their default initializer
- parameter (unamed): The Storable Type to be created
- parameter (unnamed): The Storable Type to be created
- parameter number: The number of elements to create, defaults to 1
- returns: An array containing the new inserted Storable objects
Expand Down Expand Up @@ -181,7 +181,7 @@ public final class KakapoDB {
/**
Find all the objects in the store of a given Storable Type
- parameter (unamed): The Storable Type to be found
- parameter (unnamed): The Storable Type to be found
- returns: An array containing the found Storable objects
*/
Expand All @@ -194,7 +194,7 @@ public final class KakapoDB {
/**
Filter all the objects in the store of a given Storable Type that satisfy the a given handler
- parameter (unamed): The Storable Type to be filtered
- parameter (unnamed): The Storable Type to be filtered
- parameter includeElement: The predicate to satisfy the filtering
- returns: An array containing the filtered Storable objects
Expand All @@ -206,10 +206,10 @@ public final class KakapoDB {
/**
Find the object in the store by a given id
- parameter (unamed): The Storable Type to be filtered
- parameter (unnamed): The Storable Type to be filtered
- parameter id: The id to search for
- returns: An optional thay may (or not) contain the found Storable object
- returns: An optional, which may (or may not) contain the found Storable object
*/
public func find<T: Storable>(_: T.Type, id: String) -> T? {
return filter(T.self) { $0.id == id }.first
Expand Down
40 changes: 31 additions & 9 deletions Source/KakapoServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import Foundation

/**
A server that conforms to NSURLProtocol in order to intercept outgoing network communication.
You shouldn't use this class directly but register a `Router` instead. Since frameworks like **AFNetworking** and **Alamofire** require manual registration of the `NSURLProtocol` classes you will need to register this class when needed.
A server that conforms to `NSURLProtocol` in order to intercept outgoing network communication.
You shouldn't use this class directly but register a `Router` instead.
Since frameworks like **AFNetworking** and **Alamofire** require manual registration of the `NSURLProtocol` classes
you will need to register this class when needed.
### Examples
Expand All @@ -37,15 +39,24 @@ import Foundation
```
*/
public final class KakapoServer: NSURLProtocol {

private static var routers: [Router] = []

/**
`true`, if the `request` of the `KakapoServer` instance has been cancelled, otherwise `false`.
Default: `false`
Note: calls to `stopLoading()` will set this value to `true`
*/
private(set) var requestCancelled:Bool = false

/**
Register and return a new Router in the Server
- parameter baseURL: The base URL that this Router will use
- returns: An new initializcaRouter objects can hold the same baseURL.
- returns: A newly initialized Router object, which is configured to use the `baseURL`.
*/
class func register(baseURL: String) -> Router {
NSURLProtocol.registerClass(self)
Expand Down Expand Up @@ -74,14 +85,19 @@ public final class KakapoServer: NSURLProtocol {
}

/**
KakapoServer checks if the given request matches any of the registered routes and determines if the request should be intercepted
`KakapoServer` checks if the given request matches any of the registered routes
and determines if the request should be intercepted.
Note: If this method returns `true`, then the OS will create a new `KakapoServer` instance for the `request`
via the `init(request:cachedResponse:client:)` initializer. So, this `request` is the same, which
we'll have access to later on in the `startLoading()` and `stopLoading()` methods.
- parameter request: A request
- returns: true if any of the registered route match the request URL
*/
override public class func canInitWithRequest(request: NSURLRequest) -> Bool {
return routers.filter { $0.canInitWithRequest(request) }.first != nil
return routers.indexOf({ $0.canInitWithRequest(request) }) != nil
}

/// Just returns the given request without changes
Expand All @@ -91,11 +107,17 @@ public final class KakapoServer: NSURLProtocol {

/// Start loading the matched requested, the route handler will be called and the returned object will be serialized.
override public func startLoading() {
KakapoServer.routers.filter { $0.canInitWithRequest(request) }.first!.startLoading(self)
if requestCancelled {
return
}

if let routerIndex = KakapoServer.routers.indexOf({ $0.canInitWithRequest(request) }) {
KakapoServer.routers[routerIndex].startLoading(self)
}
}

/// Not implemented yet, does nothing ATM. https://github.com/devlucky/Kakapo/issues/88
/// Stops the loading of the matched request.
override public func stopLoading() {
/* TODO: implement stopLoading for delayed requests https://github.com/devlucky/Kakapo/issues/88 */
requestCancelled = true
}
}
8 changes: 4 additions & 4 deletions Source/RouteMatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Foundation
public typealias URLInfo = (components: [String : String], queryParameters: [NSURLQueryItem])

/**
Match a route and a requestURL. A route is composed by a baseURL and a path, togheter they should match the given requestURL.
Match a route and a requestURL. A route is composed by a baseURL and a path, together they should match the given requestURL.
To match a route the baseURL must be contained in the requestURL, the substring of the requestURL following the baseURL then is tested against the path to check if they match.
A baseURL can contain a scheme, and the requestURL must match the scheme; if it doesn't contain a scheme then the baseURL is a wildcard and will be matched by any subdomain or any scheme:
Expand All @@ -23,7 +23,7 @@ public typealias URLInfo = (components: [String : String], queryParameters: [NSU
- base: `kakapo.com`, path: "any", requestURL: "https://kakapo.com/any" ✅
- base: `kakapo.com`, path: "any", requestURL: "https://api.kakapo.com/any" ✅
A path can contain wildcard components prefixed with ":" (e.g. /users/:userid) that are used to build the component dictionary, the wildcard is then used as key and the repsective component of the requestURL is used as value.
A path can contain wildcard components prefixed with ":" (e.g. /users/:userid) that are used to build the component dictionary, the wildcard is then used as key and the respective component of the requestURL is used as value.
Any component that is not a wildcard have to be exactly the same in both the path and the request, otherwise the route won't match.
- `/users/:userid` and `/users/1234` ✅ -> `[userid: 1234]`
Expand All @@ -35,7 +35,7 @@ public typealias URLInfo = (components: [String : String], queryParameters: [NSU
- parameter path: The path of the request, can contain wildcards components prefixed with ":" (e.g. /users/:id/)
- parameter requestURL: The URL of the request (e.g. https://kakapo.com/api/users/1234)
- returns: A URL info object containing `components` and `queryParamaters` or nil if `requestURL`doesn't match the route.
- returns: A URL info object containing `components` and `queryParameters` or nil if `requestURL`doesn't match the route.
*/
func matchRoute(baseURL: String, path: String, requestURL: NSURL) -> URLInfo? {

Expand Down Expand Up @@ -91,7 +91,7 @@ private extension String {
}

/**
Retrun the substring From/To a given string or nil if the string is not contained.
Return the substring From/To a given string or nil if the string is not contained.
- **From**: return the substring following the given string (e.g. `kakapo.com/users`, `kakapo.com` -> `/users`)
- **To**: return the substring preceding the given string (e.g. `kakapo.com/users?a=b`, `?` -> `kakapo.com/users`)
*/
Expand Down
47 changes: 26 additions & 21 deletions Source/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public struct Request {

/**
A protocol to adopt when a `Serializable object needs to also provide response status code and/or headerFields
For example you may use `Response` to wrap your `Serializable` object to just achieve the result or directly implement the protocol. For examply `JSONAPISerializer` implement the protocol in order to be able to provide custom status code in the response.
For example you may use `Response` to wrap your `Serializable` object to just achieve the result or directly implement the protocol.
For example `JSONAPISerializer` implement the protocol in order to be able to provide custom status code in the response.
*/
public protocol ResponseFieldsProvider: CustomSerializable {
/// The response status code
Expand All @@ -59,7 +60,7 @@ extension ResponseFieldsProvider {
/**
A ResponseFieldsProvider implementation which can be used in `RouteHandlers` to provide valid responses that can return different status code than the default (200) or headerFields.
The struct provides, appart from a Serializable `body` object, a status code and header fields.
The struct provides, apart from a Serializable `body` object, a status code and header fields.
*/
public struct Response: ResponseFieldsProvider {
/// The response status code
Expand All @@ -72,11 +73,11 @@ public struct Response: ResponseFieldsProvider {
public let headerFields: [String : String]?

/**
Initialize `Response` object that wraps another `Serializable` object for the serialization but, implemententing `ResponseFieldsProvider` can affect some parameters of the HTTP response
Initialize `Response` object that wraps another `Serializable` object for the serialization but, implementing `ResponseFieldsProvider` can affect some parameters of the HTTP response
- parameter statusCode: the status code that the response should provide to the HTTP repsonse
- parameter statusCode: the status code that the response should provide to the HTTP response
- parameter body: the body that will be serialized
- parameter headerFields: the headerFields that the response should provide to the HTTP repsonse
- parameter headerFields: the headerFields that the response should provide to the HTTP response
- returns: A wrapper `Serializable` object that affect http requests.
*/
Expand All @@ -101,17 +102,17 @@ public final class Router {
}

private var routes: [String : Route] = [:]

/// The `baseURL` of the Router
public let baseURL: String

/// The desired latency to delay the mocked responses. Default value is 0.
/// The desired latency (in seconds) to delay the mocked responses. Default value is 0.
public var latency: NSTimeInterval = 0



/**
Register a new Router in the KakapoServer.
The baseURL can contain a scheme, and the requestURL must match the scheme; if it doesn't contain a scheme then the baseURL is a wildcard and will be matched by any subdomain or any scheme:
Register a new Router in the `KakapoServer`.
The `baseURL` can contain a scheme, and the requestURL must match the scheme; if it doesn't contain a scheme then
the `baseURL` is a wildcard and will be matched by any subdomain or any scheme:
- base: `http://kakapo.com`, path: "any", requestURL: "http://kakapo.com/any" ✅
- base: `http://kakapo.com`, path: "any", requestURL: "https://kakapo.com/any" ❌ because it's **https**
Expand Down Expand Up @@ -172,7 +173,7 @@ public final class Router {
return false
}

func startLoading(server: NSURLProtocol) {
func startLoading(server: KakapoServer) {
guard let requestURL = server.request.URL,
client = server.client else { return }

Expand All @@ -185,8 +186,9 @@ public final class Router {
// If the request body is nil use `NSURLProtocol` property see swizzling in `NSMutableURLRequest.m`
// using a literal string because a bridging header in the podspec will be more problematic.
let dataBody = server.request.HTTPBody ?? NSURLProtocol.propertyForKey("kkp_requestHTTPBody", inRequest: server.request) as? NSData

serializableObject = route.handler(Request(components: info.components, queryParameters: info.queryParameters, HTTPBody: dataBody, HTTPHeaders: server.request.allHTTPHeaderFields))

let request = Request(components: info.components, queryParameters: info.queryParameters, HTTPBody: dataBody, HTTPHeaders: server.request.allHTTPHeaderFields)
serializableObject = route.handler(request)
break
}
}
Expand All @@ -210,14 +212,17 @@ public final class Router {

let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(latency * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue()) {
didFinishLoading(server)
// before reporting "finished", check if request has been canceled in the meantime
if server.requestCancelled == false {
didFinishLoading(server)
}
}
}

/**
Registers a GET request with the given path.
The path is used togheter with the `Router.baseURL` to match requests. It can contain wildcard components prefixed by ":" that are later used to retreive the components of the request:
The path is used together with the `Router.baseURL` to match requests. It can contain wildcard components prefixed by ":" that are later used to retrieve the components of the request:
- "/users/:userid" and "/users/1234" will produce [userid: 1234]
Expand All @@ -239,7 +244,7 @@ public final class Router {
/**
Registers a POST request with the given path
The path is used togheter with the `Router.baseURL` to match requests. It can contain wildcard components prefixed by ":" that are later used to retreive the components of the request:
The path is used together with the `Router.baseURL` to match requests. It can contain wildcard components prefixed by ":" that are later used to retrieve the components of the request:
- "/users/:userid" and "/users/1234" will produce [userid: 1234]
Expand All @@ -261,7 +266,7 @@ public final class Router {
/**
Registers a DEL request with the given path
The path is used togheter with the `Router.baseURL` to match requests. It can contain wildcard components prefixed by ":" that are later used to retreive the components of the request:
The path is used together with the `Router.baseURL` to match requests. It can contain wildcard components prefixed by ":" that are later used to retrieve the components of the request:
- "/users/:userid" and "/users/1234" will produce [userid: 1234]
Expand All @@ -283,7 +288,7 @@ public final class Router {
/**
Registers a PUT request with the given path
The path is used togheter with the `Router.baseURL` to match requests. It can contain wildcard components prefixed by ":" that are later used to retreive the components of the request:
The path is used together with the `Router.baseURL` to match requests. It can contain wildcard components prefixed by ":" that are later used to retrieve the components of the request:
- "/users/:userid" and "/users/1234" will produce [userid: 1234]
Expand All @@ -301,5 +306,5 @@ public final class Router {
public func put(path: String, handler: RouteHandler) {
routes[path] = (.PUT, handler)
}

}
Loading

0 comments on commit 28abbfa

Please sign in to comment.