nap_core/validation/
mod.rs1pub mod manifest;
25pub mod merge;
26pub mod sdl;
27
28use crate::merge::conflict::MergeResult;
29use crate::merge::sdl::SdlDocument;
30use serde_json::Value;
31
32#[derive(Debug, Clone)]
34pub struct ValidationError {
35 pub message: String,
37
38 pub path: Option<String>,
40}
41
42impl std::fmt::Display for ValidationError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match &self.path {
45 Some(p) => write!(f, "{}: {}", p, self.message),
46 None => write!(f, "{}", self.message),
47 }
48 }
49}
50
51pub type ValidationResult = Result<(), Vec<ValidationError>>;
53
54pub fn validate_before_persist(schema: &SdlDocument, merged: &MergeResult) -> ValidationResult {
63 match merged {
64 MergeResult::Conflicts(_) => Err(vec![ValidationError {
65 message: "cannot persist — merge produced unresolved conflicts".to_string(),
66 path: None,
67 }]),
68 MergeResult::Merged(value) => merge::validate_merged(schema, value),
69 }
70}
71
72pub fn check_required_paths(schema: &SdlDocument, value: &Value) -> Vec<ValidationError> {
74 let mut errors = Vec::new();
75
76 for required_path in &schema.schema.required {
77 match crate::merge::path::CanonicalPath::parse(required_path) {
79 Ok(path) => {
80 if crate::merge::path::resolve_path(value, &path).is_none() {
81 errors.push(ValidationError {
82 message: format!("required field '{}' is missing", required_path),
83 path: Some(required_path.clone()),
84 });
85 }
86 }
87 Err(_) => {
88 errors.push(ValidationError {
89 message: format!("invalid required path '{}'", required_path),
90 path: Some(required_path.clone()),
91 });
92 }
93 }
94 }
95
96 errors
97}
98
99pub fn check_type_match(
101 type_: &crate::merge::sdl::PropertyType,
102 value: &Value,
103 path: &str,
104) -> Option<ValidationError> {
105 if value.is_null() {
107 return None;
108 }
109
110 let type_matches = match type_ {
111 crate::merge::sdl::PropertyType::String => value.is_string(),
112 crate::merge::sdl::PropertyType::Number => value.is_number(),
113 crate::merge::sdl::PropertyType::Boolean => value.is_boolean(),
114 crate::merge::sdl::PropertyType::Object => value.is_object(),
115 crate::merge::sdl::PropertyType::Array => value.is_array(),
116 };
117
118 if !type_matches {
119 Some(ValidationError {
120 message: format!(
121 "type mismatch: expected {:?}, got {}",
122 type_,
123 type_name(value)
124 ),
125 path: Some(path.to_string()),
126 })
127 } else {
128 None
129 }
130}
131
132fn type_name(value: &Value) -> &'static str {
133 match value {
134 Value::Null => "null",
135 Value::Bool(_) => "boolean",
136 Value::Number(_) => "number",
137 Value::String(_) => "string",
138 Value::Array(_) => "array",
139 Value::Object(_) => "object",
140 }
141}
142
143impl ValidationError {
144 pub fn new(message: impl Into<String>, path: impl Into<String>) -> Self {
145 ValidationError {
146 message: message.into(),
147 path: Some(path.into()),
148 }
149 }
150
151 pub fn global(message: impl Into<String>) -> Self {
152 ValidationError {
153 message: message.into(),
154 path: None,
155 }
156 }
157}
158
159pub fn combine(results: Vec<ValidationResult>) -> ValidationResult {
161 let mut all_errors = Vec::new();
162 for result in results {
163 match result {
164 Ok(()) => {}
165 Err(errors) => all_errors.extend(errors),
166 }
167 }
168 if all_errors.is_empty() {
169 Ok(())
170 } else {
171 Err(all_errors)
172 }
173}