use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::Arc;
use crate::application::mcp::input_required::InputRound;
use crate::application::server::error::GatedError;
use crate::application::server::log_gate::LogGate;
use crate::application::session::DynFrameSink;
use crate::domain::feature::{Feature, FeatureProfile};
use crate::domain::ir::{JsonObject, LogLevel};
use crate::domain::registry::AuthContext;
use crate::domain::session::{CancellationToken, ProgressToken, SessionId};
use crate::domain::{ProtocolVersion, RequestId, TransportKind};
pub type StateMap = HashMap<TypeId, Arc<dyn Any + Send + Sync>>;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("the exchange did not complete: {reason}")]
pub struct ExchangeError {
reason: String,
}
impl ExchangeError {
#[must_use]
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
pub trait CallbackExchange {
fn exchange(
&self,
method: &str,
params: &JsonObject,
) -> impl Future<Output = Result<JsonObject, ExchangeError>> + Send;
}
pub trait DynExchange: Send + Sync {
fn exchange_boxed<'a>(
&'a self,
method: &'a str,
params: &'a JsonObject,
) -> std::pin::Pin<Box<dyn Future<Output = Result<JsonObject, ExchangeError>> + Send + 'a>>;
}
impl<T: CallbackExchange + Send + Sync> DynExchange for T {
fn exchange_boxed<'a>(
&'a self,
method: &'a str,
params: &'a JsonObject,
) -> std::pin::Pin<Box<dyn Future<Output = Result<JsonObject, ExchangeError>> + Send + 'a>>
{
Box::pin(self.exchange(method, params))
}
}
#[derive(Clone)]
pub struct PeerSession {
negotiated: ProtocolVersion,
profile: FeatureProfile,
session: SessionId,
transport: TransportKind,
auth: AuthContext,
exchange: Arc<dyn DynExchange>,
sink: Arc<dyn DynFrameSink>,
log: LogGate,
}
impl core::fmt::Debug for PeerSession {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PeerSession")
.field("negotiated", &self.negotiated)
.field("session", &self.session)
.field("principal", &self.auth.principal())
.field("feature_count", &self.profile.len())
.finish_non_exhaustive()
}
}
impl PeerSession {
#[must_use]
pub fn new(
negotiated: ProtocolVersion,
profile: FeatureProfile,
session: SessionId,
transport: TransportKind,
exchange: Arc<dyn DynExchange>,
sink: Arc<dyn DynFrameSink>,
) -> Self {
Self {
negotiated,
profile,
session,
transport,
auth: AuthContext::anonymous(),
exchange,
sink,
log: LogGate::open(),
}
}
#[must_use]
pub fn with_log_gate(mut self, gate: LogGate) -> Self {
self.log = gate;
self
}
#[must_use]
pub const fn log_gate(&self) -> &LogGate {
&self.log
}
#[must_use]
pub const fn transport(&self) -> TransportKind {
self.transport
}
#[must_use]
pub const fn auth(&self) -> &AuthContext {
&self.auth
}
#[must_use]
pub const fn with_profile(mut self, profile: FeatureProfile) -> Self {
self.profile = profile;
self
}
#[must_use]
pub fn with_auth(mut self, auth: AuthContext) -> Self {
self.auth = auth;
self
}
#[must_use]
pub fn without_auth(mut self) -> Self {
self.auth = AuthContext::anonymous();
self
}
#[must_use]
pub fn with_exchange(mut self, exchange: Arc<dyn DynExchange>) -> Self {
self.exchange = exchange;
self
}
#[must_use]
pub const fn with_session(mut self, id: SessionId) -> Self {
self.session = id;
self
}
#[must_use]
pub fn with_sink(mut self, sink: Arc<dyn DynFrameSink>) -> Self {
self.sink = sink;
self
}
#[must_use]
pub const fn negotiated(&self) -> ProtocolVersion {
self.negotiated
}
#[must_use]
pub const fn profile(&self) -> FeatureProfile {
self.profile
}
#[must_use]
pub const fn session(&self) -> SessionId {
self.session
}
#[must_use]
pub fn allows(&self, feature: Feature) -> bool {
self.profile.allows(feature)
}
#[must_use]
pub const fn exchange(&self) -> &Arc<dyn DynExchange> {
&self.exchange
}
#[must_use]
pub const fn sink(&self) -> &Arc<dyn DynFrameSink> {
&self.sink
}
fn require(&self, feature: Feature) -> Result<(), GatedError> {
if self.profile.allows(feature) {
return Ok(());
}
Err(GatedError::FeatureUnavailable {
feature,
negotiated: self.negotiated,
})
}
}
#[derive(Clone)]
pub struct RequestContext {
peer: PeerSession,
id: RequestId,
cancel: CancellationToken,
progress: Option<ProgressToken>,
state: Arc<StateMap>,
input: InputRound,
}
impl core::fmt::Debug for RequestContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("RequestContext")
.field("request", &self.id.redacted())
.field("negotiated", &self.peer.negotiated)
.field("has_progress_token", &self.progress.is_some())
.field("cancelled", &self.cancel.is_cancelled())
.field("state_entries", &self.state.len())
.finish_non_exhaustive()
}
}
impl RequestContext {
#[must_use]
pub fn for_request(
peer: &PeerSession,
id: RequestId,
cancel: CancellationToken,
progress: Option<ProgressToken>,
) -> Self {
Self {
peer: peer.clone(),
id,
cancel,
progress,
state: Arc::new(HashMap::new()),
input: InputRound::default(),
}
}
#[must_use]
pub fn for_request_with_state(
peer: &PeerSession,
id: RequestId,
cancel: CancellationToken,
progress: Option<ProgressToken>,
state: Arc<StateMap>,
) -> Self {
Self {
peer: peer.clone(),
id,
cancel,
progress,
state,
input: InputRound::default(),
}
}
#[must_use]
pub(crate) fn with_input_round(mut self, input: InputRound) -> Self {
self.input = input;
self
}
#[must_use]
pub fn input_response(&self, key: &str) -> Option<JsonObject> {
self.input.response(key)
}
#[must_use]
pub const fn has_input_responses(&self) -> bool {
self.input.has_responses()
}
#[must_use]
pub fn request_state(&self) -> Option<&str> {
self.input.state()
}
pub fn seal_state(&self, state: &str) -> Result<String, GatedError> {
self.input.seal(state).ok_or(GatedError::NoStateKey)
}
#[must_use]
pub fn state<T: Send + Sync + 'static>(&self) -> Option<&T> {
self.state
.get(&TypeId::of::<T>())
.and_then(|value| value.downcast_ref::<T>())
}
#[must_use]
pub const fn peer(&self) -> &PeerSession {
&self.peer
}
#[must_use]
pub const fn request_id(&self) -> &RequestId {
&self.id
}
#[must_use]
pub const fn cancellation(&self) -> &CancellationToken {
&self.cancel
}
#[must_use]
pub const fn progress_token(&self) -> Option<&ProgressToken> {
self.progress.as_ref()
}
pub async fn elicit(&self, params: &JsonObject) -> Result<JsonObject, GatedError> {
self.peer.require(Feature::Elicitation)?;
self.round_trip("elicitation/create", params).await
}
pub async fn sample(&self, params: &JsonObject) -> Result<JsonObject, GatedError> {
self.peer.require(Feature::Sampling)?;
self.round_trip("sampling/createMessage", params).await
}
pub async fn create_task(&self, params: &JsonObject) -> Result<JsonObject, GatedError> {
self.peer.require(Feature::Tasks)?;
self.round_trip("tasks/create", params).await
}
pub async fn report_progress(&self, params: &JsonObject) -> Result<(), GatedError> {
let Some(token) = self.progress.as_ref() else {
return Err(GatedError::NoProgressToken);
};
let frame = Self::progress_frame(token, params);
self.peer
.sink
.send(&frame)
.await
.map_err(|_| GatedError::MalformedAnswer {
reason: "the progress notification did not reach the peer".to_string(),
})
}
pub async fn log(&self, level: LogLevel, message: &str) -> Result<(), GatedError> {
self.peer.require(Feature::Logging)?;
if !self.peer.log.admits(level) {
return Ok(());
}
let escaped = serde_json::to_string(message).unwrap_or_else(|reason| {
format!("\"the log message could not be serialised: {reason}\"")
});
let frame = format!(
r#"{{"jsonrpc":"2.0","method":"notifications/message","params":{{"level":{},"data":{}}}}}"#,
serde_json::to_string(level.as_str()).unwrap_or_else(|_| "\"info\"".to_owned()),
escaped
);
self.peer
.sink
.send(&frame)
.await
.map_err(|_| GatedError::MalformedAnswer {
reason: "the log notification did not reach the peer".to_string(),
})
}
async fn round_trip(
&self,
method: &str,
params: &JsonObject,
) -> Result<JsonObject, GatedError> {
self.peer
.exchange
.exchange_boxed(method, params)
.await
.map_err(|failure| GatedError::MalformedAnswer {
reason: failure.to_string(),
})
}
fn progress_frame(token: &ProgressToken, params: &JsonObject) -> String {
let mut members = serde_json::Map::new();
let echoed = match token {
ProgressToken::Number(value) => serde_json::Value::from(*value),
ProgressToken::Text(value) => serde_json::Value::String(value.to_string()),
};
members.insert("progressToken".to_string(), echoed);
if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(params.as_str()) {
for (key, value) in map {
if key != "progressToken" {
members.insert(key, value);
}
}
}
let frame = serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": serde_json::Value::Object(members),
});
serde_json::to_string(&frame).unwrap_or_else(|reason| {
format!(r#"{{"error":"the progress frame could not be serialised: {reason}"}}"#)
})
}
}
#[cfg(test)]
mod tests {
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use super::{
CallbackExchange, ExchangeError, GatedError, PeerSession, RequestContext, StateMap,
};
use crate::application::session::{FrameSink, SinkError};
use crate::domain::feature::{Feature, FeatureProfile};
use crate::domain::ir::{JsonObject, JsonObjectError};
use crate::domain::registry::{AuthContext, Scope, ScopeSet};
use crate::domain::session::{CancellationToken, ProgressToken, SessionId};
use crate::domain::{ProtocolVersion, RequestId, TransportKind};
#[derive(Debug)]
enum Failure {
Message(String),
Fixture(JsonObjectError),
Serde(serde_json::Error),
}
impl core::fmt::Display for Failure {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Message(text) => f.write_str(text),
Self::Fixture(error) => write!(f, "bad fixture: {error}"),
Self::Serde(error) => write!(f, "not JSON: {error}"),
}
}
}
impl std::error::Error for Failure {}
impl From<String> for Failure {
fn from(text: String) -> Self {
Self::Message(text)
}
}
impl From<&str> for Failure {
fn from(text: &str) -> Self {
Self::Message(text.to_string())
}
}
impl From<JsonObjectError> for Failure {
fn from(error: JsonObjectError) -> Self {
Self::Fixture(error)
}
}
impl From<serde_json::Error> for Failure {
fn from(error: serde_json::Error) -> Self {
Self::Serde(error)
}
}
impl From<GatedError> for Failure {
fn from(error: GatedError) -> Self {
Self::Message(error.to_string())
}
}
type TestError = Failure;
fn at<'a>(
value: &'a serde_json::Value,
path: &[&str],
) -> Result<&'a serde_json::Value, Failure> {
let mut cursor = value;
for (depth, key) in path.iter().enumerate() {
let object = cursor.as_object().ok_or_else(|| {
Failure::from(format!(
"{path:?} is not an object at depth {depth}, found a {}",
match cursor {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "bool",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
))
})?;
cursor = object
.get(*key)
.ok_or_else(|| Failure::from(format!("{path:?} has no member {key:?}")))?;
}
Ok(cursor)
}
#[derive(Default)]
struct RecordingSink {
frames: Mutex<Vec<String>>,
}
impl RecordingSink {
fn written(&self) -> Vec<String> {
let frames = self
.frames
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
frames.clone()
}
}
impl FrameSink for RecordingSink {
fn send(&self, frame: &str) -> impl Future<Output = Result<(), SinkError>> + Send {
{
let mut frames = self
.frames
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
frames.push(frame.to_string());
}
std::future::ready(Ok(()))
}
}
#[derive(Default)]
struct CountingExchange {
calls: Mutex<Vec<String>>,
answer: Option<JsonObject>,
}
impl CountingExchange {
fn answering(answer: &str) -> Result<Self, TestError> {
Ok(Self {
calls: Mutex::new(Vec::new()),
answer: Some(JsonObject::new(answer)?),
})
}
}
impl CallbackExchange for CountingExchange {
fn exchange(
&self,
method: &str,
_params: &JsonObject,
) -> impl Future<Output = Result<JsonObject, ExchangeError>> + Send {
{
let mut calls = self
.calls
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
calls.push(method.to_string());
}
let outcome = self
.answer
.clone()
.ok_or_else(|| ExchangeError::new("no answer configured"));
std::future::ready(outcome)
}
}
fn peer(
version: ProtocolVersion,
features: FeatureProfile,
exchange: &Arc<CountingExchange>,
sink: &Arc<RecordingSink>,
) -> PeerSession {
PeerSession::new(
version,
features,
SessionId::new(7),
TransportKind::Stdio,
exchange.clone(),
sink.clone(),
)
}
#[test]
fn without_auth_keeps_the_session_and_drops_the_principal() -> Result<(), TestError> {
let exchange = Arc::new(CountingExchange::answering(r#"{"action":"accept"}"#)?);
let sink = Arc::new(RecordingSink::default());
let held = ScopeSet::empty().with(Scope::Read).with(Scope::Write);
let profile = crate::domain::feature::resolve(crate::domain::feature::Negotiation::bare(
ProtocolVersion::V2025_06_18,
TransportKind::Stdio,
));
let holding = peer(ProtocolVersion::V2025_06_18, profile, &exchange, &sink)
.with_auth(AuthContext::new("alice", held));
assert!(
holding.auth().holds(held),
"the premise: alice holds what she was given"
);
let stored = holding.without_auth();
assert_eq!(stored.auth(), &AuthContext::anonymous());
assert!(!stored.auth().holds(held));
assert_eq!(stored.negotiated(), ProtocolVersion::V2025_06_18);
assert_eq!(stored.profile(), profile);
assert_eq!(stored.session(), SessionId::new(7));
assert_eq!(stored.transport(), TransportKind::Stdio);
Ok(())
}
#[tokio::test]
async fn elicitation_on_an_old_session_is_refused_and_writes_nothing() -> Result<(), TestError>
{
let exchange = Arc::new(CountingExchange::answering(r#"{"action":"accept"}"#)?);
let sink = Arc::new(RecordingSink::default());
let profile = crate::domain::feature::resolve(crate::domain::feature::Negotiation::bare(
ProtocolVersion::V2025_03_26,
crate::domain::TransportKind::Stdio,
));
assert!(
!profile.allows(Feature::Elicitation),
"the premise of this test is that 2025-03-26 has no elicitation"
);
let peer = peer(ProtocolVersion::V2025_03_26, profile, &exchange, &sink);
let ctx = RequestContext::for_request(
&peer,
RequestId::number(1),
CancellationToken::new(),
None,
);
let params = JsonObject::new(r#"{"message":"name?"}"#)?;
let refusal = match ctx.elicit(¶ms).await {
Ok(answer) => {
return Err(Failure::from(format!(
"elicit unexpectedly returned {answer}"
)));
}
Err(refusal) => refusal,
};
assert_eq!(
refusal,
GatedError::FeatureUnavailable {
feature: Feature::Elicitation,
negotiated: ProtocolVersion::V2025_03_26,
}
);
assert!(refusal.wrote_nothing());
assert_eq!(refusal.feature(), Some(Feature::Elicitation));
assert!(
sink.written().is_empty(),
"the gate must run before the sink is touched"
);
assert!(
exchange
.calls
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty(),
"the gate must run before the exchange is reached"
);
Ok(())
}
#[tokio::test]
async fn a_negotiated_gate_reaches_the_peer() -> Result<(), TestError> {
let exchange = Arc::new(CountingExchange::answering(r#"{"action":"accept"}"#)?);
let sink = Arc::new(RecordingSink::default());
let peer = peer(
ProtocolVersion::V2025_06_18,
FeatureProfile::empty().with(Feature::Elicitation),
&exchange,
&sink,
);
let ctx = RequestContext::for_request(
&peer,
RequestId::number(1),
CancellationToken::new(),
None,
);
let params = JsonObject::new(r#"{"message":"name?"}"#)?;
let answer = match ctx.elicit(¶ms).await {
Ok(answer) => answer,
Err(error) => return Err(Failure::from(error)),
};
assert_eq!(answer.as_str(), r#"{"action":"accept"}"#);
assert_eq!(
exchange
.calls
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone(),
vec!["elicitation/create".to_string()]
);
Ok(())
}
#[tokio::test]
async fn every_gated_call_refuses_when_its_feature_is_absent() -> Result<(), TestError> {
let cases: [(Feature, &str); 3] = [
(Feature::Elicitation, "elicitation/create"),
(Feature::Sampling, "sampling/createMessage"),
(Feature::Tasks, "tasks/create"),
];
for (feature, method) in cases {
let exchange = Arc::new(CountingExchange::answering("{}")?);
let sink = Arc::new(RecordingSink::default());
let peer = peer(
ProtocolVersion::V2024_11_05,
FeatureProfile::empty(),
&exchange,
&sink,
);
let ctx = RequestContext::for_request(
&peer,
RequestId::number(1),
CancellationToken::new(),
None,
);
let params = JsonObject::new("{}")?;
let outcome = match feature {
Feature::Elicitation => ctx.elicit(¶ms).await,
Feature::Sampling => ctx.sample(¶ms).await,
Feature::Tasks => ctx.create_task(¶ms).await,
_ => {
return Err(Failure::from(
"the table named a feature with no gated call",
));
}
};
let refusal = match outcome {
Ok(answer) => {
return Err(Failure::from(format!(
"{method} unexpectedly returned {answer}"
)));
}
Err(refusal) => refusal,
};
assert_eq!(
refusal.feature(),
Some(feature),
"{method} refused for the wrong feature"
);
assert!(sink.written().is_empty(), "{method} wrote before refusing");
}
Ok(())
}
#[tokio::test]
async fn progress_without_a_token_refuses_without_touching_the_sink() -> Result<(), TestError> {
let exchange = Arc::new(CountingExchange::answering("{}")?);
let sink = Arc::new(RecordingSink::default());
let peer = peer(
ProtocolVersion::V2025_06_18,
FeatureProfile::all(),
&exchange,
&sink,
);
let ctx = RequestContext::for_request(
&peer,
RequestId::number(1),
CancellationToken::new(),
None,
);
let params = JsonObject::new(r#"{"progress":0.5}"#)?;
let refusal = match ctx.report_progress(¶ms).await {
Ok(()) => {
return Err(Failure::from("report_progress unexpectedly succeeded"));
}
Err(refusal) => refusal,
};
assert_eq!(refusal, GatedError::NoProgressToken);
assert!(refusal.wrote_nothing());
assert!(refusal.feature().is_none());
assert!(sink.written().is_empty());
Ok(())
}
#[tokio::test]
async fn a_progress_report_carries_the_real_token() -> Result<(), TestError> {
let exchange = Arc::new(CountingExchange::answering("{}")?);
let sink = Arc::new(RecordingSink::default());
let peer = peer(
ProtocolVersion::V2025_06_18,
FeatureProfile::all(),
&exchange,
&sink,
);
let ctx = RequestContext::for_request(
&peer,
RequestId::number(1),
CancellationToken::new(),
Some(ProgressToken::text("real-token")),
);
let params = JsonObject::new(r#"{"progressToken":"forged","progress":0.5}"#)?;
ctx.report_progress(¶ms).await?;
let frames = sink.written();
let frame = frames.first().ok_or("the sink received nothing")?;
let parsed: serde_json::Value =
serde_json::from_str(frame).map_err(|e| format!("the frame is not JSON: {e}"))?;
assert_eq!(at(&parsed, &["method"])?, "notifications/progress");
assert_eq!(at(&parsed, &["params", "progressToken"])?, "real-token");
assert_eq!(at(&parsed, &["params", "progress"])?, 0.5);
assert!(
at(&parsed, &["params", "forged"]).is_err(),
"the caller's extra members should not appear"
);
Ok(())
}
#[tokio::test]
async fn a_quote_in_the_token_still_produces_valid_json() -> Result<(), TestError> {
let exchange = Arc::new(CountingExchange::answering("{}")?);
let sink = Arc::new(RecordingSink::default());
let peer = peer(
ProtocolVersion::V2025_06_18,
FeatureProfile::all(),
&exchange,
&sink,
);
let ctx = RequestContext::for_request(
&peer,
RequestId::number(1),
CancellationToken::new(),
Some(ProgressToken::text(r#"a"b\c"#)),
);
ctx.report_progress(&JsonObject::new("{}")?).await?;
let frames = sink.written();
let frame = frames.first().ok_or("the sink received nothing")?;
let parsed: serde_json::Value =
serde_json::from_str(frame).map_err(|e| format!("the frame is not JSON: {e}"))?;
assert_eq!(at(&parsed, &["params", "progressToken"])?, r#"a"b\c"#);
Ok(())
}
#[test]
fn a_context_does_not_print_its_request_id_in_full() -> Result<(), TestError> {
let exchange = Arc::new(CountingExchange::answering("{}")?);
let sink = Arc::new(RecordingSink::default());
let peer = peer(
ProtocolVersion::V2025_06_18,
FeatureProfile::all(),
&exchange,
&sink,
);
let ctx = RequestContext::for_request(
&peer,
RequestId::text("secret-request-id"),
CancellationToken::new(),
None,
);
let rendered = format!("{ctx:?}");
assert!(
!rendered.contains("secret-request-id"),
"the debug form leaked the request id: {rendered}"
);
assert!(rendered.contains("RequestContext"));
Ok(())
}
#[test]
fn a_default_context_has_no_state() -> Result<(), TestError> {
let exchange = Arc::new(CountingExchange::answering("{}")?);
let sink = Arc::new(RecordingSink::default());
let peer = peer(
ProtocolVersion::V2025_06_18,
FeatureProfile::all(),
&exchange,
&sink,
);
let ctx = RequestContext::for_request(
&peer,
RequestId::number(1),
CancellationToken::new(),
None,
);
if ctx.state::<TestConfig>().is_some() {
return Err(Failure::Message(
"a default context returned a state value".into(),
));
}
Ok(())
}
#[test]
fn a_state_typed_lookup_finds_its_value() -> Result<(), TestError> {
let exchange = Arc::new(CountingExchange::answering("{}")?);
let sink = Arc::new(RecordingSink::default());
let peer = peer(
ProtocolVersion::V2025_06_18,
FeatureProfile::all(),
&exchange,
&sink,
);
let mut map: StateMap = HashMap::new();
map.insert(
TypeId::of::<TestConfig>(),
Arc::new(TestConfig { version: 7 }) as Arc<dyn Any + Send + Sync>,
);
let state = Arc::new(map);
let ctx = RequestContext::for_request_with_state(
&peer,
RequestId::number(1),
CancellationToken::new(),
None,
Arc::clone(&state),
);
let found = ctx.state::<TestConfig>();
if found != Some(&TestConfig { version: 7 }) {
return Err(Failure::Message(format!(
"expected Some(&TestConfig {{ version: 7 }}), got {found:?}"
)));
}
if ctx.state::<TestOtherConfig>().is_some() {
return Err(Failure::Message(
"an unrelated type was found in the state map".into(),
));
}
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
struct TestConfig {
version: u32,
}
#[derive(Debug, PartialEq, Eq)]
struct TestOtherConfig {
name: String,
}
}