Skip to main content

nova_schema/
template.rs

1//! Template schema definitions.
2//!
3//! Templates define both the visual styling and the required metadata
4//! structure for a document type.
5
6use crate::error::{Error, Result};
7#[cfg(feature = "validation")]
8use crate::schema::Schema;
9use serde::{Deserialize, Serialize};
10use std::path::Path;
11
12/// A template definition combining visual styling and metadata schema.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TemplateSchema {
15    /// Template identifier.
16    pub name: String,
17
18    /// Human-readable display name.
19    pub display_name: String,
20
21    /// Template description.
22    pub description: String,
23
24    /// Version string.
25    pub version: String,
26
27    /// Template author(s).
28    pub authors: Vec<String>,
29
30    /// Target use case (article, book, report, etc.).
31    pub category: TemplateCategory,
32
33    /// Metadata schema definition.
34    #[serde(default)]
35    pub metadata_schema: serde_json::Value,
36
37    /// Required files (main.typ, etc.).
38    #[serde(default)]
39    pub required_files: Vec<String>,
40
41    /// Optional configuration options.
42    #[serde(default)]
43    pub options: Vec<TemplateOption>,
44}
45
46impl TemplateSchema {
47    /// Load a template schema from a directory.
48    ///
49    /// Expects a `template.toml` or `template.json` file.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the template cannot be loaded.
54    pub fn from_directory(path: impl AsRef<Path>) -> Result<Self> {
55        let path = path.as_ref();
56
57        // Try TOML first
58        let toml_path = path.join("template.toml");
59        if toml_path.exists() {
60            let content = std::fs::read_to_string(&toml_path)?;
61            let template: Self = toml::from_str(&content)?;
62            return Ok(template);
63        }
64
65        // Try JSON
66        let json_path = path.join("template.json");
67        if json_path.exists() {
68            let content = std::fs::read_to_string(&json_path)?;
69            let template: Self = serde_json::from_str(&content)?;
70            return Ok(template);
71        }
72
73        Err(Error::SchemaNotFound {
74            name: path.display().to_string(),
75        })
76    }
77
78    /// Get a validator for this template's metadata schema.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if the schema is invalid.
83    #[cfg(feature = "validation")]
84    pub fn metadata_validator(&self) -> Result<Schema> {
85        Schema::new(&self.name, self.metadata_schema.clone())
86    }
87
88    /// Check if a file is required by this template.
89    #[must_use]
90    pub fn requires_file(&self, filename: &str) -> bool {
91        self.required_files.iter().any(|f| f == filename)
92    }
93}
94
95/// Template categories.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "lowercase")]
98pub enum TemplateCategory {
99    /// Scientific article.
100    Article,
101    /// Book or book chapter.
102    Book,
103    /// Business report.
104    Report,
105    /// Curriculum vitae / Resume.
106    Cv,
107    /// Presentation slides.
108    Presentation,
109    /// Technical documentation.
110    Documentation,
111    /// Letter or correspondence.
112    Letter,
113    /// Other/custom.
114    Other,
115}
116
117impl std::fmt::Display for TemplateCategory {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        match self {
120            Self::Article => write!(f, "article"),
121            Self::Book => write!(f, "book"),
122            Self::Report => write!(f, "report"),
123            Self::Cv => write!(f, "cv"),
124            Self::Presentation => write!(f, "presentation"),
125            Self::Documentation => write!(f, "documentation"),
126            Self::Letter => write!(f, "letter"),
127            Self::Other => write!(f, "other"),
128        }
129    }
130}
131
132/// A configurable option for a template.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct TemplateOption {
135    /// Option name/key.
136    pub name: String,
137
138    /// Human-readable label.
139    pub label: String,
140
141    /// Option description.
142    pub description: String,
143
144    /// Type of option.
145    pub option_type: OptionType,
146
147    /// Default value.
148    pub default: serde_json::Value,
149
150    /// Whether this option is required.
151    #[serde(default)]
152    pub required: bool,
153}
154
155/// Types of template options.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(rename_all = "lowercase")]
158pub enum OptionType {
159    /// Text string.
160    String,
161    /// Boolean flag.
162    Boolean,
163    /// Integer number.
164    Integer,
165    /// Floating point number.
166    Number,
167    /// Selection from a list.
168    Enum(Vec<String>),
169    /// Color value.
170    Color,
171    /// File path.
172    File,
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    fn sample_template() -> TemplateSchema {
180        TemplateSchema {
181            name: "ieee-article".to_string(),
182            display_name: "IEEE Article".to_string(),
183            description: "Template for IEEE journal articles".to_string(),
184            version: "1.0.0".to_string(),
185            authors: vec!["NovaType Team".to_string()],
186            category: TemplateCategory::Article,
187            metadata_schema: serde_json::json!({
188                "type": "object",
189                "properties": {
190                    "title": { "type": "string" }
191                },
192                "required": ["title"]
193            }),
194            required_files: vec!["main.typ".to_string()],
195            options: vec![],
196        }
197    }
198
199    #[test]
200    fn template_serialization() {
201        let template = sample_template();
202        let json = serde_json::to_string_pretty(&template).unwrap();
203        assert!(json.contains("ieee-article"));
204        assert!(json.contains("IEEE Article"));
205    }
206
207    #[test]
208    fn template_deserialization() {
209        let json = r#"{
210            "name": "test",
211            "display_name": "Test Template",
212            "description": "A test template",
213            "version": "0.1.0",
214            "authors": ["Test Author"],
215            "category": "article",
216            "metadata_schema": {},
217            "required_files": ["main.typ"],
218            "options": []
219        }"#;
220
221        let template: TemplateSchema = serde_json::from_str(json).unwrap();
222        assert_eq!(template.name, "test");
223        assert_eq!(template.category, TemplateCategory::Article);
224    }
225
226    #[test]
227    fn template_requires_file() {
228        let template = sample_template();
229        assert!(template.requires_file("main.typ"));
230        assert!(!template.requires_file("other.typ"));
231    }
232
233    #[test]
234    fn template_category_display() {
235        assert_eq!(TemplateCategory::Article.to_string(), "article");
236        assert_eq!(TemplateCategory::Book.to_string(), "book");
237        assert_eq!(TemplateCategory::Cv.to_string(), "cv");
238    }
239
240    #[test]
241    #[cfg(feature = "validation")]
242    fn template_metadata_validator() {
243        let template = sample_template();
244        let validator = template.metadata_validator().unwrap();
245
246        let valid = serde_json::json!({ "title": "Test" });
247        let invalid = serde_json::json!({});
248
249        assert!(validator.is_valid(&valid));
250        assert!(!validator.is_valid(&invalid));
251    }
252}