Skip to content

Commit

Permalink
Network layer refactoring (#262)
Browse files Browse the repository at this point in the history
* Added APIEndpoint inheritance to all network classes

* working implementation

* working adapter

* Updated JSONSerializable & added generic update() method

* More update refactoring

* Added generic delete request

* Added create() method

* Create comment reworked

* Added views create() method

* minor

* refactored create() for attempts and submissions

* Added retrieve() for one object by id

* Added retrieve with parameters

* minor

* Added retrieve() with fetch to NotificationsAPI

* minor

* Refactored UnitsAPI

* multiple fixes

* Refactored UserActivitiesAPI

* Refactor DiscussionProxiesAPI

* Refactored getObjectsByIds

* Refactored CommentsAPI

* Added deprecations to CommentsAPI

* Added default implementation of async fetch using DatabaseFetchService

* fixed GET requests & added deprecations to CoursesAPI

* Refactored CertificatesAPI

* Refactored CourseListsAPI

* Refactored NotificationsStatusesAPI

* fixed formatting

* Removed some checkToken() calls

* Fixed JSONSerializables

* AsyncFetchService -> DatabaseFetchService

* Cleaned up UserActivitiesAPI

* minor format

* fixed force unwrap

* Removed forced unwraps

* Renamed IdType & some more

* Added CoreDataRepresentable protocol for IdType of IDFetchable

* fixed "Submit" localization

* Deleted not passing tests file

* minor
  • Loading branch information
Ostrenkiy authored Apr 3, 2018
1 parent 583d669 commit e12221b
Show file tree
Hide file tree
Showing 79 changed files with 1,136 additions and 1,293 deletions.
116 changes: 69 additions & 47 deletions Stepic.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@
<key>orderHint</key>
<integer>86</integer>
</dict>
<key>Adaptive GMAT.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>95</integer>
</dict>
<key>SberbankUniversity.xcscheme</key>
<dict>
<key>isShown</key>
Expand Down Expand Up @@ -154,7 +159,7 @@
<key>StepikTV.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>87</integer>
<integer>96</integer>
</dict>
<key>StickerPackExtension.xcscheme</key>
<dict>
Expand Down
141 changes: 27 additions & 114 deletions Stepic/APIEndpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,23 @@ class APIEndpoint {

let manager: Alamofire.SessionManager

var update: UpdateRequestMaker
var delete: DeleteRequestMaker
var create: CreateRequestMaker
var retrieve: RetrieveRequestMaker

init() {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 15
manager = Alamofire.SessionManager(configuration: configuration)
let retrier = ApiRequestRetrier()
manager.retrier = retrier
manager.adapter = retrier

update = UpdateRequestMaker()
delete = DeleteRequestMaker()
create = CreateRequestMaker()
retrieve = RetrieveRequestMaker()
}

func cancelAllTasks() {
Expand All @@ -60,123 +73,23 @@ class APIEndpoint {
return result
}

func getObjectsByIds<T: JSONInitializable>(ids: [T.idType], updating: [T], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, printOutput: Bool = false) -> Promise<([T])> {
let name = self.name
return Promise<([T])> {
fulfill, reject in
let params: Parameters = [
"ids": ids
]

manager.request("\(StepicApplicationsInfo.apiURL)/\(name)", parameters: params, encoding: URLEncoding.default, headers: headers).validate().responseSwiftyJSON { response in
switch response.result {

case .failure(let error):
reject(RetrieveError(error: error))

case .success(let json):
let jsonArray: [JSON] = json[name].array ?? []
let resultArray: [T] = jsonArray.map {
objectJSON in
if let recoveredIndex = updating.index(where: { $0.hasEqualId(json: objectJSON) }) {
updating[recoveredIndex].update(json: objectJSON)
return updating[recoveredIndex]
} else {
return T(json: objectJSON)
}
}

CoreDataHelper.instance.save()
fulfill((resultArray))
}

}
}
//TODO: Remove this in next refactoring iterations
func getObjectsByIds<T: JSONSerializable>(ids: [T.IdType], updating: [T], printOutput: Bool = false) -> Promise<([T])> {
return retrieve.request(requestEndpoint: name, paramName: name, ids: ids, updating: updating, withManager: manager)
}

func getObjectsByIds<T: JSONInitializable>(requestString: String, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, printOutput: Bool = false, ids: [T.idType], deleteObjects: [T], refreshMode: RefreshMode, success: (([T]) -> Void)?, failure : @escaping (_ error: RetrieveError) -> Void) -> Request? {

let params: Parameters = [:]

let idString = constructIdsString(array: ids)
if idString == "" {
success?([])
return nil
}

return manager.request("\(StepicApplicationsInfo.apiURL)/\(requestString)?\(idString)", parameters: params, encoding: URLEncoding.default, headers: headers).responseSwiftyJSON({
response in

var error = response.result.error
var json: JSON = [:]
if response.result.value == nil {
if error == nil {
error = NSError()
}
} else {
json = response.result.value!
}
let response = response.response

if printOutput {
print(json)
}

if let e = error as NSError? {
print("RETRIEVE \(requestString)?\(ids): error \(e.domain) \(e.code): \(e.localizedDescription)")
if e.code == -999 {
failure(.cancelled)
return
} else {
failure(.connectionError)
return
}
}

if response?.statusCode != 200 {
print("RETRIEVE \(requestString)?\(ids)): bad response status code \(String(describing: response?.statusCode))")
failure(.badStatus)
func getObjectsByIds<T: JSONSerializable>(requestString: String, printOutput: Bool = false, ids: [T.IdType], deleteObjects: [T], refreshMode: RefreshMode, success: (([T]) -> Void)?, failure : @escaping (_ error: RetrieveError) -> Void) -> Request? {
getObjectsByIds(ids: ids, updating: deleteObjects).then {
objects in
success?(objects)
}.catch {
error in
guard let e = error as? RetrieveError else {
failure(RetrieveError(error: error))
return
}

var newObjects: [T] = []

switch refreshMode {

case .delete:

for object in deleteObjects {
CoreDataHelper.instance.deleteFromStore(object as! NSManagedObject, save: false)
}

for objectJSON in json[requestString].arrayValue {
newObjects += [T(json: objectJSON)]
}

case .update:

for objectJSON in json[requestString].arrayValue {
let existing = deleteObjects.filter({obj in obj.hasEqualId(json: objectJSON)})

switch existing.count {
case 0:
newObjects += [T(json: objectJSON)]
case 1:
let obj = existing[0]
obj.update(json: objectJSON)
newObjects += [obj]
default:
//TODO: Fix this in the next releases! We have some problems with deleting entities from CoreData
let obj = existing[0]
obj.update(json: objectJSON)
newObjects += [obj]
print("More than 1 object with the same id!")
}
}
}

CoreDataHelper.instance.save()
success?(newObjects)
})
failure(e)
}
return nil
}
}
1 change: 1 addition & 0 deletions Stepic/AdaptiveRatingsAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Alamofire
import SwiftyJSON
import PromiseKit

//TODO: Better refactor this to two classes
class AdaptiveRatingsAPI: APIEndpoint {
override var name: String { return "rating" }
var restoreName: String { return "rating-restore" }
Expand Down
34 changes: 34 additions & 0 deletions Stepic/ApiRequestRetrier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// ApiRequestRetrier.swift
// Stepic
//
// Created by Ostrenkiy on 18.03.2018.
// Copyright © 2018 Alex Karpov. All rights reserved.
//
import Foundation
import Alamofire
import PromiseKit

class ApiRequestRetrier: RequestRetrier, RequestAdapter {

func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
for (headerField, value) in AuthInfo.shared.initialHTTPHeaders {
urlRequest.setValue(value, forHTTPHeaderField: headerField)
}
return urlRequest
}

func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 && request.retryCount == 0 {
checkToken().then {
completion(true, 0.0)
}.catch {
_ in
completion(false, 0.0)
}
} else {
completion(false, 0.0)
}
}
}
4 changes: 1 addition & 3 deletions Stepic/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return
}

checkToken().then {
ApiDataDownloader.notificationsStatusAPI.retrieve()
}.then { result -> Void in
ApiDataDownloader.notificationsStatusAPI.retrieve().then { result -> Void in
NotificationsBadgesManager.shared.set(number: result.totalCount)
}.catch { _ in
print("notifications: unable to fetch badges count on launch")
Expand Down
9 changes: 3 additions & 6 deletions Stepic/Assignment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import Foundation
import CoreData
import SwiftyJSON

class Assignment: NSManagedObject, JSONInitializable {
@objc
class Assignment: NSManagedObject, JSONSerializable {

typealias idType = Int
typealias IdType = Int

convenience required init(json: JSON) {
self.init()
Expand All @@ -28,8 +29,4 @@ class Assignment: NSManagedObject, JSONInitializable {
func update(json: JSON) {
initialize(json)
}

func hasEqualId(json: JSON) -> Bool {
return id == json["id"].intValue
}
}
2 changes: 1 addition & 1 deletion Stepic/AssignmentsAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ class AssignmentsAPI: APIEndpoint {
override var name: String { return "assignments" }

@discardableResult func retrieve(ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, existing: [Assignment], refreshMode: RefreshMode, success: @escaping (([Assignment]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? {
return getObjectsByIds(requestString: name, headers: headers, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler)
return getObjectsByIds(requestString: name, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler)
}
}
41 changes: 37 additions & 4 deletions Stepic/Attempt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,45 @@
import UIKit
import SwiftyJSON

class Attempt: NSObject {
class Attempt: JSONSerializable {

var id: Int?
typealias IdType = Int

var id: Int = 0
var dataset: Dataset?
var datasetUrl: String?
var time: String?
var status: String?
var step: Int
var step: Int = 0
var timeLeft: String?
var user: Int?

func update(json: JSON) {
id = json["id"].intValue
datasetUrl = json["dataset_url"].string
time = json["time"].string
status = json["status"].string
step = json["step"].intValue
timeLeft = json["time_left"].string
user = json["user"].int
}

func hasEqualId(json: JSON) -> Bool {
return id == json["id"].int
}

init(step: Int) {
self.step = step
}

required init(json: JSON) {
self.update(json: json)
}

func initDataset(json: JSON, stepName: String) {
dataset = getDatasetFromJSON(json, stepName: stepName)
}

init(json: JSON, stepName: String) {
id = json["id"].intValue
dataset = nil
Expand All @@ -29,10 +57,15 @@ class Attempt: NSObject {
step = json["step"].intValue
timeLeft = json["time_left"].string
user = json["user"].int
super.init()
dataset = getDatasetFromJSON(json["dataset"], stepName: stepName)
}

var json: JSON {
return [
"step": step
]
}

fileprivate func getDatasetFromJSON(_ json: JSON, stepName: String) -> Dataset? {
switch stepName {
case "choice" :
Expand Down
Loading

0 comments on commit e12221b

Please sign in to comment.