use crate::{
ElicitCommunicator, ElicitError, ElicitErrorKind, ElicitIntrospect, ElicitResult, Elicitation,
ElicitationPattern, Generator, PatternDetails, Prompt, Select, TypeMetadata,
datetime_common::{DateTimeComponents, DateTimeInputMethod},
mcp,
};
use jiff::{Span, Timestamp, Zoned, civil::DateTime as CivilDateTime, tz::TimeZone};
crate::default_style!(Timestamp => TimestampStyle);
crate::default_style!(Zoned => ZonedStyle);
crate::default_style!(CivilDateTime => CivilDateTimeStyle);
crate::default_style!(TimestampGenerationMode => TimestampGenerationModeStyle);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TimestampGenerationMode {
Now,
UnixEpoch,
Offset {
seconds: i64,
},
}
impl Select for TimestampGenerationMode {
fn options() -> Vec<Self> {
vec![
TimestampGenerationMode::Now,
TimestampGenerationMode::UnixEpoch,
TimestampGenerationMode::Offset { seconds: 0 },
]
}
fn labels() -> Vec<String> {
vec![
"Now (Current UTC)".to_string(),
"Unix Epoch (1970-01-01)".to_string(),
"Offset (Custom)".to_string(),
]
}
fn from_label(label: &str) -> Option<Self> {
match label {
"Now (Current UTC)" => Some(TimestampGenerationMode::Now),
"Unix Epoch (1970-01-01)" => Some(TimestampGenerationMode::UnixEpoch),
"Offset (Custom)" => Some(TimestampGenerationMode::Offset { seconds: 0 }),
_ => None,
}
}
}
impl Prompt for TimestampGenerationMode {
fn prompt() -> Option<&'static str> {
Some("How should timestamps be generated?")
}
}
impl Elicitation for TimestampGenerationMode {
type Style = TimestampGenerationModeStyle;
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
let params = mcp::select_params(
Self::prompt().unwrap_or("Select an option:"),
&Self::labels(),
);
let result = communicator
.call_tool(rmcp::model::CallToolRequestParams {
meta: None,
name: mcp::tool_names::elicit_select().into(),
arguments: Some(params),
task: None,
})
.await?;
let value = mcp::extract_value(result)?;
let label = mcp::parse_string(value)?;
let selected = Self::from_label(&label).ok_or_else(|| {
ElicitError::new(ElicitErrorKind::ParseError(
"Invalid Timestamp generation mode".to_string(),
))
})?;
match selected {
TimestampGenerationMode::Now => Ok(TimestampGenerationMode::Now),
TimestampGenerationMode::UnixEpoch => Ok(TimestampGenerationMode::UnixEpoch),
TimestampGenerationMode::Offset { .. } => {
let seconds = i64::elicit(communicator).await?;
Ok(TimestampGenerationMode::Offset { seconds })
}
}
}
}
impl ElicitIntrospect for TimestampGenerationMode {
fn pattern() -> ElicitationPattern {
ElicitationPattern::Select
}
fn metadata() -> TypeMetadata {
TypeMetadata {
type_name: "TimestampGenerationMode",
description: Self::prompt(),
details: PatternDetails::Select {
options: Self::labels(),
},
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct TimestampGenerator {
mode: TimestampGenerationMode,
reference: Timestamp,
}
impl TimestampGenerator {
pub fn new(mode: TimestampGenerationMode) -> Self {
Self {
mode,
reference: Timestamp::now(),
}
}
pub fn with_reference(mode: TimestampGenerationMode, reference: Timestamp) -> Self {
Self { mode, reference }
}
pub fn mode(&self) -> TimestampGenerationMode {
self.mode
}
pub fn reference(&self) -> Timestamp {
self.reference
}
}
impl Generator for TimestampGenerator {
type Target = Timestamp;
fn generate(&self) -> Self::Target {
match self.mode {
TimestampGenerationMode::Now => Timestamp::now(),
TimestampGenerationMode::UnixEpoch => Timestamp::UNIX_EPOCH,
TimestampGenerationMode::Offset { seconds } => {
let span = Span::new().seconds(seconds);
self.reference.checked_add(span).unwrap_or(self.reference)
}
}
}
}
impl Prompt for Timestamp {
fn prompt() -> Option<&'static str> {
Some("Enter UTC timestamp:")
}
}
impl Elicitation for Timestamp {
type Style = TimestampStyle;
#[tracing::instrument(skip(communicator))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
tracing::debug!("Eliciting Timestamp");
let method = DateTimeInputMethod::elicit(communicator).await?;
tracing::debug!(?method, "Input method selected");
match method {
DateTimeInputMethod::Iso8601String => {
let prompt = "Enter ISO 8601 timestamp (e.g., \"2024-07-11T15:30:00Z\"):";
let params = mcp::text_params(prompt);
let result = communicator
.call_tool(rmcp::model::CallToolRequestParams {
meta: None,
name: mcp::tool_names::elicit_text().into(),
arguments: Some(params),
task: None,
})
.await?;
let value = mcp::extract_value(result)?;
let iso_string = mcp::parse_string(value)?;
iso_string.parse::<Timestamp>().map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid ISO 8601 timestamp: {}",
e
)))
})
}
DateTimeInputMethod::ManualComponents => {
let components = DateTimeComponents::elicit(communicator).await?;
let dt = CivilDateTime::new(
components.year as i16,
components.month as i8,
components.day as i8,
components.hour as i8,
components.minute as i8,
components.second as i8,
0,
)
.map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid datetime components: {}",
e
)))
})?;
dt.to_zoned(TimeZone::UTC)
.map(|z| z.timestamp())
.map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Failed to create timestamp: {}",
e
)))
})
}
}
}
#[cfg(kani)]
fn kani_proof() {
use crate::datetime_common::{DateTimeComponents, DateTimeInputMethod};
DateTimeInputMethod::kani_proof();
DateTimeComponents::kani_proof();
assert!(
true,
"jiff::Timestamp verified via component composition + trusted jiff crate"
);
}
}
impl ElicitIntrospect for Timestamp {
fn pattern() -> ElicitationPattern {
ElicitationPattern::Primitive
}
fn metadata() -> TypeMetadata {
TypeMetadata {
type_name: "jiff::Timestamp",
description: Self::prompt(),
details: PatternDetails::Primitive,
}
}
}
impl Prompt for Zoned {
fn prompt() -> Option<&'static str> {
Some("Enter datetime with timezone:")
}
}
impl Elicitation for Zoned {
type Style = ZonedStyle;
#[tracing::instrument(skip(communicator))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
tracing::debug!("Eliciting Zoned");
let method = DateTimeInputMethod::elicit(communicator).await?;
tracing::debug!(?method, "Input method selected");
match method {
DateTimeInputMethod::Iso8601String => {
let prompt = "Enter ISO 8601 datetime with timezone (e.g., \"2024-07-11T15:30:00-05[America/New_York]\"):";
let params = mcp::text_params(prompt);
let result = communicator
.call_tool(rmcp::model::CallToolRequestParams {
meta: None,
name: mcp::tool_names::elicit_text().into(),
arguments: Some(params),
task: None,
})
.await?;
let value = mcp::extract_value(result)?;
let iso_string = mcp::parse_string(value)?;
iso_string.parse::<Zoned>().map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid ISO 8601 zoned datetime: {}",
e
)))
})
}
DateTimeInputMethod::ManualComponents => {
let components = DateTimeComponents::elicit(communicator).await?;
let tz_prompt = "Enter IANA timezone (e.g., \"America/New_York\" or \"UTC\"):";
let tz_params = mcp::text_params(tz_prompt);
let tz_result = communicator
.call_tool(rmcp::model::CallToolRequestParams {
meta: None,
name: mcp::tool_names::elicit_text().into(),
arguments: Some(tz_params),
task: None,
})
.await?;
let tz_value = mcp::extract_value(tz_result)?;
let tz_string = mcp::parse_string(tz_value)?;
let tz = TimeZone::get(&tz_string).map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid timezone: {}",
e
)))
})?;
let dt = CivilDateTime::new(
components.year as i16,
components.month as i8,
components.day as i8,
components.hour as i8,
components.minute as i8,
components.second as i8,
0,
)
.map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid datetime components: {}",
e
)))
})?;
dt.to_zoned(tz).map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Failed to create zoned datetime: {}",
e
)))
})
}
}
}
}
impl ElicitIntrospect for Zoned {
fn pattern() -> ElicitationPattern {
ElicitationPattern::Primitive
}
fn metadata() -> TypeMetadata {
TypeMetadata {
type_name: "jiff::Zoned",
description: Self::prompt(),
details: PatternDetails::Primitive,
}
}
}
impl Prompt for CivilDateTime {
fn prompt() -> Option<&'static str> {
Some("Enter civil datetime (no timezone):")
}
}
impl Elicitation for CivilDateTime {
type Style = CivilDateTimeStyle;
#[tracing::instrument(skip(communicator))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
tracing::debug!("Eliciting CivilDateTime");
let method = DateTimeInputMethod::elicit(communicator).await?;
tracing::debug!(?method, "Input method selected");
match method {
DateTimeInputMethod::Iso8601String => {
let prompt = "Enter datetime (e.g., \"2024-07-11T15:30:00\"):";
let params = mcp::text_params(prompt);
let result = communicator
.call_tool(rmcp::model::CallToolRequestParams {
meta: None,
name: mcp::tool_names::elicit_text().into(),
arguments: Some(params),
task: None,
})
.await?;
let value = mcp::extract_value(result)?;
let iso_string = mcp::parse_string(value)?;
iso_string.parse::<CivilDateTime>().map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid civil datetime: {}",
e
)))
})
}
DateTimeInputMethod::ManualComponents => {
let components = DateTimeComponents::elicit(communicator).await?;
CivilDateTime::new(
components.year as i16,
components.month as i8,
components.day as i8,
components.hour as i8,
components.minute as i8,
components.second as i8,
0,
)
.map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid datetime components: {}",
e
)))
})
}
}
}
}
impl ElicitIntrospect for CivilDateTime {
fn pattern() -> ElicitationPattern {
ElicitationPattern::Primitive
}
fn metadata() -> TypeMetadata {
TypeMetadata {
type_name: "jiff::CivilDateTime",
description: Self::prompt(),
details: PatternDetails::Primitive,
}
}
}