use std::sync::Arc;
use crate::adapters::primary::stdio::{Diagnostics, ServeLoop, StdioConfig};
use crate::application::jsonrpc::ProfileBatching;
use crate::application::server::Server;
use crate::application::server::context::{DynExchange, ExchangeError, PeerSession};
use crate::application::server::dispatch::Dispatcher;
use crate::application::session::SinkError;
use crate::application::session::frame_sink::FrameSink;
use crate::domain::feature::resolve;
use crate::domain::ir::JsonObject;
use crate::domain::registry::AuthContext;
use crate::domain::session::SessionId;
use crate::domain::{
CapabilitySet, Negotiation, ProtocolVersion, SupportedVersions, TransportKind,
};
#[derive(Debug)]
pub struct StdioBuilder {
server: Server,
version: ProtocolVersion,
config: StdioConfig,
auth: AuthContext,
}
impl StdioBuilder {
#[must_use]
pub fn new(server: Server) -> Self {
Self {
server,
version: default_ceiling(),
config: StdioConfig::default(),
auth: AuthContext::anonymous(),
}
}
#[must_use]
pub fn auth(mut self, auth: AuthContext) -> Self {
self.auth = auth;
self
}
#[must_use]
pub const fn version(mut self, version: ProtocolVersion) -> Self {
self.version = version;
self
}
#[must_use]
pub const fn config(mut self, config: StdioConfig) -> Self {
self.config = config;
self
}
#[must_use]
pub fn build(
self,
) -> ServeLoop<tokio::io::Stdin, tokio::io::Stdout, tokio::io::Stderr, ProfileBatching> {
#[cfg(feature = "random-ids")]
let builder = {
let mut builder = self;
builder
.server
.ensure_request_state_key(crate::adapters::secondary::id_source::random_state_key);
builder
};
#[cfg(not(feature = "random-ids"))]
let builder = self;
let profile = resolve(Negotiation {
version: builder.version,
client_caps: CapabilitySet::empty(),
server_caps: CapabilitySet::all_for(builder.version),
transport: TransportKind::Stdio,
});
let peer = PeerSession::new(
builder.version,
profile,
SessionId::new(1),
TransportKind::Stdio,
Arc::new(NoCallbacks),
Arc::new(NullSink),
)
.with_auth(builder.auth);
ServeLoop::new(
tokio::io::stdin(),
tokio::io::stdout(),
Diagnostics::stderr(),
builder.config,
Arc::new(Dispatcher::new(Arc::new(builder.server))),
peer,
ProfileBatching,
)
}
}
fn default_ceiling() -> ProtocolVersion {
SupportedVersions::all().map_or_else(
|_| ProtocolVersion::latest_known(),
|supported| supported.latest_handshake(),
)
}
#[derive(Debug, Clone, Copy)]
struct NullSink;
impl FrameSink for NullSink {
fn send(&self, _frame: &str) -> impl std::future::Future<Output = Result<(), SinkError>> {
std::future::ready(Err(SinkError))
}
}
#[derive(Debug, Clone, Copy)]
struct NoCallbacks;
impl DynExchange for NoCallbacks {
fn exchange_boxed<'a>(
&'a self,
_method: &'a str,
_params: &'a JsonObject,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<JsonObject, ExchangeError>> + Send + 'a>,
> {
Box::pin(std::future::ready(Err(ExchangeError::new(
"this server wires no callback exchange",
))))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_ceiling_has_a_handshake() {
let ceiling = default_ceiling();
assert!(
crate::domain::handshake_defined_in(ceiling),
"{ceiling} has no initialize on it"
);
}
#[test]
fn the_ceiling_is_not_merely_the_newest_known() {
assert_ne!(default_ceiling(), ProtocolVersion::latest_known());
}
#[test]
fn the_built_session_gates_on_the_ceiling() {
let server = Server::builder("ceiling-test").build();
let loop_ = StdioBuilder::new(server)
.version(ProtocolVersion::V2024_11_05)
.build();
assert!(
!loop_
.peer()
.profile()
.allows(crate::domain::Feature::Completions),
"a 2024-11-05 session must not carry a 2025-03-26 feature"
);
}
#[test]
fn the_declared_principal_reaches_the_peer() {
use crate::domain::registry::{Scope, ScopeSet};
let declared = ScopeSet::empty().with(Scope::Read).with(Scope::Write);
let anonymous = StdioBuilder::new(Server::builder("default").build()).build();
assert_eq!(
anonymous.peer().auth(),
&AuthContext::anonymous(),
"the default is anonymous: a stdio server that says nothing is served as nobody in particular",
);
assert!(
!anonymous.peer().auth().holds(declared),
"and so it sees nothing that requires a scope"
);
let named = StdioBuilder::new(Server::builder("trusted").build())
.auth(AuthContext::new("local", declared))
.build();
assert!(
named.peer().auth().holds(declared),
"a declared principal holds what it was declared with"
);
}
}