Skip to content

Commit

Permalink
Merge pull request #4 from IanKeen/persistence
Browse files Browse the repository at this point in the history
Persistence!
  • Loading branch information
IanKeen committed Jun 16, 2016
2 parents 4540a80 + 8f76f2f commit bd3c3cc
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 62 deletions.
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ let package = Package(
dependencies: [
.Package(url: "https://github.com/Zewo/WebSocketClient.git", majorVersion: 0, minor: 1),
.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),
],
exclude: [
Expand Down
9 changes: 7 additions & 2 deletions Sources/App/Services/KarmaBot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,13 @@ final class KarmaBot: SlackBotAPI {
return nil
}
private func adjustKarma(of user: User, action: KarmaAction, storage: Storage) {
let count: Int = storage.get(.In("Karma"), key: user.id, or: 0)
storage.set(.In("Karma"), key: user.id, value: action.operation(count, 1))
do {
let count: Int = storage.get(.In("Karma"), key: user.id, or: 0)
try storage.set(.In("Karma"), key: user.id, value: action.operation(count, 1))

} catch let error {
print("Unable to update Karma: \(error)")
}
}

private func isKarmaChannel(_ target: Target) -> Bool {
Expand Down
13 changes: 12 additions & 1 deletion Sources/App/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,18 @@ let config = try SlackBotConfig()

let bot = SlackBot(
config: config,
apis: [HelloBot()]
apis: [
HelloBot(),
KarmaBot(options: KarmaBot.Options(
targets: ["*"],
addText: "++",
addReaction: "+1",
removeText: "--",
removeReaction: "-1",
textDistanceThreshold: 4
)
)
]
)

bot.start()
8 changes: 6 additions & 2 deletions Sources/Bot/Services/Storage/MemoryStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@
// Copyright © 2016 Mustard. All rights reserved.
//

/// Provides in-memory only storage of key/value pairs
public final class MemoryStorage: Storage {
//MARK: - Private
private var data = [String: [String: Any]]()

//MARK - Lifecycle
public init() { }

public func set<T>(_ in: StorageNamespace, key: String, value: T) {
//MARK: - Storage
public func set<T: StorableType>(type: T.Type, in: StorageNamespace, key: String, value: T) throws {
var data = self.data[`in`.namespace] ?? [:]
data[key] = value
self.data[`in`.namespace] = data
}
public func get<T>(_ in: StorageNamespace, key: String) -> T? {
public func get<T: StorableType>(type: T.Type, in: StorageNamespace, key: String) -> T? {
return self.data[`in`.namespace]?[key] as? T
}
}
103 changes: 53 additions & 50 deletions Sources/Bot/Services/Storage/PlistStorage.swift
Original file line number Diff line number Diff line change
@@ -1,51 +1,54 @@
////
//// PlistStorage.swift
//// Chameleon
////
//// Created by Ian Keen on 23/05/2016.
//// Copyright © 2016 Mustard. All rights reserved.
////
//
//public final class PlistStorage: Storage {
// //MARK: - Public
// public init() { }
//
// public func set<T : StorableDataType>(_ in: StorageNamespace, key: String, value: T) {
// var dataset = self.dataset()
// var data = dataset[`in`.namespace] ?? [:]
//
// data[key] = value.anyObject
// dataset[`in`.namespace] = data
//
// self.saveDataset(dataset)
// }
// public func get<T>(_ in: StorageNamespace, key: String) -> T? {
// var dataset = self.dataset()
// var data = dataset[`in`.namespace] ?? [:]
// return data[key] as? T
// }
//
// //MARK: - Private
// private var fileName: String {
// return "\(NSHomeDirectory())/storage.plist"
// }
// private func dataset() -> [String: [String: Any]] {
// guard let dict = NSDictionary(contentsOfFile: self.fileName) as? [String: [String: Any]] else {
// return self.defaultDataset()
// }
// return dict
// }
// private func defaultDataset() -> [String: [String: Any]] { return [:] }
// private func saveDataset(_ dataset: [String: [String: Any]]) {
// var result = [String: [String: Any]]()
// for (namespace, data) in dataset {
// var subset = [String: Any]()
// for (key, value) in data {
// subset[key] = value
// }
// result[namespace] = subset
// }
//
// (result as NSDictionary).write(toFile: self.fileName, atomically: true)
// }
//}
// PlistStorage.swift
// Chameleon
//
// Created by Ian Keen on 23/05/2016.
// Copyright © 2016 Mustard. All rights reserved.
//

import Foundation

// Provides Plist based storage of key/value pairs (OSX only)
public final class PlistStorage: Storage {
//MARK: - Public
public init() { }

//MARK: - Storage
public func set<T: StorableType>(type: T.Type, in: StorageNamespace, key: String, value: T) throws {
var dataset = self.dataset()
var data = dataset[`in`.namespace] ?? [:]

guard let anyObject = value.anyObject else { throw StorageError.invalidValue(value: value) }
data[key] = anyObject
dataset[`in`.namespace] = data

self.saveDataset(dataset)
}
public func get<T : StorableType>(type: T.Type, in: StorageNamespace, key: String) -> T? {
var dataset = self.dataset()
var data = dataset[`in`.namespace] ?? [:]
return data[key] as? T
}

//MARK: - Private
private var fileName: String {
return "\(NSHomeDirectory())/storage.plist"
}
private func dataset() -> [String: [String: AnyObject]] {
guard let dict = NSDictionary(contentsOfFile: self.fileName) as? [String: [String: AnyObject]] else {
return self.defaultDataset()
}
return dict
}
private func defaultDataset() -> [String: [String: AnyObject]] { return [:] }
private func saveDataset(_ dataset: [String: [String: AnyObject]]) {
(dataset as NSDictionary).write(toFile: self.fileName, atomically: true)
}
}

//MARK: - Helper
extension StorableType {
var anyObject: AnyObject? {
return self as? AnyObject
}
}
92 changes: 92 additions & 0 deletions Sources/Bot/Services/Storage/RedisStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// RedisStorage.swift
// Chameleon
//
// Created by Ian Keen on 15/06/2016.
//
//

import Redbird

/// 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 {
let config = RedbirdConfig(address: address, port: port, password: password)
self.client = try Redbird(config: config)
}

//MARK: - Storage
public func set<T: StorableType>(type: T.Type, in: StorageNamespace, key: String, value: T) throws {
guard let value = value as? RedisStorable else { throw StorageError.unsupportedType(value: T.self) }
let key = "\(`in`.namespace):\(key)"
do {
try self.client.command("DEL", params: [key])
try self.client.command(value.redisStoreCommand, params: [key] + value.redisStoreParams)
}
catch let error { throw StorageError.internalError(error: error) }
}
public func get<T: StorableType>(type: T.Type, in: StorageNamespace, key: String) -> T? {
guard let type = type as? RedisRetrievable.Type else { print("Unsupported data type: \(T.self)"); return nil }
let key = "\(`in`.namespace):\(key)"
do {
let result = try self.client.command(type.redisRetrieveCommand, params: [key])
guard let stringValue = try result.toMaybeString() else { return nil }
return type.redisValue(from: stringValue) as? T
}
catch let error { print("\(error)"); return nil }
}
}

//MARK: - Redis Serialization
protocol RedisStorable {
var redisStoreCommand: String { get }
var redisStoreParams: [String] { get }
}
extension RedisStorable {
var redisStoreCommand: String { return "SET" }
var redisStoreParams: [String] { return [String(self)] }
}

//MARK: - Redis Deserialization
protocol RedisRetrievable {
static var redisRetrieveCommand: String { get }
static func redisValue(from: String) -> Self?
}
extension RedisRetrievable {
static var redisRetrieveCommand: String { return "GET" }
}

//MARK: - RedisStorable/RedisRetrievable Implementations
extension String: RedisStorable, RedisRetrievable {
static func redisValue(from: String) -> String? { return from }
}
extension Int: RedisStorable, RedisRetrievable {
var redisStoreParams: [String] { return [ String(self)] }
static func redisValue(from: String) -> Int? { return Int(from) }
}
extension Bool: RedisStorable, RedisRetrievable {
static func redisValue(from: String) -> Bool? {
return (from == "true" ? true : false)
}
}

//TOOD
// I think these are wrong... need to test!!
extension Sequence where Iterator.Element: RedisStorable {
var redisStoreCommand: String { return "LPUSH" }
var redisStoreParams: [String] {
return [self
.flatMap { $0.redisStoreParams }
.joined(separator: ";")]
}
}
extension Sequence where Iterator.Element: RedisRetrievable {
var redisRetrieveCommand: String { return "MGET" }
static func redisValue(from: String) -> Self? {
return from.components(separatedBy: ";") as? Self
}
}
102 changes: 97 additions & 5 deletions Sources/Bot/Services/Storage/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
// Copyright © 2016 Mustard. All rights reserved.
//

/// Defines a namespace to group different settings
public enum StorageNamespace {
/// A namespace designed to store data that can be shared among services
case Shared

/// A custom namespace to keep service specific data separate
case In(String)

var namespace: String {
Expand All @@ -18,14 +22,102 @@ public enum StorageNamespace {
}
}

/// Describes a range of errors that can occur when using storage
public enum StorageError: ErrorProtocol {
/// The value being stored is invalid
case invalidValue(value: Any)

/// The value being stored is unsupported
case unsupportedType(value: Any.Type)

/// Something went wrong with an dependency
case internalError(error: ErrorProtocol)
}

/// An abstraction representing an object capable of key/value storage
public protocol Storage: class {
func set<T>(_ in: StorageNamespace, key: String, value: T) //TODO: should this be a throwing function...?
func get<T>(_ in: StorageNamespace, key: String) -> T?
func get<T>(_ in: StorageNamespace, key: String, or: T) -> T
/**
Assign a value to a key under a namespace
- parameter type: The `Type` of `value`
- parameter in: The `StorageNamespace` to store the key/value pair under
- parameter key: A unique key to store `value` under
- parameter value: The value being stored
- throws: A `StorageError` with failure details
*/
func set<T: StorableType>(type: T.Type, in: StorageNamespace, key: String, value: T) throws

/**
Retrieves a value by its key under a namespace
- parameter type: The `Type` of value being retrieved
- parameter in: The `StorageNamespace` the key/value pair is under
- parameter key: A unique key used to store the value
- returns: returns the value if a value was found with the provided key under the namespace and the type matches `type`, otherwise nil
*/
func get<T: StorableType>(type: T.Type, in: StorageNamespace, key: String) -> T?
}

extension Storage {
public func get<T>(_ in: StorageNamespace, key: String, or: T) -> T {
return self.get(`in`, key: key) ?? or
/**
Assign a value to a key under a namespace
- parameter in: The `StorageNamespace` to store the key/value pair under
- parameter key: A unique key to store `value` under
- parameter value: The value being stored
- throws: A `StorageError` with failure details
*/
public func set<T: StorableType>(_ in: StorageNamespace, key: String, value: T) throws {
try self.set(type: T.self, in: `in`, key: key, value: value)
}

/**
Retrieves a value by its key under a namespace
- parameter in: The `StorageNamespace` the key/value pair is under
- parameter key: A unique key used to store the value
- returns: returns the value if a value was found with the provided key under the namespace and the type matches `T`, otherwise nil
*/
public func get<T: StorableType>(_ in: StorageNamespace, key: String) -> T? {
return self.get(type: T.self, in: `in`, key: key)
}

/**
Retrieves a value by its key under a namespace
- parameter type: The `Type` of value being retrieved
- parameter in: The `StorageNamespace` the key/value pair is under
- parameter key: A unique key used to store the value
- parameter or: The value to return if one does not exist or something goes wrong
- returns: returns the value if a value was found with the provided key under the namespace and the type matches `type`, otherwise `or`
*/
public func get<T: StorableType>(type: T.Type = T.self, in: StorageNamespace, key: String, or: T) -> T {
return self.get(type: type, in: `in`, key: key) ?? or
}

/**
Retrieves a value by its key under a namespace
- parameter in: The `StorageNamespace` the key/value pair is under
- parameter key: A unique key used to store the value
- parameter or: The value to return if one does not exist or something goes wrong
- returns: returns the value if a value was found with the provided key under the namespace and the type matches `T`, otherwise `or`
*/
public func get<T: StorableType>(_ in: StorageNamespace, key: String, or: T) -> T {
return self.get(type: T.self, in: `in`, key: key, or: or)
}
}

/// An abstraction representing an object capable being stored by a `Storage` object
public protocol StorableType {
var storableValue: Any { get }
func value<T>() -> T?
}
extension StorableType {
public var storableValue: Any { return self }
public func value<T>() -> T? { return self as? T }
}
extension String: StorableType { }
extension Int: StorableType { }
extension Bool: StorableType { }
extension Array: StorableType { }

0 comments on commit bd3c3cc

Please sign in to comment.