Skip to content

Commit

Permalink
Merge branch 'feature/default'
Browse files Browse the repository at this point in the history
  • Loading branch information
ishkawa committed Feb 20, 2015
2 parents 87bfb2d + b82f507 commit 4b4870f
Show file tree
Hide file tree
Showing 16 changed files with 1,022 additions and 219 deletions.
311 changes: 299 additions & 12 deletions APIKit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-Mac.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,26 @@
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Debug">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7F0869931A978790001AD3E1"
BuildableName = "APIKitTests.xctest"
BlueprintName = "APIKitTests-Mac"
ReferencedContainer = "container:APIKit.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7F45FCFD1A94D04D006863BB"
BuildableName = "APIKit.framework"
BlueprintName = "APIKit-Mac"
ReferencedContainer = "container:APIKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
</TestAction>
<LaunchAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
Expand Down
8 changes: 4 additions & 4 deletions APIKit.xcodeproj/xcshareddata/xcschemes/APIKit-iOS.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7F45FD071A94D04D006863BB"
BlueprintIdentifier = "7FEC5A131A96FE2600B1D3C0"
BuildableName = "APIKitTests.xctest"
BlueprintName = "APIKitTests"
BlueprintName = "APIKitTests-iOS"
ReferencedContainer = "container:APIKit.xcodeproj">
</BuildableReference>
</BuildActionEntry>
Expand All @@ -46,9 +46,9 @@
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "7F45FD071A94D04D006863BB"
BlueprintIdentifier = "7FEC5A131A96FE2600B1D3C0"
BuildableName = "APIKitTests.xctest"
BlueprintName = "APIKitTests"
BlueprintName = "APIKitTests-iOS"
ReferencedContainer = "container:APIKit.xcodeproj">
</BuildableReference>
</TestableReference>
Expand Down
107 changes: 104 additions & 3 deletions APIKit/APIKit.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,115 @@
import Foundation
import LlamaKit

public let APIKitErrorDomain = "APIKitErrorDomain"

public protocol Request {
typealias Response: Any

var URLRequest: NSURLRequest { get }
var URLRequest: NSURLRequest? { get }

func responseFromObject(object: AnyObject) -> Response?
}

public protocol API {
class func sendRequest<T: Request>(request: T, handler: (Result<T.Response, NSError>) -> Void)
public enum Method: String {
case GET = "GET"
case POST = "POST"
case PUT = "PUT"
case HEAD = "HEAD"
case DELETE = "DELETE"
case PATCH = "PATCH"
case TRACE = "TRACE"
case OPTIONS = "OPTIONS"
case CONNECT = "CONNECT"
}

public class API {
// configurations
public class func baseURL() -> NSURL {
return NSURL()
}

public class func URLSession() -> NSURLSession {
return NSURLSession.sharedSession()
}

public class func requestBodyBuilder() -> RequestBodyBuilder {
return .JSON(nil)
}

public class func responseBodyParser() -> ResponseBodyParser {
return .JSON(nil)
}

// build NSURLRequest
public class func URLRequest(method: Method, _ path: String, _ parameters: [String: AnyObject] = [:]) -> NSURLRequest? {
if let components = NSURLComponents(URL: baseURL(), resolvingAgainstBaseURL: true) {
let request = NSMutableURLRequest()

switch method {
case .GET, .HEAD, .DELETE:
components.query = URLEncodedSerialization.stringFromObject(parameters, encoding: NSUTF8StringEncoding)

default:
switch requestBodyBuilder().buildBodyFromObject(parameters) {
case .Success(let box):
request.HTTPBody = box.unbox

case .Failure(let box):
return nil
}
}

components.path = (components.path ?? "").stringByAppendingPathComponent(path)
request.URL = components.URL
request.HTTPMethod = method.rawValue

return request
} else {
return nil
}
}

// send request and build response object
public class func sendRequest<T: Request>(request: T, handler: (Result<T.Response, NSError>) -> Void = {r in}) {
let session = URLSession()
let mainQueue = dispatch_get_main_queue()

if let URLRequest = request.URLRequest {
let task = session.dataTaskWithRequest(URLRequest) { data, URLResponse, connectionError in
if let error = connectionError {
dispatch_async(mainQueue, { handler(.Failure(Box(error))) })
return
}

let statusCode = (URLResponse as? NSHTTPURLResponse)?.statusCode ?? 0
if !contains(200..<300, statusCode) {
let userInfo = [NSLocalizedDescriptionKey: "received status code that represents error"]
let error = NSError(domain: APIKitErrorDomain, code: statusCode, userInfo: userInfo)
dispatch_async(mainQueue, { handler(.Failure(Box(error))) })
return
}

switch self.responseBodyParser().parseData(data) {
case .Failure(let box):
dispatch_async(mainQueue, { handler(.Failure(Box(box.unbox))) })

case .Success(let box):
if let response = request.responseFromObject(box.unbox) {
dispatch_async(mainQueue, { handler(.Success(Box(response))) })
} else {
let userInfo = [NSLocalizedDescriptionKey: "failed to create model object from raw object."]
let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo)
dispatch_async(mainQueue, { handler(.Failure(Box(error))) })
}
}
}

task.resume()
} else {
let userInfo = [NSLocalizedDescriptionKey: "failed to build request."]
let error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo)
dispatch_async(mainQueue, { handler(.Failure(Box(error))) })
}
}
}
44 changes: 44 additions & 0 deletions APIKit/RequestBodyBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Foundation
import LlamaKit

public let APIKitRequestBodyBuidlerErrorDomain = "APIKitRequestBodyBuidlerErrorDomain"

public enum RequestBodyBuilder {
case JSON(NSJSONWritingOptions)
case URL(NSStringEncoding)
case Custom(AnyObject -> Result<NSData, NSError>)

public func buildBodyFromObject(object: AnyObject) -> Result<NSData, NSError> {
var result: Result<NSData, NSError>

switch self {
case .JSON(let writingOptions):
if !NSJSONSerialization.isValidJSONObject(object) {
let userInfo = [NSLocalizedDescriptionKey: "invalidate object for JSON passed."]
let error = NSError(domain: APIKitRequestBodyBuidlerErrorDomain, code: 0, userInfo: userInfo)
result = Result.Failure(Box(error))
break
}

var error: NSError?
if let data = NSJSONSerialization.dataWithJSONObject(object, options: writingOptions, error: &error) {
result = Result.Success(Box(data))
} else {
result = Result.Failure(Box(error!))
}

case .URL(let encoding):
var error: NSError?
if let data = URLEncodedSerialization.dataFromObject(object, encoding: encoding, error: &error) {
result = Result.Success(Box(data))
} else {
result = Result.Failure(Box(error!))
}

case .Custom(let buildBodyFromObject):
result = buildBodyFromObject(object)
}

return result
}
}
35 changes: 35 additions & 0 deletions APIKit/ResponseBodyParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
import LlamaKit

public enum ResponseBodyParser {
case JSON(NSJSONReadingOptions)
case URL(NSStringEncoding)
case Custom(NSData -> Result<AnyObject, NSError>)

public func parseData(data: NSData) -> Result<AnyObject, NSError> {
var result: Result<AnyObject, NSError>

switch self {
case .JSON(let readingOptions):
var error: NSError?
if let object: AnyObject = NSJSONSerialization.JSONObjectWithData(data, options: readingOptions, error: &error) {
result = Result.Success(Box(object))
} else {
result = Result.Failure(Box(error!))
}

case .URL(let encoding):
var error: NSError?
if let object: AnyObject = URLEncodedSerialization.objectFromData(data, encoding: encoding, error: &error) {
result = Result.Success(Box(object))
} else {
result = Result.Failure(Box(error!))
}

case .Custom(let parseData):
result = parseData(data)
}

return result
}
}
60 changes: 60 additions & 0 deletions APIKit/URLEncodedSerialization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation

private func escape(string: String) -> String {
return CFURLCreateStringByAddingPercentEscapes(nil, string, nil, "!*'();:@&=+$,/?%#[]", CFStringBuiltInEncodings.UTF8.rawValue)
}

private func unescape(string: String) -> String {
return CFURLCreateStringByReplacingPercentEscapes(nil, string, nil)
}

public class URLEncodedSerialization {
public class func objectFromData(data: NSData, encoding: NSStringEncoding, inout error: NSError?) -> AnyObject? {
var dictionary: [String: AnyObject]?

if let string = NSString(data: data, encoding: encoding) as? String {
dictionary = [String: AnyObject]()

for pair in split(string, { $0 == "&" }) {
let contents = split(pair, { $0 == "=" })

if contents.count == 2 {
dictionary?[contents[0]] = unescape(contents[1])
}
}
}

if dictionary == nil {
let userInfo = [NSLocalizedDescriptionKey: "failed to decode urlencoded string."]
error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo)
}

return dictionary
}

public class func dataFromObject(object: AnyObject, encoding: NSStringEncoding, inout error: NSError?) -> NSData? {
let string = stringFromObject(object, encoding: encoding)
let data = string.dataUsingEncoding(encoding, allowLossyConversion: false)

if data == nil {
let userInfo = [NSLocalizedDescriptionKey: "failed to decode urlencoded string."]
error = NSError(domain: APIKitErrorDomain, code: 0, userInfo: userInfo)
}

return data
}

public class func stringFromObject(object: AnyObject, encoding: NSStringEncoding) -> String {
var pairs = [String]()

if let dictionary = object as? [String: AnyObject] {
for (key, value) in dictionary {
let string = (value as? String) ?? "\(value)"
let pair = "\(key)=\(escape(string))"
pairs.append(pair)
}
}

return join("&", pairs)
}
}
36 changes: 36 additions & 0 deletions APIKitTests/APIKitTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// APIKitTests.swift
// APIKitTests
//
// Created by Yosuke Ishikawa on 2/21/15.
// Copyright (c) 2015 Yosuke Ishikawa. All rights reserved.
//

import Cocoa
import XCTest

class APIKitTests: XCTestCase {

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}

func testExample() {
// This is an example of a functional test case.
XCTAssert(true, "Pass")
}

func testPerformanceExample() {
// This is an example of a performance test case.
self.measureBlock() {
// Put the code you want to measure the time of here.
}
}

}
24 changes: 24 additions & 0 deletions APIKitTests/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>-.$(PRODUCT_NAME:rfc1034identifier)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
Loading

0 comments on commit 4b4870f

Please sign in to comment.