[SLService] basic functionality
This commit is contained in:
parent
d6ebec9ab9
commit
72d341ff91
|
@ -0,0 +1,100 @@
|
|||
# SLChat
|
||||
|
||||
<p align="center">
|
||||
<a href="http://swift.org">
|
||||
<img src="https://img.shields.io/badge/Swift-3.1-brightgreen.svg" alt="Language" />
|
||||
</a>
|
||||
<a href="https://raw.githubusercontent.com/shial4/SLChat/master/license">
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" />
|
||||
</a>
|
||||
<a href="https://travis-ci.org/shial4/SLChat">
|
||||
<img src="https://travis-ci.org/shial4/SLChat.svg?branch=master" alt="TravisCI" />
|
||||
</a>
|
||||
<a href="https://circleci.com/gh/shial4/SLChat">
|
||||
<img src="https://circleci.com/gh/shial4/SLChat.svg?style=shield" alt="CircleCI" />
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/shial4/SLChat">
|
||||
<img src="https://codecov.io/gh/shial4/SLChat/branch/master/graph/badge.svg" alt="codecov" />
|
||||
</a>
|
||||
<a href="https://codebeat.co/projects/github-com-shial4-slchat-master">
|
||||
<img src="https://codebeat.co/badges/bafbee05-9197-4625-84f8-1e022e3a6dad" alt="codebeat" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
SLChat is a simple extension for Kitura-WebSocket. Allows you to integrate chat system with your client.
|
||||
|
||||
|
||||
## 🔧 Installation
|
||||
|
||||
Add the following dependency to your `Package.swift` file:
|
||||
```swift
|
||||
.Package(url:"https://github.com/shial4/SLChat.git", majorVersion: 0, minor: 1)
|
||||
```
|
||||
|
||||
## 💊 Usage
|
||||
|
||||
### 1 Import
|
||||
|
||||
It's really easy to get started with the SLChat library! First you need to import the library, by adding this to the top of your Swift file:
|
||||
```swift
|
||||
import SLChat
|
||||
```
|
||||
|
||||
### 2 Initialize
|
||||
|
||||
The easiest way to setup SLChat is to create object for example in your `main.swift` file. Like this:
|
||||
```swift
|
||||
let slChat = SLService<Client>()
|
||||
```
|
||||
|
||||
Perfectly works with Kitura.
|
||||
```swift
|
||||
WebSocket.register(service: SLService<Client>(), onPath: "slchat")
|
||||
```
|
||||
|
||||
### 3 Configure
|
||||
|
||||
`SLService` instance require your Client model. If you won't use it, you can simply declare an empty struct for that
|
||||
```swift
|
||||
struct Client: SLClient {}
|
||||
let chat = SLService<Client>()
|
||||
```
|
||||
|
||||
#### SLClient Protocol
|
||||
Every function in this protocol is optional. It means `SLClient` provide default implementation. However, you are allowed to override it by your own. Why do that? To provide additional functionality. For example: data base storage for your message history, handle `Data` messages, provide recipients for status messages like connected or disconnected. And many more!
|
||||
```swift
|
||||
extension SLClient {
|
||||
static func receivedData(_ message: Data, from: WebSocketConnection) -> Bool { return false }
|
||||
static func sendMessage(_ message: SLMessage, from client: String) { }
|
||||
static func statusMessage(_ command: SLMessageCommand, from client: String) -> [String]? { return nil }
|
||||
}
|
||||
```
|
||||
|
||||
#### SLService
|
||||
|
||||
Is very simple in the way it works. First of all what you should know is:
|
||||
Message sent to clinet looks like:
|
||||
`M;MESSAGE-OWNER-ID;My message content sent to others`
|
||||
First character in this case `M` is the type of message. Followed by client id responsible for sending this message and the last part is message content. All parts joined by `;`
|
||||
This message model will be delivered to your client application.
|
||||
|
||||
Beside receiving messages, you will send some as well!
|
||||
Your message should look like:
|
||||
`M{RECIPIENT_1;RECIPIENT_2;RECIPIENT_3}My message content sent to others`
|
||||
Similar to recived message, first character describe type of message. Followed by recipients places inside `{}` separated by `;` with the last component is the message content.
|
||||
|
||||
To open socket conection from client register your WebSocket on:
|
||||
`ws:// + host + /slchat?OWNER-ID`
|
||||
SLChat uses path components to send owner id.
|
||||
|
||||
## ⭐ Contributing
|
||||
|
||||
Be welcome to contribute to this project! :)
|
||||
|
||||
## ❓ Questions
|
||||
|
||||
You can create an issue on GitHub.
|
||||
|
||||
## 📝 License
|
||||
|
||||
This project was released under the [MIT](LICENSE) license.
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// SLClient.swift
|
||||
// SLChat
|
||||
//
|
||||
// Created by Shial on 19/8/17.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import KituraWebSocket
|
||||
|
||||
public protocol SLClient {
|
||||
static func receivedData(_ message: Data, from: WebSocketConnection) -> Bool
|
||||
static func sendMessage(_ message: SLMessage, from client: String)
|
||||
static func statusMessage(_ command: SLMessageCommand, from client: String) -> [String]?
|
||||
}
|
||||
|
||||
extension SLClient {
|
||||
static func receivedData(_ message: Data, from: WebSocketConnection) -> Bool { return false }
|
||||
static func sendMessage(_ message: SLMessage, from client: String) { }
|
||||
static func statusMessage(_ command: SLMessageCommand, from client: String) -> [String]? { return nil }
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// SLConnections.swift
|
||||
// SLChat
|
||||
//
|
||||
// Created by Shial on 19/8/17.
|
||||
//
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
import KituraWebSocket
|
||||
|
||||
typealias SLConnection = (client: String, connection: WebSocketConnection)
|
||||
typealias SLClientConnections = [String: SLConnection]
|
||||
|
||||
class SLConnections {
|
||||
private let connectionsLock = DispatchSemaphore(value: 1)
|
||||
private var connections = SLClientConnections()
|
||||
|
||||
private func lock() { _ = connectionsLock.wait(timeout: DispatchTime.distantFuture) }
|
||||
private func unlock() { connectionsLock.signal() }
|
||||
|
||||
func exertions(_ operation: (_ connections: inout SLClientConnections)->()) {
|
||||
lock()
|
||||
operation(&connections)
|
||||
unlock()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
//
|
||||
// SLMessage.swift
|
||||
// SLChat
|
||||
//
|
||||
// Created by Shial on 19/8/17.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum SLMessageCommand: Character {
|
||||
case base64Message = "B"
|
||||
case connected = "C"
|
||||
case disconnected = "D"
|
||||
case textMessage = "M"
|
||||
case readMessage = "R"
|
||||
case stoppedTyping = "S"
|
||||
case startedTyping = "T"
|
||||
|
||||
init(_ command: Character) throws {
|
||||
switch command {
|
||||
case "B":
|
||||
self = .base64Message
|
||||
case "C":
|
||||
self = .connected
|
||||
case "D":
|
||||
self = .disconnected
|
||||
case "M":
|
||||
self = .textMessage
|
||||
case "R":
|
||||
self = .readMessage
|
||||
case "S":
|
||||
self = .stoppedTyping
|
||||
case "T":
|
||||
self = .startedTyping
|
||||
default:
|
||||
throw SLMessageError.unsupportedType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SLMessageError: Int, Error {
|
||||
case badRequest = 400
|
||||
case notAcceptable = 406
|
||||
case unsupportedType = 415
|
||||
}
|
||||
|
||||
public struct SLMessage {
|
||||
public var command: SLMessageCommand
|
||||
public var content: String
|
||||
public var recipients: [String]?
|
||||
|
||||
init(_ data: String) throws {
|
||||
guard data.characters.count > 1 else { throw SLMessageError.badRequest }
|
||||
guard let command = data.characters.first else { throw SLMessageError.unsupportedType }
|
||||
self.command = try SLMessageCommand(command)
|
||||
let payload = String(data.characters.dropFirst(2))
|
||||
guard let end = data.characters.index(of: "}") else { throw SLMessageError.notAcceptable }
|
||||
self.recipients = payload.substring(to: end).components(separatedBy: ";")
|
||||
self.content = payload.substring(from: end)
|
||||
}
|
||||
|
||||
init(command: SLMessageCommand, recipients: [String]? = nil) {
|
||||
self.command = command
|
||||
self.content = ""
|
||||
self.recipients = recipients
|
||||
}
|
||||
|
||||
func make(_ client: String) -> String {
|
||||
return [String(describing: command) + client + content].joined(separator: ";")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
//
|
||||
// SLService.swift
|
||||
// SLChat
|
||||
//
|
||||
// Created by Shial on 19/8/17.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import KituraWebSocket
|
||||
|
||||
public class SLService<T: SLClient>: WebSocketService {
|
||||
private var connection: SLConnections = SLConnections()
|
||||
|
||||
public func connected(connection: WebSocketConnection) {
|
||||
guard let clientId = connection.request.urlURL.query else {
|
||||
inconsonantClient(from: connection, description: "Missing client id")
|
||||
return;
|
||||
}
|
||||
connectClient(from: connection, client: clientId)
|
||||
}
|
||||
|
||||
public func disconnected(connection: WebSocketConnection, reason: WebSocketCloseReasonCode) {
|
||||
disconnectClient(from: connection)
|
||||
}
|
||||
|
||||
public func received(message: Data, from: WebSocketConnection) {
|
||||
guard T.receivedData(message, from: from) else {
|
||||
inconsonantClient(from: from, description: "Data not supoerted")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
public func received(message: String, from: WebSocketConnection) {
|
||||
do {
|
||||
let message = try SLMessage(message)
|
||||
guard let clientId = from.request.urlURL.query else {
|
||||
inconsonantClient(from: from, description: "Missing client id")
|
||||
return
|
||||
}
|
||||
broadcast(from: clientId, message: message)
|
||||
} catch SLMessageError.badRequest {
|
||||
inconsonantClient(from: from, description: "Bad data")
|
||||
} catch SLMessageError.unsupportedType {
|
||||
inconsonantClient(from: from, description: "Unexpected Data")
|
||||
} catch SLMessageError.notAcceptable {
|
||||
inconsonantClient(from: from, description: "Corrupted Data")
|
||||
} catch let error {
|
||||
inconsonantClient(from: from, description: "Something went wrong, error: \(error)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func connectClient(from: WebSocketConnection, client: String) {
|
||||
connection.exertions({ connections in
|
||||
connections[from.id] = (client, from)
|
||||
})
|
||||
if let recipients = T.statusMessage(.connected, from: client) {
|
||||
broadcast(from: client, message: SLMessage(command: .connected, recipients: recipients))
|
||||
}
|
||||
}
|
||||
|
||||
private func disconnectClient(from: WebSocketConnection) {
|
||||
var connectionInfo: SLConnection?
|
||||
connection.exertions({ connections in
|
||||
connectionInfo = connections.removeValue(forKey: from.id)
|
||||
})
|
||||
guard let info = connectionInfo else { return }
|
||||
if let recipients = T.statusMessage(.disconnected, from: info.client) {
|
||||
broadcast(from: info.client, message: SLMessage(command: .disconnected, recipients: recipients))
|
||||
}
|
||||
}
|
||||
|
||||
private func inconsonantClient(from: WebSocketConnection, description: String) {
|
||||
from.close(reason: .invalidDataContents, description: description)
|
||||
disconnectClient(from: from)
|
||||
}
|
||||
|
||||
private func broadcast(from client: String, message: SLMessage) {
|
||||
guard let recipients = message.recipients else { return }
|
||||
connection.exertions({ connections in
|
||||
for (_, (client: clienId, connection: connection)) in connections where recipients.contains(clienId) {
|
||||
connection.send(message: message.make(client))
|
||||
}
|
||||
})
|
||||
|
||||
if message.command == .base64Message || message.command == .textMessage {
|
||||
T.sendMessage(message, from: client)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
#if os(Linux)
|
||||
import XCTest
|
||||
@testable import SLChatTests
|
||||
|
||||
XCTMain([
|
||||
testCase(testSLClient.allTests),
|
||||
testCase(testSLConnection.allTests),
|
||||
testCase(testSLMessage.allTests),
|
||||
testCase(testSLService.allTests),
|
||||
])
|
||||
#endif
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// testSLClient.swift
|
||||
// SLChat
|
||||
//
|
||||
// Created by Shial on 19/08/2017.
|
||||
//
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SLChat
|
||||
|
||||
class testSLClient: XCTestCase {
|
||||
static let allTests = [
|
||||
("testSLClient", testSLClient),
|
||||
("testSLClientProtocol", testSLClientProtocol)
|
||||
]
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testSLClient() {
|
||||
struct Client: SLClient {}
|
||||
let cl = Client()
|
||||
XCTAssertNotNil(cl)
|
||||
}
|
||||
|
||||
func testSLClientSendMessage() {
|
||||
struct Client: SLClient {
|
||||
static var handler: Bool = false
|
||||
|
||||
static func sendMessage(_ message: SLMessage, from client: String) {
|
||||
handler = true
|
||||
}
|
||||
}
|
||||
Client.sendMessage(SLMessage(command: .connected), from: "")
|
||||
XCTAssert(Client.handler)
|
||||
}
|
||||
|
||||
func testSLClientStatusMessageDefault() {
|
||||
struct Client: SLClient {}
|
||||
XCTAssertNil(Client.statusMessage(.disconnected, from: ""))
|
||||
}
|
||||
|
||||
func testSLClientStatusMessage() {
|
||||
struct Client: SLClient {
|
||||
static func statusMessage(_ command: SLMessageCommand, from client: String) -> [String]? { return [] }
|
||||
}
|
||||
XCTAssertNotNil(Client.statusMessage(.disconnected, from: ""))
|
||||
}
|
||||
|
||||
func testSLClientProtocol() {
|
||||
struct Client: SLClient {
|
||||
static func sendMessage(_ message: SLMessage, from client: String) { }
|
||||
static func statusMessage(_ command: SLMessageCommand, from client: String) -> [String]? { return nil }
|
||||
}
|
||||
let cl = Client()
|
||||
XCTAssertNotNil(cl)
|
||||
}
|
||||
|
||||
func testSLClientStatus() {
|
||||
struct Client: SLClient {
|
||||
static func statusMessage(_ command: SLMessageCommand, from client: String) -> [String]? { return nil }
|
||||
}
|
||||
let cl = Client()
|
||||
XCTAssertNotNil(cl)
|
||||
}
|
||||
|
||||
func testSLClientMessage() {
|
||||
struct Client: SLClient {
|
||||
static func sendMessage(_ message: SLMessage, from client: String) { }
|
||||
}
|
||||
let cl = Client()
|
||||
XCTAssertNotNil(cl)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// testSLConnection.swift
|
||||
// SLChat
|
||||
//
|
||||
// Created by Shial on 19/08/2017.
|
||||
//
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SLChat
|
||||
@testable import KituraWebSocket
|
||||
@testable import KituraNet
|
||||
@testable import Socket
|
||||
|
||||
class testSLConnection: XCTestCase {
|
||||
static let allTests = [
|
||||
("testSLConnection", testSLConnection)
|
||||
]
|
||||
|
||||
private var connection: SLConnections = SLConnections()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testSLConnection() {
|
||||
connection.exertions({ connections in
|
||||
XCTAssertNotNil(connection)
|
||||
})
|
||||
}
|
||||
|
||||
func testSLConnectionAdd() {
|
||||
let socket = try! Socket.create()
|
||||
let connectionInfo: SLConnection = ("client", WebSocketConnection(request: HTTPServerRequest(socket: socket, httpParser: nil)))
|
||||
connection.exertions({ connections in
|
||||
connections["id"] = connectionInfo
|
||||
XCTAssertNotNil(connections["id"])
|
||||
})
|
||||
}
|
||||
|
||||
func testSLConnectionRemove() {
|
||||
testSLConnectionAdd()
|
||||
var connectionInfo: SLConnection?
|
||||
connection.exertions({ connections in
|
||||
connectionInfo = connections.removeValue(forKey: "id")
|
||||
})
|
||||
XCTAssertNotNil(connectionInfo)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
//
|
||||
// testSLMessage.swift
|
||||
// SLChat
|
||||
//
|
||||
// Created by Shial on 19/08/2017.
|
||||
//
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SLChat
|
||||
|
||||
class testSLMessage: XCTestCase {
|
||||
static let allTests = [
|
||||
("testSLMessage", testSLMessage)
|
||||
]
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testSLMessage() {
|
||||
do {
|
||||
let message = try SLMessage("C{A;B;C}Message")
|
||||
XCTAssertNotNil(message)
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testSLMessageBadRequest() {
|
||||
do {
|
||||
let _ = try SLMessage("")
|
||||
XCTFail()
|
||||
} catch SLMessageError.badRequest {
|
||||
XCTAssert(true)
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testSLMessageNotAcceptable() {
|
||||
do {
|
||||
let _ = try SLMessage("C{A;B;CMessage")
|
||||
XCTFail()
|
||||
} catch SLMessageError.notAcceptable {
|
||||
XCTAssert(true)
|
||||
}catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testSLMessageUnsupportedType() {
|
||||
do {
|
||||
let _ = try SLMessage("A{A;B;C}Message")
|
||||
XCTFail()
|
||||
} catch SLMessageError.unsupportedType {
|
||||
XCTAssert(true)
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testSLMessageBase64() {
|
||||
do {
|
||||
let message = try SLMessage("B{A;B;C}Message")
|
||||
XCTAssertTrue(message.command == .base64Message)
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testSLMessageConnected() {
|
||||
do {
|
||||
let message = try SLMessage("C{A;B;C}Message")
|
||||
XCTAssertTrue(message.command == .connected)
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testSLMessageDisconnected() {
|
||||
do {
|
||||
let message = try SLMessage("D{A;B;C}Message")
|
||||
XCTAssertTrue(message.command == .disconnected)
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testSLMessageTextMessage() {
|
||||
do {
|
||||
let message = try SLMessage("M{A;B;C}Message")
|
||||
XCTAssertTrue(message.command == .textMessage)
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testSLMessageReadMessage() {
|
||||
do {
|
||||
let message = try SLMessage("R{A;B;C}Message")
|
||||
XCTAssertTrue(message.command == .readMessage)
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testSLMessageStoppedTyping() {
|
||||
do {
|
||||
let message = try SLMessage("S{A;B;C}Message")
|
||||
XCTAssertTrue(message.command == .stoppedTyping)
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testSLMessageStart() {
|
||||
do {
|
||||
let message = try SLMessage("T{A;B;C}Message")
|
||||
XCTAssertTrue(message.command == .startedTyping)
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// testSLService.swift
|
||||
// SLChat
|
||||
//
|
||||
// Created by Shial on 19/08/2017.
|
||||
//
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import SLChat
|
||||
|
||||
class testSLService: XCTestCase {
|
||||
static let allTests = [
|
||||
("testSLService", testSLService)
|
||||
]
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testSLService() {
|
||||
struct Client: SLClient {}
|
||||
let chat = SLService<Client>()
|
||||
XCTAssertNotNil(chat)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue