Wednesday, August 8, 2018

Building an Oxford API Client: Defining Delegate Methods for Handling the Server Response

In the previous section, we defined subclasses for generating API request objects that would be able to connect to different API endpoints.  Before that, we used pseudo-code and method stubs to provide a high-level overview of how intend to implement the OxfordAPIClient class.  Here we will finish implementing all the helper methods in our OxfordAPIClient that will be needed for us to implement higher-level, public functions that will connect directly to the API and also take readable, user-friendly parameters that can be understood in the context of the requirements for each API endpoint.
Before we begin, we will define several delegate methods that will be used to handle the server response to our API request:
import Foundation

protocol OxfordDictionaryAPIDelegate{
    
    typealias JSONResponse = [String: Any]
    
    func didFailToConnectToEndpoint(withError error: Error)
   
    func didFailToGetJSONData(withHTTPResponse httpResponse: HTTPURLResponse)
    
    func didFailToSerializeJSONData(withHTTPResponse httpResponse: HTTPURLResponse)
   
    func didFinishReceivingHTTPResponse(withHTTPResponse httpResponse: HTTPURLResponse)
    
    func didFinishReceivingJSONData(withJSONResponse jsonResponse: JSONResponse, withHTTPResponse httpResponse: HTTPURLResponse)

    
}


These delegate methods will provide the callbacks for different possible server responses that occur in the course of interacting with the REST API server – from connecting to the server to receiving the final JSON response.  The delegate methods will make more sense as we start to implement them in our OxfordAPIClient class.  With that in mind, let’s go back to our OxfordAPIClient class and notice that in our class declaration we’ve indicated that the OxfordAPIClient must conform to the OxfordDictionaryAPIDelegate, which we just defined above.  Furthermore, we’ve defined a private variable for the delegate, of optional type, which by default is set to the OxfordAPIClient singleton instance in the initializer, though it can be set to other objects (e.g. our unit testing class).
class OxfordAPIClient: OxfordDictionaryAPIDelegate{
    
    static let sharedClient = OxfordAPIClient()
    
    /** Instance variables **/
    
    private var urlSession: URLSession!
    private var delegate: OxfordDictionaryAPIDelegate?
    
    private init(){
        urlSession = URLSession.shared
        delegate = self
    }

  func setOxfordDictionaryAPIClientDelegate(with apiDelegate: OxfordDictionaryAPIDelegate){
        
        self.delegate = apiDelegate
        
    }
    
    func resetDefaultDelegate(){
        self.delegate = self
    }
    
    
    ...

Since we’ve indicated in our class declaration that the OxfordAPIClass must conform to the OxfordDictionaryAPIDelegate, the compiler will provide a warning to indicate the the class has not yet implemented the delegate methods.  So let’s not keep compiler waiting.  We will define an extension to the OxfordAPIClient where we will implement all of the delegate methods, as shown below:
//MARK: ********* Conformance to DictionaryAPIClient protocol methods

extension OxfordAPIClient{
    
    /** Unable to establish a connection with the server **/
    
    internal func didFailToConnectToEndpoint(withError error: Error) {
        
        print("Error occurred while attempting to connect to the server: \(error.localizedDescription)")
        
        
    }
    
    /** Proper credentials are provided, the API request can be authenticated; an HTTP Response is received but no data is provided **/
    
    
    internal func didFailToGetJSONData(withHTTPResponse httpResponse: HTTPURLResponse){
        print("Unable to get JSON data with http status code: \(httpResponse.statusCode)")
        showOxfordStatusCodeMessage(forHTTPResponse: httpResponse)
        
        
        
    }
    
    /** Proper credentials are provided, and the API request is fully authenticated; an HTTP Response is received and the data is provided by the raw data could not be parsed into a JSON object **/
    
    internal func didFailToSerializeJSONData(withHTTPResponse httpResponse: HTTPURLResponse){
        
        print("Unable to serialize the data into a json response, http status code: \(httpResponse.statusCode)")
        showOxfordStatusCodeMessage(forHTTPResponse: httpResponse)
        
    }
    
    
    
    /** If erroneous credentials are provided, the API request can't be authenticated; an HTTP Response is received but no data is provided **/
    
    internal func didFinishReceivingHTTPResponse(withHTTPResponse httpResponse: HTTPURLResponse){
        
        print("HTTP response received with status code: \(httpResponse.statusCode)")
        showOxfordStatusCodeMessage(forHTTPResponse: httpResponse)
    }
    
    /** Proper credentials are provided, and the API request is fully authenticated; an HTTP Response is received and serialized JSON data is provided **/
    
    internal func didFinishReceivingJSONData(withJSONResponse jsonResponse: JSONResponse, withHTTPResponse httpResponse: HTTPURLResponse) {
        
        print("JSON response received, http status code: \(httpResponse.statusCode)")
        showOxfordStatusCodeMessage(forHTTPResponse: httpResponse)
        
        
        print("JSON data received as follows:")
        print(jsonResponse)
    }
    
    func showOxfordStatusCodeMessage(forHTTPResponse httpResponse: HTTPURLResponse){
        
        if let oxfordStatusCode = OxfordHTTPStatusCode(rawValue: httpResponse.statusCode){
            let statusCodeMessage = oxfordStatusCode.statusCodeMessage()
            print("Status Code Message: \(statusCodeMessage)")
        }
        
        
    }
}



You will notice that the delegate methods handle the different possible HTTP status codes that are returned from the server.  If you will recall, in aprevious tutorial, we defined an enum type to represent each of these HTTP status codes as well as instance method that will print out a status code message for a given enum case.  That is why we used the HTTP status code returned by the server to instantiate an OxfordHTTPStatusCode enum instance, which we unwrap through conditional binding in order to call the instance method statusCodeMessage so as to print out the status code message corresponding to the returned HTTP status code.
In order to understand how these delegate method are called, let’s return to the helper function startDataTask(withURLRequest:), which we briefly examined in a previous tutorial.  Here, we provide the full implementation of this method.  The delegate is first unwrapped via a guard statement, after which the appropriate methods are called on it depending on the kind of response we get from the server.  In the completionHandler for  the URLSession method dataTask(with:completionHandler), we use a switch statement to handle the different possible combination of nil values that can occur for the arguments associated with the completionHandler.  Different methods are called on the delegate based on the server response, as shown below:
 
    /** Wrapper function for executing aynchronous download of JSON data from Oxford Dictionary API **/
    
    private func startDataTask(withURLRequest request: URLRequest){
        
        guard let delegate = self.delegate else {
            fatalError("Error: no delegate specified for Oxford API download task")
        }
        
        
        
        _ = self.urlSession.dataTask(with: request, completionHandler: { data, response, error in
            
            switch (data,response,error){
            case (.some,.some,.none),(.some,.some,.some): //Able to connect to the server, data received
                
                let httpResponse = (response! as! HTTPURLResponse)
                
                
                if let jsonResponse = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! JSONResponse{
                    //Data received, and JSON serialization successful
                    
                    delegate.didFinishReceivingJSONData(withJSONResponse: jsonResponse, withHTTPResponse: httpResponse)
                    
                } else{
                    //Data received, but JSON serialization not successful
                    delegate.didFailToGetJSONData(withHTTPResponse: httpResponse)
                }
                
                break
            case (.none,.some,.none),(.none,.some,.some): //Able to connect to the server but failed to get data or received a bad HTTP Response
                if let httpResponse = (response as? HTTPURLResponse){
                    delegate.didFailToGetJSONData(withHTTPResponse: httpResponse)
                }
                break
            case (.none,.none,.some): //Unable to connect to the server, with an error received
                delegate.didFailToConnectToEndpoint(withError: error!)
                break
            case (.none,.none,.none): //Unable to connect to the server, no error received
                let error = NSError(domain: "Failed to get a response: Error occurred while attempting to connect to the server", code: 0, userInfo: nil)
                delegate.didFailToConnectToEndpoint(withError: error)
                break
            default:
                break
            }
            
        }).resume()
    }
    

When we get nil values for all of the completionHandler arguments, we instantiate an error object and call the delegate method didFailToConnectToEndpoint(withError:).  If there is no JSON response obtained or if we are unable to serialize the received JSON data, we call didFailToGetJSONData(withHTTPResponse🙂 on the delegate.
Now that we have a means of performing an asynchronous server request for a given URL request, we proceed to implement the high-level methods that will be responsible for generating URL requests that will be specific to the different API endpoints for the REST API server:
  func downloadDictionaryEntryJSONData(forWord queryWord: String, andForLanguage language:OxfordAPILanguage){
        
        if let apiRequest = OxfordAPIRequest(withQueryWord: queryWord, forRegions: nil, forLanguage: language, withFilterForDictionaryEntryLookup: nil, withQueryFilters: nil){
            
            let urlRequest = apiRequest.generateURLRequest()
            
            self.startDataTask(withURLRequest: urlRequest)
            
        } else {
            
            print("Unable to instantiate the dictionary api request")
            let error = NSError(domain: "Unable to instantiate the dictionary api request", code: 0, userInfo: nil)
            
            self.delegate?.didFailToConnectToEndpoint(withError: error)
        }

    
       
    }
    
     func downloadFilteredDictionaryEntryJSONData(forWord word: String, andForEntryFilter entryFilter: DictionaryLookupFilter){
        
        let apiFilter = entryFilter.getOxfordAPIFiler()
        
        downloadDictionaryEntryJSONData(forWord: word, andForEntryFilter: apiFilter)
    }

    
    
    private func downloadDictionaryEntryJSONData(forWord word: String, andForEntryFilter entryFilter: OxfordAPIEndpoint.OxfordAPIFilter){
        
        if let apiRequest = OxfordAPIRequest(withWord: word, withDictionaryEntryFilter: entryFilter){
            
            let urlRequest = apiRequest.generateURLRequest()
            
            self.startDataTask(withURLRequest: urlRequest)
            
        } else {
            
            print("Unable to instantiate the dictionary api request")
            let error = NSError(domain: "Unable to instantiate the dictionary api request", code: 0, userInfo: nil)
            
            self.delegate?.didFailToConnectToEndpoint(withError: error)
        }
        
       
    }
    
    func downloadExampleSentencesJSONData(forWord word: String){
        
        let apiRequest = OxfordSentencesAPIRequest(withQueryWord: word)
        
        print("The url string for this request is: \(apiRequest.getURLString())")
        
        let urlRequest = apiRequest.generateURLRequest()
        
        self.startDataTask(withURLRequest: urlRequest)
    }
    
    func downloadAPIUtilityRequestJSONData(forRESTEndpoint endpoint: OxfordUtilityAPIRequest.UtiltyRequestEndpoint, andWithTargetLanguage targetLanguage: OxfordAPILanguage?, andWithEndpointForAllFiltersRequest endpointForAllFiltersRequest: OxfordAPIEndpoint){
        
        let apiRequest = OxfordUtilityAPIRequest(withUtilityRequestEndpoint: endpoint, andWithTargetLanguage: targetLanguage, andWithEndpointForAllFiltersRequest: endpointForAllFiltersRequest)
        
        print("The url string generated from this request is: \(apiRequest.getURLString())")
        
        let urlRequest = apiRequest.generateURLRequest()
        
        self.startDataTask(withURLRequest: urlRequest)
    }
    
    func downloadThesaurusJSONData(forWord word: String, withAntonyms isAntonymRequest: Bool, withSynonyms isSynonymRequest: Bool){
        
        let apiRequest = OxfordThesaurusAPIRequest(withWord: word, isAntonymRequest: isAntonymRequest, isSynonymRequest: isSynonymRequest)
        
        print("The url string generated from this api request is: \(apiRequest.getURLString())")
        
        let urlRequest = apiRequest.generateURLRequest()
        
        self.startDataTask(withURLRequest: urlRequest)
    }
    
    
    
    


As you can see, each of these methods instantiates a different OxfordAPIRequest subclass, which in turn is used to generate a corresponding URLRequest object, which in turn is passed into thestartDataTask(withURLRequest:) method.
Back in our ViewController.swift file, in the viewDidLoad function, we can call our OxfordAPIClient methods.  For example, in the code shown below, we call the method downloadThesaurusJSONData(forWord:withAntonyms: withSynonyms:):
import UIKit

class ViewController: UIViewController {

    let client = OxfordAPIClient.sharedClient
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        client.downloadThesaurusJSONData(forWord: "justice", withAntonyms: true, withSynonyms: true)
    
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}


The received JSON response should be as show below:

You can try calling the different API client methods and entering different kind of parameters to see what kind of JSON responses you get.   You can also compare JSON result shown in the console with that obtained using Postman Now that we have a user-friendly interface for interacting the OxfordDictionaryAPI, we can go on to develop data models and helper classes that can parse the returned JSON data, but that will be a task for another day.

No comments:

Post a Comment