use serde::{Deserialize, Serialize};
use crate::domain::entity::projection::{PluginDispatch, Projection};
use crate::domain::entity::TargetForm;
use crate::domain::error::WireResult;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NamedProjection {
pub name: String,
pub spec_ref: String,
pub template: String,
pub target_form: TargetForm,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub template_engine: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub projection_kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub projection_config: Option<serde_json::Value>,
}
pub fn dto_to_projection(dto: NamedProjection) -> WireResult<Projection> {
let plugin = PluginDispatch::from_optional_parts(
dto.template_engine,
dto.projection_kind,
dto.projection_config,
)?;
Projection::from_parts(
dto.name,
dto.spec_ref,
dto.template,
dto.target_form,
plugin,
)
}
pub fn projection_to_dto(p: &Projection) -> NamedProjection {
let (engine, kind, config) = p.plugin().to_optional_parts();
NamedProjection {
name: p.name().as_str().to_owned(),
spec_ref: p.spec_ref().as_str().to_owned(),
template: p.template().as_str().to_owned(),
target_form: p.target_form(),
template_engine: engine.map(str::to_owned),
projection_kind: kind.map(str::to_owned),
projection_config: config.cloned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::error::{DomainError, WireError};
fn sample_projection() -> Projection {
Projection::from_parts(
"_persona_toc",
"active_personas",
"Active personas ({{count}}): {{names}}",
TargetForm::Prompt,
PluginDispatch::Default,
)
.unwrap()
}
#[test]
fn projection_to_dto_to_projection_roundtrip() {
let p = sample_projection();
let dto = projection_to_dto(&p);
let back = dto_to_projection(dto).unwrap();
assert_eq!(back, p);
}
#[test]
fn dto_to_projection_rejects_illegal_plugin_state() {
let dto = NamedProjection {
name: "p".into(),
spec_ref: "s".into(),
template: "t".into(),
target_form: TargetForm::Prompt,
template_engine: Some("handlebars".into()),
projection_kind: None,
projection_config: None,
};
let err = dto_to_projection(dto).expect_err("should reject");
assert!(matches!(
err,
WireError::Domain(DomainError::InvalidProjection(_))
));
}
#[test]
fn dto_to_projection_to_dto_roundtrip_default_plugin() {
let original = NamedProjection {
name: "_persona_toc".into(),
spec_ref: "active_personas".into(),
template: "Active personas ({{count}}): {{names}}".into(),
target_form: TargetForm::Prompt,
template_engine: None,
projection_kind: None,
projection_config: None,
};
let entity = dto_to_projection(original.clone()).unwrap();
let back = projection_to_dto(&entity);
assert_eq!(back, original);
}
#[test]
fn roundtrip_preserves_custom_plugin_with_config() {
let cfg = serde_json::json!({"model": "claude-haiku", "temperature": 0.7});
let p = Projection::from_parts(
"render_llm",
"active_personas",
"{{prompt}}",
TargetForm::Markdown,
PluginDispatch::custom("handlebars", "llm", Some(cfg.clone())).unwrap(),
)
.unwrap();
let dto = projection_to_dto(&p);
assert_eq!(dto.template_engine.as_deref(), Some("handlebars"));
assert_eq!(dto.projection_kind.as_deref(), Some("llm"));
assert_eq!(dto.projection_config, Some(cfg));
let back = dto_to_projection(dto).unwrap();
assert_eq!(back, p);
}
#[test]
fn dto_to_projection_propagates_vo_errors() {
for (name, spec, tmpl) in [("", "s", "t"), ("n", "", "t"), ("n", "s", "")] {
let dto = NamedProjection {
name: name.into(),
spec_ref: spec.into(),
template: tmpl.into(),
target_form: TargetForm::Prompt,
template_engine: None,
projection_kind: None,
projection_config: None,
};
let err = dto_to_projection(dto).expect_err("VO empty must reject");
assert!(matches!(
err,
WireError::Domain(DomainError::InvalidProjection(_))
));
}
}
#[test]
fn projection_to_dto_preserves_all_target_form_variants() {
for tf in [
TargetForm::Prompt,
TargetForm::Markdown,
TargetForm::Json,
TargetForm::Ascii,
] {
let p = Projection::from_parts("n", "s", "t", tf, PluginDispatch::Default).unwrap();
let dto = projection_to_dto(&p);
assert_eq!(dto.target_form, tf);
let back = dto_to_projection(dto).unwrap();
assert_eq!(back.target_form(), tf);
}
}
}