So I have a problem… my Language Server Protocol implementation has a pluggable API surface. The transport mechanism and how you encode the data within the VS Code message format are both abstracted out so you can plug in a different implementation, say an IPC transport mechanism instead of stdin/stdout.
Anyhow, the spec is a bit under restricted. That is, there are places that allow for Any
type to be stored within there. Now… for the spec, it ties its implementation to JSONRPC, so the actual potential types of Any
must be one of the valid JSON types.
The way I handle encoding and decoding are via two very simple interfaces:
public protocol Encodable { associatedtype EncodableType func encode() -> EncodableType } public protocol Decodable { associatedtype EncodableType static func decode(_ data: EncodableType?) throws -> Self } public typealias Codeable = Encodable & Decodable
OK, pretty straight forward. The first problem though is the associated type. This already caused me some troubles earlier with my default extension for encode()
: I couldn’t figure out how to re-write it now that it used an associated type.
The next design choice I have is that all LSP commands are modeled within an enum.
public enum LanguageServerCommand { case initialize(requestId: RequestId, params: InitializeParams) case initialized ///... case workspaceDidChangeConfiguration(params: DidChangeConfigurationParams) /// ... }
I really like this as it makes it clear what commands have not been implemented yet.
So here’s where the problem really comes in: DidChangeConfigurationParams
is one of those APIs that has an Any
type as one of its members. Updating that type looks like this:
public struct DidChangeConfigurationParams<SettingsType> { public init(settings: SettingsType) { self.settings = settings } public var settings: SettingsType }
But this requires changes to the LanguageServerCommand
now.
public enum LanguageServerCommand<SettingsType> { case workspaceDidChangeConfiguration(params: DidChangeConfigurationParams<SettingsType>) }
And now everywhere that uses LanguageServerCommand
needs to be updated… not to mention that I need to do this for each time that uses Any
.
What to do?
I basically have a handful of options:
- Re-design the
Encodable
andDecodable
interfaces to remove the associated type or merge with the Swift 4 design. I don’t really like the approach of the Swift 4 model much though, so I’m not really keen on doing this until absolutely necessary. - Re-design how I’m handling my responses and not use an
enum
. However, I don’t really like this either.
What did I do?
Well… I said, “Screw you type system! I know what I want and when I want it!”. Seriously… I cannot express what I want to express in a simple manner, so I created this:
public protocol AnyEncodable { func encode() -> Any }
Then I updated my type definitions to look like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public struct Registration { | |
public init(id: String, method: String, registerOptions: AnyEncodable? = nil) { | |
self.id = id | |
self.method = method | |
self.registerOptions = registerOptions | |
} | |
/// The id used to register the request. The id can be used to deregister the request again. | |
public var id: String | |
/// The method / capability to register for. | |
public var method: String | |
/// Options necessary for the registration. | |
public var registerOptions: AnyEncodable? | |
} |
The change is registerOptions
is now AnyEncodable?
instead of JSValue?
. This removes the leaky abstraction of the serialization mechanism and moves it back into the serialization layer itself.
Is this dirty? Is this bad? Eh… I don’t really care. It’s what I needed to do so I did it.
Is there a better way? I don’t know…
You can see the full change here if you’re interested: https://github.com/owensd/swift-lsp/commit/8e2de14124fae91cf0d02c873acd16f9e93f5ef2