Skip to main content

leviath_core/region/
schema.rs

1//! What a region will accept: content-format schemas and their validators.
2//!
3//! Split out of `region.rs` because it is a separate question from how a region
4//! holds and evicts entries - this decides whether a write is well-formed at
5//! all, before any of that applies.
6
7use serde::{Deserialize, Serialize};
8
9/// Enforces that content matches expected format (e.g., mermaid diagrams only,
10/// JSON only, code only). Schemas can include multiple validators that are
11/// checked when content is added to a region.
12#[derive(Debug, Serialize, Deserialize)]
13pub struct RegionSchema {
14    /// Expected content format
15    pub format: ContentFormat,
16
17    /// Optional custom validation script (Rhai)
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub custom_script: Option<String>,
20}
21
22impl Clone for RegionSchema {
23    fn clone(&self) -> Self {
24        Self {
25            format: self.format.clone(),
26            custom_script: self.custom_script.clone(),
27        }
28    }
29}
30
31impl RegionSchema {
32    /// Create a new schema with the specified format.
33    pub fn new(format: ContentFormat) -> Self {
34        Self {
35            format,
36            custom_script: None,
37        }
38    }
39
40    /// Add a custom validation script.
41    pub fn with_custom_script(mut self, script: String) -> Self {
42        self.custom_script = Some(script);
43        self
44    }
45
46    /// Validate content against this schema.
47    pub fn validate(&self, content: &str) -> crate::error::Result<()> {
48        match &self.format {
49            ContentFormat::Json => {
50                serde_json::from_str::<serde_json::Value>(content).map_err(|e| {
51                    crate::error::Error::ValidationFailed(format!("Invalid JSON: {}", e))
52                })?;
53            }
54            ContentFormat::Mermaid => {
55                // Basic mermaid syntax validation
56                if !content.contains("graph")
57                    && !content.contains("sequenceDiagram")
58                    && !content.contains("classDiagram")
59                    && !content.contains("stateDiagram")
60                    && !content.contains("erDiagram")
61                    && !content.contains("journey")
62                    && !content.contains("gantt")
63                    && !content.contains("pie")
64                    && !content.contains("flowchart")
65                {
66                    return Err(crate::error::Error::ValidationFailed(
67                        "Mermaid diagrams must contain a valid diagram type (graph, sequenceDiagram, etc.)".to_string()
68                    ));
69                }
70            }
71            ContentFormat::Code { .. } => {
72                // Basic code validation - just check it's not empty
73                if content.trim().is_empty() {
74                    return Err(crate::error::Error::ValidationFailed(
75                        "Code cannot be empty".to_string(),
76                    ));
77                }
78            }
79            ContentFormat::Markdown => {
80                // Markdown is very permissive, just check it's not empty
81                if content.trim().is_empty() {
82                    return Err(crate::error::Error::ValidationFailed(
83                        "Markdown content cannot be empty".to_string(),
84                    ));
85                }
86            }
87            ContentFormat::Text | ContentFormat::Custom { .. } => {
88                // Text has no restrictions, Custom is handled by scripting layer
89            }
90        }
91
92        Ok(())
93    }
94}
95
96/// Content format types that can be enforced via schemas.
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
98pub enum ContentFormat {
99    /// Plain text, no formatting requirements
100    Text,
101
102    /// Valid JSON
103    Json,
104
105    /// Mermaid diagram syntax
106    Mermaid,
107
108    /// Source code in a specific language
109    Code {
110        /// The language label, used for the fence and nothing else - no
111        /// per-language parsing happens.
112        language: String,
113    },
114
115    /// Markdown formatted text
116    Markdown,
117
118    /// Custom format with user-defined validation
119    Custom {
120        /// The author's own name for the format, matched against the validator
121        /// registered for it.
122        format_name: String,
123    },
124}
125
126/// Trait for content validators.
127///
128/// Validators check whether content meets specific requirements before
129/// it's added to a region. This enables enforcing architectural constraints
130/// like "only mermaid diagrams in the architecture region".
131pub trait Validator: Send + Sync {
132    /// Validate content and return an error message if invalid.
133    fn validate(&self, content: &str) -> std::result::Result<(), crate::error::ValidationError>;
134
135    /// Get a description of what this validator checks.
136    fn description(&self) -> &str;
137}