1use crate::ast::{AstNode, NodeId, NodeType, PdfAstGraph};
2use crate::types::PdfValue;
3
4pub struct TransformValidator {
6 strict_mode: bool,
7 preserve_structure: bool,
8}
9
10#[derive(Debug, Clone)]
12pub struct ValidationResult {
13 pub is_valid: bool,
14 pub errors: Vec<ValidationError>,
15 pub warnings: Vec<ValidationWarning>,
16}
17
18#[derive(Debug, Clone)]
20pub struct ValidationError {
21 pub code: String,
22 pub message: String,
23 pub node_id: Option<NodeId>,
24}
25
26#[derive(Debug, Clone)]
28pub struct ValidationWarning {
29 pub code: String,
30 pub message: String,
31 pub node_id: Option<NodeId>,
32}
33
34impl TransformValidator {
35 pub fn new() -> Self {
37 Self {
38 strict_mode: false,
39 preserve_structure: true,
40 }
41 }
42
43 pub fn strict() -> Self {
45 Self {
46 strict_mode: true,
47 preserve_structure: true,
48 }
49 }
50
51 pub fn permissive() -> Self {
53 Self {
54 strict_mode: false,
55 preserve_structure: false,
56 }
57 }
58
59 pub fn with_strict_mode(mut self, strict: bool) -> Self {
61 self.strict_mode = strict;
62 self
63 }
64
65 pub fn with_structure_preservation(mut self, preserve: bool) -> Self {
67 self.preserve_structure = preserve;
68 self
69 }
70
71 pub fn validate_operation(
73 &self,
74 operation: &super::operations::TransformOperation,
75 graph: &PdfAstGraph,
76 ) -> ValidationResult {
77 let mut result = ValidationResult {
78 is_valid: true,
79 errors: Vec::new(),
80 warnings: Vec::new(),
81 };
82
83 match operation {
84 super::operations::TransformOperation::ReplaceNode { target, new_node } => {
85 self.validate_replace(&mut result, *target, new_node, graph);
86 }
87 super::operations::TransformOperation::InsertChild { parent, child, .. } => {
88 self.validate_insert(&mut result, *parent, child, graph);
89 }
90 super::operations::TransformOperation::RemoveNode {
91 target,
92 preserve_children,
93 } => {
94 self.validate_remove(&mut result, *target, *preserve_children, graph);
95 }
96 super::operations::TransformOperation::MoveNode {
97 target, new_parent, ..
98 } => {
99 self.validate_move(&mut result, *target, *new_parent, graph);
100 }
101 super::operations::TransformOperation::UpdateValue { target, new_value } => {
102 self.validate_update(&mut result, *target, new_value, graph);
103 }
104 super::operations::TransformOperation::Batch(operations) => {
105 for op in operations {
106 let op_result = self.validate_operation(op, graph);
107 result.errors.extend(op_result.errors);
108 result.warnings.extend(op_result.warnings);
109 if !op_result.is_valid {
110 result.is_valid = false;
111 }
112 }
113 }
114 }
115
116 result
117 }
118
119 fn validate_replace(
120 &self,
121 result: &mut ValidationResult,
122 target: NodeId,
123 new_node: &AstNode,
124 graph: &PdfAstGraph,
125 ) {
126 if graph.get_node(target).is_none() {
128 result.errors.push(ValidationError {
129 code: "REPLACE_TARGET_NOT_FOUND".to_string(),
130 message: format!("Target node {} not found", target.0),
131 node_id: Some(target),
132 });
133 result.is_valid = false;
134 return;
135 }
136
137 let old_node = graph.get_node(target).unwrap();
138
139 if self.strict_mode && old_node.node_type != new_node.node_type {
141 result.errors.push(ValidationError {
142 code: "REPLACE_TYPE_MISMATCH".to_string(),
143 message: format!(
144 "Cannot replace {:?} with {:?} in strict mode",
145 old_node.node_type, new_node.node_type
146 ),
147 node_id: Some(target),
148 });
149 result.is_valid = false;
150 }
151
152 if graph.get_root() == Some(target) && self.preserve_structure {
154 result.warnings.push(ValidationWarning {
155 code: "REPLACE_ROOT_NODE".to_string(),
156 message: "Replacing root node may affect document structure".to_string(),
157 node_id: Some(target),
158 });
159 }
160 }
161
162 fn validate_insert(
163 &self,
164 result: &mut ValidationResult,
165 parent: NodeId,
166 child: &AstNode,
167 graph: &PdfAstGraph,
168 ) {
169 if graph.get_node(parent).is_none() {
171 result.errors.push(ValidationError {
172 code: "INSERT_PARENT_NOT_FOUND".to_string(),
173 message: format!("Parent node {} not found", parent.0),
174 node_id: Some(parent),
175 });
176 result.is_valid = false;
177 return;
178 }
179
180 let parent_node = graph.get_node(parent).unwrap();
181
182 if self.strict_mode
184 && !self.is_valid_parent_child_relationship(&parent_node.node_type, &child.node_type)
185 {
186 result.errors.push(ValidationError {
187 code: "INSERT_INVALID_RELATIONSHIP".to_string(),
188 message: format!(
189 "Invalid parent-child relationship: {:?} -> {:?}",
190 parent_node.node_type, child.node_type
191 ),
192 node_id: Some(parent),
193 });
194 result.is_valid = false;
195 }
196 }
197
198 fn validate_remove(
199 &self,
200 result: &mut ValidationResult,
201 target: NodeId,
202 preserve_children: bool,
203 graph: &PdfAstGraph,
204 ) {
205 if graph.get_node(target).is_none() {
207 result.errors.push(ValidationError {
208 code: "REMOVE_TARGET_NOT_FOUND".to_string(),
209 message: format!("Target node {} not found", target.0),
210 node_id: Some(target),
211 });
212 result.is_valid = false;
213 return;
214 }
215
216 if graph.get_root() == Some(target) {
218 result.errors.push(ValidationError {
219 code: "REMOVE_ROOT_NODE".to_string(),
220 message: "Cannot remove root node".to_string(),
221 node_id: Some(target),
222 });
223 result.is_valid = false;
224 }
225
226 let children = graph.get_children(target);
228 if !children.is_empty() && !preserve_children {
229 result.warnings.push(ValidationWarning {
230 code: "REMOVE_WITH_CHILDREN".to_string(),
231 message: format!("Removing node with {} children", children.len()),
232 node_id: Some(target),
233 });
234 }
235 }
236
237 fn validate_move(
238 &self,
239 result: &mut ValidationResult,
240 target: NodeId,
241 new_parent: NodeId,
242 graph: &PdfAstGraph,
243 ) {
244 if graph.get_node(target).is_none() {
246 result.errors.push(ValidationError {
247 code: "MOVE_TARGET_NOT_FOUND".to_string(),
248 message: format!("Target node {} not found", target.0),
249 node_id: Some(target),
250 });
251 result.is_valid = false;
252 return;
253 }
254
255 if graph.get_node(new_parent).is_none() {
256 result.errors.push(ValidationError {
257 code: "MOVE_PARENT_NOT_FOUND".to_string(),
258 message: format!("New parent node {} not found", new_parent.0),
259 node_id: Some(new_parent),
260 });
261 result.is_valid = false;
262 return;
263 }
264
265 if self.would_create_cycle(target, new_parent, graph) {
267 result.errors.push(ValidationError {
268 code: "MOVE_CREATES_CYCLE".to_string(),
269 message: "Move operation would create a cycle".to_string(),
270 node_id: Some(target),
271 });
272 result.is_valid = false;
273 }
274
275 if self.strict_mode {
277 let target_node = graph.get_node(target).unwrap();
278 let parent_node = graph.get_node(new_parent).unwrap();
279
280 if !self
281 .is_valid_parent_child_relationship(&parent_node.node_type, &target_node.node_type)
282 {
283 result.errors.push(ValidationError {
284 code: "MOVE_INVALID_RELATIONSHIP".to_string(),
285 message: format!(
286 "Invalid parent-child relationship: {:?} -> {:?}",
287 parent_node.node_type, target_node.node_type
288 ),
289 node_id: Some(target),
290 });
291 result.is_valid = false;
292 }
293 }
294 }
295
296 fn validate_update(
297 &self,
298 result: &mut ValidationResult,
299 target: NodeId,
300 new_value: &PdfValue,
301 graph: &PdfAstGraph,
302 ) {
303 if graph.get_node(target).is_none() {
305 result.errors.push(ValidationError {
306 code: "UPDATE_TARGET_NOT_FOUND".to_string(),
307 message: format!("Target node {} not found", target.0),
308 node_id: Some(target),
309 });
310 result.is_valid = false;
311 return;
312 }
313
314 let node = graph.get_node(target).unwrap();
315
316 if self.strict_mode && !self.is_valid_value_for_type(&node.node_type, new_value) {
318 result.warnings.push(ValidationWarning {
319 code: "UPDATE_TYPE_MISMATCH".to_string(),
320 message: format!(
321 "Value type may not be compatible with node type {:?}",
322 node.node_type
323 ),
324 node_id: Some(target),
325 });
326 }
327 }
328
329 fn is_valid_parent_child_relationship(
330 &self,
331 parent_type: &NodeType,
332 child_type: &NodeType,
333 ) -> bool {
334 matches!(
335 (parent_type, child_type),
336 (NodeType::Catalog, NodeType::Pages)
337 | (NodeType::Catalog, NodeType::Outline)
338 | (NodeType::Catalog, NodeType::Metadata)
339 | (NodeType::Pages, NodeType::Page)
340 | (NodeType::Pages, NodeType::Pages)
341 | (NodeType::Page, NodeType::ContentStream)
342 | (NodeType::Page, NodeType::Annotation)
343 | (NodeType::Page, NodeType::XObject)
344 | (NodeType::Page, NodeType::Font)
345 )
346 }
347
348 fn is_valid_value_for_type(&self, node_type: &NodeType, value: &PdfValue) -> bool {
349 match (node_type, value) {
350 (NodeType::Catalog, PdfValue::Dictionary(_)) => true,
351 (NodeType::Pages, PdfValue::Dictionary(_)) => true,
352 (NodeType::Page, PdfValue::Dictionary(_)) => true,
353 (NodeType::ContentStream, PdfValue::Stream(_)) => true,
354 (NodeType::Font, PdfValue::Dictionary(_)) => true,
355 _ => true, }
357 }
358
359 fn would_create_cycle(&self, target: NodeId, new_parent: NodeId, graph: &PdfAstGraph) -> bool {
360 let mut to_check = vec![target];
362 let mut visited = std::collections::HashSet::new();
363
364 while let Some(current) = to_check.pop() {
365 if visited.contains(¤t) {
366 continue;
367 }
368 visited.insert(current);
369
370 if current == new_parent {
371 return true;
372 }
373
374 let children = graph.get_children(current);
375 to_check.extend(children);
376 }
377
378 false
379 }
380}
381
382impl Default for TransformValidator {
383 fn default() -> Self {
384 Self::new()
385 }
386}