Skip to main content

pdf_ast/transform/
validator.rs

1use crate::ast::{AstNode, NodeId, NodeType, PdfAstGraph};
2use crate::types::PdfValue;
3
4/// Validates transformations before they are applied
5pub struct TransformValidator {
6    strict_mode: bool,
7    preserve_structure: bool,
8}
9
10/// Validation result for a transformation
11#[derive(Debug, Clone)]
12pub struct ValidationResult {
13    pub is_valid: bool,
14    pub errors: Vec<ValidationError>,
15    pub warnings: Vec<ValidationWarning>,
16}
17
18/// Validation error for a transformation
19#[derive(Debug, Clone)]
20pub struct ValidationError {
21    pub code: String,
22    pub message: String,
23    pub node_id: Option<NodeId>,
24}
25
26/// Validation warning for a transformation
27#[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    /// Create a new validator
36    pub fn new() -> Self {
37        Self {
38            strict_mode: false,
39            preserve_structure: true,
40        }
41    }
42
43    /// Create a strict validator
44    pub fn strict() -> Self {
45        Self {
46            strict_mode: true,
47            preserve_structure: true,
48        }
49    }
50
51    /// Create a permissive validator
52    pub fn permissive() -> Self {
53        Self {
54            strict_mode: false,
55            preserve_structure: false,
56        }
57    }
58
59    /// Enable strict mode
60    pub fn with_strict_mode(mut self, strict: bool) -> Self {
61        self.strict_mode = strict;
62        self
63    }
64
65    /// Enable structure preservation
66    pub fn with_structure_preservation(mut self, preserve: bool) -> Self {
67        self.preserve_structure = preserve;
68        self
69    }
70
71    /// Validate a transformation operation
72    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        // Check if target node exists
127        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        // In strict mode, check type compatibility
140        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        // Check if replacing root node
153        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        // Check if parent exists
170        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        // Check parent-child compatibility
183        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        // Check if target exists
206        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        // Check if removing root node
217        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        // Check children
227        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        // Check if both nodes exist
245        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        // Check for circular reference
266        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        // Check parent-child compatibility in strict mode
276        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        // Check if target exists
304        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        // In strict mode, validate value compatibility with node type
317        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, // Allow other combinations in permissive mode
356        }
357    }
358
359    fn would_create_cycle(&self, target: NodeId, new_parent: NodeId, graph: &PdfAstGraph) -> bool {
360        // Check if new_parent is a descendant of target
361        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(&current) {
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}