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}