forked from pdx-cocoaheads/pdxcocoaheads.com
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Finishes pdx-cocoaheads#4 "Pull data from Meetup API"
The "/events" route handler now uses a MeetupAPI object, which makes the connection to api.meetup.com. The MeetupAPI.getUpcomingEvents() method pulls down future events. The events are provided by Meetup.com as a JSON array. The JSON objects are unpacked into a parallel array of MeetupEvent structs, which is passed back out to the route handler to be used as the context for the view. A private Configuration struct supplies MeetupAPI with the appropriate URL information. It gets the URL pieces from the Config/meetup.json file, accessing that file via the active Droplet's Config object: the Droplet aleady knows where the config files are, rather than us having to find them again. The Droplet's Config is passed in to MeetupAPI on its creation, which hands it off to its Configuration.
- Loading branch information
Josh Caswell
committed
Aug 24, 2016
1 parent
c1f5a45
commit 78cfff9
Showing
12 changed files
with
457 additions
and
269 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import class Vapor.Config | ||
import class Engine.HTTPClient | ||
import class Engine.TCPClientStream | ||
|
||
/** Access to the Meetup.com API */ | ||
struct MeetupAPI { | ||
|
||
enum Error : ErrorProtocol { | ||
|
||
/** Necessary config file info was not found */ | ||
case missingConfigInfo | ||
/** Could not connect or response from API was absent */ | ||
case noResponse | ||
/** Response from API could not be parsed into JSON */ | ||
case responseInvalidJSON | ||
/** Response was not the expected array of events */ | ||
case responseNotAnArray | ||
} | ||
|
||
// `Vapor.Droplet.client` uses the class object and calls static | ||
// methods, so we will do the same. | ||
private let client = HTTPClient<TCPClientStream>.self | ||
|
||
private let configuration: Configuration | ||
|
||
init(config: Config) { | ||
self.configuration = Configuration(config: config) | ||
} | ||
|
||
/** | ||
* Pulls down future events for the group from Meetup.com's API and | ||
* converts them to `MeetupEvent`s for eventual display. | ||
* | ||
* -Throws: `MeetupAPI.Error.missingConfigInfo` if the necessary URL info | ||
* is somehow missing from the config file. | ||
* - Throws: `MeetupAPI.Error.noResponse` if the connection fails or | ||
* produces an empty response. | ||
* - Throws: `MeetupAPI.Error.responseInvalidJSON` if the response cannot | ||
* be parsed into JSON | ||
* - Throws: `MeetupAPI.Error.responseNotAnArray` if the response parses | ||
* into something other than an array. | ||
* | ||
* - Returns: An array of `MeetupEvent` representing the event data, in | ||
* the same order Meetup.com provided them. | ||
*/ | ||
func getUpcomingEvents() throws -> [MeetupEvent] { | ||
|
||
guard let requestURL = self.configuration.eventsEndpoint else { | ||
throw Error.missingConfigInfo | ||
} | ||
|
||
guard let response = try? self.client.get(requestURL) else { | ||
throw Error.noResponse | ||
} | ||
|
||
guard let parsed = response.json else { | ||
throw Error.responseInvalidJSON | ||
} | ||
|
||
guard let events = try? MeetupEvent.unpack(fromJSON: parsed) else { | ||
throw Error.responseNotAnArray | ||
} | ||
|
||
return events | ||
} | ||
} | ||
|
||
/** | ||
* Vend values, via the given `Vapor.Config`, from the meetup.json file. | ||
*/ | ||
private struct Configuration { | ||
|
||
/** | ||
* The underlying `Vapor.Config` object. | ||
* | ||
* This is recieved from the active `Droplet`, rather than created from | ||
* scratch, because the `Droplet` knows the correct working directory. | ||
*/ | ||
private let config: Config | ||
|
||
init(config: Config) { | ||
self.config = config | ||
} | ||
|
||
/** Keys for the meetup.json file, including the filename itself. */ | ||
private struct Keys { | ||
|
||
static let file = "meetup" | ||
static let host = "host" | ||
static let groupName = "group-name" | ||
static let eventsPath = "events-path" | ||
} | ||
|
||
// Scheme is not really configurable, as -- according to Vapor -- using | ||
// HTTPS on Linux requires an additional component | ||
var scheme: String { | ||
return "http" | ||
} | ||
|
||
/** API hostname */ | ||
var host: String? { | ||
return self.config[Keys.file, Keys.host]?.string | ||
} | ||
|
||
/** PDX CocoaHeads group's URL segment; what Meetup calls `:urlname` */ | ||
var groupName: String? { | ||
return self.config[Keys.file, Keys.groupName]?.string | ||
} | ||
|
||
/** API path for fetching all events of a given group */ | ||
var eventsPath: String? { | ||
return self.config[Keys.file, Keys.eventsPath]?.string | ||
} | ||
|
||
/** | ||
* Base URL for any PDX CocoaHeads group endpoint; includes scheme, | ||
* host, and group name. `nil` if any of that info is unavailable. | ||
*/ | ||
var groupURL: String? { | ||
|
||
guard | ||
let host = self.host, | ||
let groupName = self.groupName | ||
else { | ||
return nil | ||
} | ||
|
||
return self.scheme.finished(with: "://") + | ||
host.finished(with: "/") + | ||
groupName | ||
} | ||
|
||
/** API endpoint for all CocoaHeads-PDX events */ | ||
var eventsEndpoint: String? { | ||
|
||
guard | ||
let groupURL = self.groupURL, | ||
let eventsPath = self.eventsPath | ||
else { | ||
return nil | ||
} | ||
|
||
return groupURL.finished(with: "/") + eventsPath | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import Mustache | ||
|
||
extension MeetupEvent : MustacheBoxable { | ||
|
||
/** Allow lookup of fields by name in Mustache rendering context */ | ||
func mustacheBox(forKey key: String) -> MustacheBox { | ||
|
||
switch key { | ||
case "name": return Box(boxable: self.name) | ||
case "time": return Box(boxable: self.time) | ||
case "utcOffset": return Box(boxable: self.utcOffset) | ||
case "rsvpCount": return Box(boxable: self.rsvpCount) | ||
case "link": return Box(boxable: self.link) | ||
default: return Box() | ||
} | ||
} | ||
|
||
var mustacheBox: MustacheBox { | ||
return MustacheBox(value: self, | ||
keyedSubscript: self.mustacheBox(forKey:)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import enum Vapor.JSON | ||
|
||
extension MeetupEvent { | ||
|
||
/** Errors during unpacking of Meetup API's JSON data. */ | ||
enum UnpackError : ErrorProtocol { | ||
case notAnArray | ||
} | ||
|
||
/** | ||
* Transform an array of JSON-coded events returned from Meetup into | ||
* `MeetupEvent` instances, skipping any that are malformed. | ||
* | ||
* - Parameter fromJSON: Vapor.JSON wrapping an array of objects | ||
* | ||
* - Throws: `MeetupEvent.UnpackError.NotAnArray` if the `fromJSON` | ||
* parameter cannot be unwrapped into a Swift array | ||
* | ||
* - Returns: An array of `MeetupEvent`s in the order the event | ||
* descriptions were presented in the JSON | ||
*/ | ||
static func unpack(fromJSON json: JSON) throws -> [MeetupEvent] { | ||
|
||
guard let events: [JSON] = json.array else { | ||
throw UnpackError.notAnArray | ||
} | ||
|
||
return events.flatMap { | ||
// Just skip an individual item if it's not usable | ||
MeetupEvent(fromJSON: $0) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import enum Vapor.JSON | ||
|
||
/** | ||
* Holder for values returned from Meetup API that we want to display on | ||
* "Upcoming Events" page. | ||
*/ | ||
struct MeetupEvent { | ||
|
||
/** Event title */ | ||
let name: String | ||
/** Event date in milliseconds since epoch */ | ||
let time: Int | ||
/** Event location timezone's milliseconds from UTC */ | ||
let utcOffset: Int | ||
/** Number of people who have said they're attending */ | ||
let rsvpCount: Int | ||
/** URL (as string) of event's page on Meetup.com */ | ||
let link: String | ||
|
||
init?(fromJSON json: JSON) { | ||
|
||
guard | ||
let event = json.object, | ||
let name = event[Keys.name.rawValue].string, | ||
let time = event[Keys.time.rawValue].int, | ||
let utcOffset = event[Keys.utcOffset.rawValue].int, | ||
let rsvpCount = event[Keys.rsvpCount.rawValue].int, | ||
let link = event[Keys.link.rawValue].string | ||
else { | ||
return nil | ||
} | ||
|
||
self.name = name | ||
self.time = time | ||
self.utcOffset = utcOffset | ||
self.rsvpCount = rsvpCount | ||
self.link = link | ||
} | ||
|
||
/** Keys in the Meetup API's JSON "event" object */ | ||
private enum Keys : String { | ||
|
||
case name, time, link | ||
case utcOffset = "utc_offset" | ||
case rsvpCount = "yes_rsvp_count" | ||
} | ||
} | ||
|
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.