use crate::rpc::{self, Id, Response};
use serde_json::{Value, json};
#[derive(Debug, Clone)]
pub enum Inbound {
Elicit {
message: String,
requested_schema: Value,
},
ListRoots,
}
#[derive(Debug, Clone)]
pub enum Answer {
Accept(Value),
Decline,
Cancel,
Roots(Vec<Root>),
}
#[derive(Debug, Clone)]
pub struct Root {
pub uri: String,
pub name: Option<String>,
}
pub trait Handler: Send + Sync {
fn handle(&self, req: Inbound) -> Option<Answer>;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Capabilities {
pub elicitation: bool,
pub roots: bool,
}
impl Capabilities {
pub fn to_json(self) -> Value {
let mut caps = serde_json::Map::new();
if self.elicitation {
caps.insert("elicitation".into(), json!({}));
}
if self.roots {
caps.insert("roots".into(), json!({"listChanged": false}));
}
Value::Object(caps)
}
pub fn is_empty(self) -> bool {
!self.elicitation && !self.roots
}
}
pub fn answer(req: &rpc::Request, caps: Capabilities, handler: Option<&dyn Handler>) -> Response {
let id = req.id.clone();
match req.method.as_str() {
"ping" => Response::ok(id, json!({})),
"elicitation/create" if caps.elicitation => {
let params = req.params.clone().unwrap_or(Value::Null);
let message = params
.get("message")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let requested_schema = params
.get("requestedSchema")
.cloned()
.unwrap_or_else(|| json!({"type": "object"}));
match handler.and_then(|h| {
h.handle(Inbound::Elicit {
message,
requested_schema,
})
}) {
Some(Answer::Accept(content)) => {
Response::ok(id, json!({"action": "accept", "content": content}))
}
Some(Answer::Decline) => Response::ok(id, json!({"action": "decline"})),
Some(Answer::Cancel) | None => Response::ok(id, json!({"action": "cancel"})),
Some(Answer::Roots(_)) => Response::err(
id,
rpc::INTERNAL_ERROR,
"handler answered elicitation with roots",
),
}
}
"roots/list" if caps.roots => match handler.and_then(|h| h.handle(Inbound::ListRoots)) {
Some(Answer::Roots(roots)) => Response::ok(
id,
json!({
"roots": roots.iter().map(|r| match &r.name {
Some(n) => json!({"uri": r.uri, "name": n}),
None => json!({"uri": r.uri}),
}).collect::<Vec<_>>()
}),
),
_ => Response::ok(id, json!({"roots": []})),
},
other => Response::err(
id,
rpc::METHOD_NOT_FOUND,
format!("client does not implement {other}"),
),
}
}
pub fn as_request(v: &Value) -> Option<rpc::Request> {
if v.get("method").is_some() && v.get("id").is_some() {
serde_json::from_value::<rpc::Request>(v.clone()).ok()
} else {
None
}
}
pub fn frame_id(v: &Value) -> Option<Id> {
serde_json::from_value::<Id>(v.get("id")?.clone()).ok()
}
#[cfg(test)]
mod tests {
use super::*;
struct Yes(Answer);
impl Handler for Yes {
fn handle(&self, _req: Inbound) -> Option<Answer> {
Some(self.0.clone())
}
}
struct No;
impl Handler for No {
fn handle(&self, _req: Inbound) -> Option<Answer> {
None
}
}
fn req(method: &str, params: Value) -> rpc::Request {
rpc::Request::new(1, method, Some(params))
}
#[test]
fn ping_is_answered_even_with_no_handler_or_capabilities() {
let r = answer(&req("ping", json!({})), Capabilities::default(), None);
assert_eq!(r.result, Some(json!({})));
assert!(r.error.is_none());
}
#[test]
fn elicitation_maps_the_three_outcomes() {
let caps = Capabilities {
elicitation: true,
roots: false,
};
let p = json!({"message": "Which environment?", "requestedSchema": {"type": "object"}});
let accept = Yes(Answer::Accept(json!({"env": "staging"})));
let r = answer(&req("elicitation/create", p.clone()), caps, Some(&accept));
assert_eq!(r.result.as_ref().unwrap()["action"], "accept");
assert_eq!(r.result.unwrap()["content"]["env"], "staging");
let decline = Yes(Answer::Decline);
let r = answer(&req("elicitation/create", p.clone()), caps, Some(&decline));
assert!(r.error.is_none());
assert_eq!(r.result.unwrap()["action"], "decline");
let r = answer(&req("elicitation/create", p.clone()), caps, Some(&No));
assert_eq!(r.result.unwrap()["action"], "cancel");
let r = answer(&req("elicitation/create", p), caps, None);
assert_eq!(r.result.unwrap()["action"], "cancel");
}
#[test]
fn an_undeclared_capability_is_refused_not_half_answered() {
let none = Capabilities::default();
let r = answer(&req("elicitation/create", json!({})), none, Some(&No));
assert_eq!(r.error.as_ref().unwrap().code, rpc::METHOD_NOT_FOUND);
let r = answer(&req("roots/list", json!({})), none, Some(&No));
assert_eq!(r.error.as_ref().unwrap().code, rpc::METHOD_NOT_FOUND);
let r = answer(&req("sampling/createMessage", json!({})), none, None);
assert_eq!(r.error.unwrap().code, rpc::METHOD_NOT_FOUND);
}
#[test]
fn roots_are_listed_when_declared() {
let caps = Capabilities {
elicitation: false,
roots: true,
};
let h = Yes(Answer::Roots(vec![Root {
uri: "file:///work".into(),
name: Some("workspace".into()),
}]));
let r = answer(&req("roots/list", json!({})), caps, Some(&h));
let roots = &r.result.unwrap()["roots"];
assert_eq!(roots[0]["uri"], "file:///work");
assert_eq!(roots[0]["name"], "workspace");
}
#[test]
fn capabilities_serialize_to_what_we_can_actually_answer() {
assert_eq!(Capabilities::default().to_json(), json!({}));
assert!(Capabilities::default().is_empty());
let both = Capabilities {
elicitation: true,
roots: true,
};
assert_eq!(
both.to_json(),
json!({"elicitation": {}, "roots": {"listChanged": false}})
);
assert!(both.to_json().get("ping").is_none());
}
#[test]
fn request_classification_separates_requests_from_notifications() {
assert!(as_request(&json!({"jsonrpc":"2.0","id":1,"method":"ping"})).is_some());
assert!(as_request(&json!({"jsonrpc":"2.0","method":"notifications/x"})).is_none());
assert!(as_request(&json!({"jsonrpc":"2.0","id":1,"result":{}})).is_none());
}
}