use crate::domain::feature::FeatureProfile;
use crate::domain::ir::JsonObject;
use crate::domain::{
CLIENT_CAPABILITY_WIRE, CapabilitySet, ProtocolVersion, SERVER_CAPABILITY_WIRE,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Offer {
pub version: ProtocolVersion,
pub capabilities: CapabilitySet,
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum InitializeError {
#[error("initialize params were missing or malformed")]
Malformed,
}
pub fn read_offer(params: &JsonObject) -> Result<Offer, InitializeError> {
let parsed: serde_json::Value =
serde_json::from_str(params.as_str()).map_err(|_| InitializeError::Malformed)?;
let object = parsed.as_object().ok_or(InitializeError::Malformed)?;
let offered = object
.get("protocolVersion")
.and_then(serde_json::Value::as_str)
.ok_or(InitializeError::Malformed)?
.parse::<ProtocolVersion>()
.map_err(|_| InitializeError::Malformed)?;
let capabilities = object
.get("capabilities")
.and_then(serde_json::Value::as_object)
.ok_or(InitializeError::Malformed)?;
let client = object
.get("clientInfo")
.filter(|value| value.is_object())
.ok_or(InitializeError::Malformed)?;
let name = client
.get("name")
.and_then(serde_json::Value::as_str)
.ok_or(InitializeError::Malformed)?;
Ok(Offer {
version: offered,
capabilities: client_capabilities(capabilities),
name: name.to_owned(),
})
}
pub fn client_capabilities(object: &serde_json::Map<String, serde_json::Value>) -> CapabilitySet {
let mut set = CapabilitySet::empty();
for (capability, path) in CLIENT_CAPABILITY_WIRE {
if path_present(object, path) {
set.insert(*capability);
}
}
set
}
fn path_present(root: &serde_json::Map<String, serde_json::Value>, path: &[&str]) -> bool {
let Some((last, parents)) = path.split_last() else {
return false;
};
let mut cursor = root;
for key in parents {
let Some(next) = cursor.get(*key).and_then(serde_json::Value::as_object) else {
return false;
};
cursor = next;
}
cursor.contains_key(*last)
}
fn insert_path(root: &mut serde_json::Map<String, serde_json::Value>, path: &[&str]) {
let Some((last, parents)) = path.split_last() else {
return;
};
let mut cursor = root;
for key in parents {
let entry = cursor
.entry((*key).to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
let serde_json::Value::Object(parent) = entry else {
return;
};
cursor = parent;
}
let leaf = if parents.is_empty() {
serde_json::Value::Object(serde_json::Map::new())
} else {
serde_json::Value::Bool(true)
};
cursor.insert((*last).to_string(), leaf);
}
#[must_use]
pub fn server_capabilities_json(profile: FeatureProfile) -> serde_json::Value {
let advertised = profile.advertised_by_server();
let mut root = serde_json::Map::new();
for (capability, path) in SERVER_CAPABILITY_WIRE {
if !advertised.contains(*capability) {
continue;
}
insert_path(&mut root, path);
}
serde_json::Value::Object(root)
}
#[cfg(test)]
mod tests {
use super::{InitializeError, read_offer, server_capabilities_json};
use crate::domain::ir::JsonObject;
use crate::domain::{Capability, CapabilitySet, ProtocolVersion};
#[derive(Debug)]
enum Failure {
Fixture(crate::domain::ir::JsonObjectError),
Message(String),
}
impl core::fmt::Display for Failure {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Fixture(error) => write!(f, "bad fixture: {error}"),
Self::Message(text) => f.write_str(text),
}
}
}
impl std::error::Error for Failure {}
impl From<crate::domain::ir::JsonObjectError> for Failure {
fn from(error: crate::domain::ir::JsonObjectError) -> Self {
Self::Fixture(error)
}
}
impl From<String> for Failure {
fn from(text: String) -> Self {
Self::Message(text)
}
}
fn params(text: &str) -> Result<JsonObject, Failure> {
JsonObject::new(text).map_err(Failure::from)
}
#[test]
fn reads_a_minimal_offer() -> Result<(), Failure> {
let body = params(
r#"{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe"}}"#,
)?;
let offer = read_offer(&body)
.map_err(|_| Failure::from("a minimal offer was refused".to_string()))?;
assert_eq!(offer.version, ProtocolVersion::V2025_06_18);
assert_eq!(offer.capabilities, CapabilitySet::empty());
assert_eq!(offer.name, "probe");
Ok(())
}
#[test]
fn an_unknown_revision_is_still_read() -> Result<(), Failure> {
let body = params(
r#"{"protocolVersion":"2099-01-01","capabilities":{},"clientInfo":{"name":"future"}}"#,
)?;
let offer = read_offer(&body)
.map_err(|_| Failure::from("a future revision was refused".to_string()))?;
assert!(matches!(offer.version, ProtocolVersion::Unknown(_)));
Ok(())
}
#[test]
fn capabilities_are_read_through_the_tables() -> Result<(), Failure> {
let body = params(
r#"{"protocolVersion":"2025-06-18","capabilities":{"roots":{}},"clientInfo":{"name":"r"}}"#,
)?;
let offer =
read_offer(&body).map_err(|_| Failure::from("roots were refused".to_string()))?;
assert!(
offer.capabilities.contains(Capability::Roots),
"an empty roots object is the wire form for the capability being present"
);
Ok(())
}
#[test]
fn malformed_bodies_are_all_refused() -> Result<(), Failure> {
let cases = [
r#"{"capabilities":{},"clientInfo":{"name":"x"}}"#,
r#"{"protocolVersion":7,"capabilities":{},"clientInfo":{"name":"x"}}"#,
r#"{"protocolVersion":"not-a-date","capabilities":{},"clientInfo":{"name":"x"}}"#,
r#"{"protocolVersion":"2025-06-18","clientInfo":{"name":"x"}}"#,
r#"{"protocolVersion":"2025-06-18","capabilities":true,"clientInfo":{"name":"x"}}"#,
r#"{"protocolVersion":"2025-06-18","capabilities":{}}"#,
r#"{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{}}"#,
r#"{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":3}}"#,
];
for (index, case) in cases.iter().enumerate() {
let body = params(case)?;
if read_offer(&body) != Err(InitializeError::Malformed) {
return Err(Failure::from(format!("case {index} was accepted: {case}")));
}
}
Ok(())
}
#[test]
fn a_client_without_a_version_string_is_accepted() -> Result<(), Failure> {
let body = params(
r#"{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"x"}}"#,
)?;
read_offer(&body)
.map_err(|_| Failure::from("a nameless version was refused".to_string()))?;
Ok(())
}
#[test]
fn the_writer_advertises_only_what_the_profile_allows() -> Result<(), Failure> {
let version = ProtocolVersion::V2025_06_18;
let everything = crate::domain::feature::resolve(crate::domain::Negotiation {
version,
client_caps: CapabilitySet::all_for(version),
server_caps: CapabilitySet::all_for(version),
transport: crate::domain::TransportKind::Stdio,
});
let nothing = crate::domain::feature::resolve(crate::domain::Negotiation {
version,
client_caps: CapabilitySet::all_for(version),
server_caps: CapabilitySet::empty(),
transport: crate::domain::TransportKind::Stdio,
});
let serde_json::Value::Object(generous) = server_capabilities_json(everything) else {
return Err(Failure::from(
"tools should render as an object".to_string(),
));
};
let serde_json::Value::Object(stingy) = server_capabilities_json(nothing) else {
return Err(Failure::from(
"tools should render as an object".to_string(),
));
};
assert!(
stingy.is_empty(),
"a server that claimed nothing may claim nothing"
);
assert!(
!generous.is_empty(),
"a server claiming everything must claim something"
);
Ok(())
}
#[test]
fn a_parent_capability_renders_as_an_object() -> Result<(), Failure> {
let version = ProtocolVersion::V2025_06_18;
let profile = crate::domain::feature::resolve(crate::domain::Negotiation {
version,
client_caps: CapabilitySet::all_for(version),
server_caps: CapabilitySet::all_for(version),
transport: crate::domain::TransportKind::Stdio,
});
let serde_json::Value::Object(object) = server_capabilities_json(profile) else {
return Err(Failure::from(
"server_capabilities_json always renders an object".to_string(),
));
};
match object.get("experimental") {
Some(experimental) => assert!(
experimental.is_object(),
"experimental must be an object; `true` is not a form the schema admits"
),
None => Err(Failure::from(
"experimental was in the profile but not in the wire form".to_string(),
))?,
}
Ok(())
}
}