use serde_json::Value;
use crate::application::client::{ClientConfig, ClientTransport, TransportError};
use crate::domain::ir::{JsonObject, JsonObjectError};
use crate::domain::{
CLIENT_CAPABILITY_WIRE, CapabilitySet, NegotiatedSession, NegotiationError, PeerInfo,
ProtocolVersion, SERVER_CAPABILITY_WIRE, SupportedVersions, handshake_defined_in,
negotiate_client,
};
pub const DISCOVER: &str = "server/discover";
pub const INITIALIZE: &str = "initialize";
pub const INITIALIZED: &str = "notifications/initialized";
fn self_info() -> PeerInfo {
PeerInfo {
name: env!("CARGO_PKG_NAME").to_owned(),
version: env!("CARGO_PKG_VERSION").to_owned(),
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum HandshakeError {
#[error("this client was offered no revision, so there is nothing to negotiate")]
NothingOffered,
#[error("the {method} answer has no usable {field}: {detail}")]
MalformedResult {
method: &'static str,
field: &'static str,
detail: String,
},
#[error(transparent)]
Negotiation(#[from] NegotiationError),
#[error("the handshake did not complete: {0}")]
Transport(#[from] TransportError),
#[error("the {method} request could not be assembled: {source}")]
Assembling {
method: &'static str,
source: JsonObjectError,
},
}
pub async fn handshake(
transport: &(impl ClientTransport + ?Sized),
config: &ClientConfig,
) -> Result<NegotiatedSession, HandshakeError> {
let (preferred, supported) = offer(config).ok_or(HandshakeError::NothingOffered)?;
if !handshake_defined_in(preferred) {
let answer = transport.send(DISCOVER, &empty_params()?).await?;
return discover_session(&answer, &supported, config);
}
let answer = transport
.send(INITIALIZE, &initialize_params(preferred, config)?)
.await?;
let session = initialize_session(&answer, preferred, &supported, config)?;
transport.notify(INITIALIZED, &empty_params()?).await?;
Ok(session)
}
#[must_use]
pub fn offer(config: &ClientConfig) -> Option<(ProtocolVersion, SupportedVersions)> {
let supported = SupportedVersions::from_slice(config.versions()).ok()?;
let preferred = *config.versions().first()?;
Some((preferred, supported))
}
pub fn initialize_params(
preferred: ProtocolVersion,
config: &ClientConfig,
) -> Result<JsonObject, HandshakeError> {
let mut info = serde_json::Map::new();
info.insert("name".to_owned(), Value::from(self_info().name));
info.insert("version".to_owned(), Value::from(self_info().version));
let mut params = serde_json::Map::new();
params.insert(
"protocolVersion".to_owned(),
Value::from(preferred.to_string()),
);
params.insert(
"capabilities".to_owned(),
Value::Object(client_capabilities(config)),
);
params.insert("clientInfo".to_owned(), Value::Object(info));
to_params(INITIALIZE, &Value::Object(params))
}
fn empty_params() -> Result<JsonObject, HandshakeError> {
to_params(DISCOVER, &Value::Object(serde_json::Map::new()))
}
fn to_params(method: &'static str, value: &Value) -> Result<JsonObject, HandshakeError> {
JsonObject::new(value.to_string())
.map_err(|source| HandshakeError::Assembling { method, source })
}
pub(super) fn client_capabilities(config: &ClientConfig) -> serde_json::Map<String, Value> {
let mut root = serde_json::Map::new();
for (capability, path) in CLIENT_CAPABILITY_WIRE {
if config.capabilities().contains(*capability) {
insert_path(&mut root, path);
}
}
root
}
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);
}
pub fn initialize_session(
answer: &JsonObject,
preferred: ProtocolVersion,
supported: &SupportedVersions,
config: &ClientConfig,
) -> Result<NegotiatedSession, HandshakeError> {
let result = parse(answer);
let answered = required_version(&result, INITIALIZE, "protocolVersion")?;
let server_caps = server_capabilities(&result, INITIALIZE)?;
let server_info = peer_info(&result, "serverInfo");
let version = negotiate_client(preferred, answered, supported)?;
let session = NegotiatedSession::new(
version,
config.transport(),
config.capabilities(),
server_caps,
)
.with_client_info(self_info());
Ok(match server_info {
Some(info) => session.with_server_info(info),
None => session,
})
}
pub fn discover_session(
answer: &JsonObject,
supported: &SupportedVersions,
config: &ClientConfig,
) -> Result<NegotiatedSession, HandshakeError> {
let result = parse(answer);
let offered = required_version_set(&result)?;
let server_caps = server_capabilities(&result, DISCOVER)?;
let version = supported.best_match(&offered).ok_or_else(|| {
HandshakeError::MalformedResult {
method: DISCOVER,
field: "supportedVersions",
detail: format!(
"it offers {offered:?}, none of which this client speaks (it speaks {supported})"
),
}
})?;
Ok(NegotiatedSession::new(
version,
config.transport(),
config.capabilities(),
server_caps,
)
.with_client_info(self_info()))
}
fn parse(answer: &JsonObject) -> Value {
serde_json::from_str(answer.as_str()).unwrap_or_else(|_| Value::Object(serde_json::Map::new()))
}
fn required_version(
result: &Value,
method: &'static str,
field: &'static str,
) -> Result<ProtocolVersion, HandshakeError> {
let raw = result.get(field).and_then(Value::as_str).ok_or_else(|| {
HandshakeError::MalformedResult {
method,
field,
detail: describe(result.get(field)),
}
})?;
raw.parse::<ProtocolVersion>()
.map_err(|error| HandshakeError::MalformedResult {
method,
field,
detail: format!("{raw:?} is not a protocol version this build knows: {error}"),
})
}
fn required_version_set(result: &Value) -> Result<Vec<ProtocolVersion>, HandshakeError> {
let raw = result
.get("supportedVersions")
.and_then(Value::as_array)
.ok_or_else(|| HandshakeError::MalformedResult {
method: DISCOVER,
field: "supportedVersions",
detail: describe(result.get("supportedVersions")),
})?;
Ok(raw
.iter()
.filter_map(Value::as_str)
.filter_map(|text| text.parse::<ProtocolVersion>().ok())
.collect())
}
fn server_capabilities(
result: &Value,
method: &'static str,
) -> Result<CapabilitySet, HandshakeError> {
let raw = result
.get("capabilities")
.and_then(Value::as_object)
.ok_or_else(|| HandshakeError::MalformedResult {
method,
field: "capabilities",
detail: describe(result.get("capabilities")),
})?;
let mut set = CapabilitySet::empty();
for (capability, path) in SERVER_CAPABILITY_WIRE {
if read_path(raw, path) {
set.insert(*capability);
}
}
Ok(set)
}
fn read_path(object: &serde_json::Map<String, Value>, path: &[&str]) -> bool {
let Some((last, parents)) = path.split_last() else {
return false;
};
let mut cursor = object;
for key in parents {
let Some(next) = cursor.get(*key).and_then(Value::as_object) else {
return false;
};
cursor = next;
}
match cursor.get(*last) {
Some(Value::Bool(flag)) => *flag,
Some(Value::Object(_)) if parents.is_empty() => true,
_ => false,
}
}
fn peer_info(result: &Value, field: &str) -> Option<PeerInfo> {
let object = result.get(field)?.as_object()?;
Some(PeerInfo {
name: object.get("name")?.as_str()?.to_owned(),
version: object.get("version")?.as_str()?.to_owned(),
})
}
fn describe(value: Option<&Value>) -> String {
value.map_or_else(
|| "the field is absent".to_owned(),
|value| format!("it holds {value}"),
)
}
#[cfg(test)]
#[path = "handshake/tests.rs"]
mod tests;