blob: ee731726102c100e6f59cd7d81c53619a8079ad0 [file]
/-
Copyright (c) 2020 Marc Huisinga. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Marc Huisinga, Wojciech Nawrocki
-/
import Init.Control
import Init.System.IO
import Lean.Data.RBTree
import Lean.Data.Json
/-! Implementation of JSON-RPC 2.0 (https://www.jsonrpc.org/specification)
for use in the LSP server. -/
namespace Lean.JsonRpc
open Json
/-- In JSON-RPC, each request from the client editor to the language server comes with a
request id so that the corresponding response can be identified or cancelled. -/
inductive RequestID where
| str (s : String)
| num (n : JsonNumber)
| null
deriving Inhabited, BEq, Ord
instance : OfNat RequestID n := ⟨RequestID.num n⟩
instance : ToString RequestID where
toString
| RequestID.str s => s!"\"{s}\""
| RequestID.num n => toString n
| RequestID.null => "null"
/-- Error codes defined by
[JSON-RPC](https://www.jsonrpc.org/specification#error_object) and
[LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#errorCodes). -/
inductive ErrorCode where
/-- Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. -/
| parseError
/-- The JSON sent is not a valid Request object. -/
| invalidRequest
/-- The method does not exist / is not available. -/
| methodNotFound
/-- Invalid method parameter(s). -/
| invalidParams
/-- Internal JSON-RPC error. -/
| internalError
/-- Error code indicating that a server received a notification or
request before the server has received the `initialize` request. -/
| serverNotInitialized
| unknownErrorCode
-- LSP-specific codes below.
/-- The server detected that the content of a document got
modified outside normal conditions. A server should
NOT send this error code if it detects a content change
in it unprocessed messages. The result even computed
on an older state might still be useful for the client.
If a client decides that a result is not of any use anymore
the client should cancel the request. -/
| contentModified
/-- The client has canceled a request and a server as detected the cancel. -/
| requestCancelled
-- Lean-specific codes below.
| rpcNeedsReconnect
| workerExited
| workerCrashed
deriving Inhabited, BEq
instance : FromJson ErrorCode := ⟨fun
| num (-32700 : Int) => return ErrorCode.parseError
| num (-32600 : Int) => return ErrorCode.invalidRequest
| num (-32601 : Int) => return ErrorCode.methodNotFound
| num (-32602 : Int) => return ErrorCode.invalidParams
| num (-32603 : Int) => return ErrorCode.internalError
| num (-32002 : Int) => return ErrorCode.serverNotInitialized
| num (-32001 : Int) => return ErrorCode.unknownErrorCode
| num (-32801 : Int) => return ErrorCode.contentModified
| num (-32800 : Int) => return ErrorCode.requestCancelled
| num (-32900 : Int) => return ErrorCode.rpcNeedsReconnect
| num (-32901 : Int) => return ErrorCode.workerExited
| num (-32902 : Int) => return ErrorCode.workerCrashed
| _ => throw "expected error code"⟩
instance : ToJson ErrorCode := ⟨fun
| ErrorCode.parseError => (-32700 : Int)
| ErrorCode.invalidRequest => (-32600 : Int)
| ErrorCode.methodNotFound => (-32601 : Int)
| ErrorCode.invalidParams => (-32602 : Int)
| ErrorCode.internalError => (-32603 : Int)
| ErrorCode.serverNotInitialized => (-32002 : Int)
| ErrorCode.unknownErrorCode => (-32001 : Int)
| ErrorCode.contentModified => (-32801 : Int)
| ErrorCode.requestCancelled => (-32800 : Int)
| ErrorCode.rpcNeedsReconnect => (-32900 : Int)
| ErrorCode.workerExited => (-32901 : Int)
| ErrorCode.workerCrashed => (-32902 : Int)⟩
/-- A JSON-RPC message.
Uses separate constructors for notifications and errors because client and server
behavior is expected to be wildly different for both.
-/
inductive Message where
/-- A request message to describe a request between the client and the server. Every processed request must send a response back to the sender of the request. -/
| request (id : RequestID) (method : String) (params? : Option Structured)
/-- A notification message. A processed notification message must not send a response back. They work like events. -/
| notification (method : String) (params? : Option Structured)
/-- A Response Message sent as a result of a request. -/
| response (id : RequestID) (result : Json)
/-- A non-successful response. -/
| responseError (id : RequestID) (code : ErrorCode) (message : String) (data? : Option Json)
def Batch := Array Message
/-- Generic version of `Message.request`.
A request message to describe a request between the client and the server. Every processed request must send a response back to the sender of the request.
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
- [JSON-RPC](https://www.jsonrpc.org/specification#request_object)
-/
structure Request (α : Type u) where
id : RequestID
method : String
param : α
deriving Inhabited, BEq
instance [ToJson α] : CoeOut (Request α) Message :=
⟨fun r => Message.request r.id r.method (toStructured? r.param).toOption⟩
/-- Generic version of `Message.notification`.
A notification message. A processed notification message must not send a response back. They work like events.
- [JSON-RPC](https://www.jsonrpc.org/specification#notification)
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage).
-/
structure Notification (α : Type u) where
method : String
param : α
deriving Inhabited, BEq
instance [ToJson α] : CoeOut (Notification α) Message :=
⟨fun r => Message.notification r.method (toStructured? r.param).toOption⟩
/-- Generic version of `Message.response`.
A Response Message sent as a result of a request. If a request doesn’t provide a
result value the receiver of a request still needs to return a response message
to conform to the JSON-RPC specification. The result property of the ResponseMessage
should be set to null in this case to signal a successful request.
References:
- [JSON-RPC](https://www.jsonrpc.org/specification#response_object)
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage)
-/
structure Response (α : Type u) where
id : RequestID
result : α
deriving Inhabited, BEq
instance [ToJson α] : CoeOut (Response α) Message :=
⟨fun r => Message.response r.id (toJson r.result)⟩
/-- Generic version of `Message.responseError`.
References:
- [JSON-RPC](https://www.jsonrpc.org/specification#error_object)
- [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseError).
-/
structure ResponseError (α : Type u) where
id : RequestID
code : ErrorCode
/-- A string providing a short description of the error. -/
message : String
/-- A primitive or structured value that contains additional
information about the error. Can be omitted. -/
data? : Option α := none
deriving Inhabited, BEq
instance [ToJson α] : CoeOut (ResponseError α) Message :=
⟨fun r => Message.responseError r.id r.code r.message (r.data?.map toJson)⟩
instance : Coe String RequestID := ⟨RequestID.str⟩
instance : Coe JsonNumber RequestID := ⟨RequestID.num⟩
private def RequestID.lt : RequestID → RequestID → Bool
| RequestID.str a, RequestID.str b => a < b
| RequestID.num a, RequestID.num b => a < b
| RequestID.null, RequestID.num _ => true
| RequestID.null, RequestID.str _ => true
| RequestID.num _, RequestID.str _ => true
| _, _ /- str < *, num < null, null < null -/ => false
private def RequestID.ltProp : LT RequestID :=
⟨fun a b => RequestID.lt a b = true⟩
instance : LT RequestID :=
RequestID.ltProp
instance (a b : RequestID) : Decidable (a < b) :=
inferInstanceAs (Decidable (RequestID.lt a b = true))
instance : FromJson RequestID := ⟨fun j =>
match j with
| str s => return RequestID.str s
| num n => return RequestID.num n
| _ => throw "a request id needs to be a number or a string"⟩
instance : ToJson RequestID := ⟨fun rid =>
match rid with
| RequestID.str s => s
| RequestID.num n => num n
| RequestID.null => null⟩
instance : ToJson Message := ⟨fun m =>
mkObj $ ⟨"jsonrpc", "2.0"⟩ :: match m with
| Message.request id method params? =>
[ ⟨"id", toJson id⟩,
⟨"method", method⟩
] ++ opt "params" params?
| Message.notification method params? =>
⟨"method", method⟩ ::
opt "params" params?
| Message.response id result =>
[ ⟨"id", toJson id⟩,
⟨"result", result⟩]
| Message.responseError id code message data? =>
[ ⟨"id", toJson id⟩,
⟨"error", mkObj $ [
⟨"code", toJson code⟩,
⟨"message", message⟩
] ++ opt "data" data?⟩
]⟩
instance : FromJson Message where
fromJson? j := do
let "2.0" ← j.getObjVal? "jsonrpc" | throw "only version 2.0 of JSON RPC is supported"
(do let id ← j.getObjValAs? RequestID "id"
let method ← j.getObjValAs? String "method"
let params? := j.getObjValAs? Structured "params"
pure (Message.request id method params?.toOption)) <|>
(do let method ← j.getObjValAs? String "method"
let params? := j.getObjValAs? Structured "params"
pure (Message.notification method params?.toOption)) <|>
(do let id ← j.getObjValAs? RequestID "id"
let result ← j.getObjVal? "result"
pure (Message.response id result)) <|>
(do let id ← j.getObjValAs? RequestID "id"
let err ← j.getObjVal? "error"
let code ← err.getObjValAs? ErrorCode "code"
let message ← err.getObjValAs? String "message"
let data? := err.getObjVal? "data"
pure (Message.responseError id code message data?.toOption))
-- TODO(WN): temporary until we have deriving FromJson
instance [FromJson α] : FromJson (Notification α) where
fromJson? j := do
let msg : Message ← fromJson? j
if let Message.notification method params? := msg then
let params := params?
let param : α ← fromJson? (toJson params)
pure $ ⟨method, param⟩
else throw "not a notfication"
end Lean.JsonRpc
namespace IO.FS.Stream
open Lean
open Lean.JsonRpc
section
def readMessage (h : FS.Stream) (nBytes : Nat) : IO Message := do
let j ← h.readJson nBytes
match fromJson? j with
| Except.ok m => pure m
| Except.error inner => throw $ userError s!"JSON '{j.compress}' did not have the format of a JSON-RPC message.\n{inner}"
def readRequestAs (h : FS.Stream) (nBytes : Nat) (expectedMethod : String) (α) [FromJson α] : IO (Request α) := do
let m ← h.readMessage nBytes
match m with
| Message.request id method params? =>
if method = expectedMethod then
let j := toJson params?
match fromJson? j with
| Except.ok v => pure ⟨id, expectedMethod, v⟩
| Except.error inner => throw $ userError s!"Unexpected param '{j.compress}' for method '{expectedMethod}'\n{inner}"
else
throw $ userError s!"Expected method '{expectedMethod}', got method '{method}'"
| _ => throw $ userError s!"Expected JSON-RPC request, got: '{(toJson m).compress}'"
def readNotificationAs (h : FS.Stream) (nBytes : Nat) (expectedMethod : String) (α) [FromJson α] : IO (Notification α) := do
let m ← h.readMessage nBytes
match m with
| Message.notification method params? =>
if method = expectedMethod then
let j := toJson params?
match fromJson? j with
| Except.ok v => pure ⟨expectedMethod, v⟩
| Except.error inner => throw $ userError s!"Unexpected param '{j.compress}' for method '{expectedMethod}'\n{inner}"
else
throw $ userError s!"Expected method '{expectedMethod}', got method '{method}'"
| _ => throw $ userError s!"Expected JSON-RPC notification, got: '{(toJson m).compress}'"
partial def readResponseAs (h : FS.Stream) (nBytes : Nat) (expectedID : RequestID) (α) [FromJson α] : IO (Response α) := do
let m ← h.readMessage nBytes
match m with
| Message.response id result =>
if id == expectedID then
match fromJson? result with
| Except.ok v => pure ⟨expectedID, v⟩
| Except.error inner => throw $ userError s!"Unexpected result '{result.compress}'\n{inner}"
else
throw $ userError s!"Expected id {expectedID}, got id {id}"
| Message.notification .. => readResponseAs h nBytes expectedID α
| _ => throw $ userError s!"Expected JSON-RPC response, got: '{(toJson m).compress}'"
end
section
variable [ToJson α]
def writeMessage (h : FS.Stream) (m : Message) : IO Unit :=
h.writeJson (toJson m)
def writeRequest (h : FS.Stream) (r : Request α) : IO Unit :=
h.writeMessage r
def writeNotification (h : FS.Stream) (n : Notification α) : IO Unit :=
h.writeMessage n
def writeResponse (h : FS.Stream) (r : Response α) : IO Unit :=
h.writeMessage r
def writeResponseError (h : FS.Stream) (e : ResponseError Unit) : IO Unit :=
h.writeMessage (Message.responseError e.id e.code e.message none)
def writeResponseErrorWithData (h : FS.Stream) (e : ResponseError α) : IO Unit :=
h.writeMessage e
end
end IO.FS.Stream