Skip to main content

dotloom_document/
validate.rs

1//! Structural invariants.
2//!
3//! [`Document::validate`] checks everything that can be checked without plugin
4//! definitions: IDs, references, groups, layers, built-in geometry and anchors,
5//! numeric sanity and size limits. The engine adds registry-aware checks (plugin
6//! anchors and property schemas) on top.
7
8use std::collections::{BTreeMap, BTreeSet};
9
10use crate::{
11    AnchorRef, DocError, Document, EntityId, GroupId, ParamRef, ParamSlot, PropValue, RuleSpec,
12    builtin::{builtin_shape_kind, get_geometry_param, is_builtin, types},
13};
14
15/// Size limits applied by validation (and by the `.dotl` reader).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Limits {
18    /// Maximum entities.
19    pub max_entities: usize,
20    /// Maximum constraints.
21    pub max_constraints: usize,
22    /// Maximum layers.
23    pub max_layers: usize,
24    /// Maximum groups.
25    pub max_groups: usize,
26    /// Maximum properties per entity.
27    pub max_props: usize,
28    /// Maximum length of any string field in bytes.
29    pub max_string: usize,
30}
31
32impl Default for Limits {
33    fn default() -> Self {
34        Self {
35            max_entities: 1_000_000,
36            max_constraints: 1_000_000,
37            max_layers: 10_000,
38            max_groups: 100_000,
39            max_props: 1_000,
40            max_string: 100_000,
41        }
42    }
43}
44
45/// Built-in anchors of an entity, if it is a built-in geometry entity.
46fn builtin_anchor_exists(doc: &Document, a: &AnchorRef) -> Option<bool> {
47    let e = doc.entity(a.entity)?;
48    if !is_builtin(&e.type_id) || e.type_id.as_str() == types::DIMENSION {
49        return None;
50    }
51    Some(e.geometry.as_ref().is_some_and(|g| g.anchor(&a.anchor).is_some()))
52}
53
54fn check_anchor(doc: &Document, a: &AnchorRef, out: &mut Vec<DocError>) {
55    match doc.entity(a.entity) {
56        None => out.push(DocError::BrokenReference { from: "constraint".into(), target: a.entity.to_string() }),
57        Some(_) => {
58            if builtin_anchor_exists(doc, a) == Some(false) {
59                out.push(DocError::MissingAnchor { entity: a.entity, anchor: a.anchor.clone() });
60            }
61        }
62    }
63}
64
65fn check_param(doc: &Document, p: &ParamRef, out: &mut Vec<DocError>) {
66    let Some(e) = doc.entity(p.entity) else {
67        out.push(DocError::BrokenReference { from: "constraint".into(), target: p.entity.to_string() });
68        return;
69    };
70    match &p.slot {
71        ParamSlot::Geom(name) => {
72            let ok = e.geometry.as_ref().is_some_and(|g| get_geometry_param(g, name).is_some());
73            if !ok {
74                out.push(DocError::MissingParam { entity: p.entity, param: name.clone() });
75            }
76        }
77        ParamSlot::Prop(name) => {
78            if !matches!(e.props.get(name), Some(PropValue::Number(_))) {
79                out.push(DocError::MissingParam { entity: p.entity, param: name.clone() });
80            }
81        }
82    }
83}
84
85fn check_circle(doc: &Document, id: EntityId, out: &mut Vec<DocError>) {
86    match doc.entity(id) {
87        None => out.push(DocError::BrokenReference { from: "constraint".into(), target: id.to_string() }),
88        Some(e) if is_builtin(&e.type_id) => {
89            let ok = matches!(e.type_id.as_str(), types::CIRCLE | types::ARC);
90            if !ok {
91                out.push(DocError::InvalidValue(format!("{id} is not a circle or arc")));
92            }
93        }
94        // Plugin entities are checked by the engine against their definition.
95        Some(_) => {}
96    }
97}
98
99impl Document {
100    /// First invariant violation, if any.
101    pub fn validate(&self) -> Result<(), DocError> {
102        match self.validate_all(&Limits::default()).into_iter().next() {
103            Some(e) => Err(e),
104            None => Ok(()),
105        }
106    }
107
108    /// All invariant violations (bounded to the first 1000).
109    #[must_use]
110    pub fn validate_all(&self, limits: &Limits) -> Vec<DocError> {
111        let mut out = Vec::new();
112        self.check_limits(limits, &mut out);
113        self.check_ids(&mut out);
114        self.check_entities(limits, &mut out);
115        self.check_groups(&mut out);
116        self.check_constraints(&mut out);
117        out.truncate(1000);
118        out
119    }
120
121    fn check_limits(&self, l: &Limits, out: &mut Vec<DocError>) {
122        if self.entity_count() > l.max_entities {
123            out.push(DocError::LimitExceeded(format!("{} entities > {}", self.entity_count(), l.max_entities)));
124        }
125        if self.constraint_count() > l.max_constraints {
126            out.push(DocError::LimitExceeded(format!(
127                "{} constraints > {}",
128                self.constraint_count(),
129                l.max_constraints
130            )));
131        }
132        if self.layers().len() > l.max_layers {
133            out.push(DocError::LimitExceeded(format!("{} layers > {}", self.layers().len(), l.max_layers)));
134        }
135        if self.groups().count() > l.max_groups {
136            out.push(DocError::LimitExceeded(format!("too many groups (> {})", l.max_groups)));
137        }
138        if self.meta.title.len() > l.max_string {
139            out.push(DocError::LimitExceeded("title too long".into()));
140        }
141    }
142
143    fn check_ids(&self, out: &mut Vec<DocError>) {
144        // One allocator for all kinds: IDs must be unique across kinds and below next_id.
145        let mut seen: BTreeMap<u64, &'static str> = BTreeMap::new();
146        let mut add = |raw: u64, kind: &'static str, out: &mut Vec<DocError>| {
147            if raw >= self.next_id() {
148                out.push(DocError::InvalidValue(format!("{kind} id {raw} is not below nextId {}", self.next_id())));
149            }
150            if let Some(prev) = seen.insert(raw, kind) {
151                out.push(DocError::DuplicateId(format!("{raw} used by {prev} and {kind}")));
152            }
153        };
154        for l in self.layers() {
155            add(l.id.0, "layer", out);
156        }
157        for e in self.entities() {
158            add(e.id.0, "entity", out);
159        }
160        for g in self.groups() {
161            add(g.id.0, "group", out);
162        }
163        for c in self.constraints() {
164            add(c.id.0, "constraint", out);
165        }
166        let order: BTreeSet<EntityId> = self.order().iter().copied().collect();
167        if order.len() != self.order().len() || order.len() != self.entity_count() {
168            out.push(DocError::BadOrder("draw order is not a permutation of the entities".into()));
169        }
170    }
171
172    fn check_entities(&self, l: &Limits, out: &mut Vec<DocError>) {
173        for e in self.entities() {
174            if self.layer(e.layer).is_none() {
175                out.push(DocError::UnknownLayer(e.layer));
176            }
177            if !e.transform.is_finite() || e.transform.inverse().is_err() {
178                out.push(DocError::InvalidValue(format!("{} has a non-finite or singular transform", e.id)));
179            }
180            if let Some(kind) = builtin_shape_kind(&e.type_id) {
181                match &e.geometry {
182                    None => out.push(DocError::InvalidGeometry { entity: e.id, reason: "missing geometry".into() }),
183                    Some(g) if g.kind() != kind => out.push(DocError::InvalidGeometry {
184                        entity: e.id,
185                        reason: format!(
186                            "type {} requires {} geometry, found {}",
187                            e.type_id,
188                            kind.name(),
189                            g.kind().name()
190                        ),
191                    }),
192                    Some(g) => {
193                        if let Err(err) = g.validate() {
194                            out.push(DocError::InvalidGeometry { entity: e.id, reason: err.to_string() });
195                        }
196                    }
197                }
198            } else if let Some(g) = &e.geometry
199                && let Err(err) = g.validate()
200            {
201                out.push(DocError::InvalidGeometry { entity: e.id, reason: err.to_string() });
202            }
203            if e.props.len() > l.max_props {
204                out.push(DocError::LimitExceeded(format!("{} has more than {} properties", e.id, l.max_props)));
205            }
206            for (k, v) in &e.props {
207                if k.len() > 256 {
208                    out.push(DocError::LimitExceeded(format!("{} property name too long", e.id)));
209                }
210                if !v.is_finite() {
211                    out.push(DocError::InvalidValue(format!("{}.{k} is not finite", e.id)));
212                }
213                if let PropValue::Text(t) = v
214                    && t.len() > l.max_string
215                {
216                    out.push(DocError::LimitExceeded(format!("{}.{k} text too long", e.id)));
217                }
218                match v {
219                    PropValue::Ref(r) if self.entity(r.entity).is_none() => {
220                        out.push(DocError::BrokenReference {
221                            from: format!("{}.{k}", e.id),
222                            target: r.entity.to_string(),
223                        });
224                    }
225                    PropValue::Anchor(a) => {
226                        if self.entity(a.entity).is_none() {
227                            out.push(DocError::BrokenReference {
228                                from: format!("{}.{k}", e.id),
229                                target: a.entity.to_string(),
230                            });
231                        } else if builtin_anchor_exists(self, a) == Some(false) {
232                            out.push(DocError::MissingAnchor { entity: a.entity, anchor: a.anchor.clone() });
233                        }
234                    }
235                    _ => {}
236                }
237            }
238            if e.name.as_ref().is_some_and(|n| n.len() > l.max_string) {
239                out.push(DocError::LimitExceeded(format!("{} name too long", e.id)));
240            }
241            if let Err(err) = e.style.validate() {
242                out.push(err);
243            }
244            for f in e.fallback.iter().flatten() {
245                if let Err(err) = f.validate() {
246                    out.push(DocError::InvalidGeometry { entity: e.id, reason: format!("fallback: {err}") });
247                }
248            }
249        }
250    }
251
252    fn check_groups(&self, out: &mut Vec<DocError>) {
253        let mut entity_parent: BTreeMap<EntityId, GroupId> = BTreeMap::new();
254        let mut group_parent: BTreeMap<GroupId, GroupId> = BTreeMap::new();
255        for g in self.groups() {
256            for m in &g.members {
257                if self.entity(*m).is_none() {
258                    out.push(DocError::BrokenReference { from: g.id.to_string(), target: m.to_string() });
259                }
260                if entity_parent.insert(*m, g.id).is_some() {
261                    out.push(DocError::MultipleParents(m.to_string()));
262                }
263            }
264            for c in &g.children {
265                if self.group(*c).is_none() {
266                    out.push(DocError::UnknownGroup(*c));
267                }
268                if group_parent.insert(*c, g.id).is_some() {
269                    out.push(DocError::MultipleParents(c.to_string()));
270                }
271            }
272        }
273        // Cycle detection: walk parents; each group has at most one parent.
274        for g in self.groups() {
275            let mut cur = g.id;
276            let mut steps = 0usize;
277            while let Some(p) = group_parent.get(&cur) {
278                if *p == g.id || steps > group_parent.len() {
279                    out.push(DocError::GroupCycle(g.id));
280                    break;
281                }
282                cur = *p;
283                steps += 1;
284            }
285        }
286    }
287
288    fn check_constraints(&self, out: &mut Vec<DocError>) {
289        for c in self.constraints() {
290            if !c.rule.values_valid() {
291                out.push(DocError::InvalidValue(format!("{} ({}) has invalid values", c.id, c.rule.kind_name())));
292            }
293            if let Some(owner) = c.owner
294                && self.entity(owner).is_none()
295            {
296                out.push(DocError::BrokenReference { from: c.id.to_string(), target: owner.to_string() });
297            }
298            for a in c.rule.anchors() {
299                check_anchor(self, a, out);
300            }
301            for p in c.rule.params() {
302                check_param(self, p, out);
303            }
304            match &c.rule {
305                RuleSpec::PointOnCircle { circle, .. }
306                | RuleSpec::Radius { circle, .. }
307                | RuleSpec::TangentLineCircle { circle, .. } => check_circle(self, *circle, out),
308                RuleSpec::Concentric { a, b }
309                | RuleSpec::EqualRadius { a, b }
310                | RuleSpec::TangentCircles { a, b, .. } => {
311                    check_circle(self, *a, out);
312                    check_circle(self, *b, out);
313                }
314                RuleSpec::Expression { entity, .. } if self.entity(*entity).is_none() => {
315                    out.push(DocError::BrokenReference { from: c.id.to_string(), target: entity.to_string() });
316                }
317                _ => {}
318            }
319        }
320    }
321}