In order for our API Client to function smoothly, we will define a wrapper struct to encapsulate all of the data required to build an url string for the API request, and we'll call this struct OxfordAPIRequest, though you can call it anything else you want. So let's go ahead and define an empty struct and name it accordintly as follows:

    struct OxfordAPIRequest{

    }

For this struct, we will also define three private static constants, one for the baseURL string, one for the appID, and other for the appKey. The latter two, the appID and the appKey, are unique to the app or user making the request and must obtained from Oxford Dictionary's websites. THhe ones provided in this tutorial are arbitrary and will not work if you are attempting to implement this code in your own app. So please make sure to apply for your own appID and appKey. These private static variables are defined as shown below:

    private static let baseURLString = "https://od-api.oxforddictionaries.com/api/v1"
    private static let appID = "bdeb81211"
    private static let appKey = "481e889739r497ff881168989b4a111b"
    
    private static var baseURL: URL{
        return URL(string: baseURLString)!
    }

You'll notice that an additional computed property is defined for the baseURL for convenience. This makes it possible to access the baseURL as an instance variable for any given OxfordAPIRequest object.

In addition, we will also define some variables whose values the user will provide via selective initializers. Specifically, the user can specify a word (which will be a string), as well as the endpoint, language, and an array of query parameter filters(all of which have types that were defined previously). In addition, we will define some Boolean variables whose default values are set to false, as these will only be relevant to API requests that connect to the dictionary entries endpoint

    var  endpoint: OxfordAPIEndpoint
    var  word: String
    var  language: OxfordAPILanguage
    var  filters: [OxfordAPIEndpoint.OxfordAPIFilter]?
    
    var  hasRequestedSynonyms: Bool = false
    var  hasRequestAntonyms: Bool = false
    var  hasRequestedExampleSentences: Bool = false
    

Next, we will define a set of selective intializers whose parameters are specific to the particular enpoint being called on. Some parameters are available only for specific endpoints, while others are available for several endpoints. In either case, since some of these parameteres are mutually exclusive, we use selective initialization to create API requests tailored to specific enpoints.

This initializer will be used to obtain all of the example sentences that are provided for a specific word and a specific langauge, where English is set as the default value. This initializer will create an API request that connects to the dictionary entries enpoint, but since the "sentences" parameters is mutually exclusive with the synonyms and antonyms parameters, we provide default values for those in the initializer function. Furthemore, since no filters are available for this kind of API request, the filters variable is automatically set to zero.

     init(withWord queryWord: String , hasRequestedExampleSentencesQuery: Bool ,forLanguage queryLanguage: OxfordAPILanguage  = .English){
        
        self.endpoint = OxfordAPIEndpoint.entries
        self.word = queryWord
        self.language = OxfordAPILanguage.English
        self.filters = nil
        
        self.hasRequestedExampleSentences = hasRequestedExampleSentencesQuery
        self.hasRequestedSynonyms = false
        self.hasRequestAntonyms = false
    }

The initializer provided below will also connect to the dictionary entries endpoint, but it will specify antonym and synonym parameters, which will enable the user to obtain either the antonyms,synonyms, or both from the Oxford Thesaurus for the queryWord passeed into the initializer. This kind of API request is mutually exclusive with the previous one, which requested example sentences for a specific word. Hence, the hasRequestedExamplesSentences boolean flag is set to zero.

    init(withWord queryWord: String, hasRequestedAntonymsQuery: Bool, hasRequestedSynonymsQuery: Bool, forLanguage queryLanguage: OxfordAPILanguage = .English){
        
        self.endpoint = OxfordAPIEndpoint.entries
        self.word = queryWord
        self.language = OxfordAPILanguage.English
        self.filters = nil
        
        self.hasRequestedExampleSentences = false
        self.hasRequestedSynonyms = hasRequestedSynonymsQuery
        self.hasRequestAntonyms = hasRequestedAntonymsQuery
    }

Other selective initalizers are provided defined as shown below:

      
    init(withWord queryWord: String, withFilters filters: [OxfordAPIEndpoint.OxfordAPIFilter]?,forLanguage queryLanguage: OxfordAPILanguage = .English){
        
        self.endpoint = OxfordAPIEndpoint.entries
        self.word = queryWord
        self.language = OxfordAPILanguage.English
        self.filters = filters
        
        self.hasRequestedExampleSentences = false
        self.hasRequestedSynonyms = false
        self.hasRequestAntonyms = false
    }


    
    init(withHeadword headWord: String, withFilters queryFilters: [OxfordAPIEndpoint.OxfordAPIFilter]?, withQueryLanguage queryLanguage: OxfordAPILanguage = .English){
        
        self.endpoint = OxfordAPIEndpoint.inflections
        self.word = headWord
        self.filters = queryFilters
        self.language = queryLanguage
        
        self.hasRequestedExampleSentences = false
        self.hasRequestAntonyms = false
        self.hasRequestedSynonyms = false
    }
    
    
    init(withEndpoint queryEndpoint: OxfordAPIEndpoint, withQueryWord queryWord: String, withFilters queryFilters: [OxfordAPIEndpoint.OxfordAPIFilter]?, withQueryLanguage queryLanguage: OxfordAPILanguage = .English){
        
        
        self.endpoint = queryEndpoint
        self.word = queryWord
        self.filters = queryFilters
        self.language = queryLanguage
        
        self.hasRequestedExampleSentences = false
        self.hasRequestAntonyms = false
        self.hasRequestedSynonyms = false
        
    }

Furthermore, it's always good to provide a default initializer to serve as a placeholder for debugging and other purposes.

      init(){
        self.endpoint = OxfordAPIEndpoint.entries
        self.word = "love"
        self.language = OxfordAPILanguage.English
        self.filters = nil
        
        self.hasRequestedExampleSentences = false
        self.hasRequestedSynonyms = false
        self.hasRequestAntonyms = false
        
    }

The intializer with the most parameters will be that which calls to the wordlist endpoint, wince wordlists can be obtained for different regions, different language registers, different language domains, as well as different translations and lexical categories.

    
    init(withDomainFilters domainFilters: [OxfordAPIEndpoint.OxfordAPIFilter], withRegionFilters regionFilters: [OxfordAPIEndpoint.OxfordAPIFilter], withRegisterFilters registerFilters: [OxfordAPIEndpoint.OxfordAPIFilter], withTranslationsFilters translationsFilters: [OxfordAPIEndpoint.OxfordAPIFilter], withLexicalCategoryFilters lexicalCategoryFilters: [OxfordAPIEndpoint.OxfordAPIFilter], withQueryLanguage queryLanguage: OxfordAPILanguage = .English){
        
        
        self.endpoint = OxfordAPIEndpoint.wordlist
        self.word = String()
        self.filters = domainFilters + regionFilters + registerFilters + translationsFilters + lexicalCategoryFilters
        self.language = queryLanguage
        
        self.hasRequestedExampleSentences = false
        self.hasRequestAntonyms = false
        self.hasRequestedSynonyms = false
        
    }

The heart of this struct's functionality will be implemented via the private function below, getURLString(), which constructs the URL string based on the values provided via the initializer. Thi sfunction will append endpoint and language parameter values to the base url, after which it will append on ther types of parameters based on the endpoint to which the user is connecting. If you want to re-implement this function using switch statements or some other configuration of if-then statements, feel free to do so, but the one below has been tested and is adequate for our purposes here.



    private func  getURLString() -> String{
        
        
        let  baseURLString = OxfordAPIRequest.baseURLString.appending("/")
        
        let  endpointStr = self.endpoint.rawValue.appending("/")
        
        let  endpointURLString = baseURLString.appending(endpointStr)
        
        let  languageStr = self.language.rawValue.appending("/")
        
        var  languageURLString = endpointURLString.appending(languageStr)
        
        
        if(self.endpoint == .wordlist){
            
            if(self.filters == nil){
                
                /** Remove the final slash **/
                languageURLString.removeLast()
                
                return  languageURLString
                
            } else  {
                
                let allFilters = self.filters!
                
                addFilters(filters: allFilters, toURLString: &languageURLString)
                
                return  languageURLString

            }
            
        } else  {
            
            let  wordStr = self.getProcessedWord().appending("/")
            
            var  wordURLString = languageURLString.appending(wordStr)
            
            if(self.endpoint == .entries){
                
                if(hasRequestAntonyms || hasRequestedSynonyms){
                    
                    if(hasRequestedSynonyms && hasRequestAntonyms){
                        
                        let  finalURLString = wordURLString.appending("synonyms;antonyms")
                        
                        return  finalURLString
                        
                    } else if(hasRequestedSynonyms){
                        
                        let  finalURLString = wordURLString.appending("synonyms")
                        
                        return  finalURLString
                        
                    } else if(hasRequestAntonyms){
                        
                        let  finalURLString = wordURLString.appending("antonyms")
                        
                        return  finalURLString
                    }
                    
                } else if(hasRequestedExampleSentences) {
                    
                    let  finalURLString = wordURLString.appending("sentences")
                    
                    return  finalURLString
                    
                } else if(self.filters != nil) {
                    //Add filters for dictionary entries request for the given word
                    
                    let  allFilters = self.filters!
                    
                    addFilters(filters: allFilters, toURLString: &wordURLString)
                   
                    return  wordURLString
                     
                    
                    
                } else  {
                    
                    wordURLString.removeLast()
                    
                    return  wordURLString
                    
                }
                
            } else  {
                
                if(self.filters != nil){
                    
                    let  allFilters = self.filters!
                    
                    addFilters(filters: allFilters, toURLString: &wordURLString)
                    
                    return  wordURLString
                }
            }
            
        }
        
        return  String()
    }
    
    

The URL constructor function above calls upon a private helper function that adds filter parameters to the end of the url string. Since filter parameters can be added for different endpoints, we make use of a single function to perform this role. I've implemented this function such that it modifies the url string that is passed in by reference, which is why the inout prefix is used for the parameter type, making it unnecessary to return any value, but you can implement such that it return a new string, inwhich case the parameter type wouldn't need the inout prefix.

     private func  addFilters(filters: [OxfordAPIEndpoint.OxfordAPIFilter], toURLString urlString: inout String){

        
        if(filters.isEmpty){
            return
        }
        
        filters.forEach({
            var filterString = $0.getQueryParameterString(isLastQueryParameter: false)
            urlString = urlString.appending(filterString)
        })
        
        
        repeat {
            
            if(urlString.last! == ";"){
                urlString.removeLast()
            }
            
        }while(urlString.last! == ";")
        
    }

An additional private function is provided in order to make sure that the queryWord passed into the initializer and used to build the query string is properly formatted. This function makes sure that the word is percent-encoded and lowercased, and that spaces are replaced with underscoes. In this way, the user can passed in words such as "historical cost accounting" and get a valid JSON response.

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

But the fun doesn't end there. Who would've thought that building a URL string could such a hassle? In any case, we also provided an additional helper function that helps to validate the filters that are passed into the initializers, since each endpoint is compatible with different kinds of filters, and it's not inconceivable that at some future time, you may forget which filters correspond to which endpoint. For that reason, it would be nice to do a little validation on the filters passed in, in case invalid filters get passed in accidentally in the future:

      private 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 validator function will be used to throw an error in the generateURLRequest() function, which the only publicly accessible function for this struct and which we define as a throwing function. Before we get to this publicly-available function, though, we still have some work to do. There are still implementation details that need to be ironed out. Specifically, we have to define the authorization headers that are sent with API request. A URL string alone will not suffice. The API request must be authenticated, and this can only be achieved by providing values for the different header fields that will be checked by the server, namely for the appID, appKey, and the type of data (in this case, JSON) reqeusted:

      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
    }

Finally, the fruit of our labor culimnates in the publicly-accessiblle function that generates the URL request, as shown below. I've provided two versions of this function, one that validates the query filters parameter and another that doesn't. The latter is merely convenient for development purposes, while the former will ultimately be the function of choice for the release version of any software or app you develop. Note that the generateValidatedURLRequest function is a throwing function, which means that it will throw an error if the user passes in an invalid query filter.

    func generateValidatedURLRequest() throws -> URLRequest{
        
       
        guard hasValidFilters(filters: self.filters, forEndpoint: self.endpoint) else {
            
            throw NSError(domain: "OxfordAPIClientErrorDomain", code: 0, userInfo: nil)
            
        }
        
        let urlString = getURLString()
        
        let url = URL(string: urlString)!
        
        return getURLRequest(forURL: url)
    }
    
    func generateURLRequest() -> URLRequest{
        
        let urlString = getURLString()
        
        let url = URL(string: urlString)!
        
        return getURLRequest(forURL: url)
    }

If you made it this far, congratulations. That was a lot of work just to get a query string constructed.

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.