Skip to content

Commit 0818ad9

Browse files
author
NobodyNada
authored
Merge pull request #4 from NobodyNada/feature/apiresponse-class
Feature/apiresponse class
2 parents c23f66f + 09239c6 commit 0818ad9

11 files changed

Lines changed: 279 additions & 35 deletions

Sources/APIClient.swift

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ open class APIClient: NSObject, URLSessionDataDelegate {
4646
///Which API filter to use if none is specified.
4747
open var defaultFilter: String?
4848

49+
///Whether to use a secure connection to communicate with the API.
50+
open var useSSL: Bool = true
51+
4952
///Which site to use if none is specified.
5053
open var defaultSite: String = "stackoverflow"
5154

@@ -91,11 +94,11 @@ open class APIClient: NSObject, URLSessionDataDelegate {
9194
///
9295
///- parameter request: The request to make, for example `users/{ids}/answers`.
9396
///- parameter parameters: Parameters to be URLEncoded into the request.
94-
open func performAPIRequest(
97+
open func performAPIRequest<T>(
9598
_ request: String,
9699
_ parameters: [String:String] = [:],
97100
backoffBehavior: BackoffBehavior = .wait
98-
) throws -> [Any] {
101+
) throws -> APIResponse<T> {
99102

100103
var params = parameters
101104

@@ -111,9 +114,12 @@ open class APIClient: NSObject, URLSessionDataDelegate {
111114
if params["site"] == nil {
112115
params["site"] = defaultSite
113116
}
117+
else if params["site"] == "" {
118+
params["site"] = nil
119+
}
114120

115121
//Build the URL.
116-
var url = "https://api.stackexchange.com/2.2"
122+
var url = "\(useSSL ? "http" : "https")://api.stackexchange.com/2.2"
117123

118124
let prefixedRequest = (request.hasPrefix("/") ? request : "/" + request)
119125
url += prefixedRequest
@@ -151,19 +157,22 @@ open class APIClient: NSObject, URLSessionDataDelegate {
151157
throw APIError.notDictionary(response: response)
152158
}
153159

154-
if let backoff = json["backoff"] as? Int {
160+
let apiResponse = APIResponse<T>(dictionary: json)
161+
162+
if let backoff = apiResponse.backoff {
155163
backoffs[backoffName] = Date().addingTimeInterval(TimeInterval(backoff))
156164
}
157165
cleanBackoffs()
158166

159-
guard json["error_id"] == nil, json["error_message"] == nil else {
167+
guard apiResponse.error_id == nil, apiResponse.error_message == nil else {
160168
throw APIError.apiError(id: json["error_id"] as? Int, message: json["error_message"] as? String)
161169
}
162170

163-
maxQuota = (json["quota_max"] as? Int) ?? maxQuota
164-
quota = (json["quota_remaining"] as? Int) ?? quota
165171

166-
return (json["items"] as? [Any]) ?? []
172+
maxQuota = apiResponse.quota_max ?? maxQuota
173+
quota = apiResponse.quota_remaining ?? quota
174+
175+
return apiResponse
167176
}
168177

169178
internal func wait(until date: Date) {

Sources/APIResponse.swift

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//
2+
// APIResponse.swift
3+
// SwiftStack
4+
//
5+
// Created by FelixSFD on 11.12.16.
6+
//
7+
//
8+
9+
import Foundation
10+
11+
/**
12+
The common wrapper object that is returned by the StackExchange API.
13+
14+
- authors: FelixSFD, NobodyNada
15+
16+
- seealso: [StackExchange API](https://api.stackexchange.com/docs/wrapper)
17+
*/
18+
public class APIResponse<T: JsonConvertible>: JsonConvertible {
19+
20+
// - MARK: Items
21+
22+
/**
23+
The items that are returned of the generic type `T`.
24+
25+
- note: It's always an array. Even if only a single item was requested.
26+
27+
- author: FelixSFD
28+
*/
29+
public var items: [T]?
30+
31+
32+
// - MARK: Initializers
33+
34+
/**
35+
Basic initializer without default values
36+
*/
37+
public init() {
38+
39+
}
40+
41+
public required convenience init?(jsonString json: String) {
42+
do {
43+
guard let dictionary = try JSONSerialization.jsonObject(with: json.data(using: String.Encoding.utf8)!) as? [String: Any] else {
44+
return nil
45+
}
46+
47+
self.init(dictionary: dictionary)
48+
} catch {
49+
return nil
50+
}
51+
}
52+
53+
public required init(dictionary: [String: Any]) {
54+
self.backoff = dictionary["backoff"] as? Int
55+
self.error_id = dictionary["error_id"] as? Int
56+
self.error_message = dictionary["error_message"] as? String
57+
self.error_name = dictionary["error_name"] as? String
58+
self.has_more = dictionary["has_more"] as? Bool
59+
self.page = dictionary["page"] as? Int
60+
self.page_size = dictionary["page_size"] as? Int
61+
self.quota_remaining = dictionary["quota_remaining"] as? Int
62+
self.quota_max = dictionary["quota_max"] as? Int
63+
self.total = dictionary["total"] as? Int
64+
self.type = dictionary["type"] as? String
65+
66+
67+
//items
68+
if let array = dictionary["items"] as? [[String: Any]] {
69+
items = array.map { T(dictionary: $0) }
70+
}
71+
72+
}
73+
74+
// - MARK: JsonConvertible
75+
76+
public var dictionary: [String: Any] {
77+
var dict = [String: Any]()
78+
79+
dict["backoff"] = backoff
80+
dict["error_id"] = error_id
81+
dict["error_message"] = error_message
82+
dict["error_name"] = error_name
83+
dict["has_more"] = has_more
84+
dict["page"] = page
85+
dict["page_size"] = page_size
86+
dict["quota_max"] = quota_max
87+
dict["quota_remaining"] = quota_remaining
88+
dict["total"] = total
89+
dict["type"] = type
90+
91+
return dict
92+
}
93+
94+
public var jsonString: String? {
95+
return (try? JsonHelper.jsonString(from: self)) ?? nil
96+
}
97+
98+
99+
// - MARK: Fields
100+
101+
public var backoff: Int?
102+
103+
public var error_id: Int?
104+
105+
public var error_message: String?
106+
107+
public var error_name: String?
108+
109+
public var has_more: Bool?
110+
111+
public var page: Int?
112+
113+
public var page_size: Int?
114+
115+
public var quota_max: Int?
116+
117+
public var quota_remaining: Int?
118+
119+
public var total: Int?
120+
121+
public var type: String?
122+
123+
}

Sources/BadgeCount.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,9 @@ public struct BadgeCount: JsonConvertible, CustomStringConvertible {
8686

8787
- author: FelixSFD
8888
*/
89-
public init?(dictionary: [String: Any]) {
89+
public init(dictionary: [String: Any]) {
9090
self.bronze = dictionary["bronze"] as? Int
9191
self.silver = dictionary["silver"] as? Int
9292
self.gold = dictionary["gold"] as? Int
93-
94-
if bronze == nil, silver == nil, gold == nil {
95-
return nil
96-
}
9793
}
9894
}

Sources/DictionaryConvertible.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public protocol DictionaryConvertible {
1717
/**
1818
Initializes the object from a dictionary
1919
*/
20-
init?(dictionary: [String: Any])
20+
init(dictionary: [String: Any])
2121

2222
/**
2323
Returns the dictionary-representation of the object

Sources/Question.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public class Question: Post {
129129
}
130130
}
131131

132-
public init?(dictionary: [String : Any]) {
132+
public init(dictionary: [String : Any]) {
133133
if let timestamp = dictionary["on_date"] as? Double {
134134
self.on_date = Date(timeIntervalSince1970: timestamp)
135135
}

Sources/RequestsSites.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// RequestsSites.swift
3+
// SwiftStack
4+
//
5+
// Created by FelixSFD on 11.12.16.
6+
//
7+
//
8+
9+
import Foundation
10+
import Dispatch
11+
12+
/**
13+
This extension contains all requests in the SITES section of the StackExchange API Documentation.
14+
15+
- authors: FelixSFD, NobodyNada
16+
*/
17+
public extension APIClient {
18+
19+
// - MARK: /sites
20+
/**
21+
Fetches all `Sites` in the Stack Exchange network synchronously.
22+
23+
- parameter parameters: The dictionary of parameters used in the request
24+
25+
- parameter backoffBehavior: The behavior when an APIRequest has a backoff
26+
27+
- returns: The list of sites as `APIResponse<Site>`
28+
29+
- author: NobodyNada
30+
*/
31+
public func fetchSites(
32+
_ parameters: [String:String] = [:],
33+
backoffBehavior: BackoffBehavior = .wait) throws -> APIResponse<Site> {
34+
35+
36+
var params = parameters
37+
params["site"] = ""
38+
39+
return try performAPIRequest(
40+
"sites",
41+
params,
42+
backoffBehavior: backoffBehavior
43+
)
44+
}
45+
46+
/**
47+
Fetches all `Sites` in the Stack Exchange network asynchronously.
48+
49+
- parameter parameters: The dictionary of parameters used in the request
50+
51+
- parameter backoffBehavior: The behavior when an APIRequest has a backoff
52+
53+
- author: FelixSFD
54+
*/
55+
public func fetchSites(_ parameters: [String: String] = [:], backoffBehavior: BackoffBehavior = .wait, completionHandler: @escaping (APIResponse<Site>?, Error?) -> ()) {
56+
57+
queue.async {
58+
var params = parameters
59+
params["site"] = ""
60+
61+
do {
62+
let response: APIResponse<Site> = try self.performAPIRequest("sites", params, backoffBehavior: backoffBehavior)
63+
completionHandler(response, nil)
64+
} catch {
65+
completionHandler(nil, error)
66+
}
67+
}
68+
}
69+
70+
}

Sources/Site.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public class Site: JsonConvertible {
3737

3838
// - MARK: Initializers
3939

40-
public init?(dictionary: [String : Any]) {
40+
public init(dictionary: [String : Any]) {
4141
self.api_site_parameter = dictionary["api_site_parameter"] as? String
4242
self.name = dictionary["name"] as? String
4343

@@ -140,7 +140,7 @@ public class Site: JsonConvertible {
140140

141141
// - MARK: Initializers
142142

143-
public init?(dictionary: [String : Any]) {
143+
public init(dictionary: [String : Any]) {
144144
self.link_color = dictionary["link_color"] as? String
145145
self.tag_background_color = dictionary["tag_background_color"] as? String
146146
self.tag_foreground_color = dictionary["tag_foreground_color"] as? String
@@ -203,7 +203,7 @@ public class Site: JsonConvertible {
203203

204204
}
205205

206-
public required init?(dictionary: [String : Any]) {
206+
public required init(dictionary: [String : Any]) {
207207
self.aliases = dictionary["aliases"] as? [String]
208208
self.api_site_parameter = dictionary["api_site_parameter"] as? String
209209
self.audience = dictionary["audience"] as? String
@@ -243,9 +243,7 @@ public class Site: JsonConvertible {
243243
var relatedArray = [Related]()
244244

245245
for related in relatedSites {
246-
if let tmp = Related(dictionary: related) {
247-
relatedArray.append(tmp)
248-
}
246+
relatedArray.append(Related(dictionary: related))
249247
}
250248

251249
if relatedArray.count > 0 {

Sources/User.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,7 @@ public class User: JsonConvertible, CustomStringConvertible {
109109
self.answer_count = dictionary["answer_count"] as? Int
110110

111111
if let badgeCounts = dictionary["badge_counts"] as? [String: Any] {
112-
if let badges = BadgeCount(dictionary: badgeCounts) {
113-
self.badge_counts = badges
114-
}
112+
self.badge_counts = BadgeCount(dictionary: badgeCounts)
115113
}
116114

117115
if let creationTimestamp = dictionary["creation_date"] as? Double {

0 commit comments

Comments
 (0)