use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use serde_json::json;
use tokio::sync::mpsc;
use lspf::{
CancellationToken, Context, LspError, RawMessage, RequestId, Server, Transport, TransportError,
TransportReader, TransportWriter,
};
struct AppState {
bias: i64,
}
async fn add(
state: Arc<AppState>,
_ctx: Context,
args: (i64, i64),
ct: CancellationToken,
) -> Result<i64, LspError> {
assert!(!ct.is_cancelled(), "a fresh request token is not cancelled");
Ok(args.0 + args.1 + state.bias)
}
struct ChannelTransport {
in_rx: mpsc::UnboundedReceiver<RawMessage>,
out_tx: mpsc::UnboundedSender<RawMessage>,
}
struct ChannelReader {
in_rx: mpsc::UnboundedReceiver<RawMessage>,
}
struct ChannelWriter {
out_tx: mpsc::UnboundedSender<RawMessage>,
}
impl Transport for ChannelTransport {
type Reader = ChannelReader;
type Writer = ChannelWriter;
fn split(self) -> (Self::Reader, Self::Writer) {
(
ChannelReader { in_rx: self.in_rx },
ChannelWriter {
out_tx: self.out_tx,
},
)
}
}
impl TransportReader for ChannelReader {
async fn recv(&mut self) -> Result<RawMessage, TransportError> {
self.in_rx.recv().await.ok_or(TransportError::Closed)
}
}
impl TransportWriter for ChannelWriter {
async fn send(&mut self, msg: RawMessage) -> Result<(), TransportError> {
self.out_tx.send(msg).map_err(|_| TransportError::Closed)
}
async fn shutdown(self) -> Result<(), TransportError> {
Ok(())
}
}
fn request(id: i32, method: &'static str, params: serde_json::Value) -> RawMessage {
RawMessage::Request {
id: RequestId::Number(id),
method: Cow::Borrowed(method),
params: Bytes::from(serde_json::to_vec(¶ms).unwrap()),
}
}
fn initialize_request(id: i32) -> RawMessage {
request(
id,
"initialize",
json!({ "processId": null, "rootUri": null, "capabilities": {} }),
)
}
fn execute_command(id: i32, command: &str, arguments: serde_json::Value) -> RawMessage {
request(
id,
"workspace/executeCommand",
json!({ "command": command, "arguments": arguments }),
)
}
fn exit() -> RawMessage {
RawMessage::Notification {
method: Cow::Borrowed("exit"),
params: Bytes::from_static(b"null"),
}
}
async fn drive(messages: Vec<RawMessage>) -> Vec<RawMessage> {
let (in_tx, in_rx) = mpsc::unbounded_channel::<RawMessage>();
let (out_tx, mut out_rx) = mpsc::unbounded_channel::<RawMessage>();
let transport = ChannelTransport { in_rx, out_tx };
let server = Server::builder(AppState { bias: 100 })
.command::<(i64, i64), i64, _, _>("math.add", add)
.build()
.expect("server builds");
let handle = tokio::spawn(async move { server.serve(transport).await });
let mut outbox = Vec::new();
for msg in messages {
let response_id = msg.id().cloned();
in_tx.send(msg).unwrap();
if let Some(response_id) = response_id {
let response = tokio::time::timeout(Duration::from_secs(2), out_rx.recv())
.await
.expect("response arrived within 2s")
.expect("writer remained open");
assert_eq!(response.id(), Some(&response_id));
outbox.push(response);
}
}
drop(in_tx);
tokio::time::timeout(Duration::from_secs(2), handle)
.await
.expect("serve returned within 2s")
.expect("server task did not panic")
.expect("serve ended cleanly");
outbox.extend(std::iter::from_fn(|| out_rx.try_recv().ok()));
outbox
}
fn response(outbox: &[RawMessage], id: i32) -> Option<&RawMessage> {
outbox.iter().find(
|m| matches!(m, RawMessage::Response { id: rid, .. } if *rid == RequestId::Number(id)),
)
}
fn ok_result(outbox: &[RawMessage], id: i32) -> Option<serde_json::Value> {
match response(outbox, id)? {
RawMessage::Response {
result: Ok(bytes), ..
} => Some(serde_json::from_slice(bytes).unwrap()),
_ => None,
}
}
fn error_code(outbox: &[RawMessage], id: i32) -> Option<i32> {
match response(outbox, id)? {
RawMessage::Response { result: Err(e), .. } => Some(e.code),
_ => None,
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn initialize_advertises_the_command_and_dispatches_it() {
let outbox = drive(vec![
initialize_request(1),
execute_command(2, "math.add", json!([2, 3])),
request(3, "shutdown", json!(null)),
exit(),
])
.await;
let init: lspf::types::InitializeResult =
serde_json::from_value(ok_result(&outbox, 1).expect("initialize response")).unwrap();
let provider = init
.capabilities
.execute_command_provider
.expect("a registered command advertises execute-command support");
assert_eq!(provider.commands, vec!["math.add".to_string()]);
assert_eq!(
ok_result(&outbox, 2),
Some(json!(105)),
"2 + 3 + bias(100) routed through the typed command handler"
);
assert_eq!(ok_result(&outbox, 3), Some(serde_json::Value::Null));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_unknown_command_is_an_invalid_param() {
let outbox = drive(vec![
initialize_request(1),
execute_command(2, "math.subtract", json!([2, 3])),
exit(),
])
.await;
assert_eq!(
error_code(&outbox, 2),
Some(-32602),
"an unregistered command name is an invalid parameter for executeCommand"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn malformed_command_arguments_return_invalid_params() {
let outbox = drive(vec![
initialize_request(1),
execute_command(2, "math.add", json!(["two", 3])),
execute_command(3, "math.add", json!([4, 5])),
exit(),
])
.await;
assert_eq!(error_code(&outbox, 2), Some(-32602));
assert_eq!(ok_result(&outbox, 3), Some(json!(109)));
}