use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use rmcp::model::{GetPromptResult, JsonObject, Prompt};
use crate::context::AdapterContext;
use crate::error::AdapterError;
pub mod daily_options_summary;
pub mod funding_snapshot;
pub mod position_review;
pub type PromptFuture<'a> =
Pin<Box<dyn Future<Output = Result<GetPromptResult, AdapterError>> + Send + 'a>>;
pub type PromptHandlerFn =
Arc<dyn for<'a> Fn(&'a AdapterContext, JsonObject) -> PromptFuture<'a> + Send + Sync + 'static>;
#[derive(Clone)]
pub struct PromptEntry {
pub(crate) descriptor: Prompt,
pub(crate) handler: PromptHandlerFn,
}
impl PromptEntry {
#[must_use]
pub fn descriptor(&self) -> &Prompt {
&self.descriptor
}
}
impl std::fmt::Debug for PromptEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PromptEntry")
.field("descriptor", &self.descriptor)
.field("handler", &"<dyn Fn>")
.finish()
}
}
#[derive(Debug, Default, Clone)]
pub struct PromptRegistry {
entries: HashMap<String, PromptEntry>,
}
impl PromptRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn build(_ctx: &AdapterContext) -> Self {
let mut registry = Self::new();
daily_options_summary::register(&mut registry);
funding_snapshot::register(&mut registry);
position_review::register(&mut registry);
registry
}
#[allow(dead_code)]
pub(crate) fn insert(&mut self, entry: PromptEntry) -> Option<PromptEntry> {
let name = entry.descriptor.name.clone();
self.entries.insert(name, entry)
}
#[must_use]
pub fn list(&self) -> Vec<Prompt> {
let mut prompts: Vec<Prompt> = self
.entries
.values()
.map(|e| e.descriptor.clone())
.collect();
prompts.sort_by(|a, b| a.name.cmp(&b.name));
prompts
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.entries.contains_key(name)
}
#[must_use]
pub fn get_entry(&self, name: &str) -> Option<&PromptEntry> {
self.entries.get(name)
}
pub async fn get(
&self,
ctx: &AdapterContext,
name: &str,
args: JsonObject,
) -> Result<GetPromptResult, AdapterError> {
let Some(entry) = self.entries.get(name) else {
return Err(AdapterError::Validation {
field: "name".to_string(),
message: format!("prompt `{name}` is not registered"),
});
};
(entry.handler)(ctx, args).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, LogFormat, OrderTransport, Transport};
use std::net::SocketAddr;
fn ctx() -> Arc<AdapterContext> {
let cfg = Config {
endpoint: "https://test.deribit.com".to_string(),
client_id: None,
client_secret: None,
allow_trading: false,
max_order_usd: None,
transport: Transport::Stdio,
http_listen: SocketAddr::from(([127, 0, 0, 1], 8723)),
http_bearer_token: None,
log_format: LogFormat::Text,
order_transport: OrderTransport::Http,
};
Arc::new(AdapterContext::new(Arc::new(cfg)).expect("ctx"))
}
#[test]
fn build_populates_v05_prompts() {
let registry = PromptRegistry::build(&ctx());
assert!(registry.contains("daily_options_summary"));
assert!(registry.contains("funding_snapshot"));
assert!(registry.contains("position_review"));
}
#[tokio::test]
async fn unknown_prompt_returns_validation() {
let ctx = ctx();
let registry = PromptRegistry::new();
let err = registry
.get(&ctx, "no_such_prompt", JsonObject::new())
.await
.unwrap_err();
match err {
AdapterError::Validation { field, .. } => assert_eq!(field, "name"),
other => panic!("unexpected: {other:?}"),
}
}
}