use crate::render::{DetailLevel, JsonPorcelainConfig, render_json_porcelain};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt;
use thiserror::Error;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
const OVERVIEW_BOUNDS: SurfacePorcelainBounds = SurfacePorcelainBounds {
concise: JsonPorcelainConfig {
max_lines: 10,
max_inline_chars: 144,
},
full: JsonPorcelainConfig {
max_lines: 28,
max_inline_chars: 240,
},
};
const LIST_BOUNDS: SurfacePorcelainBounds = SurfacePorcelainBounds {
concise: JsonPorcelainConfig {
max_lines: 16,
max_inline_chars: 128,
},
full: JsonPorcelainConfig {
max_lines: 32,
max_inline_chars: 176,
},
};
const READ_BOUNDS: SurfacePorcelainBounds = SurfacePorcelainBounds {
concise: JsonPorcelainConfig {
max_lines: 18,
max_inline_chars: 176,
},
full: JsonPorcelainConfig {
max_lines: 40,
max_inline_chars: 320,
},
};
const MUTATION_BOUNDS: SurfacePorcelainBounds = SurfacePorcelainBounds {
concise: JsonPorcelainConfig {
max_lines: 12,
max_inline_chars: 160,
},
full: JsonPorcelainConfig {
max_lines: 24,
max_inline_chars: 256,
},
};
const OPS_BOUNDS: SurfacePorcelainBounds = SurfacePorcelainBounds {
concise: JsonPorcelainConfig {
max_lines: 8,
max_inline_chars: 160,
},
full: JsonPorcelainConfig {
max_lines: 24,
max_inline_chars: 240,
},
};
#[derive(Debug, Error)]
pub enum ProjectionError {
#[error("failed to serialize projection: {0}")]
Serialize(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceKind {
Overview,
List,
Read,
Mutation,
Ops,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SurfacePorcelainBounds {
pub concise: JsonPorcelainConfig,
pub full: JsonPorcelainConfig,
}
impl SurfacePorcelainBounds {
#[must_use]
pub const fn for_detail(self, detail: DetailLevel) -> JsonPorcelainConfig {
match detail {
DetailLevel::Concise => self.concise,
DetailLevel::Full => self.full,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProjectionPolicy {
pub kind: SurfaceKind,
pub forbid_opaque_ids: bool,
pub reference_only: bool,
pub porcelain: SurfacePorcelainBounds,
}
impl ProjectionPolicy {
#[must_use]
pub fn from_surface(kind: SurfaceKind, forbid_opaque_ids: bool, reference_only: bool) -> Self {
let porcelain = match kind {
SurfaceKind::Overview => OVERVIEW_BOUNDS,
SurfaceKind::List => LIST_BOUNDS,
SurfaceKind::Read => READ_BOUNDS,
SurfaceKind::Mutation => MUTATION_BOUNDS,
SurfaceKind::Ops => OPS_BOUNDS,
};
Self {
kind,
forbid_opaque_ids,
reference_only,
porcelain,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SelectorRef {
pub slug: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct TimestampText(String);
impl TimestampText {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for TimestampText {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl From<OffsetDateTime> for TimestampText {
fn from(timestamp: OffsetDateTime) -> Self {
Self(
timestamp
.format(&Rfc3339)
.unwrap_or_else(|_| timestamp.unix_timestamp().to_string()),
)
}
}
pub trait SelectorProjection {
fn selector_ref(&self) -> SelectorRef;
}
pub trait SurfacePolicy {
const KIND: SurfaceKind;
const FORBID_OPAQUE_IDS: bool = true;
const REFERENCE_ONLY: bool = false;
#[must_use]
fn projection_policy(&self) -> ProjectionPolicy {
ProjectionPolicy::from_surface(Self::KIND, Self::FORBID_OPAQUE_IDS, Self::REFERENCE_ONLY)
}
}
pub trait StructuredProjection {
fn concise_projection(&self) -> Result<Value, ProjectionError>;
fn full_projection(&self) -> Result<Value, ProjectionError>;
}
pub trait ToolProjection: StructuredProjection + SurfacePolicy {
fn structured_projection(&self, detail: DetailLevel) -> Result<Value, ProjectionError> {
match detail {
DetailLevel::Concise => self.concise_projection(),
DetailLevel::Full => self.full_projection(),
}
}
fn porcelain_projection(&self, detail: DetailLevel) -> Result<String, ProjectionError> {
let policy = self.projection_policy();
let value = self.structured_projection(detail)?;
let config = policy.porcelain.for_detail(detail);
Ok(render_json_porcelain(&value, config))
}
}
impl<T> ToolProjection for T where T: StructuredProjection + SurfacePolicy {}
#[derive(Debug, Clone)]
pub struct FallbackJsonProjection {
concise: Value,
full: Value,
kind: SurfaceKind,
forbid_opaque_ids: bool,
reference_only: bool,
}
impl FallbackJsonProjection {
pub fn new(
concise: impl Serialize,
full: impl Serialize,
kind: SurfaceKind,
) -> Result<Self, ProjectionError> {
Ok(Self {
concise: serde_json::to_value(concise)?,
full: serde_json::to_value(full)?,
kind,
forbid_opaque_ids: true,
reference_only: false,
})
}
pub fn with_policy(
concise: impl Serialize,
full: impl Serialize,
kind: SurfaceKind,
forbid_opaque_ids: bool,
reference_only: bool,
) -> Result<Self, ProjectionError> {
Ok(Self {
concise: serde_json::to_value(concise)?,
full: serde_json::to_value(full)?,
kind,
forbid_opaque_ids,
reference_only,
})
}
}
impl StructuredProjection for FallbackJsonProjection {
fn concise_projection(&self) -> Result<Value, ProjectionError> {
Ok(self.concise.clone())
}
fn full_projection(&self) -> Result<Value, ProjectionError> {
Ok(self.full.clone())
}
}
impl SurfacePolicy for FallbackJsonProjection {
const KIND: SurfaceKind = SurfaceKind::Read;
fn projection_policy(&self) -> ProjectionPolicy {
ProjectionPolicy::from_surface(self.kind, self.forbid_opaque_ids, self.reference_only)
}
}
#[cfg(test)]
mod tests {
use super::{StructuredProjection as _, SurfaceKind, SurfacePolicy as _};
use crate::{DetailLevel, SelectorProjection, SelectorRef, ToolProjection};
use time::OffsetDateTime;
#[derive(Clone, SelectorProjection)]
struct HypothesisSelector {
slug: String,
title: String,
}
#[derive(ToolProjection)]
#[libmcp(kind = "read")]
struct ExperimentProjection {
slug: String,
title: String,
hypothesis: SelectorRef,
#[libmcp(skip_none)]
summary: Option<String>,
#[libmcp(full_only)]
analysis: String,
}
#[test]
fn derived_projection_shapes_detail_levels() {
let owner = HypothesisSelector {
slug: "native-lp-sink".to_owned(),
title: "Native LP sink".to_owned(),
};
let projection = ExperimentProjection {
slug: "matched-lp-site-traces".to_owned(),
title: "Matched LP site traces".to_owned(),
hypothesis: owner.selector_ref(),
summary: Some("Node LP work dominates traced native spend.".to_owned()),
analysis: "Native LP spends most traced wallclock in node reoptimization.".to_owned(),
};
assert_eq!(ExperimentProjection::KIND, SurfaceKind::Read);
let concise = projection.concise_projection();
assert!(concise.is_ok());
let concise = match concise {
Ok(value) => value,
Err(_) => return,
};
let full = projection.full_projection();
assert!(full.is_ok());
let full = match full {
Ok(value) => value,
Err(_) => return,
};
assert!(concise.get("analysis").is_none());
assert_eq!(
concise
.get("hypothesis")
.and_then(|value| value.get("slug"))
.and_then(serde_json::Value::as_str),
Some("native-lp-sink")
);
assert_eq!(
full.get("analysis").and_then(serde_json::Value::as_str),
Some("Native LP spends most traced wallclock in node reoptimization.")
);
let porcelain = projection.porcelain_projection(DetailLevel::Concise);
assert!(porcelain.is_ok());
let porcelain = match porcelain {
Ok(value) => value,
Err(_) => return,
};
assert!(porcelain.contains("slug: \"matched-lp-site-traces\""));
}
#[test]
fn timestamp_text_serializes_as_rfc3339_string() {
let timestamp = OffsetDateTime::from_unix_timestamp(0);
assert!(timestamp.is_ok());
let timestamp = match timestamp {
Ok(value) => value,
Err(_) => return,
};
let rendered = super::TimestampText::from(timestamp);
let json = serde_json::to_value(&rendered);
assert!(json.is_ok());
let json = match json {
Ok(value) => value,
Err(_) => return,
};
assert_eq!(json, serde_json::json!("1970-01-01T00:00:00Z"));
}
}