Skip to content

Commit

Permalink
Refactor AlpacaClient and AlpacaDataClient
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewBarba committed Aug 26, 2020
1 parent 46e4747 commit ebb7a20
Show file tree
Hide file tree
Showing 43 changed files with 1,183 additions and 624 deletions.
151 changes: 3 additions & 148 deletions Sources/Alpaca/AlpacaClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,159 +4,14 @@ import NIO
import NIOHTTP1
import OpenCombine

public struct AlpacaClient {
public struct Environment {
public let api: String
public let key: String
public let secret: String

public static func data(key: String, secret: String) -> Self {
Environment(api: "https://data.alpaca.markets/v1", key: key, secret: secret)
}

public static func live(key: String, secret: String) -> Self {
Environment(api: "https://api.alpaca.markets/v2", key: key, secret: secret)
}

public static func paper(key: String, secret: String) -> Self {
Environment(api: "https://paper-api.alpaca.markets/v2", key: key, secret: secret)
}
}

enum RequestError: Error {
case invalidURL
case status(HTTPResponseStatus)
}

public struct EmptyResponse: Decodable {
public static let jsonData = try! JSONSerialization.data(withJSONObject: [:], options: [])
}

public typealias ResponsePublisher<T: Decodable> = AnyPublisher<T, Error>

public typealias EmptyResponsePublisher = ResponsePublisher<EmptyResponse>

public typealias SearchParams = [String: String?]

public typealias BodyParams = [String: Any?]
public struct AlpacaClient: AlpacaClientProtocol {

public let environment: Environment

private let httpClient = HTTPClient(eventLoopGroupProvider: .shared(Utils.eventLoopGroup))
public let data: AlpacaDataClient

public init(_ environment: Environment) {
self.environment = environment
}
}

// MARK: - Requests

extension AlpacaClient {

public func get<T>(_ urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(.GET, urlPath: urlPath, searchParams: searchParams)
}

public func delete<T>(_ urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(.DELETE, urlPath: urlPath, searchParams: searchParams)
}

public func post<T>(_ urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(.POST, urlPath: urlPath, searchParams: searchParams)
}

public func post<T, V>(_ urlPath: String, searchParams: SearchParams? = nil, body: V) -> ResponsePublisher<T> where T: Decodable, V: Encodable {
request(.POST, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func post<T>(_ urlPath: String, searchParams: SearchParams? = nil, body: BodyParams) -> ResponsePublisher<T> where T: Decodable {
request(.POST, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func put<T>(_ urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(.PUT, urlPath: urlPath, searchParams: searchParams)
}

public func put<T, V>(_ urlPath: String, searchParams: SearchParams? = nil, body: V) -> ResponsePublisher<T> where T: Decodable, V: Encodable {
request(.PUT, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func put<T>(_ urlPath: String, searchParams: SearchParams? = nil, body: BodyParams) -> ResponsePublisher<T> where T: Decodable {
request(.PUT, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func patch<T>(_ urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(.PATCH, urlPath: urlPath, searchParams: searchParams)
}

public func patch<T, V>(_ urlPath: String, searchParams: SearchParams? = nil, body: V) -> ResponsePublisher<T> where T: Decodable, V: Encodable {
request(.PATCH, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func patch<T>(_ urlPath: String, searchParams: SearchParams? = nil, body: BodyParams) -> ResponsePublisher<T> where T: Decodable {
request(.PATCH, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func request<T>(_ method: HTTPMethod, urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(method, urlPath: urlPath, searchParams: searchParams, body: nil)
}

public func request<T, V>(_ method: HTTPMethod, urlPath: String, searchParams: SearchParams? = nil, body: V) -> ResponsePublisher<T> where T: Decodable, V: Encodable {
do {
let data = try Utils.jsonEncoder.encode(body)
return request(method, urlPath: urlPath, searchParams: searchParams, body: .data(data))
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
}

public func request<T>(_ method: HTTPMethod, urlPath: String, searchParams: SearchParams? = nil, body: BodyParams) -> ResponsePublisher<T> where T: Decodable {
do {
let data = try JSONSerialization.data(withJSONObject: body.compactMapValues { $0 }, options: [])
return request(method, urlPath: urlPath, searchParams: searchParams, body: .data(data))
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
}

private func request<T>(_ method: HTTPMethod, urlPath: String, searchParams: SearchParams? = nil, body: HTTPClient.Body? = nil) -> ResponsePublisher<T> where T: Decodable {
do {
var components = URLComponents(string: "\(environment.api)/\(urlPath)")
components?.queryItems = searchParams?.compactMapValues { $0 }.map(URLQueryItem.init)

guard let url = components?.url else {
throw RequestError.invalidURL
}

let headers = HTTPHeaders([
("APCA-API-KEY-ID", self.environment.key),
("APCA-API-SECRET-KEY", self.environment.secret),
("Content-Type", "application/json"),
("User-Agent", "alpaca-swift/1.0")
])

let httpRequest = try HTTPClient.Request(url: url, method: method, headers: headers, body: body)

return request(httpRequest)
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
}

private func request<T>(_ httpRequest: HTTPClient.Request) -> ResponsePublisher<T> where T: Decodable {
Future<HTTPClient.Response, Error> { resolver in
self.httpClient.execute(request: httpRequest).whenComplete(resolver)
}
.tryMap { response in
guard (200..<300).contains(response.status.code) else {
throw RequestError.status(response.status)
}
guard let body = response.body, let bytes = body.getBytes(at: 0, length: body.readableBytes) else {
return try Utils.jsonDecoder.decode(T.self, from: EmptyResponse.jsonData)
}
let data = Data(bytes)
return try Utils.jsonDecoder.decode(T.self, from: data)
}
.eraseToAnyPublisher()
self.data = AlpacaDataClient(key: environment.key, secret: environment.secret)
}
}
136 changes: 136 additions & 0 deletions Sources/Alpaca/AlpacaClientProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// File.swift
//
//
// Created by Andrew Barba on 8/25/20.
//

import AsyncHTTPClient
import Foundation
import NIO
import NIOHTTP1
import OpenCombine

public protocol AlpacaClientProtocol {
typealias ResponsePublisher<T: Decodable> = AnyPublisher<T, Error>

typealias EmptyResponsePublisher = ResponsePublisher<EmptyResponse>

typealias SearchParams = [String: String?]

typealias BodyParams = [String: Any?]

var environment: Environment { get }
}

// MARK: - Requests

extension AlpacaClientProtocol {

public func get<T>(_ urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(.GET, urlPath: urlPath, searchParams: searchParams)
}

public func delete<T>(_ urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(.DELETE, urlPath: urlPath, searchParams: searchParams)
}

public func post<T>(_ urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(.POST, urlPath: urlPath, searchParams: searchParams)
}

public func post<T, V>(_ urlPath: String, searchParams: SearchParams? = nil, body: V) -> ResponsePublisher<T> where T: Decodable, V: Encodable {
request(.POST, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func post<T>(_ urlPath: String, searchParams: SearchParams? = nil, body: BodyParams) -> ResponsePublisher<T> where T: Decodable {
request(.POST, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func put<T>(_ urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(.PUT, urlPath: urlPath, searchParams: searchParams)
}

public func put<T, V>(_ urlPath: String, searchParams: SearchParams? = nil, body: V) -> ResponsePublisher<T> where T: Decodable, V: Encodable {
request(.PUT, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func put<T>(_ urlPath: String, searchParams: SearchParams? = nil, body: BodyParams) -> ResponsePublisher<T> where T: Decodable {
request(.PUT, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func patch<T>(_ urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(.PATCH, urlPath: urlPath, searchParams: searchParams)
}

public func patch<T, V>(_ urlPath: String, searchParams: SearchParams? = nil, body: V) -> ResponsePublisher<T> where T: Decodable, V: Encodable {
request(.PATCH, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func patch<T>(_ urlPath: String, searchParams: SearchParams? = nil, body: BodyParams) -> ResponsePublisher<T> where T: Decodable {
request(.PATCH, urlPath: urlPath, searchParams: searchParams, body: body)
}

public func request<T>(_ method: HTTPMethod, urlPath: String, searchParams: SearchParams? = nil) -> ResponsePublisher<T> where T: Decodable {
return request(method, urlPath: urlPath, searchParams: searchParams, body: nil)
}

public func request<T, V>(_ method: HTTPMethod, urlPath: String, searchParams: SearchParams? = nil, body: V) -> ResponsePublisher<T> where T: Decodable, V: Encodable {
do {
let data = try Utils.jsonEncoder.encode(body)
return request(method, urlPath: urlPath, searchParams: searchParams, body: .data(data))
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
}

public func request<T>(_ method: HTTPMethod, urlPath: String, searchParams: SearchParams? = nil, body: BodyParams) -> ResponsePublisher<T> where T: Decodable {
do {
let data = try JSONSerialization.data(withJSONObject: body.compactMapValues { $0 }, options: [])
return request(method, urlPath: urlPath, searchParams: searchParams, body: .data(data))
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
}

private func request<T>(_ method: HTTPMethod, urlPath: String, searchParams: SearchParams? = nil, body: HTTPClient.Body? = nil) -> ResponsePublisher<T> where T: Decodable {
do {
var components = URLComponents(string: "\(environment.api)/\(urlPath)")
components?.queryItems = searchParams?.compactMapValues { $0 }.map(URLQueryItem.init)

guard let url = components?.url else {
throw RequestError.invalidURL
}

let headers = HTTPHeaders([
("APCA-API-KEY-ID", self.environment.key),
("APCA-API-SECRET-KEY", self.environment.secret),
("Content-Type", "application/json"),
("User-Agent", "alpaca-swift/1.0")
])

let httpRequest = try HTTPClient.Request(url: url, method: method, headers: headers, body: body)

return request(httpRequest)
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
}

private func request<T>(_ httpRequest: HTTPClient.Request) -> ResponsePublisher<T> where T: Decodable {
Future<HTTPClient.Response, Error> { resolver in
Utils.httpClient.execute(request: httpRequest).whenComplete(resolver)
}
.tryMap { response in
guard (200..<300).contains(response.status.code) else {
throw RequestError.status(response.status)
}
guard let body = response.body, let bytes = body.getBytes(at: 0, length: body.readableBytes) else {
return try Utils.jsonDecoder.decode(T.self, from: EmptyResponse.jsonData)
}
let data = Data(bytes)
return try Utils.jsonDecoder.decode(T.self, from: data)
}
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public struct Bar: Codable {
public var volume: Double { v }
}

extension AlpacaClient {
extension AlpacaDataClient {

public func bars(_ timeframe: Bar.Timeframe, symbols: [String], limit: Int? = nil, start: Date? = nil, end: Date? = nil, after: Date? = nil, until: Date? = nil) -> ResponsePublisher<[String: [Bar]]> {
return get("bars/\(timeframe.rawValue)", searchParams: [
Expand Down
18 changes: 18 additions & 0 deletions Sources/Alpaca/AlpacaDataClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// File.swift
//
//
// Created by Andrew Barba on 8/25/20.
//

import AsyncHTTPClient
import Foundation

public struct AlpacaDataClient: AlpacaClientProtocol {

public let environment: Environment

internal init(key: String, secret: String) {
self.environment = .data(key: key, secret: secret)
}
}
10 changes: 10 additions & 0 deletions Sources/Alpaca/Common.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
//

import Foundation
import NIOHTTP1

public struct EmptyResponse: Decodable {
internal static let jsonData = try! JSONSerialization.data(withJSONObject: [:], options: [])
}

public enum RequestError: Error {
case invalidURL
case status(HTTPResponseStatus)
}

public struct MultiResponse<T>: Codable where T: Codable {
public let status: Int
Expand Down
26 changes: 26 additions & 0 deletions Sources/Alpaca/Environment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// File.swift
//
//
// Created by Andrew Barba on 8/25/20.
//

import Foundation

public struct Environment {
public let api: String
public let key: String
public let secret: String

internal static func data(key: String, secret: String) -> Self {
Environment(api: "https://data.alpaca.markets/v1", key: key, secret: secret)
}

public static func live(key: String, secret: String) -> Self {
Environment(api: "https://api.alpaca.markets/v2", key: key, secret: secret)
}

public static func paper(key: String, secret: String) -> Self {
Environment(api: "https://paper-api.alpaca.markets/v2", key: key, secret: secret)
}
}
3 changes: 3 additions & 0 deletions Sources/Alpaca/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
// Created by Andrew Barba on 8/15/20.
//

import AsyncHTTPClient
import Foundation
import NIO

internal enum Utils {

static let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

static let httpClient = HTTPClient(eventLoopGroupProvider: .shared(Self.eventLoopGroup))

static let iso8601DateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Foundation.Calendar(identifier: .iso8601)
Expand Down
Loading

0 comments on commit ebb7a20

Please sign in to comment.