If you would like to download the source code for this tutorial, please check here. Before we begin to develop a set of classes that will be responsible for building the URLs and generating the API requests used to connect to the REST API, it will be helpful to define some custom data types First, we define an OxfordAPILanguage enum, whose underlying type is a string. Although the API client designed here mainly assumes English to be the default language for API requests, the language parameter has values that allow for other languages to be specified:
enum OxfordAPILanguage: String{
case English = "en"
case Spanish = "es"
case Malay = "ms"
case Setswana = "tn"
case Swahili = "sw"
case NorthernSoho = "nso"
case Indonesia = "id"
case Latvian = "lv"
case Urdu = "ur"
case Romanian = "ro"
case Hindi = "hi"
case German = "de"
case Portuguese = "pt"
case Tamil = "ta"
case Gujarati = "gu"
}
In addition to different languages, the user can also specify different regions (i.e. Great Britain and the United States) to obtain language data that reflects regional variations between British English and American English:
enum OxfordRegion: String{
case gb,us
}
Language data is also grouped into a large number of domains, most of which have been represented below with the enum type OxfordDomain, which also has a String rawValue in order to make it more convenient to enter values for query parameters later:
enum OxfordDomain: String{
case air_force, alcoholic,american_civil_war,american_football,amerindian
case anatomy,ancient_history,angling,anthropology, archaeology, archery, architecture
case art, artefacts, arts_and_humanities, astrology, astronomy
case athletics, audio, australian_rules, aviation, ballet, baseball, basketball, bellringing
case biblical, billiards, biochemistry, biology, bird, bookbinding, botany, bowling, bowls,boxing
case breed, brewing
case bridge,broadcasting,buddhism,building,bullfighting,camping,canals,cards,carpentry
case chemistry, chess, christian,church_architecture,civil_engineering,clock_making
case clothing, coffee,commerce,commercial_fishing,complementary_medicine,computing
case cooking,cosmetics,cricket,crime,croquet,crystallography,currency,cycling
case dance,dentistry,drink,dyeing,early_modern_history,ecclesiastical
case ecology,economics,education,egyptian_history,electoral,electrical,electronics,element
case english_civil_war,falconry,farming,fashion,fencing,film,finance,fire_service,first_world_war
case fish, food, forestr,freemasonry,french_revolution,furniture,gambling,games,gaming,genetics
case geography,geology,geometry,glassmaking,golf,goods_vehicles,grammar,greek_histroy,gymnastics
case hairdressing,handwriting,heraldry,hinduism,history,hockey,honour,horology,horticulture,hotels
case hunting,insect,instruments,intelligence,invertebrate,islam,jazz,jewellery
case journalism,judaism,knitting,language,law,leather,linguistics
case literature,logic,lower_plant,mammal,marriage,martial_arts
case mathematics,measure,mechanics,medicine,medieval_histor
case metallurgy,meteorology,microbiology,military,military_history
case mineral,mining,motor_racing,motoring,music,mountaineering,musical_direction,mythology
case napoleonic_wars,narcotics,nautical,naval,needlework,numismatics,occult,oceanography
case office, oil_industry,optics,palaeontology,parliament,pathology,penal
case people,pharmaceutics,philately,philosophy,phonetics,photography,physics
case physiology,plant,plumbing,politics, police,popular_music,postal,potter,printing,professions
case prosody,psychiatry,psychology,publishing,racing,railways,rank,relationships
case religion, reptile,restaurants,retail,rhetoric, riding,roads,rock,roman_catholic_church
case roman_history,rowing,royalty,rugby,savoury,scouting,second_world_war
case sex,shoemaking,sikhism,skateboarding,skating,skiing,smoking,snowboarding,soccer
case sociology,space,sport,statistics,stock_exchange,surfing,surgery,surveying,sweet,swimming
case tea, team_sports,technology,telecommunications,tennis,textiles,theatre,theology,timber,title
case tools, trade_unionism,transport,university,variety,veterinar,video
case war_of_american_independence,weapons,weightlifting,wine,wrestling,yoga,zoology
}
An enum type OxfordLanguageRegister is also defined here to represent different styles and modes in which English is spoken based on a variety of social, historical, literary, and cultural contexts:
enum OxfordLanguageRegister: String{
case allusive
case archaic
case allusively
case army_slang
case black_english
case coarse_slang
case cant
case college_slang
case concrete
case contemptuous
case dated
case depreciative,depreciatively
case derogatory
case dialect
case dismissive
case disused
case emphatically
case especially
case euphemism
case euphemistic
case figurative
case generally
case historical
case humorous, humorously
case hyperbolical, hyperbolically
case informal
case ironic, ironically
case literal
case literary
case military_slang
case nautical_slang
case nursery
case obsolete
case offensive
case personified
case poetic
case police_slang
case prison_slang
case proverb
case pseuodo_archaic
case rare,rarely
case rhyming_slang
case school_slang
case slang
case technical
case temporary
case theatrical_slang
case trademark
case trademark_in_uk
case trademark_in_us
case transferred
case university_slang
case vulgar_slang
case non_standard = "non-standard"
case nonce_use = "nonce-use"
case RAF_slang = "R.A.F_slang"
}
Words and lemmas can also be categorized in terms of their lexical category (i.e. part of speech), whose categories are represented with the OxfordLexicalCategory enum. For this enum, we also define a static method that will return an array representing all possible lexical categories(which can be used for validation purposes later):
enum OxfordLexicalCategory: String{
case noun, verb
case combining_form
case adjective,adverb
case conjunction, contraction
case determiner,idiomatic,interjection
case numeral, particle, other
case predeterminer, prefix, suffix
case preposition,pronoun,residual
static let allPartsOfSpeech: [OxfordLexicalCategory] = [
.noun, .verb, .combining_form, .adjective, .adverb, .conjunction, .contraction,
.determiner, .idiomatic, .interjection, .numeral, .particle, .other,
.predeterminer, .prefix,.suffix,.preposition,.pronoun,.residual
]
}
Another enum type OxfordGrammaticalFeature is defined to represent the different possible grammatical inflections that often result in a variety of wordforms being used for a given headword. This enum, combined with the enum for lexical categories above, will make it easier to narrow down the possible range of returned JSON data to a more manageable and specific subset:
enum OxfordGrammaticalFeature: String{
//mass
case mass
//collectivity
case collective
//adjective function
case attributive
case predicative
//subcategorization
case intransitive
case transitive
//auxiliary
case auxiliary
//residual
case abbreviation
case symbol
case interrogative
case possessive
case relative
//person
case third
//unit structure
case phrasal
//number
case singular
case plural
//numeral
case cardinal
case ordinal
//tense
case past
case present
//degree
case comparative
case positive
case superlative
//event modality
case modal
//gender
case feminine
//mood
case conditional
case subjunctive
//non finiteness
case infinitive
case past_participle
case present_participle
}
In addition, we will define another enum type OxfordHTTPStatusCode, to help us implement error handling as it relates to our API requests. Each enum case represents a different possible HTTP status code from the REST API server (for more information about the different status codes, see the documentation). This enum also defines an instance method statusCodeMessage that returns an error message specific to the status code represented by an enum case:
enum OxfordHTTPStatusCode: Int{
case Success = 200
case BadRequest = 400
case AuthenticationFailed = 403
case NotFound = 404
case InternalServerError = 500
case BadGateway = 502
case ServiceUnavailable = 503
case GatewayTimeout = 504
case OtherStatusCode
func statusCodeMessage() -> String{
switch self {
case .AuthenticationFailed:
return "The request failed due to invalid credentials.Please check that the app_id and app_key are correct, and that the URL you are trying to access is correct. These can be found in the API Credentials page"
case .BadGateway:
return "Oxford Dictionaries API is down or being upgraded."
case .BadRequest:
return "The request was invalid or cannot be otherwise served. An accompanying error message will explain further.For example, when the filters provided are unknown, the source and target languages in the translation endpoint are the same, or a numeric parameter such as offset and limit in the wordlist endpoint cannot be evaluated as a number."
case .GatewayTimeout:
return "The Oxford Dictionaries API servers are up, but the request couldn’t be serviced due to some failure within our stack. Please try again later."
case .InternalServerError:
return "Something is broken. Please contact us so the Oxford Dictionaries API team can investigate."
case .NotFound:
return "No information available or the requested URL was not found on the server.For example, when the headword could not be found, a region or domain identifier do not exist, or the headword does not contain any attribute that match the filters in the request. It may also be the case that the URL is misspelled or incomplete."
case .ServiceUnavailable:
return "The Oxford Dictionaries API servers are up, but overloaded with requests. Please try again later."
case .Success:
return "Success!"
case .OtherStatusCode:
return "Unknown http status code received"
default:
return "Unknown http status code received"
}
}
}
An OxfordAPIEndpoint enum will also be defined to represent the different REST API endpoints that can be called via our API request. Each enum case will have an underlying string value, with a few exceptions: the ‘utility’ case does not have an underlying string per se (this API is used to get information about the REST API itself, such as the different filters available for a given endpoint); the Lexistats API for retrieving word frequency data is represented here with three enum cases, each of which has its underlying string set to a specific string identifier that will be useful in constructing the API request URL (however, connecting to the Lexistats API will be the subject for another tutorial, as this is a service/feature added only recently to the REST API).
enum OxfordAPIEndpoint: String{
case entries, inflections, search, translations, wordlist
case utility = ""
case stats_word_frequency = "stats/frequency/word"
case stats_words_frequency = "stats/frequency/words"
case stats_ngrams_frequency = "stats/frequency/ngrams"
}
Within the OxfordAPIEndpoint enum, we will define a nested enum, OxfordAPIFilter, to represent the different filters available for the endpoints. We will make this enum a nested type for semantic reasons – mainly, a given ‘filter’ query parameter is closely connected with endpoint whose results it helps to filter. However, seeing as filters have a many-to-many relationship with API endpoints, it would also be reasonable to define the OxfordAPIFilterenum as a standalone enum. Later, an instance method getAvailableFilterswill be defined for the OxfordAPIEndpoint enum type. This instance method will return the filters available for a specific endpoint enum and will be useful for validating arguments for methods that take OxfordAPIFilter as an optional parameter. (A more readable result could probably be achieved by defining a nested enum type within each OxfordAPIRequest subclass – to be defined later – so that the methods defined for those subclasses achieve validation through type-checking rather than checking against the set of valid filters, but for purposes of this tutorial, we hope to explore the power of Swift enums in more depths, as well as other topics such as failable intializers).
The OxfordAPIFilter enum has associated types corresponding to the parameter values specific to that enum. Hence, the domain and register filters take an array of strings (i.e. [String]) as associated values, whereas the limit and offset filters take values of type Int for their associated values. Since an enum with associated types cannot have underlying rawValues, we make the OxfordAPI filter conform to the Hashable protocol by implementing the hashValue variable as well as the equality == operator. This will also allow us to define sets whose elements consist of OxfordAPIFilter enum types. The integers associated with each enum case in the implementation of hashValue are totally arbitrary. In order to get the string corresponding to a given OxfordAPIFilter, we also define an instance method, getDebugName, which returns the string representation for that filter as a parameter in the API request URL string.
enum OxfordAPIFilter: Hashable{
var hashValue: Int{
switch self {
case .definitions( _):
return 0
case .domains( _):
return 1
case .etymologies( _):
return 2
case .examples( _):
return 3
case .grammaticalFeatures( _):
return 4
case .lexicalCategory( _):
return 5
case .pronunciations( _):
return 6
case .regions( _):
return 7
case .registers( _):
return 8
case .translations( _):
return 9
case .variantForms( _):
return 10
case .trueCase(_):
return 11
case .format(_):
return 12
case .collate(_):
return 13
case .sort(_):
return 14
case .punctuation(_):
return 15
case .limit(_):
return 16
case .offset(_):
return 17
case .tokens(_):
return 18
case .contains(_):
return 19
case .minFrequency(_):
return 20
case .minDocumentFrequency(_):
return 21
case .minNormalizedFrequency(_):
return 22
case .maxFrequency(_):
return 23
case .maxDocumentFrequency(_):
return 24
case .maxNormalizedFrequency(_):
return 25
case .wordform(_):
return 26
case .wordforms(_):
return 27
case .lemma(_):
return 28
}
}
static func ==(lhs: OxfordAPIEndpoint.OxfordAPIFilter, rhs: OxfordAPIEndpoint.OxfordAPIFilter) -> Bool {
return lhs.hashValue == rhs.hashValue
}
func getDebugName() -> String{
switch self {
case .domains(_):
return "domains"
case .lexicalCategory(_):
return "lexicalCategory"
case .regions(_):
return "regions"
case .registers(_):
return "registers"
case .translations(_):
return "translations"
case .definitions(_):
return "definitions"
case .etymologies(_):
return "etymologies"
case .examples(_):
return "examples"
case .grammaticalFeatures(_):
return "grammaticalFeatures"
case .pronunciations(_):
return "pronunciations"
case .variantForms(_):
return "variantForms"
case .lemma(_):
return "lemma"
case .wordform(_):
return "wordform"
case .wordforms(_):
return "wordforms"
case .trueCase(_):
return "trueCase"
case .limit(_):
return "limit"
case .offset(_):
return "offset"
case .collate(_):
return "collate"
case .contains(_):
return "contains"
case .sort(_):
return "sort"
case .format(_):
return "format"
case .minFrequency(_):
return "minFrequency"
case .minNormalizedFrequency(_):
return "minNormalizedFrequency"
case .minDocumentFrequency(_):
return "minDocumentFrequency"
case .maxFrequency(_):
return "maxFrequency"
case .maxDocumentFrequency(_):
return "maxDocumentFrequency"
case .maxNormalizedFrequency(_):
return "maxNormalizedFrequency"
case .tokens(_):
return "tokens"
case .punctuation(_):
return "punctuation"
}
}
case domains([String])
case lexicalCategory([String])
case regions([String])
case registers([String])
case translations([String])
case definitions([String])
case etymologies([String])
case examples([String])
case grammaticalFeatures([String])
case pronunciations([String])
case variantForms([String])
case wordform(String)
case wordforms([String])
case lemma([String])
case trueCase([String])
case limit(Int)
case offset(Int)
case collate([OxfordAPIFilter])
case sort([OxfordAPIFilter])
case minFrequency(Int)
case maxFrequency(Int)
case minNormalizedFrequency(Int)
case maxNormalizedFrequency(Int)
case minDocumentFrequency(Int)
case maxDocumentFrequency(Int)
case punctuation(Bool)
case tokens([String])
case contains([String])
case format(Bool)
The OxfordAPIFilter enum will also define an instance method getQueryParameterString(isLastQueryParameter:), which will use the debugName function together with its associated values to generate a parameter query string in which both the parameter name and values are returned in a single string that can be appended to a URL string:
func getQueryParameterString(isLastQueryParameter: Bool) -> String{
var queryString = "\(getDebugName())="
switch self {
case .lexicalCategory(let parameterValues):
queryString = parameterValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .grammaticalFeatures(let parameterValues):
queryString = parameterValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .regions(let parameterValues):
queryString = parameterValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .domains(let parameterValues):
queryString = parameterValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .registers(let parameterValues):
queryString = parameterValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .definitions(let parameterValues):
queryString = parameterValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .etymologies(let parameterValues):
queryString = parameterValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .pronunciations(let parameterValues):
queryString = "\(getDebugName())="
queryString = parameterValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .variantForms(let parameterValues):
queryString = parameterValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .wordforms(let parameterValues):
queryString = parameterValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .collate(let filters):
let filterStrings = filters.map({ return $0.getDebugName()} )
queryString = filterStrings.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .sort(let filters):
let filterStrings = filters.map({ return $0.getDebugName()} )
queryString = filterStrings.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .wordform(let wordform):
queryString = queryString.appending("\(wordform)")
break
case .limit(let resultsLimit):
queryString = queryString.appending("\(resultsLimit)")
break
case .offset(let resultsOffset):
queryString = queryString.appending("\(resultsOffset)")
break
case .maxFrequency(let maxFrequency):
queryString = queryString.appending("\(maxFrequency)")
break
case .minFrequency(let minFrequency):
queryString = queryString.appending("\(minFrequency)")
break
case .maxNormalizedFrequency(let maxNormalizedFrequency):
queryString = queryString.appending("\(maxNormalizedFrequency)")
break
case .minNormalizedFrequency(let minNormalizedFrequency):
queryString = queryString.appending("\(minNormalizedFrequency)")
break
case .minDocumentFrequency(let minDocumentFrequency):
queryString = queryString.appending("\(minDocumentFrequency)")
break
case .maxDocumentFrequency(let maxDocumentFrequency):
queryString = queryString.appending("\(maxDocumentFrequency)")
break
case .punctuation(let shouldIncludePunctuation):
queryString = queryString.appending("\(shouldIncludePunctuation ? "true" : "false")")
break
case .format(let shouldReturnAsSingleString):
queryString = queryString.appending("\(shouldReturnAsSingleString ? "google" : "oup")")
break
case .tokens(let tokenValues):
queryString = tokenValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
case .contains(let tokenValues):
queryString = tokenValues.reduce(queryString, {$0.appending("\($1),")})
queryString.removeLast()
break
default:
queryString = String()
}
if(!isLastQueryParameter){
queryString = queryString.appending(";")
}
return queryString
}
}
Lastly, for the OxfordAPIEndpoint enum, we define an instance method that returns the set of filters possible for a given enum case. This set of valid filters will be used for validation purposes when we implement our OxfordAPIRequest base class and its subclasses. The OxfordAPIFilter values used to populate the set of possible filters need not have any associated values, since this method is just used to get the OxfordAPIFilter cases only and not the parameter values for a given filter. For this reason, we pass in empty arrays and arbitrary values for the associated values depending on the specific enum case:
func getAvailableFilters() -> Set{
let resultLimitationFilters: Set = Set([OxfordAPIFilter.offset(0),OxfordAPIFilter.limit(1000)])
switch self {
case .entries:
return Set([.definitions([]),.domains([]),.etymologies([]),.examples([]),.grammaticalFeatures([]),.lexicalCategory([]),
.pronunciations([]),.regions([]),.registers([]), .variantForms([])])
case .inflections:
return Set([.grammaticalFeatures([]),.lexicalCategory([])])
case .translations:
return Set([])
case .wordlist:
return Set([ .domains([]),.lexicalCategory([]),.regions([]),.registers([]),.translations([])])
case .stats_word_frequency:
return Set([ .wordform(""),.trueCase([]),.lemma([]),.lexicalCategory([]) ])
case .stats_words_frequency:
return Set([.collate([]),.sort([]),.minFrequency(0),.maxFrequency(10000),.minNormalizedFrequency(0),.maxNormalizedFrequency(10000),.wordforms([]),.trueCase([]),.lemma([]),.lexicalCategory([]),.grammaticalFeatures([])]).union(resultLimitationFilters)
case .stats_ngrams_frequency:
return Set([.minDocumentFrequency(0),.maxDocumentFrequency(10000),.minFrequency(0),.maxFrequency(10000),.contains([]),.tokens([])]).union(resultLimitationFilters)
case .search:
return Set([])
case .utility:
return Set([])
}
}
Now that we have defined our custom data types and data models, we are ready to develop an OxfordAPIRequest base class, which will be used to connect to the Dictionary API, as well as subclasses that will inherit from this class and which can be used to connect to the other API endpoints.
No comments:
Post a Comment