Wednesday, August 8, 2018

Building an Oxford Dictionary API Client: Using Variations of the Factory Method to Generate API Requests

This tutorial is a follow-up to a previous tutorial, the first part of which you can view here.  You can also clone or download the source code from theGithub repository here.
For those interested in design patterns, we will discuss the implementation of the DictionaryAPIClient in terms of one of the common design patterns discussed in the work Design Patterns (by Gamma, Helm, Johnson, and Vlissides), where they mention the Factory Method in their discussion of Class Creational design patterns.  However, while working on the implementation itself, I wasn’t consciously trying to adapt such a design pattern – rather, the idea for this particular implementation developed organically in the course of solving a particular problem and not as result of rigidly applying some formula or paradigm (which is not an approach I would recommend for learning how to code).  It was only until consulting the book later that I realized the similarity with the design pattern discussed in the book.
The DictionaryAPIClient class here will act as a creator class that will rely on selective parameterization to allow the user to instantiate different “products” – that is, OxfordAPIRequests.  That is, depending on which creator method is called on the DictionaryAPIClient singleton instance, a different subclass of the OxfordAPIRequest will be instantiated.  This will allow us to subclass the OxfordAPIRequest base class in the future so to create different kinds of OxfordAPIRequest objects if we ever want to accommodate new endpoints that might be added to the REST API.
Therefore, without further ado, I will provide a skeleton of the OxfordAPIClient here with method stubs and pseudo-code so that we can get a high-level overview of how we will eventually go about implementing the OxfordAPIRequest class and the OxfordAPIRequest subclasses.
import Foundation

class OxfordAPIClient: OxfordDictionaryAPIDelegate{
    
    static let sharedClient = OxfordAPIClient()
    
    /** Instance variables **/
    
    private var urlSession: URLSession!
    private var delegate: OxfordDictionaryAPIDelegate?
    
    private init(){
        urlSession = URLSession.shared
        delegate = self
    }
    
   
/**TODO:  Call this method to instantiate an OxfordAPIRequest object, which is the base class whose subclasses are used to generate different kinds of API request objects.  Since it the base class also provides a default implementation, it can also be instantiated to generate API requests **/

    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)
            
        
       
    }

/**TODO:  Call this method to instantiate an OxfordSentenceAPIRequest object, which is a subclass of the OxfordAPIRequest base class **/

    
    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)
    }
    
    
/**TODO:  Call this method to instantiate an OxfordUtilityAPIRequest object, which is a subclass of the OxfordAPIRequest base class **/

    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)
    }
    
/**TODO:  Call this method to instantiate an OxfordThesaurusAPIRequest object, which is a subclass of the OxfordAPIRequest base class **/ 

    func downloadThesaurusJSONData(forWord word: String, withAntonyms isAntonymRequest: Bool, withSynonyms isSynonymRequest: Bool){
        
        let apiRequest = OxfordThesaurusAPIRequest(withWord: word, isAntonymRequest: isAntonymRequest, isSynonymRequest: isSynonymRequest)
       
        
        let urlRequest = apiRequest.generateURLRequest()
        
        self.startDataTask(withURLRequest: urlRequest)
    }
    
    
  
    /** Wrapper function for executing aynchronous download of JSON data from Oxford Dictionary API **/
    
    private func startDataTask(withURLRequest request: URLRequest){
       
/**TODO: Call the dataTask method on the shared URL session object to execute a URLRequest object.  Perform any error handling or parse the JSON data received from the server endpoint. **/



}




Above is rough guideline of how the OxfordAPIClient will be implemented.   Essentially, it will use the URLSession.shared singleton to make API requests, which is why the private helper function startDataTask(withURLRequest:) is defined above.  This helper method will mainly act as a wrapper for the URLSession instance method dataTask(with:completionHandler:), which has a completion handler in which arguments of type DataURLResponse, and Error are handled.
As already mentioned, a commonly used design pattern for varying the instantiation of concrete objects involves sub-classing a base class in which a factory method is defined for instantiating a certain type of object, and then allowing the subclasses of this base class to override this factory method so that each subclass will produce a different kind of base class. If the base class is concrete, then the factory method may provide a default implementation for producing a certain type of object.  If the factory method is abstract, then only the subclasses can provide actual implementations for the factory method that will produce a desired object.  Since the Oxford Dictionary REST API has several different endpoints, each of which has different query parameters that can be set to filter the returned JSON data, and in order to account for the requirements of the different endpoints and develop helper methods that are individually suited to building URL strings specific to a particular endpoint, we will define a base class (OxfordAPIRequest) whose factory method will produce a default API request (i.e. one that can access the Dictionary API, since that is the one used most frequently by users) as well as several subclasses whose factory methods will produce API requests that connect to different kinds of API endpoints (i.e.  utility endpoint, thesaurus endpoint, etc.).  This is show schematically below:


Now we can begin by defining our OxfordAPIRequest base class:
class OxfordAPIRequest{

}

We will define static constants for the app_id and app_key (for which you should substitute your own credentials, as the ones provided here are not correct) and the baseURLString, which will also be accessible via a computed property of the same name that can be accessed by the individual OxfordAPIRequest objects.  An additional computed property, baseURL, is also implemented as convenience for generating URLRequest objects.
    static let baseURLString = "https://od-api.oxforddictionaries.com/api/v1/"
     private static let appID = "acb61904"
     private static let appKey = "383d6f9739d4974fb81168976b6e991b"
    
    
    //Computed Properties
    
    private static var baseURL: URL{
        return URL(string: baseURLString)!
    }
    
    var baseURLString: String{
        return OxfordAPIRequest.baseURLString
    }
    

We will also define some properties, some optional and others that must be initialized, which correspond to parameters that are common to many of the endpoints for this REST API:
 //Properties that must be initialized
    
     var endpoint: OxfordAPIEndpoint
     var word: String
     var language: OxfordAPILanguage
    
    
    //Optional properites
    
     var filterForDictionaryEntriesLookup: OxfordAPIEndpoint.OxfordAPIFilter?
     var queryFilters: [OxfordAPIEndpoint.OxfordAPIFilter]?
     var regions: [OxfordRegion]?

Having defined the properties, we must also define a designated initializer that can be used to initialize those properties .  This particular initializer is a failable initializer – that is, if the user-provided qFilterForDictionaryEntryLookup is invalid, then a nil value is return and initialization fails to proceed. This validation is not absolutely essential for our purposes here – I’m only using it to illustrate the usefulness of failable initializers as a Swift language feature.
 /** Designated Initializer  **/
    
    init?(withQueryWord queryWord: String,forRegions qRegions: [OxfordRegion]?, forLanguage qLanguage: OxfordAPILanguage, withFilterForDictionaryEntryLookup qFilterForDictionaryEntryLookup: OxfordAPIEndpoint.OxfordAPIFilter?, withQueryFilters qFilters: [OxfordAPIEndpoint.OxfordAPIFilter]?){
        
        if let qFilterForDictionaryEntryLookup = qFilterForDictionaryEntryLookup, !OxfordAPIRequest.isValidDictionaryEntryLookUpFilter(filter: qFilterForDictionaryEntryLookup){
            
                return nil
            
        }
       
        
        self.endpoint = OxfordAPIEndpoint.entries
        self.word = queryWord
        self.regions = qRegions
        self.language = qLanguage
        self.filterForDictionaryEntriesLookup = qFilterForDictionaryEntryLookup
        self.queryFilters = qFilters
    }

Finally, our OxfordAPIRequest class will also define several helper method that will be useful in building and encoding the string parameters for the URL string needed to generate an API request object.  These helper methods are also utilized by many of the subclasses as well, so it is convenient to define them in the base class:
 func getURLStringFromAppendingQueryWord(relativeToURLString urlString: String) -> String{
        
        let encodedQueryWord = getEncodedQueryWord()
        
        return urlString.appending("\(encodedQueryWord)/")

        
    }


private func getEncodedQueryWord() -> String{
        
        //Declare mutable, local version of word parameter
        return getEncodedString(fromRawString: self.word)
    }
    
    private func getEncodedString(fromRawString rawString: String) -> String{
        
        //Declare mutable, local version of string parameter
        var copiedString = rawString
        
        //Make the string lowercased
        copiedString = copiedString.lowercased()
        
        //Add percent encoding to the query parameter
        let percentEncoded_string = copiedString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
        
        copiedString = percentEncoded_string == nil ? copiedString : percentEncoded_string!
        
        //Replace spaces with underscores
        copiedString = copiedString.replacingOccurrences(of: " ", with: "_")
        
        return copiedString
    }


The helper methods below add the language and endpoint query parameter strings to the base URL string:
    func getURLStringFromAppendingLanguageSpecifier(relativeToURLString urlString: String) -> String{
        
        
        return urlString.appending("\(self.language.rawValue)/")
    }
    
     func getURLStringFromAppendingEndpointSpecifier(relativeToURLString urlString: String) -> String{
        
        
        return urlString.appending("\(self.endpoint.rawValue)/")
        
    }
    
The helper method below takes a query parameter (i.e. String) and several corresponding query parameter values (i.e. an array of Strings) to construct a query parameter string that can be appended to the baseURL string.  Notice that this method acts by modifying a string in place rather than by appending to an existing string and returning a copy.  This is why the Stringtype is modified with an inout qualifier.
  
    func appendStringArrayElementsToURLString(forQueryParameter queryParameter: String, forParameterValues parameterValues: [String],toCurrentURLString urlString: inout String){
        
        
        var queryString = "\(queryParameter)="
            
        var paramValueStr = parameterValues.reduce(String(), { $0.appending("\($1),")})
        paramValueStr.removeLast()
        paramValueStr.append(";")
        
        queryString.append(paramValueStr)
        
        urlString = urlString.appending(queryString)
    }
    
    
    
    
    func appendStringToURLString(forQueryParameter queryParameter: String, usingParameterValue parameterValue: String,toCurrentURLString urlString: inout String){
        
        
        let concatenatedString = "\(queryParameter)=\(parameterValue);"
       
        urlString = urlString.appending(concatenatedString)
    }
    
    
    func appendBooleanToURLString(forQueryParameter queryParameter: String, usingParameterValue parameterValue: Bool,toCurrentURLString urlString: inout String){
        
        
        let parameterValString = parameterValue ? "true" : "false";
        
        let concatenatedString = "\(queryParameter)=\(parameterValString);"
        
        urlString = urlString.appending(concatenatedString)
    }

This helper method will be useful for converting an array of OxfordAPIFiltersobjects to query string parameter that can be appended to a URL string.  If you don’t remember what the OxfordAPIFilter enum is, then you can go back to the previous tutorial, where we defined custom models and data types:
 func addFilters(filters: [OxfordAPIEndpoint.OxfordAPIFilter], toURLString urlString: inout String){

        
        if(filters.isEmpty || filters.count <= 0){
            return
        }
        
        filters.forEach({
        
            let filterString = $0.getQueryParameterString(isLastQueryParameter: false)
            urlString = urlString.appending(filterString)
        })
        
        
        if let lastChar = urlString.last, lastChar == ";"{
            urlString.removeLast()
        }
       
        
    }

The getURLString method is essentially the factory method that will be overridden in each of the base classes.  Each different subclass of OxfordAPIRequest will override this method and provide its own implementation so as to generate URL strings that are tailored to the requirements of specific API endpoints:
func getURLString() -> String{
        
        
        var nextURLStr = OxfordAPIRequest.baseURLString
        
        nextURLStr = getURLStringFromAppendingEndpointSpecifier(relativeToURLString: nextURLStr)
        
        nextURLStr = getURLStringFromAppendingLanguageSpecifier(relativeToURLString: nextURLStr)
        
        nextURLStr = getURLStringFromAppendingQueryWord(relativeToURLString: nextURLStr)
        
        
    
        if let filterForDictEntryLookup = self.filterForDictionaryEntriesLookup?.getDebugName(){
            
            return nextURLStr.appending("\(filterForDictEntryLookup)")
            
        } else if let regions = self.regions{
            
            let regionStringArray = regions.map({$0.rawValue})
            
            var regionStr = regionStringArray.reduce("", { $0.appending("\($1),")})
            
            regionStr.removeLast()
            
            nextURLStr = nextURLStr.appending(regionStr)
            
            return nextURLStr
        } else {
            
            nextURLStr.removeLast()
        }
        
    
        return nextURLStr


    }
        


The generateURLRequest and getURLRequest methods will be called in each of the OxfordAPIRequest subclasses. There will be no need for overriding these methods, since the base class implementation is consistent across all the different API endpoints:
 func generateURLRequest() -> URLRequest{
        
        let urlString = getURLString()
        
        let url = URL(string: urlString)!
        
        return getURLRequest(forURL: url)
    }
    
    private func getURLRequest(forURL url: URL) -> URLRequest{
        
        var request = URLRequest(url: url)
        
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue(OxfordAPIRequest.appID, forHTTPHeaderField: "app_id")
        request.addValue(OxfordAPIRequest.appKey, forHTTPHeaderField: "app_key")
        
        return request
    }

The method below can be skipped if you wish.  It is basically a static method that validates the dictionary entry look-up filter used to initialize the OxfordAPIRequest with the base class initializer.  The other static method hasValidFilters  will likewise be used to validate other arguments passed in for methods that take an array of OxfordAPIFilter objects as a parameter.
    private static func isValidDictionaryEntryLookUpFilter(filter: OxfordAPIEndpoint.OxfordAPIFilter) -> Bool{
        
        let validFilters = OxfordAPIEndpoint.entries.getAvailableFilters()
        
        return validFilters.contains(filter)
    }
 
    
    private static func hasValidFilters(filters: [OxfordAPIEndpoint.OxfordAPIFilter]?,forEndpoint endpoint: OxfordAPIEndpoint) -> Bool {
        
        if(filters == nil || (filters != nil && filters!.isEmpty)){
            return true
        }
        
        let toCheckFilters = filters!
        
        let allowableFilterSet = endpoint.getAvailableFilters()

        print("Checking if the filters passed in are allowable")
        
        for filter in toCheckFilters{
            print("Testing the filter: \(filter.getDebugName())")
            if !allowableFilterSet.contains(filter){
                print("The allowable filters don't contain: \(filter.getDebugName())")
                return false
            }
        }
        
        return true
        
        
    }

The helper function used for validating filters can be called in a function generateValidatedURLRequest, which is a variation of the function generateURLRequest, defined above.  The variation defined here is a throwingfunction, which means that wrapper functions that call this method will have wrap this function in a do-catch block in order to perform error handling that may result from using an invalid OxfordAPIRequest filter to initialize one of the OxfordAPIRequest subclasses.
func generateValidatedURLRequest() throws -> URLRequest{
        
       
        guard OxfordAPIRequest.hasValidFilters(filters: self.queryFilters, forEndpoint: self.endpoint) else {
            
            throw NSError(domain: "OxfordAPIClientErrorDomain", code: 0, userInfo: nil)
            
        }
        
        let urlString = getURLString()
        
        let url = URL(string: urlString)!
        
        return getURLRequest(forURL: url)
    }

While we’ve already defined a designated initializer, we can also define several convenience initializers that perform different kinds of queries against the Oxford Dictionary API endpoint:
    /** This initializer allows the user to specify the language and region without specifying other optional query parameters **/
    
    convenience init?(withWord queryWord: String, withDictionaryEntryLookupFilter dictEntryLookupFilter: OxfordAPIEndpoint.OxfordAPIFilter,forRegions regions: [OxfordRegion]?, forLanguage queryLanguage: OxfordAPILanguage){
        
        self.init(withQueryWord: queryWord, forRegions: regions, forLanguage: queryLanguage, withFilterForDictionaryEntryLookup: dictEntryLookupFilter, withQueryFilters: nil)
    
        
       
    }
    
    /** This convenience initializer only uses a dictionary entry lookup filter to filter the returned JSON data; it avoids using other query filter parameters in order to provide a more user-friendly initializer **/
    
    convenience init?(withWord queryWord: String, withDictionaryEntryFilter dictionaryEntryLookupFilter: OxfordAPIEndpoint.OxfordAPIFilter){
        
        self.init(withQueryWord: queryWord, forRegions: nil, forLanguage: .English, withFilterForDictionaryEntryLookup: dictionaryEntryLookupFilter, withQueryFilters: nil)
      
    
    }
    
    /** A default initializer is provided as convenience initializer **/
    
    convenience init(){
        
        let randomWords = ["Love","Justice","Friendship","Big","Expensive"]
        let randomIdx = Int(arc4random_uniform(UInt32(randomWords.count)))
        let randomWord = randomWords[randomIdx]
        
        /** Since we know that definitions is a valid dictionary entry lookup filter, we force unwrap this initializer **/
        
        self.init(withQueryWord: randomWord, forRegions: nil, forLanguage: .English, withFilterForDictionaryEntryLookup: .definitions([]), withQueryFilters: nil)!
        
    
    }

Now that we have defined the OxfordAPIRequest subclass, we can go on to define the different subclasses that inherit from this base class.  Each of these subclasses will have its own implementation of the getURLStringmethod, which we defined above and which acts the factory method, as we’ve pointed out.  Let’s get on with the task of defining this subclasses – to continue, click here.

No comments:

Post a Comment