#![allow(clippy::doc_markdown)]
use super::adapter::{
ChatGptAdapter, McpAppsAdapter, McpUiAdapter, TransformedResource, UIAdapter,
};
use crate::types::mcp_apps::{ChatGptToolMeta, HostType, WidgetCSP, WidgetMeta};
use std::collections::HashMap;
pub struct MultiPlatformResource {
uri: String,
name: String,
html: String,
adapters: Vec<Box<dyn UIAdapter>>,
cache: HashMap<HostType, TransformedResource>,
}
impl std::fmt::Debug for MultiPlatformResource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MultiPlatformResource")
.field("uri", &self.uri)
.field("name", &self.name)
.field("html", &self.html)
.field("adapters", &format!("[{} adapters]", self.adapters.len()))
.field("cache", &self.cache)
.finish()
}
}
impl MultiPlatformResource {
#[must_use]
pub fn new(uri: impl Into<String>, name: impl Into<String>, html: impl Into<String>) -> Self {
Self {
uri: uri.into(),
name: name.into(),
html: html.into(),
adapters: Vec::new(),
cache: HashMap::new(),
}
}
#[must_use]
pub fn with_adapter<A: UIAdapter + 'static>(mut self, adapter: A) -> Self {
self.adapters.push(Box::new(adapter));
self
}
#[must_use]
pub fn with_all_adapters(self) -> Self {
self.with_adapter(ChatGptAdapter::new())
.with_adapter(McpAppsAdapter::new())
.with_adapter(McpUiAdapter::new())
}
pub fn for_host(&mut self, host: HostType) -> Option<&TransformedResource> {
if self.cache.contains_key(&host) {
return self.cache.get(&host);
}
let adapter = self.adapters.iter().find(|a| a.host_type() == host)?;
let transformed = adapter.transform(&self.uri, &self.name, &self.html);
self.cache.insert(host, transformed);
self.cache.get(&host)
}
pub fn all_transforms(&mut self) -> Vec<&TransformedResource> {
for adapter in &self.adapters {
let host = adapter.host_type();
if !self.cache.contains_key(&host) {
let transformed = adapter.transform(&self.uri, &self.name, &self.html);
self.cache.insert(host, transformed);
}
}
self.cache.values().collect()
}
#[must_use]
pub fn html(&self) -> &str {
&self.html
}
#[must_use]
pub fn uri(&self) -> &str {
&self.uri
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Default)]
pub struct UIResourceBuilder {
uri: String,
name: String,
html: Option<String>,
description: Option<String>,
chatgpt_meta: ChatGptToolMeta,
widget_meta: WidgetMeta,
csp: WidgetCSP,
}
impl UIResourceBuilder {
#[must_use]
pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
Self {
uri: uri.into(),
name: name.into(),
..Default::default()
}
}
#[must_use]
pub fn html(mut self, content: impl Into<String>) -> Self {
self.html = Some(content.into());
self
}
#[must_use]
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
#[must_use]
pub fn chatgpt_output_template(mut self, uri: impl Into<String>) -> Self {
self.chatgpt_meta = self.chatgpt_meta.output_template(uri);
self
}
#[must_use]
pub fn chatgpt_invoking(mut self, message: impl Into<String>) -> Self {
self.chatgpt_meta = self.chatgpt_meta.invoking(message);
self
}
#[must_use]
pub fn chatgpt_invoked(mut self, message: impl Into<String>) -> Self {
self.chatgpt_meta = self.chatgpt_meta.invoked(message);
self
}
#[must_use]
pub fn chatgpt_widget_accessible(mut self, accessible: bool) -> Self {
self.chatgpt_meta = self.chatgpt_meta.widget_accessible(accessible);
self
}
#[must_use]
pub fn widget_prefers_border(mut self, border: bool) -> Self {
self.widget_meta = self.widget_meta.prefers_border(border);
self
}
#[must_use]
pub fn csp_connect(mut self, domain: impl Into<String>) -> Self {
self.csp = self.csp.connect(domain);
self
}
#[must_use]
pub fn csp_resource(mut self, domain: impl Into<String>) -> Self {
self.csp = self.csp.resources(domain);
self
}
#[must_use]
pub fn csp_frame(mut self, domain: impl Into<String>) -> Self {
self.csp = self.csp.frame(domain);
self
}
#[must_use]
pub fn build(self) -> MultiPlatformResource {
let html = self.html.unwrap_or_default();
MultiPlatformResource::new(&self.uri, &self.name, html)
}
#[must_use]
pub fn build_chatgpt(self) -> MultiPlatformResource {
let widget_meta = self.widget_meta.clone();
let html = self.html.clone().unwrap_or_default();
MultiPlatformResource::new(&self.uri, &self.name, html)
.with_adapter(ChatGptAdapter::new().with_widget_meta(widget_meta))
}
#[must_use]
pub fn build_mcp_apps(self) -> MultiPlatformResource {
let csp = self.csp.clone();
let html = self.html.clone().unwrap_or_default();
MultiPlatformResource::new(&self.uri, &self.name, html)
.with_adapter(McpAppsAdapter::new().with_csp(csp))
}
#[must_use]
pub fn build_all(self) -> MultiPlatformResource {
let widget_meta = self.widget_meta.clone();
let csp = self.csp.clone();
let html = self.html.clone().unwrap_or_default();
MultiPlatformResource::new(&self.uri, &self.name, html)
.with_adapter(ChatGptAdapter::new().with_widget_meta(widget_meta))
.with_adapter(McpAppsAdapter::new().with_csp(csp))
.with_adapter(McpUiAdapter::new())
}
#[must_use]
pub fn chatgpt_tool_meta(&self) -> &ChatGptToolMeta {
&self.chatgpt_meta
}
#[must_use]
pub fn widget_meta(&self) -> &WidgetMeta {
&self.widget_meta
}
#[must_use]
pub fn csp(&self) -> &WidgetCSP {
&self.csp
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::mcp_apps::ExtendedUIMimeType;
#[test]
fn test_ui_resource_builder_basic() {
let multi = UIResourceBuilder::new("ui://test/widget.html", "Test Widget")
.html("<html><body>Test</body></html>")
.description("A test widget")
.build();
assert_eq!(multi.uri(), "ui://test/widget.html");
assert_eq!(multi.name(), "Test Widget");
}
#[test]
fn test_ui_resource_builder_chatgpt_config() {
let builder = UIResourceBuilder::new("ui://chess/board.html", "Chess Board")
.html("<html><body>Board</body></html>")
.chatgpt_invoking("Loading...")
.chatgpt_invoked("Ready!")
.chatgpt_widget_accessible(true);
let meta = builder.chatgpt_tool_meta();
assert!(serde_json::to_value(meta).is_ok());
}
#[test]
fn test_ui_resource_builder_csp() {
let builder = UIResourceBuilder::new("ui://test/widget.html", "Test")
.html("<html></html>")
.csp_connect("https://api.example.com")
.csp_resource("https://cdn.example.com");
let csp = builder.csp();
assert!(!csp.connect_domains.is_empty());
assert!(!csp.resource_domains.is_empty());
}
#[test]
fn test_multi_platform_resource() {
let mut multi = MultiPlatformResource::new(
"ui://test/widget.html",
"Test Widget",
"<html><body>Test</body></html>",
)
.with_all_adapters();
let chatgpt = multi.for_host(HostType::ChatGpt);
assert!(chatgpt.is_some());
assert_eq!(
chatgpt.unwrap().mime_type,
ExtendedUIMimeType::HtmlSkybridge
);
let generic = multi.for_host(HostType::Generic);
assert!(generic.is_some());
assert_eq!(generic.unwrap().mime_type, ExtendedUIMimeType::HtmlMcpApp);
}
#[test]
fn test_build_chatgpt() {
let mut multi = UIResourceBuilder::new("ui://test/widget.html", "Test")
.html("<html></html>")
.chatgpt_invoking("Loading...")
.build_chatgpt();
let transformed = multi.for_host(HostType::ChatGpt);
assert!(transformed.is_some());
}
#[test]
fn test_build_all() {
let mut multi = UIResourceBuilder::new("ui://test/widget.html", "Test")
.html("<html></html>")
.build_all();
let transforms = multi.all_transforms();
assert!(!transforms.is_empty());
}
}