Skip to content

Commit

Permalink
Merge pull request #5 from IanKeen/bootstrap-improvements
Browse files Browse the repository at this point in the history
Bootstrap Improvements
  • Loading branch information
IanKeen committed Jun 17, 2016
2 parents 6c1e369 + deb7b78 commit 40a24bd
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 39 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
Procfile
Packages
.build
.DS_Store
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ let package = Package(
.Package(url: "https://github.com/IanKeen/Jay.git", majorVersion: 0, minor: 0),
.Package(url: "https://github.com/czechboy0/Redbird.git", majorVersion: 0, minor: 7),
.Package(url: "https://github.com/ketzusaka/Strand.git", majorVersion: 1, minor: 3),
.Package(url: "https://github.com/czechboy0/Environment.git", majorVersion: 0),
],
exclude: [
"XcodeProject"
Expand Down
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
worker: App
4 changes: 3 additions & 1 deletion Sources/App/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import Services
import WebAPI
import RTMAPI
import Bot
import Environment

let config = try SlackBotConfig()
let config = try SlackBotConfig.makeConfig(from: Environment())

let bot = SlackBot(
config: config,
storage: try RedisStorage(url: config.storageUrl!),
apis: [
HelloBot(),
KarmaBot(options: KarmaBot.Options(
Expand Down
14 changes: 13 additions & 1 deletion Sources/Bot/Services/Storage/RedisStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,26 @@
//

import Redbird
import Foundation

/// Provides Redis based storage of key/value pairs
public final class RedisStorage: Storage {
//MARKL - Private
private let client: Redbird

//MARK: - Public
public init(address: String, port: UInt16, password: String?) throws {
public convenience init(url urlString: String) throws {
guard let url = NSURL(string: urlString) else { throw StorageError.invalidURL(url: urlString) }

guard
let host = url.host,
let port = url.port?.uint16Value,
let password = url.password
else { throw StorageError.invalidURL(url: urlString) }

try self.init(address: host, port: port, password: password)
}
public required init(address: String, port: UInt16, password: String?) throws {
let config = RedbirdConfig(address: address, port: port, password: password)
self.client = try Redbird(config: config)
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/Bot/Services/Storage/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public enum StorageNamespace {

/// Describes a range of errors that can occur when using storage
public enum StorageError: ErrorProtocol {
/// The connection url supplied is invalid
case invalidURL(url: String)

/// The value being stored is invalid
case invalidValue(value: Any)

Expand Down
140 changes: 105 additions & 35 deletions Sources/Bot/SlackBot+Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,56 +9,126 @@
import Foundation
import Common
import WebAPI
import Environment

extension SlackBotConfig {
/// Supported Variables
public enum Variables {
/// URL for the Storage layer to use
public static let StorageURL = "STORAGE_URL"

/// Token to use for the bot
public static let Token = "TOKEN"

/// How many times to retry connecting before giving up
public static let ReconnectionAttempts = "RECONNECTION_ATTEMPTS"

/// How often to send a PING to slack once conencted to keep the connection alive
public static let PingPongInterval = "PING_PONG_INTERVAL"

/// All available environment variables
public static var allVariables: [String] {
return [Variables.StorageURL, Variables.Token, Variables.ReconnectionAttempts, Variables.PingPongInterval]
}
}
}

extension SlackBotConfig {
/// Describes a range of errors that can occur when build configuration data
public enum Error: ErrorProtocol {
/// A required parameter was not provided
case missingRequiredParameter(parameter: String)

/// An invalid parameter was provided
case invalidParameter(parameter: String)

/// An unsupported parameter was provided
case unsupportedParameter(parameter: String)
}
}

/// Defines the configuration options that can be used
public struct SlackBotConfig {
//MARK: - Public Properties
/// The token of the bot user
public let token: String

/// Options passed to the RTMStart `WebAPIMethod` when connecting
public var startOptions: [RTMStart.Option] = []

/// How many times to retry connecting before giving up
public var reconnectionAttempts: Int = 10

/// How often to send a PING to slack once conencted to keep the connection alive
public var pingPongInterval: NSTimeInterval = 5.0

public init(token: String, startOptions: [RTMStart.Option]?, reconnectionAttempts: Int?, pingPongInterval: NSTimeInterval?) {
/// Optional: The url that can be used by the `Storage` object
public var storageUrl: String? = nil

//MARK: - Lifecycle
public init(token: String?, startOptions: [RTMStart.Option]?, reconnectionAttempts: Int?, pingPongInterval: NSTimeInterval?, storageUrl: String?) throws {
guard let token = token else { throw Error.missingRequiredParameter(parameter: "token") }

self.token = token
if let startOptions = startOptions { self.startOptions = startOptions }
if let reconnectionAttempts = reconnectionAttempts { self.reconnectionAttempts = reconnectionAttempts }
if let pingPongInterval = pingPongInterval { self.pingPongInterval = pingPongInterval }
self.startOptions = startOptions ?? self.startOptions
self.reconnectionAttempts = reconnectionAttempts ?? self.reconnectionAttempts
self.pingPongInterval = pingPongInterval ?? self.pingPongInterval
self.storageUrl = storageUrl ?? self.storageUrl
}
}

//
//TOOD: this is horrible/half assed...
// it was added last minute just to provide a simple was on injecting the token
// dont judge me! I will built something more robust eventually
//

public enum SlackBotConfigError: ErrorProtocol {
case MissingRequiredParameter(String)
case InvalidArgument(String)
}

//MARK: - Command line parameters
extension SlackBotConfig {
public init(input: [String] = NSProcessInfo.processInfo().arguments) throws {
var dict = [String: String]()
/**
Creates a `SlackBotConfig` from a `NSProcessInfo`s command line arguments
- parameter process: The `NSProcessInfo` containing the arguments
- throws: A `SlackBotConfig.Error` with failure details
- returns: A new `SlackBotConfig` instance
*/
public static func makeConfig(from process: NSProcessInfo) throws -> SlackBotConfig {
let supportedArguments = Variables.allVariables.map { "--\($0.snakeToLowerCamel)=" }
let arguments = try process.arguments.dropFirst().flatMap { argument -> (key: String, value: String) in
let pair = argument.components(separatedBy: "=")
guard
let key = pair[safe: 0]?.components(separatedBy: "--").last,
let value = pair[safe: 1]
else { throw Error.invalidParameter(parameter: argument) }

guard supportedArguments.contains("--\(key)=") else { throw Error.unsupportedParameter(parameter: argument) }

return (key: key.lowercased(), value: value)
}

//Attempt to filter out invalid parameters
try input
.filter { $0.characters.count > 4 && $0.hasPrefix("--") && $0.characters.contains("=") }
.map { argument in
let sanitizedArgument = argument.substring(from: argument.index(argument.startIndex, offsetBy: 2))
let pair = sanitizedArgument.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: true)
guard let key = pair[safe: 0], let value = pair[safe: 1] else { throw SlackBotConfigError.InvalidArgument(argument) }
return (key: key, value: value)
}
.forEach { key, value in
dict[key] = value
}

guard let token = dict["token"] else { throw SlackBotConfigError.MissingRequiredParameter("token") }
var dict = [String: String]()
arguments.forEach { dict[$0.key] = $0.value }

self = SlackBotConfig(
token: token,
return try SlackBotConfig(
token: dict[Variables.Token.lowercased()],
startOptions: nil,
reconnectionAttempts: Int(dict[Variables.ReconnectionAttempts.lowercased()] ?? ""),
pingPongInterval: NSTimeInterval(dict[Variables.PingPongInterval.lowercased()] ?? ""),
storageUrl: dict[Variables.StorageURL.lowercased()]
)
}
}

//MARK: - Environment Variables
extension SlackBotConfig {
/**
Creates a `SlackBotConfig` from a `Environment` variables
- parameter environment: The `Environment` object containing the variables
- throws: A `SlackBotConfig.Error` with failure details
- returns: A new `SlackBotConfig` instance
*/
public static func makeConfig(from environment: Environment) throws -> SlackBotConfig {
return try SlackBotConfig(
token: environment.getVar(Variables.Token),
startOptions: nil,
reconnectionAttempts: Int(dict["reconnectionAttempts"] ?? ""),
pingPongInterval: NSTimeInterval(dict["pingPongInterval"] ?? "")
reconnectionAttempts: Int(environment.getVar(Variables.ReconnectionAttempts.lowercased()) ?? ""),
pingPongInterval: NSTimeInterval(environment.getVar(Variables.PingPongInterval.lowercased()) ?? ""),
storageUrl: environment.getVar(Variables.StorageURL.lowercased())
)
}
}
2 changes: 1 addition & 1 deletion Sources/Common/Sequence+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public extension Collection {
- returns: The relevant element if the index is within the bounds of the array; otherwise nil
*/
public subscript(safe safe: Index) -> Iterator.Element? {
guard safe >= self.startIndex && safe <= self.endIndex else { return nil }
guard safe >= self.startIndex && safe < self.endIndex else { return nil }
return self[safe]
}
}
18 changes: 18 additions & 0 deletions Sources/Common/String+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// String+Extensions.swift
// Chameleon
//
// Created by Ian Keen on 16/06/2016.
//
//

extension String {
public var snakeToLowerCamel: String {
return self
.components(separatedBy: "_")
.enumerated()
.reduce("") { (result: String, item: (index: Int, part: String)) in
return result + (item.index == 0 ? item.part.lowercased() : item.part.capitalized)
}
}
}

0 comments on commit 40a24bd

Please sign in to comment.