Now that we have defined our EdgarAPIRequest wrapper struct as well helper classes for creating linked lists and nodes to contain individual EdgarAPIRequest objects, we proceed to define our EdgarAPIClient class, which will be responsible for connecting to the REST API and downloading the JSON-formatted SEC filing data.
class EdgarAPIClient{ typealias JSONData = [String: Any] typealias JSONTaskCompletionHandler = (JSONData?,NSError?) -> (Void) static let sharedClient = EdgarAPIClient() let kReasonForFailure = "ReasonForFailure" let kHttpStatusCode = "httpStatusCode" var session = URLSession.shared private init(){ } }
As can be seen above, the EdgarAPIClient class has private initializer and a sharedClient singleton variable that will be used for calling the class instance methods. We also define two string constants kReasonForFailure and kHttpStatusCode, which will be used for creating userInfo dicts for error handling later one. In addition, we define typealias JSONData, which will be a short hand for [String:Any] types, and a JSONTaskCompletionHandler, which will be a short hand for a closure type (JSONData?,NSError?) -> (Void) that we will use for handling the received JSON data. In addition, also note that we store a reference to the URLSession.sharedSession singleton, whose methods are used to connect to the REST API. The singleton we define here is essentially a wrapper for the URLSession.sharedSession singleton, and the functionality defined here is built upon the functionalit provided the URLSession class.
Next, we define two completion handlers that will be used for handling the received JSON data, one (i.e. persistDataCompletionHandler) for printing out the contentes of the JSON respones for debug purposes, and the other (i.e.debugCompletionHandler) for converting the received JSON data to NSManagedObjects that can be saved in the persistent store of a CoreData stack that we will use for this client.
var persistDataCompletionHandler: JSONTaskCompletionHandler = { jsonData, error in if(jsonData != nil){ print("About to save JSON data to persistent store....") //Use jsonReader to save data let jsonReader = JSONReader() jsonReader.saveData(forJSONResponseDict: jsonData!) print("Data successfully saved!") } else { print("An error occurred while attempting to get the JSON data: \(error!)") } } var debugCompletionHandler: JSONTaskCompletionHandler = { jsonData, error in if(jsonData != nil){ print("The following JSON data was obtained....") print(jsonData!) } else { print("An error occurred while attempting to get the JSON data: \(error!)") } }
The JSONReader object used in the persistDataCompletionHandler hasn't been defined yet but is basically responsible for converting the JSON data to NSManagedObjects that are stored in an NSManagedObjectContext in CoreData. It's okay to ignore this for now, as it will make sense later.
Next, we define a private helper function performURLRequest(urlRequest:withCompletionHandler:) which will take a URLRequest object and use that object to connect to the REST API endpoint, after which it executes a completion handler whose type we defined above with the alias JSONTaskCompletionHandler:
private func performURLRequest(urlRequest: URLRequest, withCompletionHandler completion: @escaping JSONTaskCompletionHandler){ let _ = session.dataTask(with: urlRequest, completionHandler: { data, response, error in guard let httpResponse = response as? HTTPURLResponse else { var userInfoDict = [String: Any]() userInfoDict[self.kReasonForFailure] = "Failed to connect to the server, no HTTP status code obtained" let error = NSError(domain: "APIRequestError", code: 0, userInfo: userInfoDict) completion(nil, error) return } guard httpResponse.statusCode == 200 else { var userInfoDict = [String: Any]() userInfoDict[self.kReasonForFailure] = "Connect to the server with a status code other than 200" let error = NSError(domain: "APIRequestError", code: 0, userInfo: userInfoDict) completion(nil, error) return } if(data != nil){ do{ let jsonData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! JSONData completion(jsonData, nil) } catch let error as NSError{ completion(nil, error) } } else { var userInfoDict = [String: Any]() userInfoDict[self.kReasonForFailure] = "Nil values obtained for JSON data" let error = NSError(domain: "APIRequestError", code: 0, userInfo: userInfoDict) completion(nil, error) } }).resume() }
As you can see below, we use guard statements to perform error handling for cases where no HTTPURLResponse is obtained or when the obtained HTTPURLResponse has a status code other than 200. We provide further levels of error handling to deal with the cases where no JSON data is obtained as well as when the obtained JSON data cannot be parsed. This is by no means the authoritative way to do error handling for server responses but it will be sufficient for our purposes here.
The private helper method above will in turn be used to define a recursive method performURLRequest(withAPIRequestNode:withCompletionHandler:) whose arguments include an APIRequestNode and a completion handler of type JSONTaskCompletionHandler. As you will notice from the body of the method, the url request corresponding to the EdgarAPIRequest contained in a given API request node is used to call the API REST endpoint, after which the next node linked to the passed-in APIRequestNode is tested for a nil value. If the current APIRequestNode node has non-nil value for the nextAPIRequestNode, then the method performURLRequest(withAPIRequestNode:withCompletionHandler) is called again recursively, taking the next APIRequestNode object as a value. If the next APIRequestNode node were nil, the the function would stop calling itself recursively. This represents the base case for this linked list.
func performURLRequest(withAPIRequestNode apiRequestNode: APIRequestNode, withCompletionHandler completion: @escaping JSONTaskCompletionHandler){ let urlRequest = apiRequestNode.getAPIRequest().getURLRequest() self.performURLRequest(urlRequest: urlRequest, withCompletionHandler: { jsonData, error in if(jsonData != nil){ completion(jsonData!,nil) } else { completion(nil,error) } if let nextAPIRequestNode = apiRequestNode.getNextAPIRequestNode(){ self.performURLRequest(withAPIRequestNode: nextAPIRequestNode, withCompletionHandler: completion) } }) }
Finally, we define two convenience methods that will call the recursive method just defined. One of these methods performLinkedListPersistDataTraverse(forAPIRequestNode:), will use the stored completion handler self.persistDataCompletionHandler to handle the JSON data obtained for each API request made for each node in the linked list being traversed via the recursive function. The other method performLinkedListDebugTraverse(forAPIRequestNode:) will use the stored completion handler self. debugCompletionHandler to print out the contents of the JSON data obtained for each API request made for each node in the linked list.
/** Recursive function that process the URL request for each node in a linked list **/
func performLinkedListPersistDataTraverse(forAPIRequestNode apiRequestNode: APIRequestNode){
self.performURLRequest(withAPIRequestNode: apiRequestNode, withCompletionHandler: self.persistDataCompletionHandler)
}
func performLinkedListDebugTraverse(forAPIRequestNode apiRequestNode: APIRequestNode){
self.performURLRequest(withAPIRequestNode: apiRequestNode, withCompletionHandler: self.debugCompletionHandler)
}
To continue, please click here
If you feel confused or are having trouble following, you can go back to the previous page or back to the table of contents table of contents.