Skip to content

Commit

Permalink
REFACTOR: Separated the functionality that's shared across GPEvent ty…
Browse files Browse the repository at this point in the history
…pe, into their own protocols

If applied, this commit will:
- [T] Introduce tests for event router
- [C] Add extra functionality of GPEventRouter, to easily stop the routing process
- [C] Extend the Foundation's Data type, to add a convenience toString method
  • Loading branch information
Vinncz committed Oct 2, 2024
1 parent f303f3b commit 26e5231
Show file tree
Hide file tree
Showing 15 changed files with 344 additions and 130 deletions.
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ let package = Package(
"Events/Implementations/GPAcquaintanceEvent.swift",
"Events/Implementations/GPBlacklistedEvent.swift",
"Events/Implementations/GPTerminationEvent.swift",
"Events/Traits/GPEasilyReadablePayloadKeys.swift",
"Utilities/GPRepresentableAsData.swift",
"Extensions/Data.swift",
"GPGameProcess.swift",
"GPGameTemporaryStorage.swift",
"GPGameProcessConfiguration.swift",
Expand Down
51 changes: 35 additions & 16 deletions Sources/GamePantry/Events/Controls/GPEventRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@ import Combine

open class GPEventRouter {

private var subjects : [ ObjectIdentifier: Any ] = [:]
private var cancellables : Set<AnyCancellable> = [ ]
private var subjects : [ObjectIdentifier: Any] = [:]
private var cancellables : Set<AnyCancellable> = [ ]

public init () {}

}

extension GPEventRouter {

public func registerSubject <T: GPEvent, S: Subject> ( for eventType: T.Type, subject: S ) -> Bool where S.Output == T, S.Failure == Never {
public func registerSubject <T: GPEvent, S: Subject> ( for eventType: T.Type, _ subject: S ) -> Bool where S.Output == T, S.Failure == Never {
guard self.subject(for: eventType) as GPAnySubject<T, Never>? == nil else { return false }

let key = ObjectIdentifier(eventType)
let key = generateKey(for: eventType)
let anySubject = GPAnySubject(subject)

self.subjects[key] = anySubject
return true
}

public func subject <T: GPEvent> ( for eventType: T.Type ) -> GPAnySubject<T, Never>? {
let key: ObjectIdentifier = ObjectIdentifier(eventType)
let key: ObjectIdentifier = generateKey(for: eventType)
guard let subject = subjects[key] as? GPAnySubject<T, Never> else {
return nil
}
Expand All @@ -34,7 +34,7 @@ extension GPEventRouter {

extension GPEventRouter {

public func registerPublisher <T: GPEvent, P: Publisher> ( for eventType: T.Type, publisher: P, _ applyOperators: (AnyPublisher<T, P.Failure>) -> AnyPublisher<T, P.Failure> ) -> AnyCancellable? where P.Output == T, P.Failure == Never {
public func registerPublisher <T: GPEvent, P: Publisher> ( for eventType: T.Type, _ publisher: P, _ applyOperators: (AnyPublisher<T, P.Failure>) -> AnyPublisher<T, P.Failure> ) -> AnyCancellable? where P.Output == T, P.Failure == Never {
let modifiedPublisher = applyOperators(publisher.eraseToAnyPublisher())

if let existingSubject = subject(for: eventType) {
Expand All @@ -43,7 +43,7 @@ extension GPEventRouter {
}
}

if registerSubject ( for: eventType, subject: PassthroughSubject<T, Never>() ) {
if registerSubject ( for: eventType, PassthroughSubject<T, Never>() ) {
return modifiedPublisher.sink { value in
self.subject(for: eventType)?.send(value)
}
Expand All @@ -57,7 +57,7 @@ extension GPEventRouter {
extension GPEventRouter {

public func route <T: GPEvent> ( _ event: T ) -> Bool {
let key: ObjectIdentifier = ObjectIdentifier( T.self )
let key: ObjectIdentifier = generateKey(for: T.self)
guard let subject = self.subjects[key] as? GPAnySubject<T, Never> else {
return false
}
Expand All @@ -70,18 +70,37 @@ extension GPEventRouter {

extension GPEventRouter {

public func stopRouting <T: GPEvent> ( _ eventType: T.Type ) {
let key: ObjectIdentifier = ObjectIdentifier(eventType)
self.subjects.removeValue(forKey: key)
private func generateKey <T: GPEvent> ( for eventType: T.Type ) -> ObjectIdentifier {
return ObjectIdentifier(eventType)
}

public func findSubject <T: GPEvent> ( _ eventType: T.Type ) -> GPAnySubject<T, Error>? {
let key: ObjectIdentifier = ObjectIdentifier(eventType)
guard let subject = self.subjects[key] as? GPAnySubject<T, Error> else {
return nil
public func forceRegisterSubject <T: GPEvent, S: Subject> ( for eventType: T.Type, _ subject: S ) -> Bool where S.Output == T, S.Failure == Never {
let key: ObjectIdentifier = generateKey(for: eventType)

if self.subject(for: eventType) != nil {
self.subjects[key] = GPAnySubject(subject)
return true
}

return subject
self.subjects[key] = GPAnySubject(subject)
return false
}

public func stopRouting <T: GPEvent> ( _ eventType: [T.Type] ) {
eventType.forEach { type in
let key: ObjectIdentifier = generateKey(for: type)
self.subjects.removeValue(forKey: key)
}
}

public func stopRouting ( _ eventType: [ObjectIdentifier] ) {
eventType.forEach { key in
self.subjects.removeValue(forKey: key)
}
}

public func allKeys () -> [ObjectIdentifier] {
return Array(self.subjects.keys)
}

}
6 changes: 3 additions & 3 deletions Sources/GamePantry/Events/GPEvent.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
public protocol GPEvent {

var purpose : String { get }
var purpose : String { get }

var time : Date { get }
var time : Date { get }

var payload : [String: Any]? { get }
var payload : [String: Any] { get }

}
46 changes: 32 additions & 14 deletions Sources/GamePantry/Events/Implementations/GPAcquaintanceEvent.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
import MultipeerConnectivity

public struct GPAcquaintanceEvent : GPEvent {

public let who : MCPeerID
public let newState: MCSessionState
public class GPAcquaintanceEvent : GPEvent {

public var purpose : String
public var time : Date
public var payload : [String: Any]?
public var payload : [String: Any]

public init ( _ payload: [String: Any] ) {
self.purpose = "An event which indicates that the state of one's acquaintance has changed"
self.time = .now
self.payload = payload
}

}

extension GPAcquaintanceEvent : GPEasilyReadableEventPayloadKeys {

public enum PayloadKeys : String, CaseIterable {
case subject = "subject",
acquaintanceState = "acquaintanceState",
updatedAt = "updatedAt"
}

public func value ( for key: PayloadKeys ) -> Any {
return self.payload[key.rawValue]!
}

}

public init ( who: MCPeerID, newState: MCSessionState, payload: [String: Any]? ) {
self.who = who
self.newState = newState

self.purpose = "An event which indicates that the state of one's acquaintance has changed."
self.time = .now
self.payload = payload
extension GPAcquaintanceEvent : GPRepresentableAsData {

public func representedAsData () -> Data {
return dataFrom {
PayloadKeys.allCases.reduce(into: [String: String]()) { (result, key) in
result[key.rawValue] = self.payload[key.rawValue] as? String ?? ""
}
} ?? Data()
}

}
6 changes: 2 additions & 4 deletions Sources/GamePantry/Events/Implementations/GPAnyEvent.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import Foundation

public class GPAnyEvent : GPEvent {

public var purpose : String
public var time : Date
public var payload : [String: Any]?
public var payload : [String: Any]

public init ( purpose: String, time: Date, payload: [String: Any]? ) {
public init ( purpose: String, time: Date, payload: [String: Any] ) {
self.purpose = purpose
self.time = time
self.payload = payload
Expand Down
3 changes: 2 additions & 1 deletion Sources/GamePantry/Events/Implementations/GPAnySubject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class GPAnySubject <Output, Failure: Error> : Subject {
private let _forwardingSubscriptionClosure : ( Subscription ) -> Void
private let _forwardingSubscriberClosure : ( AnySubscriber<Output, Failure> ) -> Void

public init <S: Subject> (_ subject: S) where S.Output == Output, S.Failure == Failure {
public init <S: Subject> ( _ subject: S ) where S.Output == Output, S.Failure == Failure {
_forwardingOutputClosure = { value in
subject.send(value)
}
Expand All @@ -34,6 +34,7 @@ public class GPAnySubject <Output, Failure: Error> : Subject {
}

public func send ( subscription: Subscription ) {
subscription.request(.max(<#T##value: Int##Int#>))
_forwardingSubscriptionClosure(subscription)
}

Expand Down
56 changes: 26 additions & 30 deletions Sources/GamePantry/Events/Implementations/GPBlacklistedEvent.swift
Original file line number Diff line number Diff line change
@@ -1,43 +1,39 @@
import MultipeerConnectivity

public struct GPBlacklistedEvent : GPEvent {

public let who : MCPeerID
public let reason : String
public class GPBlacklistedEvent : GPEvent {

public let purpose : String
public let time : Date
public let payload : [String: Any]?

public init ( who: MCPeerID, reason: String, payload: [String: Any]? ) {
self.who = who
self.reason = reason

self.purpose = "An event that marks a peer as blacklisted."
self.time = .now
self.payload = payload
public let payload : [String: Any]

public init ( _ payload: [String: Any] ) {
self.purpose = "An event that marks a peer as blacklisted"
self.time = .now
self.payload = payload
}

}

extension GPBlacklistedEvent : GPEasilyReadableEventPayloadKeys {

public enum PayloadKeys : String, CaseIterable {
case subject = "subject",
reason = "causeOfBlacklist",
effectiveTime = "effectiveTime"
}

public func representation () -> Data {
return (
try? JSONSerialization.data (
withJSONObject: [
"who" : who,
"purpose" : purpose,
"time" : time,
"payload" : payload ?? [:]
],
options: .prettyPrinted
)
) ?? Data()
public func value ( for key: PayloadKeys ) -> Any {
return self.payload[key.rawValue]!
}

}

extension GPBlacklistedEvent {
extension GPBlacklistedEvent : GPRepresentableAsData {

public enum payloadKeys : String {
case reason = "causeOfBlacklist"
public func representedAsData () -> Data {
return dataFrom {
PayloadKeys.allCases.reduce(into: [String: String]()) { (result, key) in
result[key.rawValue] = self.payload[key.rawValue] as? String ?? ""
}
} ?? Data()
}

}
55 changes: 25 additions & 30 deletions Sources/GamePantry/Events/Implementations/GPTerminationEvent.swift
Original file line number Diff line number Diff line change
@@ -1,44 +1,39 @@
import MultipeerConnectivity

public struct GPTerminationEvent : GPEvent {
public class GPTerminationEvent : GPEvent {

public let who : MCPeerID
public let validOn : Date

public let purpose : String
public let time : Date
public let payload : [String: Any]?
public let payload : [String: Any]

public init ( who: MCPeerID, validOn: Date, payload: [String: Any]? ) {
self.who = who
self.validOn = validOn

self.purpose = "An event that marks the termination of relationship of a game client with the server its connected to."
public init ( _ payload: [String: Any] ) {
self.purpose = "An event that marks the termination of relationship of a game client with the server its connected to"
self.time = .now
self.payload = nil
self.payload = payload
}

}

extension GPTerminationEvent : GPEasilyReadableEventPayloadKeys {

public enum PayloadKeys : String, CaseIterable {
case subject = "subject",
terminationReason = "causeOfTermination",
effectiveTime = "effectiveTime"
}

public func representation () -> Data {
return (
try? JSONSerialization.data (
withJSONObject: [
"who" : who,
"validOn" : validOn,
"purpose" : purpose,
"time" : time,
"payload" : payload ?? [:]
],
options: .prettyPrinted
)
) ?? Data()
public func value ( for key: PayloadKeys ) -> Any {
return self.payload[key.rawValue]!
}

}

extension GPTerminationEvent {
extension GPTerminationEvent : GPRepresentableAsData {

public enum payloadKeys : String {
case verificationKey = "verificationKey",
terminationReason = "causeOfTermination"
public func representedAsData () -> Data {
return dataFrom {
PayloadKeys.allCases.reduce(into: [String: String]()) { (result, key) in
result[key.rawValue] = String(describing: self.payload[key.rawValue]!)
}
} ?? Data()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public protocol GPEasilyReadableEventPayloadKeys {

associatedtype PayloadKeys : CaseIterable

func value ( for: PayloadKeys ) -> Any

}
7 changes: 7 additions & 0 deletions Sources/GamePantry/Extensions/Data.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extension Data {

public func toString ( encoder: String.Encoding = .utf8 ) -> String? {
String(data: self, encoding: encoder)
}

}
3 changes: 2 additions & 1 deletion Sources/GamePantry/GPGameProcess.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@_exported import MultipeerConnectivity
@_exported import Combine
@_exported import Foundation
@_exported import MultipeerConnectivity

public protocol GPGameProcess {

Expand Down
2 changes: 2 additions & 0 deletions Sources/GamePantry/GPGameProcessConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ public struct GPGameProcessConfiguration {
public let gameName : String
public let gameVersion : String
public let serviceType : String
public let commLimit : Int

public init ( debugEnabled: Bool, gameName: String, gameVersion: String, serviceType: String ) {
self.debugEnabled = debugEnabled
self.gameName = gameName
self.gameVersion = gameVersion
self.serviceType = serviceType
self.commLimit = .max
}

}
Loading

0 comments on commit 26e5231

Please sign in to comment.