Skip to content

Instantly share code, notes, and snippets.

@vzsg
Last active December 27, 2017 20:31
Show Gist options
  • Save vzsg/45f8da8843651f2b418bb73000d0dc99 to your computer and use it in GitHub Desktop.
Save vzsg/45f8da8843651f2b418bb73000d0dc99 to your computer and use it in GitHub Desktop.
Wrapping an external API for Vapor apps – service class approach
import Foundation
// A simple protocol that describes the services available on the external service
// FIXME: Beer is an immutable, Codable struct defined elsewhere, not important now
public protocol BreweryDatabase {
func search(_ query: String) throws -> [Beer]
func findBeer(id: String) throws -> Beer
}
import Foundation
import Vapor
// Vapor-specific implementation of the external service
public struct VaporBreweryDatabase: BreweryDatabase {
private let decoder: JSONDecoder
private let client: ClientFactoryProtocol
private let baseURL = "https://api.brewerydb.com/v2/"
private let defaultQuery: [String: Node]
public init(apiKey: String, client: ClientFactoryProtocol) {
self.decoder = JSONDecoder()
self.client = client
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
defaultQuery = [
"key": Node.string(apiKey),
"format": "json"
]
}
public func search(_ query: String) throws -> [Beer] {
let queryParameters = ["type": "beer", "q": query, "withBreweries": "Y"]
return try apiCall(path: "search", query: queryParameters)
}
public func findBeer(id: String) throws -> Beer {
return try apiCall(path: "beer/\(id)", query: ["type": "beer", "withBreweries": "Y"])
}
private func apiCall<T: Codable>(path: String, query: [String: String] = [:]) throws -> T {
let request = Request(method: .get, uri: baseURL + path)
var queryObject = Node.object(defaultQuery)
try query.forEach { (key: String, value: String) in
try queryObject.set(key, value)
}
request.query = queryObject
let response = try client.respond(to: request)
let data = Data(bytes: response.body.bytes ?? [])
let apiResponse: ApiResponse<T> = try decoder.decode(ApiResponse<T>.self, from: data)
guard let responseData = apiResponse.data else {
throw BreweryError.serviceError(status: apiResponse.status, errorMessage: apiResponse.errorMessage)
}
return responseData
}
}
import Foundation
// All response types are wrapped in an object like this.
// This is an implementation detail of the brewerydb.com API!
struct ApiResponse<T: Codable>: Codable {
let message: String?
let data: T?
let errorMessage: String?
let status: String
}
import Vapor
let dropletStorageKey = "brewery-database"
// This extension will make it possible to access the preconfigured service
// from the Droplet anywhere.
// Assumption: it's a programmer error if you want to use brewery without setting it up correctly.
// ==> a fatal error is cleaner than mucking around with optionals.
public extension Droplet {
public var brewery: BreweryDatabase {
guard let brewery = self.storage[dropletStorageKey] as? BreweryDatabase else {
fatalError("BreweryProvider is not configured! Make sure that the Provider is properly registered!")
}
return brewery
}
}
import Vapor
// Use this in Config+Setup with the addProvider function.
public final class Provider: Vapor.Provider {
public static let repositoryName = "brewery-provider"
public func boot(_ config: Config) throws {
}
public func boot(_ droplet: Droplet) throws {
let apiKey: String = try droplet.config.get("brewery.key")
let client = droplet.client
let brewery = VaporBreweryDatabase(apiKey: apiKey, client: client)
droplet.storage[dropletStorageKey] = brewery
}
public func beforeRun(_ droplet: Droplet) throws {
}
public required init(config: Config) throws {
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment