[SLService] basic functionality

This commit is contained in:
Shial 2017-08-20 11:37:27 +10:00
parent d6ebec9ab9
commit 72d341ff91
10 changed files with 616 additions and 0 deletions

100
README.md Normal file
View File

@ -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.

View File

@ -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 }
}

View File

@ -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()
}
}

View File

@ -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: ";")
}
}

91
Sources/SLChat/SLService.swift Executable file
View File

@ -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)
}
}
}

11
Tests/LinuxMain.swift Normal file
View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}