Skip to content

Upgrade CustomCodable to handle optional values using encodeIfPresent #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions Sources/LanguageServerProtocol/CustomCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,11 @@
/// init(wrappedValue: WrappedValue) { self.wrappedValue = wrappedValue }
/// }
/// ```
///
/// * Note: Unfortunately this wrapper does not work perfectly with `Optional`.
/// While you can add a conformance for it, it will `encodeNil` rather than
/// using `encodeIfPresent` on the containing type, since the synthesized
/// implementation only uses `encodeIfPresent` if the property itself is
/// `Optional`.
@propertyWrapper
public struct CustomCodable<CustomCoder: CustomCodableWrapper> {

public typealias CustomCoder = CustomCoder

/// The underlying value.
public var wrappedValue: CustomCoder.WrappedValue

Expand Down Expand Up @@ -80,3 +76,34 @@ public protocol CustomCodableWrapper: Codable {
/// Create a wrapper from an underlying value.
init(wrappedValue: WrappedValue)
}

extension Optional: CustomCodableWrapper where Wrapped: CustomCodableWrapper {
public var wrappedValue: Wrapped.WrappedValue? { self?.wrappedValue }
public init(wrappedValue: Wrapped.WrappedValue?) {
self = wrappedValue.flatMap { Wrapped.init(wrappedValue: $0) }
}
}

// The following extensions allow us to encode `CustomCodable<Optional<T>>`
// using `encodeIfPresent` (and `decodeIfPresent`) in synthesized `Codable`
// conformances. Without these, we would encode `nil` using `encodeNil` instead
// of skipping the key.

extension KeyedDecodingContainer {
public func decode<T: CustomCodableWrapper>(
_ type: CustomCodable<Optional<T>>.Type,
forKey key: Key
) throws -> CustomCodable<Optional<T>> {
CustomCodable<Optional<T>>(wrappedValue: try decodeIfPresent(T.self, forKey: key)?.wrappedValue)
}
}

extension KeyedEncodingContainer {
public mutating func encode<T: CustomCodableWrapper>(
_ value: CustomCodable<Optional<T>>,
forKey key: Key
) throws {
try encodeIfPresent(value.wrappedValue.map {
type(of: value).CustomCoder(wrappedValue: $0) }, forKey: key)
}
}
27 changes: 2 additions & 25 deletions Sources/LanguageServerProtocol/Hover.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ public struct HoverResponse: ResponseType, Hashable {
public var contents: HoverResponseContents

/// An optional range to visually distinguish during hover.
@CustomCodable<PositionRange?>
public var range: Range<Position>?

public init(contents: HoverResponseContents, range: Range<Position>?) {
self.contents = contents
self.range = range
self._range = CustomCodable(wrappedValue: range)
}
}

Expand All @@ -70,30 +71,6 @@ public enum MarkedString: Hashable {
case codeBlock(language: String, value: String)
}

// Needs a custom implementation for range, because `Optional` is the only type that uses
// `encodeIfPresent` in the synthesized conformance, and the
// [LSP specification does not allow `null` in most places](https://github.com/microsoft/language-server-protocol/issues/355).
extension HoverResponse: Codable {
private enum CodingKeys: String, CodingKey {
case contents
case range
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.contents = try container.decode(HoverResponseContents.self, forKey: .contents)
self.range = try container
.decodeIfPresent(PositionRange.self, forKey: .range)?
.wrappedValue
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(contents, forKey: .contents)
try container.encodeIfPresent(range.map { PositionRange(wrappedValue: $0) }, forKey: .range)
}
}

extension MarkedString: Codable {
public init(from decoder: Decoder) throws {
if let value = try? decoder.singleValueContainer().decode(String.self) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,18 @@
/// If `range` and `rangeLength` are unspecified, the whole document content is replaced.
///
/// The `range.end` and `rangeLength` are potentially redundant. Based on https://github.com/Microsoft/language-server-protocol/issues/9, servers should be lenient and accept either.
public struct TextDocumentContentChangeEvent: Hashable {
public struct TextDocumentContentChangeEvent: Codable, Hashable {

@CustomCodable<PositionRange?>
public var range: Range<Position>?

public var rangeLength: Int?

public var text: String

public init(range: Range<Position>? = nil, rangeLength: Int? = nil, text: String) {
self.range = range
self._range = CustomCodable(wrappedValue: range)
self.rangeLength = rangeLength
self.text = text
}
}

// Needs a custom implementation for range, because `Optional` is the only type that uses
// `encodeIfPresent` in the synthesized conformance, and the
// [LSP specification does not allow `null` in most places](https://github.com/microsoft/language-server-protocol/issues/355).
extension TextDocumentContentChangeEvent: Codable {
private enum CodingKeys: String, CodingKey {
case range
case rangeLength
case text
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.range = try container
.decodeIfPresent(PositionRange.self, forKey: .range)?
.wrappedValue
self.rangeLength = try container.decodeIfPresent(Int.self, forKey: .rangeLength)
self.text = try container.decode(String.self, forKey: .text)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(range.map { PositionRange(wrappedValue: $0) }, forKey: .range)
try container.encodeIfPresent(rangeLength, forKey: .rangeLength)
try container.encode(text, forKey: .text)
}
}
29 changes: 29 additions & 0 deletions Tests/LanguageServerProtocolTests/CodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,35 @@ final class CodingTests: XCTestCase {
}
""")
}

func testCustomCodableOptional() {
struct WithPosRange: Codable, Equatable {
@CustomCodable<PositionRange?>
var range: Range<Position>?
}

let range = Position(line: 5, utf16index: 23) ..< Position(line: 6, utf16index: 0)
checkCoding(WithPosRange(range: range), json: """
{
"range" : {
"end" : {
"character" : 0,
"line" : 6
},
"start" : {
"character" : 23,
"line" : 5
}
}
}
""")

checkCoding(WithPosRange(range: nil), json: """
{

}
""")
}
}

func with<T>(_ value: T, mutate: (inout T) -> Void) -> T {
Expand Down
1 change: 1 addition & 0 deletions Tests/LanguageServerProtocolTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ extension CodingTests {
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__CodingTests = [
("testCustomCodableOptional", testCustomCodableOptional),
("testPositionRange", testPositionRange),
("testValueCoding", testValueCoding),
]
Expand Down