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.