Skip to content

Commit 1b91d29

Browse files
committed
Make Packet Equatable, refactor, add test, expand documentation
1 parent 718ce30 commit 1b91d29

File tree

10 files changed

+142
-38
lines changed

10 files changed

+142
-38
lines changed

README.md

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,83 @@
44

55
## What?
66

7-
SwiftLSP employs a quite dynamic Swift representation of the [LSP (Language Server Protocol)](https://microsoft.github.io/language-server-protocol) and helps with:
7+
SwiftLSP offers a quite dynamic Swift representation of the [LSP (Language Server Protocol)](https://microsoft.github.io/language-server-protocol) and helps with many related use cases. It is foundational for [LSPService](https://github.com/codeface-io/LSPService) and [LSPServiceKit](https://github.com/codeface-io/LSPServiceKit).
8+
9+
Since the LSP standard defines a complex amorphous multitude of valid JSON objects, it doesn't exactly lend itself to being represented as a strict type system that would mirror the standard down to every permutation and property. So SwiftLSP is strictly typed at the higher level of LSP messages but falls back onto a more dynamic and flexible JSON representation for the details. The strict typing can easily be expanded on client demand.
10+
11+
## Code Examples
12+
13+
Some of these examples build upon preceding ones, so it's best to read them from the beginning.
14+
15+
### Create Messages
16+
17+
```swift
18+
let myRequest = LSP.Request(method: "myMethod", params: nil)
19+
let myRequestMessage = LSP.Message.request(myRequest)
20+
21+
let myNotification = LSP.Notification(method: "myMethod", params: nil)
22+
let myNotificationMessage = LSP.Message.notification(myNotification)
23+
```
24+
25+
### Encode and Decode Messages
26+
27+
SwiftLSP encodes LSP messages with the [LSP-conform JSON-RPC encoding](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#abstractMessage).
28+
29+
```swift
30+
let myRequestMessageEncoded = try myRequestMessage.encode() // Data
31+
let myRequestMessageDecoded = try LSP.Message(myRequestMessageEncoded)
32+
```
33+
34+
### Wrap Messages in Packets
35+
36+
To send LSP messages via data channels, the standard defines how to [wrap each message](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#baseProtocol) in what we call an `LSP.Packet`, which holds the `Data` of its `header`- an `content` part.
37+
38+
```swift
39+
let myRequestMessagePacket = try LSP.Packet(myRequestMessage)
40+
let packetHeader = myRequestMessagePacket.header // Data
41+
let packetContent = myRequestMessagePacket.content // Data
42+
let packetTotalData = myRequestMessagePacket.data // Data
43+
```
44+
45+
### Extract Messages From Packets
46+
47+
```swift
48+
let myRequestMessageUnpacked = try myRequestMessagePacket.message() // LSP.Message
49+
```
50+
51+
### Extract LSP Packets From Data
52+
53+
A client talking to an LSP server might need to extract `LSP.Packet`s from the server's output `Data` stream.
54+
55+
SwiftLSP can detect an `LSP.Packet` at the beginning of any `Data` instance and also offers the `LSP.PacketDetector` for parsing a stream of `Data` incrementally.
56+
57+
```swift
58+
let dataStartingWithPacket = packetTotalData + "Some other data".data(using: .utf8)!
59+
let parsedPacket = try LSP.Packet(parsingPrefixOf: dataStartingWithPacket)
60+
61+
// now parsedPacket == myRequestMessagePacket
62+
63+
var detectedPacket: LSP.Packet? = nil
64+
65+
let detector = LSP.PacketDetector { packet in
66+
detectedPacket = packet
67+
}
68+
69+
for byte in dataStartingWithPacket {
70+
detector.read(Data([byte]))
71+
}
72+
73+
// now detectedPacket == myRequestMessagePacket
74+
```
75+
76+
## More Use Cases
77+
78+
Beyond what the examples above have touched, SwiftLSP also helps with:
879

980
* Launching an LSP server executable
10-
* Extracting LSP Packets from a data stream
11-
* Encoding and decoding LSP messages
12-
* Representing, creating and working with LSP messages
1381
* Matching response messages to request messages
14-
* Exchanging LSP Messages with an LSP Server
15-
* Exchanging LSP Messages with an LSP Server via WebSocket
16-
17-
SwiftLSP is the basis for [LSPService](https://github.com/codeface-io/LSPService) and [LSPServiceKit](https://github.com/codeface-io/LSPServiceKit).
82+
* Making requests to an LSP Server through `async` **functions**
83+
* Using an LSP Server via WebSocket
1884

1985
## Architecture
2086

Sources/Message/LSP.Message.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import SwiftyToolz
33

44
extension LSP
55
{
6+
typealias Request = Message.Request
7+
typealias Response = Message.Response
68
typealias ErrorResult = Message.Response.ErrorResult
9+
typealias Notification = Message.Notification
710

811
/**
912
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#abstractMessage

Sources/Packet/LSP.Message+Packet.swift

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Foundation
2+
3+
public extension LSP.Packet
4+
{
5+
init(_ message: LSP.Message) throws
6+
{
7+
try self.init(withContent: message.encode())
8+
}
9+
10+
func message() throws -> LSP.Message
11+
{
12+
try LSP.Message(content)
13+
}
14+
}

Sources/Packet/LSP.Packet.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import SwiftyToolz
33

44
public extension LSP
55
{
6-
struct Packet
6+
struct Packet: Equatable
77
{
88
public init(parsingPrefixOf data: Data) throws
99
{

Sources/Packet/LSP.PacketDetector.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ extension LSP
88
{
99
// MARK: - Public API
1010

11-
public init() {}
11+
public init(_ handleDetectedPacket: @escaping (Packet) -> Void)
12+
{
13+
didDetect = handleDetectedPacket
14+
}
1215

1316
public func read(_ data: Data)
1417
{
@@ -20,7 +23,7 @@ extension LSP
2023
}
2124
}
2225

23-
public var didDetect: (Packet) -> Void = { _ in }
26+
public var didDetect: (Packet) -> Void
2427

2528
// MARK: - Data Buffer
2629

Sources/Server Communication/LSP.ServerExecutable.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@ public extension LSP {
1111
public override init(config: Configuration) throws {
1212
try super.init(config: config)
1313

14-
setupPacketOutput()
14+
didSendOutput = { [weak self] in self?.packetDetector.read($0) }
1515
}
1616

1717
// MARK: - LSP Packet Output
1818

19-
private func setupPacketOutput() {
20-
didSendOutput = { [weak self] in self?.packetDetector.read($0) }
21-
packetDetector.didDetect = { [weak self] in self?.didSend($0) }
22-
}
23-
24-
private let packetDetector = LSP.PacketDetector()
19+
private lazy var packetDetector: LSP.PacketDetector = {
20+
.init { [weak self] in self?.didSend($0) }
21+
}()
2522

2623
public var didSend: (LSP.Packet) -> Void = { _ in
2724
log(warning: "LSP server did send lsp packet, but handler has not been set")

Sources/Server Communication/LSP.WebSocketConnection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public extension LSP
5959

6060
public func sendToServer(_ message: LSP.Message) async throws
6161
{
62-
try await webSocket.send(try message.packet().data)
62+
try await webSocket.send(try LSP.Packet(message).data)
6363
}
6464

6565
// MARK: - Manage Connection

Sources/Use Cases/Server Life Cycle/LSP.Message.Notification+Initialized.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ public extension LSP.Message.Notification
22
{
33
static var initialized: Self
44
{
5-
.init(method: "initialized", params: .object([:]))
5+
.init(method: "initialized", params: .emptyObject)
66
}
77
}

Tests/SwiftLSPTests.swift

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,42 @@ final class SwiftLSPTests: XCTestCase {
77

88
// MARK: - Message
99

10+
func testCodeExamplesFromREADME() throws {
11+
let myRequest = LSP.Request(method: "myMethod", params: nil)
12+
let myRequestMessage = LSP.Message.request(myRequest)
13+
14+
let myNotification = LSP.Notification(method: "myMethod", params: nil)
15+
_ = LSP.Message.notification(myNotification)
16+
17+
let myRequestMessageEncoded = try myRequestMessage.encode() // Data
18+
let myRequestMessageDecoded = try LSP.Message(myRequestMessageEncoded)
19+
XCTAssertEqual(myRequestMessageDecoded, myRequestMessage)
20+
21+
let myRequestMessagePacket = try LSP.Packet(myRequestMessage)
22+
_ = myRequestMessagePacket.header // Data
23+
_ = myRequestMessagePacket.content // Data
24+
let packetTotalData = myRequestMessagePacket.data // Data
25+
26+
let myRequestMessageUnpacked = try myRequestMessagePacket.message() // LSP.Message
27+
XCTAssertEqual(myRequestMessageUnpacked, myRequestMessage)
28+
29+
let dataStartingWithPacket = packetTotalData + "Some other data".data(using: .utf8)!
30+
let parsedPacket = try LSP.Packet(parsingPrefixOf: dataStartingWithPacket)
31+
XCTAssertEqual(parsedPacket, myRequestMessagePacket)
32+
33+
var detectedPacket: LSP.Packet? = nil
34+
35+
let detector = LSP.PacketDetector { packet in
36+
detectedPacket = packet
37+
}
38+
39+
for byte in dataStartingWithPacket {
40+
detector.read(Data([byte]))
41+
}
42+
43+
XCTAssertEqual(detectedPacket, myRequestMessagePacket)
44+
}
45+
1046
func testNewRequestMessageHasUUIDasID() throws {
1147

1248
let message = LSP.Message.Request(method: "method", params: nil)
@@ -177,11 +213,11 @@ final class SwiftLSPTests: XCTestCase {
177213
func testPacket() throws {
178214
let messageJSONString = #"{"jsonrpc":"2.0", "method":"someMethod"}"#
179215
let packet1 = try LSP.Packet(withContent: messageJSONString.data!)
180-
_ = try LSP.Message(packet1)
216+
_ = try packet1.message()
181217

182218
let packetBufferString = "Content-Length: 40\r\n\r\n" + messageJSONString + "Next packet or other data"
183219
let packet2 = try LSP.Packet(parsingPrefixOf: packetBufferString.data!)
184-
_ = try LSP.Message(packet2)
220+
_ = try packet2.message()
185221

186222
XCTAssertThrowsError(try LSP.Packet(withContent: Data()))
187223
XCTAssertThrowsError(try LSP.Packet(withContent: (messageJSONString + "{}").data!))
@@ -192,9 +228,8 @@ final class SwiftLSPTests: XCTestCase {
192228
// MARK: - Packet Detector
193229

194230
func testPacketDetector() {
195-
let detector = LSP.PacketDetector()
196231
var detectedPackets = [LSP.Packet]()
197-
detector.didDetect = { detectedPackets += $0 }
232+
let detector = LSP.PacketDetector { detectedPackets += $0 }
198233

199234
let header = "Content-Length: 40".data!
200235
let separator = "\r\n\r\n".data!

0 commit comments

Comments
 (0)