1use crate::diagram::{ParsedDiagramRender, RenderSemanticModel};
4#[cfg(any(
5 feature = "diagram-flowchart",
6 feature = "diagram-swimlane",
7 feature = "diagram-agentflow"
8))]
9use crate::diagrams::flowchart::FlowchartModel;
10#[cfg(feature = "diagram-ishikawa")]
11use crate::diagrams::ishikawa::IshikawaDiagramRenderModel;
12#[cfg(feature = "diagram-kanban")]
13use crate::diagrams::kanban::KanbanDiagramRenderModel;
14#[cfg(feature = "diagram-mindmap")]
15use crate::diagrams::mindmap::MindmapDiagramRenderModel;
16#[cfg(feature = "diagram-radar")]
17use crate::diagrams::radar::RadarDiagramRenderModel;
18#[cfg(feature = "diagram-requirement")]
19use crate::diagrams::requirement::RequirementDiagramRenderModel;
20#[cfg(feature = "diagram-sequence")]
21use crate::diagrams::sequence::SequenceDiagramRenderModel;
22#[cfg(feature = "diagram-treemap")]
23use crate::diagrams::treemap::TreemapDiagramRenderModel;
24#[cfg(feature = "diagram-zenuml")]
25use crate::diagrams::zenuml::{ZenumlDiagramRenderModel, ZenumlStatementKind};
26#[cfg(feature = "diagram-class")]
27use crate::models::class_diagram::ClassDiagram;
28use serde::Serialize;
29use serde::ser::{
30 SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
31 SerializeTupleStruct, SerializeTupleVariant,
32};
33
34const KIB: usize = 1024;
35const MIB: usize = 1024 * KIB;
36
37pub const RESOURCE_PROFILE_COUNT: usize = 4;
38pub const INPUT_RESOURCE_LIMIT_COUNT: usize = 4;
39
40#[repr(usize)]
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum ResourceProfile {
43 Interactive,
44 Constrained,
45 TrustedNative,
46 UnboundedForTrustedInput,
47}
48
49impl ResourceProfile {
50 pub const ALL: [Self; RESOURCE_PROFILE_COUNT] = [
51 Self::Interactive,
52 Self::Constrained,
53 Self::TrustedNative,
54 Self::UnboundedForTrustedInput,
55 ];
56
57 pub const fn descriptor(self) -> &'static ResourceProfileDescriptor {
58 &RESOURCE_PROFILE_DESCRIPTORS[self as usize]
59 }
60
61 pub const fn id(self) -> &'static str {
62 self.descriptor().id
63 }
64
65 pub fn from_id(id: &str) -> Option<Self> {
66 RESOURCE_PROFILE_DESCRIPTORS
67 .iter()
68 .find(|descriptor| descriptor.id == id)
69 .map(|descriptor| descriptor.profile)
70 }
71}
72
73impl std::fmt::Display for ResourceProfile {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.write_str(self.id())
76 }
77}
78
79impl std::str::FromStr for ResourceProfile {
80 type Err = String;
81
82 fn from_str(value: &str) -> Result<Self, Self::Err> {
83 Self::from_id(value).ok_or_else(|| {
84 let supported = RESOURCE_PROFILE_DESCRIPTORS
85 .iter()
86 .map(|descriptor| descriptor.id)
87 .collect::<Vec<_>>()
88 .join(", ");
89 format!("unsupported resource profile `{value}`; expected one of: {supported}")
90 })
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95#[non_exhaustive]
96pub struct ResourceProfileDescriptor {
97 pub profile: ResourceProfile,
98 pub id: &'static str,
99 pub purpose: &'static str,
100 pub trust_assumption: &'static str,
101 pub recommended_binding_default: bool,
102}
103
104pub static RESOURCE_PROFILE_DESCRIPTORS: [ResourceProfileDescriptor; RESOURCE_PROFILE_COUNT] = [
105 ResourceProfileDescriptor {
106 profile: ResourceProfile::Interactive,
107 id: "interactive",
108 purpose: "General interactive applications and public binding surfaces",
109 trust_assumption: "Cooperative user-authored input; not a hostile or multi-tenant isolation boundary",
110 recommended_binding_default: true,
111 },
112 ResourceProfileDescriptor {
113 profile: ResourceProfile::Constrained,
114 id: "constrained",
115 purpose: "Constrained rendering for untrusted or publicly submitted documents",
116 trust_assumption: "The host must provide timeout, memory, concurrency, and preemption controls",
117 recommended_binding_default: false,
118 },
119 ResourceProfileDescriptor {
120 profile: ResourceProfile::TrustedNative,
121 id: "trusted-native",
122 purpose: "Local CLI and controlled native batch rendering",
123 trust_assumption: "Input is trusted and the native host controls the workload",
124 recommended_binding_default: false,
125 },
126 ResourceProfileDescriptor {
127 profile: ResourceProfile::UnboundedForTrustedInput,
128 id: "unbounded-for-trusted-input",
129 purpose: "Explicitly disable policy budgets while retaining hard backend capabilities",
130 trust_assumption: "Input is fully trusted and the host provides outer isolation",
131 recommended_binding_default: false,
132 },
133];
134
135pub const GENERAL_BINDING_DEFAULT_RESOURCE_PROFILE: ResourceProfile = ResourceProfile::Interactive;
136pub const CLI_DEFAULT_RESOURCE_PROFILE: ResourceProfile = ResourceProfile::TrustedNative;
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
139pub enum InputResourceLimitPhase {
140 Source,
141 Model,
142}
143
144impl InputResourceLimitPhase {
145 pub const fn as_str(self) -> &'static str {
146 match self {
147 Self::Source => "source",
148 Self::Model => "layout_model",
149 }
150 }
151}
152
153impl std::fmt::Display for InputResourceLimitPhase {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.write_str(self.as_str())
156 }
157}
158
159#[repr(usize)]
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
161pub enum InputResourceLimitId {
162 MaxSourceBytes,
163 MaxModelItems,
164 MaxModelTextBytes,
165 MaxModelNestingDepth,
166}
167
168impl InputResourceLimitId {
169 pub const ALL: [Self; INPUT_RESOURCE_LIMIT_COUNT] = [
170 Self::MaxSourceBytes,
171 Self::MaxModelItems,
172 Self::MaxModelTextBytes,
173 Self::MaxModelNestingDepth,
174 ];
175
176 pub const fn index(self) -> usize {
177 self as usize
178 }
179
180 pub fn from_stable_id(id: &str) -> Option<Self> {
181 INPUT_RESOURCE_LIMIT_DESCRIPTORS
182 .iter()
183 .find(|descriptor| descriptor.stable_id == id)
184 .map(|descriptor| descriptor.id)
185 }
186
187 pub const fn descriptor(self) -> &'static InputResourceLimitDescriptor {
188 &INPUT_RESOURCE_LIMIT_DESCRIPTORS[self.index()]
189 }
190
191 pub const fn as_str(self) -> &'static str {
192 self.descriptor().stable_id
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197#[non_exhaustive]
198pub struct InputResourceLimitDescriptor {
199 pub id: InputResourceLimitId,
200 pub stable_id: &'static str,
201 pub phase: InputResourceLimitPhase,
202 pub description: &'static str,
203 pub overridable: bool,
204 pub minimum_value: usize,
205}
206
207macro_rules! input_limit_descriptors {
208 ($($id:ident => ($stable:literal, $phase:ident, $description:literal)),+ $(,)?) => {
209 pub static INPUT_RESOURCE_LIMIT_DESCRIPTORS:
210 [InputResourceLimitDescriptor; INPUT_RESOURCE_LIMIT_COUNT] = [
211 $(InputResourceLimitDescriptor {
212 id: InputResourceLimitId::$id,
213 stable_id: $stable,
214 phase: InputResourceLimitPhase::$phase,
215 description: $description,
216 overridable: true,
217 minimum_value: 1,
218 }),+
219 ];
220 };
221}
222
223input_limit_descriptors! {
224 MaxSourceBytes => ("max_source_bytes", Source, "Maximum UTF-8 Mermaid source bytes"),
225 MaxModelItems => ("max_model_items", Model, "Maximum semantic model entities and relationships across all diagram families"),
226 MaxModelTextBytes => ("max_model_text_bytes", Model, "Maximum aggregate UTF-8 text retained by a semantic model"),
227 MaxModelNestingDepth => ("max_model_nesting_depth", Model, "Maximum semantic nesting depth accepted before layout"),
228}
229
230const PROFILE_VALUES: [[Option<usize>; RESOURCE_PROFILE_COUNT]; INPUT_RESOURCE_LIMIT_COUNT] = [
231 [Some(2 * MIB), Some(MIB), Some(16 * MIB), None],
232 [Some(32_000), Some(16_000), Some(200_000), None],
235 [Some(2 * MIB), Some(MIB), Some(16 * MIB), None],
236 [Some(256), Some(128), Some(1_024), None],
237];
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub struct InputResourcePolicy {
241 profile: ResourceProfile,
242 base_values: [Option<usize>; INPUT_RESOURCE_LIMIT_COUNT],
243 effective_values: [Option<usize>; INPUT_RESOURCE_LIMIT_COUNT],
244 explicit_overrides: [Option<usize>; INPUT_RESOURCE_LIMIT_COUNT],
245}
246
247impl Default for InputResourcePolicy {
248 fn default() -> Self {
249 Self::for_profile(GENERAL_BINDING_DEFAULT_RESOURCE_PROFILE)
250 }
251}
252
253impl InputResourcePolicy {
254 pub const fn for_profile(profile: ResourceProfile) -> Self {
255 let mut values = [None; INPUT_RESOURCE_LIMIT_COUNT];
256 let mut index = 0;
257 while index < INPUT_RESOURCE_LIMIT_COUNT {
258 values[index] = PROFILE_VALUES[index][profile as usize];
259 index += 1;
260 }
261 Self {
262 profile,
263 base_values: values,
264 effective_values: values,
265 explicit_overrides: [None; INPUT_RESOURCE_LIMIT_COUNT],
266 }
267 }
268
269 pub const fn profile(self) -> ResourceProfile {
270 self.profile
271 }
272
273 pub const fn value(self, id: InputResourceLimitId) -> Option<usize> {
274 self.effective_values[id.index()]
275 }
276
277 pub const fn base_value(self, id: InputResourceLimitId) -> Option<usize> {
278 self.base_values[id.index()]
279 }
280
281 pub const fn explicit_override(self, id: InputResourceLimitId) -> Option<usize> {
282 self.explicit_overrides[id.index()]
283 }
284
285 pub fn explicit_overrides(&self) -> impl Iterator<Item = (InputResourceLimitId, usize)> + '_ {
286 InputResourceLimitId::ALL
287 .into_iter()
288 .filter_map(|id| self.explicit_override(id).map(|value| (id, value)))
289 }
290
291 pub fn apply_override(
292 &mut self,
293 stable_id: &str,
294 value: usize,
295 ) -> Result<(), InputResourceLimitOverrideError> {
296 let id = InputResourceLimitId::from_stable_id(stable_id)
297 .ok_or_else(|| InputResourceLimitOverrideError::UnknownLimit(stable_id.to_string()))?;
298 self.apply_limit(id, value)
299 }
300
301 pub fn apply_limit(
302 &mut self,
303 id: InputResourceLimitId,
304 value: usize,
305 ) -> Result<(), InputResourceLimitOverrideError> {
306 if value == 0 {
307 return Err(InputResourceLimitOverrideError::NonPositive(id.as_str()));
308 }
309 self.effective_values[id.index()] = Some(value);
310 self.explicit_overrides[id.index()] = Some(value);
311 Ok(())
312 }
313
314 pub fn with_limit(
315 mut self,
316 id: InputResourceLimitId,
317 value: usize,
318 ) -> Result<Self, InputResourceLimitOverrideError> {
319 self.apply_limit(id, value)?;
320 Ok(self)
321 }
322
323 fn check_limit(
324 &self,
325 phase: InputResourceLimitPhase,
326 id: InputResourceLimitId,
327 actual: usize,
328 ) -> Result<(), InputResourceLimitExceeded> {
329 let Some(max) = self.value(id) else {
330 return Ok(());
331 };
332 if actual <= max {
333 return Ok(());
334 }
335 Err(InputResourceLimitExceeded {
336 phase,
337 limit: id.as_str(),
338 actual,
339 max,
340 profile: self.profile,
341 explicit_overrides: self
342 .explicit_overrides()
343 .map(|(id, value)| InputResourceLimitOverride { id, value })
344 .collect(),
345 })
346 }
347
348 pub fn check_source_bytes(&self, source: &str) -> Result<(), InputResourceLimitExceeded> {
349 self.check_limit(
350 InputResourceLimitPhase::Source,
351 InputResourceLimitId::MaxSourceBytes,
352 source.len(),
353 )
354 }
355
356 pub fn check_render_model(
361 &self,
362 model: &RenderSemanticModel,
363 ) -> Result<(), InputResourceLimitExceeded> {
364 self.check_model_complexity(ModelComplexity::from_render_model(model))
365 }
366
367 pub fn check_parsed_render(
368 &self,
369 parsed: &ParsedDiagramRender,
370 ) -> Result<(), InputResourceLimitExceeded> {
371 let mut complexity = ModelComplexity::from_render_model(parsed.model());
372 complexity.text_bytes = complexity
373 .text_bytes
374 .saturating_add(parsed.retained_render_context_bytes());
375 self.check_model_complexity(complexity)
376 }
377
378 pub fn check_model_complexity(
379 &self,
380 complexity: ModelComplexity,
381 ) -> Result<(), InputResourceLimitExceeded> {
382 self.check_limit(
383 InputResourceLimitPhase::Model,
384 InputResourceLimitId::MaxModelItems,
385 complexity.items,
386 )?;
387 self.check_limit(
388 InputResourceLimitPhase::Model,
389 InputResourceLimitId::MaxModelTextBytes,
390 complexity.text_bytes,
391 )?;
392 self.check_limit(
393 InputResourceLimitPhase::Model,
394 InputResourceLimitId::MaxModelNestingDepth,
395 complexity.nesting_depth,
396 )
397 }
398
399 #[cfg(any(
400 feature = "diagram-flowchart",
401 feature = "diagram-swimlane",
402 feature = "diagram-agentflow"
403 ))]
404 pub fn check_flowchart_complexity(
405 &self,
406 model: &FlowchartModel,
407 ) -> Result<FlowchartComplexity, InputResourceLimitExceeded> {
408 let complexity = FlowchartComplexity::from_model(model);
409 self.check_model_complexity(complexity.as_model_complexity())?;
410 Ok(complexity)
411 }
412
413 #[cfg(feature = "diagram-class")]
414 pub fn check_class_complexity(
415 &self,
416 model: &ClassDiagram,
417 ) -> Result<ClassComplexity, InputResourceLimitExceeded> {
418 let complexity = ClassComplexity::from_model(model);
419 self.check_model_complexity(complexity.as_model_complexity())?;
420 Ok(complexity)
421 }
422
423 #[cfg(feature = "diagram-mindmap")]
424 pub fn check_mindmap_complexity(
425 &self,
426 model: &MindmapDiagramRenderModel,
427 ) -> Result<MindmapComplexity, InputResourceLimitExceeded> {
428 let complexity = MindmapComplexity::from_model(model);
429 self.check_model_complexity(complexity.as_model_complexity())?;
430 Ok(complexity)
431 }
432
433 #[cfg(feature = "diagram-zenuml")]
434 pub fn check_zenuml_complexity(
435 &self,
436 model: &ZenumlDiagramRenderModel,
437 ) -> Result<ZenumlComplexity, InputResourceLimitExceeded> {
438 let complexity = ZenumlComplexity::from_model(model);
439 self.check_model_complexity(complexity.as_model_complexity())?;
440 Ok(complexity)
441 }
442
443 #[cfg(feature = "diagram-sequence")]
444 pub fn check_sequence_complexity(
445 &self,
446 model: &SequenceDiagramRenderModel,
447 ) -> Result<SequenceComplexity, InputResourceLimitExceeded> {
448 let complexity = SequenceComplexity::from_model(model);
449 self.check_model_complexity(complexity.as_model_complexity())?;
450 Ok(complexity)
451 }
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
455pub enum InputResourceLimitOverrideError {
456 #[error("resource limit id `{0}` is not part of input resource contract schema 1")]
457 UnknownLimit(String),
458 #[error("resource limit `{0}` must be a positive integer")]
459 NonPositive(&'static str),
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
463#[error("resource limit exceeded during {phase}: {limit} actual={actual} max={max}")]
464#[non_exhaustive]
465pub struct InputResourceLimitExceeded {
466 pub phase: InputResourceLimitPhase,
467 pub limit: &'static str,
468 pub actual: usize,
469 pub max: usize,
470 pub profile: ResourceProfile,
471 pub explicit_overrides: Vec<InputResourceLimitOverride>,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub struct InputResourceLimitOverride {
476 pub id: InputResourceLimitId,
477 pub value: usize,
478}
479
480#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
481pub struct ModelComplexity {
482 pub items: usize,
483 pub text_bytes: usize,
484 pub nesting_depth: usize,
485}
486
487impl ModelComplexity {
488 pub const fn new(items: usize, text_bytes: usize, nesting_depth: usize) -> Self {
489 Self {
490 items,
491 text_bytes,
492 nesting_depth,
493 }
494 }
495
496 pub fn from_render_model(model: &RenderSemanticModel) -> Self {
497 match model {
498 RenderSemanticModel::Error(model) => Self::from_serializable(model),
499 RenderSemanticModel::CustomJson(model) => {
500 let mut complexity = Self::from_serializable(model.value());
501 complexity.text_bytes = complexity
502 .text_bytes
503 .saturating_add(model.model_name().len());
504 complexity
505 }
506 #[cfg(feature = "diagram-mindmap")]
507 RenderSemanticModel::Mindmap(model) => {
508 MindmapComplexity::from_model(model).as_model_complexity()
509 }
510 #[cfg(feature = "diagram-state")]
511 RenderSemanticModel::State(model) => Self::from_serializable(model),
512 #[cfg(feature = "diagram-sequence")]
513 RenderSemanticModel::Sequence(model) => {
514 SequenceComplexity::from_model(model).as_model_complexity()
515 }
516 #[cfg(feature = "diagram-zenuml")]
517 RenderSemanticModel::Zenuml(model) => {
518 ZenumlComplexity::from_model(model).as_model_complexity()
519 }
520 #[cfg(any(feature = "diagram-flowchart", feature = "diagram-swimlane"))]
521 RenderSemanticModel::Flowchart(model) => {
522 FlowchartComplexity::from_model(model).as_model_complexity()
523 }
524 #[cfg(feature = "diagram-architecture")]
525 RenderSemanticModel::Architecture(model) => Self::from_serializable(model),
526 #[cfg(feature = "diagram-class")]
527 RenderSemanticModel::Class(model) => {
528 ClassComplexity::from_model(model).as_model_complexity()
529 }
530 #[cfg(feature = "diagram-c4")]
531 RenderSemanticModel::C4(model) => Self::from_serializable(model),
532 #[cfg(feature = "diagram-cynefin")]
533 RenderSemanticModel::Cynefin(model) => Self::from_serializable(model),
534 #[cfg(feature = "diagram-railroad")]
535 RenderSemanticModel::Railroad(model) => Self::from_serializable(model),
536 #[cfg(feature = "diagram-kanban")]
537 RenderSemanticModel::Kanban(model) => Self::from_kanban(model),
538 #[cfg(feature = "diagram-gantt")]
539 RenderSemanticModel::Gantt(model) => Self::from_serializable(model),
540 #[cfg(feature = "diagram-pie")]
541 RenderSemanticModel::Pie(model) => Self::from_serializable(model),
542 #[cfg(feature = "diagram-packet")]
543 RenderSemanticModel::Packet(model) => Self::from_serializable(model),
544 #[cfg(feature = "diagram-timeline")]
545 RenderSemanticModel::Timeline(model) => Self::from_serializable(model),
546 #[cfg(feature = "diagram-journey")]
547 RenderSemanticModel::Journey(model) => Self::from_serializable(model),
548 #[cfg(feature = "diagram-requirement")]
549 RenderSemanticModel::Requirement(model) => Self::from_requirement(model),
550 #[cfg(feature = "diagram-sankey")]
551 RenderSemanticModel::Sankey(model) => Self::from_serializable(model),
552 #[cfg(feature = "diagram-radar")]
553 RenderSemanticModel::Radar(model) => Self::from_radar(model),
554 #[cfg(feature = "diagram-info")]
555 RenderSemanticModel::Info(model) => Self::from_serializable(model),
556 #[cfg(feature = "diagram-treemap")]
557 RenderSemanticModel::Treemap(model) => {
558 TreemapComplexity::from_model(model).as_model_complexity()
559 }
560 #[cfg(feature = "diagram-block")]
561 RenderSemanticModel::Block(model) => Self::from_serializable(model),
562 #[cfg(feature = "diagram-er")]
563 RenderSemanticModel::Er(model) => Self::from_serializable(model),
564 #[cfg(feature = "diagram-quadrant-chart")]
565 RenderSemanticModel::QuadrantChart(model) => Self::from_serializable(model),
566 #[cfg(feature = "diagram-xychart")]
567 RenderSemanticModel::XyChart(model) => Self::from_serializable(model),
568 #[cfg(feature = "diagram-git-graph")]
569 RenderSemanticModel::GitGraph(model) => Self::from_serializable(model),
570 #[cfg(feature = "diagram-tree-view")]
571 RenderSemanticModel::TreeView(model) => Self::from_serializable(model),
572 #[cfg(feature = "diagram-ishikawa")]
573 RenderSemanticModel::Ishikawa(model) => {
574 IshikawaComplexity::from_model(model).as_model_complexity()
575 }
576 #[cfg(feature = "diagram-event-modeling")]
577 RenderSemanticModel::EventModeling(model) => Self::from_serializable(model),
578 #[cfg(feature = "diagram-venn")]
579 RenderSemanticModel::Venn(model) => Self::from_serializable(model),
580 #[cfg(feature = "diagram-wardley")]
581 RenderSemanticModel::Wardley(model) => Self::from_serializable(model),
582 #[cfg(feature = "diagram-usecase")]
583 RenderSemanticModel::Usecase(model) => Self::from_serializable(model),
584 #[cfg(feature = "diagram-agentflow")]
585 RenderSemanticModel::Agentflow(model) => Self::from_serializable(model),
586 }
587 }
588
589 pub fn from_serializable<T: Serialize + ?Sized>(model: &T) -> Self {
591 let mut counter = ModelComplexitySerializer::default();
592 model
593 .serialize(&mut counter)
594 .expect("model complexity serialization is infallible");
595 counter.finish()
596 }
597
598 #[cfg(feature = "diagram-kanban")]
599 pub fn from_kanban(model: &KanbanDiagramRenderModel) -> Self {
600 let text_bytes = model.nodes.iter().fold(0usize, |total, node| {
601 [
602 Some(node.id.as_str()),
603 Some(node.label.as_str()),
604 node.parent_id.as_deref(),
605 node.ticket.as_deref(),
606 node.priority.as_deref(),
607 node.assigned.as_deref(),
608 node.icon.as_deref(),
609 ]
610 .into_iter()
611 .flatten()
612 .fold(total, |subtotal, value| {
613 subtotal.saturating_add(value.len())
614 })
615 });
616 let nesting_depth = kanban_nesting_depth(model);
617 Self::new(model.nodes.len(), text_bytes, nesting_depth)
618 }
619
620 #[cfg(feature = "diagram-radar")]
621 pub fn from_radar(model: &RadarDiagramRenderModel) -> Self {
622 let common_text_bytes = [
623 model.title.as_deref(),
624 model.acc_title.as_deref(),
625 model.acc_descr.as_deref(),
626 Some(model.options.graticule.as_str()),
627 ]
628 .into_iter()
629 .flatten()
630 .fold(0usize, |total, value| total.saturating_add(value.len()));
631 let axis_text_bytes = model.axes.iter().fold(0usize, |total, axis| {
632 total
633 .saturating_add(axis.name.len())
634 .saturating_add(axis.label.len())
635 });
636 let (curve_items, curve_text_bytes) =
637 model
638 .curves
639 .iter()
640 .fold((0usize, 0usize), |(items, text_bytes), curve| {
641 (
642 items.saturating_add(curve.entries.len()),
643 text_bytes
644 .saturating_add(curve.name.len())
645 .saturating_add(curve.label.len()),
646 )
647 });
648 Self::new(
649 model
650 .axes
651 .len()
652 .saturating_add(model.curves.len())
653 .saturating_add(curve_items),
654 common_text_bytes
655 .saturating_add(axis_text_bytes)
656 .saturating_add(curve_text_bytes),
657 0,
658 )
659 }
660
661 #[cfg(feature = "diagram-requirement")]
662 pub fn from_requirement(model: &RequirementDiagramRenderModel) -> Self {
663 let common_text_bytes = [
664 model.acc_title.as_deref(),
665 model.acc_descr.as_deref(),
666 Some(model.direction.as_str()),
667 ]
668 .into_iter()
669 .flatten()
670 .fold(0usize, |total, value| total.saturating_add(value.len()));
671 let requirement_text_bytes = model.requirements.iter().fold(0usize, |total, node| {
672 let total = [
673 node.name.as_str(),
674 node.node_type.as_str(),
675 node.requirement_id.as_str(),
676 node.text.as_str(),
677 node.risk.as_str(),
678 node.verify_method.as_str(),
679 ]
680 .into_iter()
681 .fold(total, |subtotal, value| {
682 subtotal.saturating_add(value.len())
683 });
684 node.css_styles
685 .iter()
686 .chain(&node.classes)
687 .fold(total, |subtotal, value| {
688 subtotal.saturating_add(value.len())
689 })
690 });
691 let element_text_bytes = model.elements.iter().fold(0usize, |total, node| {
692 let total = [
693 node.name.as_str(),
694 node.element_type.as_str(),
695 node.doc_ref.as_str(),
696 ]
697 .into_iter()
698 .fold(total, |subtotal, value| {
699 subtotal.saturating_add(value.len())
700 });
701 node.css_styles
702 .iter()
703 .chain(&node.classes)
704 .fold(total, |subtotal, value| {
705 subtotal.saturating_add(value.len())
706 })
707 });
708 let relationship_text_bytes =
709 model
710 .relationships
711 .iter()
712 .fold(0usize, |total, relationship| {
713 [
714 relationship.rel_type.as_str(),
715 relationship.src.as_str(),
716 relationship.dst.as_str(),
717 ]
718 .into_iter()
719 .fold(total, |subtotal, value| {
720 subtotal.saturating_add(value.len())
721 })
722 });
723 let class_text_bytes = model.classes.iter().fold(0usize, |total, (key, class)| {
724 class.styles.iter().chain(&class.text_styles).fold(
725 total
726 .saturating_add(key.len())
727 .saturating_add(class.id.len()),
728 |subtotal, value| subtotal.saturating_add(value.len()),
729 )
730 });
731 Self::new(
732 model
733 .requirements
734 .len()
735 .saturating_add(model.elements.len())
736 .saturating_add(model.relationships.len())
737 .saturating_add(model.classes.len()),
738 common_text_bytes
739 .saturating_add(requirement_text_bytes)
740 .saturating_add(element_text_bytes)
741 .saturating_add(relationship_text_bytes)
742 .saturating_add(class_text_bytes),
743 0,
744 )
745 }
746}
747
748#[derive(Debug, Default)]
749struct ModelComplexitySerializer {
750 items: usize,
751 text_bytes: usize,
752 current_depth: usize,
753 max_depth: usize,
754}
755
756impl ModelComplexitySerializer {
757 fn enter(&mut self) {
758 self.current_depth = self.current_depth.saturating_add(1);
759 self.max_depth = self.max_depth.max(self.current_depth);
760 }
761
762 fn leave(&mut self) {
763 self.current_depth = self.current_depth.saturating_sub(1);
764 }
765
766 fn finish(self) -> ModelComplexity {
767 ModelComplexity::new(
768 self.items.max(1),
769 self.text_bytes,
770 self.max_depth.saturating_sub(1),
771 )
772 }
773}
774
775#[derive(Debug)]
776struct ModelComplexitySerializationError;
777
778impl std::fmt::Display for ModelComplexitySerializationError {
779 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
780 formatter.write_str("model complexity serialization failed")
781 }
782}
783
784impl std::error::Error for ModelComplexitySerializationError {}
785
786impl serde::ser::Error for ModelComplexitySerializationError {
787 fn custom<T: std::fmt::Display>(_message: T) -> Self {
788 Self
789 }
790}
791
792struct ModelCompound<'a> {
793 counter: &'a mut ModelComplexitySerializer,
794 count_items: bool,
795}
796
797impl<'a> ModelCompound<'a> {
798 fn new(counter: &'a mut ModelComplexitySerializer, count_items: bool) -> Self {
799 counter.enter();
800 Self {
801 counter,
802 count_items,
803 }
804 }
805
806 fn serialize_value<T: Serialize + ?Sized>(
807 &mut self,
808 value: &T,
809 ) -> Result<(), ModelComplexitySerializationError> {
810 if self.count_items {
811 self.counter.items = self.counter.items.saturating_add(1);
812 }
813 value.serialize(&mut *self.counter)
814 }
815}
816
817impl Drop for ModelCompound<'_> {
818 fn drop(&mut self) {
819 self.counter.leave();
820 }
821}
822
823impl SerializeSeq for ModelCompound<'_> {
824 type Ok = ();
825 type Error = ModelComplexitySerializationError;
826
827 fn serialize_element<T: Serialize + ?Sized>(
828 &mut self,
829 value: &T,
830 ) -> Result<Self::Ok, Self::Error> {
831 self.serialize_value(value)
832 }
833
834 fn end(self) -> Result<Self::Ok, Self::Error> {
835 Ok(())
836 }
837}
838
839impl SerializeTuple for ModelCompound<'_> {
840 type Ok = ();
841 type Error = ModelComplexitySerializationError;
842
843 fn serialize_element<T: Serialize + ?Sized>(
844 &mut self,
845 value: &T,
846 ) -> Result<Self::Ok, Self::Error> {
847 self.serialize_value(value)
848 }
849
850 fn end(self) -> Result<Self::Ok, Self::Error> {
851 Ok(())
852 }
853}
854
855impl SerializeTupleStruct for ModelCompound<'_> {
856 type Ok = ();
857 type Error = ModelComplexitySerializationError;
858
859 fn serialize_field<T: Serialize + ?Sized>(
860 &mut self,
861 value: &T,
862 ) -> Result<Self::Ok, Self::Error> {
863 self.serialize_value(value)
864 }
865
866 fn end(self) -> Result<Self::Ok, Self::Error> {
867 Ok(())
868 }
869}
870
871impl SerializeTupleVariant for ModelCompound<'_> {
872 type Ok = ();
873 type Error = ModelComplexitySerializationError;
874
875 fn serialize_field<T: Serialize + ?Sized>(
876 &mut self,
877 value: &T,
878 ) -> Result<Self::Ok, Self::Error> {
879 self.serialize_value(value)
880 }
881
882 fn end(self) -> Result<Self::Ok, Self::Error> {
883 Ok(())
884 }
885}
886
887impl SerializeMap for ModelCompound<'_> {
888 type Ok = ();
889 type Error = ModelComplexitySerializationError;
890
891 fn serialize_key<T: Serialize + ?Sized>(&mut self, key: &T) -> Result<Self::Ok, Self::Error> {
892 self.serialize_value(key)
893 }
894
895 fn serialize_value<T: Serialize + ?Sized>(
896 &mut self,
897 value: &T,
898 ) -> Result<Self::Ok, Self::Error> {
899 value.serialize(&mut *self.counter)
900 }
901
902 fn end(self) -> Result<Self::Ok, Self::Error> {
903 Ok(())
904 }
905}
906
907impl SerializeStruct for ModelCompound<'_> {
908 type Ok = ();
909 type Error = ModelComplexitySerializationError;
910
911 fn serialize_field<T: Serialize + ?Sized>(
912 &mut self,
913 _key: &'static str,
914 value: &T,
915 ) -> Result<Self::Ok, Self::Error> {
916 value.serialize(&mut *self.counter)
917 }
918
919 fn end(self) -> Result<Self::Ok, Self::Error> {
920 Ok(())
921 }
922}
923
924impl SerializeStructVariant for ModelCompound<'_> {
925 type Ok = ();
926 type Error = ModelComplexitySerializationError;
927
928 fn serialize_field<T: Serialize + ?Sized>(
929 &mut self,
930 _key: &'static str,
931 value: &T,
932 ) -> Result<Self::Ok, Self::Error> {
933 value.serialize(&mut *self.counter)
934 }
935
936 fn end(self) -> Result<Self::Ok, Self::Error> {
937 Ok(())
938 }
939}
940
941impl<'a> serde::Serializer for &'a mut ModelComplexitySerializer {
942 type Ok = ();
943 type Error = ModelComplexitySerializationError;
944 type SerializeSeq = ModelCompound<'a>;
945 type SerializeTuple = ModelCompound<'a>;
946 type SerializeTupleStruct = ModelCompound<'a>;
947 type SerializeTupleVariant = ModelCompound<'a>;
948 type SerializeMap = ModelCompound<'a>;
949 type SerializeStruct = ModelCompound<'a>;
950 type SerializeStructVariant = ModelCompound<'a>;
951
952 fn serialize_bool(self, _value: bool) -> Result<Self::Ok, Self::Error> {
953 Ok(())
954 }
955
956 fn serialize_i8(self, _value: i8) -> Result<Self::Ok, Self::Error> {
957 Ok(())
958 }
959
960 fn serialize_i16(self, _value: i16) -> Result<Self::Ok, Self::Error> {
961 Ok(())
962 }
963
964 fn serialize_i32(self, _value: i32) -> Result<Self::Ok, Self::Error> {
965 Ok(())
966 }
967
968 fn serialize_i64(self, _value: i64) -> Result<Self::Ok, Self::Error> {
969 Ok(())
970 }
971
972 fn serialize_i128(self, _value: i128) -> Result<Self::Ok, Self::Error> {
973 Ok(())
974 }
975
976 fn serialize_u8(self, _value: u8) -> Result<Self::Ok, Self::Error> {
977 Ok(())
978 }
979
980 fn serialize_u16(self, _value: u16) -> Result<Self::Ok, Self::Error> {
981 Ok(())
982 }
983
984 fn serialize_u32(self, _value: u32) -> Result<Self::Ok, Self::Error> {
985 Ok(())
986 }
987
988 fn serialize_u64(self, _value: u64) -> Result<Self::Ok, Self::Error> {
989 Ok(())
990 }
991
992 fn serialize_u128(self, _value: u128) -> Result<Self::Ok, Self::Error> {
993 Ok(())
994 }
995
996 fn serialize_f32(self, _value: f32) -> Result<Self::Ok, Self::Error> {
997 Ok(())
998 }
999
1000 fn serialize_f64(self, _value: f64) -> Result<Self::Ok, Self::Error> {
1001 Ok(())
1002 }
1003
1004 fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
1005 self.text_bytes = self.text_bytes.saturating_add(value.len_utf8());
1006 Ok(())
1007 }
1008
1009 fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
1010 self.text_bytes = self.text_bytes.saturating_add(value.len());
1011 Ok(())
1012 }
1013
1014 fn serialize_bytes(self, value: &[u8]) -> Result<Self::Ok, Self::Error> {
1015 self.items = self.items.saturating_add(value.len());
1016 Ok(())
1017 }
1018
1019 fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
1020 Ok(())
1021 }
1022
1023 fn serialize_some<T: Serialize + ?Sized>(self, value: &T) -> Result<Self::Ok, Self::Error> {
1024 value.serialize(self)
1025 }
1026
1027 fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
1028 Ok(())
1029 }
1030
1031 fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
1032 Ok(())
1033 }
1034
1035 fn serialize_unit_variant(
1036 self,
1037 _name: &'static str,
1038 _variant_index: u32,
1039 _variant: &'static str,
1040 ) -> Result<Self::Ok, Self::Error> {
1041 Ok(())
1042 }
1043
1044 fn serialize_newtype_struct<T: Serialize + ?Sized>(
1045 self,
1046 _name: &'static str,
1047 value: &T,
1048 ) -> Result<Self::Ok, Self::Error> {
1049 value.serialize(self)
1050 }
1051
1052 fn serialize_newtype_variant<T: Serialize + ?Sized>(
1053 self,
1054 _name: &'static str,
1055 _variant_index: u32,
1056 _variant: &'static str,
1057 value: &T,
1058 ) -> Result<Self::Ok, Self::Error> {
1059 value.serialize(self)
1060 }
1061
1062 fn serialize_seq(self, _length: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
1063 Ok(ModelCompound::new(self, true))
1064 }
1065
1066 fn serialize_tuple(self, _length: usize) -> Result<Self::SerializeTuple, Self::Error> {
1067 Ok(ModelCompound::new(self, true))
1068 }
1069
1070 fn serialize_tuple_struct(
1071 self,
1072 _name: &'static str,
1073 _length: usize,
1074 ) -> Result<Self::SerializeTupleStruct, Self::Error> {
1075 Ok(ModelCompound::new(self, true))
1076 }
1077
1078 fn serialize_tuple_variant(
1079 self,
1080 _name: &'static str,
1081 _variant_index: u32,
1082 _variant: &'static str,
1083 _length: usize,
1084 ) -> Result<Self::SerializeTupleVariant, Self::Error> {
1085 Ok(ModelCompound::new(self, true))
1086 }
1087
1088 fn serialize_map(self, _length: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
1089 Ok(ModelCompound::new(self, true))
1090 }
1091
1092 fn serialize_struct(
1093 self,
1094 _name: &'static str,
1095 _length: usize,
1096 ) -> Result<Self::SerializeStruct, Self::Error> {
1097 Ok(ModelCompound::new(self, false))
1098 }
1099
1100 fn serialize_struct_variant(
1101 self,
1102 _name: &'static str,
1103 _variant_index: u32,
1104 _variant: &'static str,
1105 _length: usize,
1106 ) -> Result<Self::SerializeStructVariant, Self::Error> {
1107 Ok(ModelCompound::new(self, false))
1108 }
1109
1110 fn collect_str<T: std::fmt::Display + ?Sized>(
1111 self,
1112 value: &T,
1113 ) -> Result<Self::Ok, Self::Error> {
1114 self.text_bytes = self.text_bytes.saturating_add(value.to_string().len());
1115 Ok(())
1116 }
1117
1118 fn is_human_readable(&self) -> bool {
1119 true
1120 }
1121}
1122
1123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1124#[cfg(any(
1125 feature = "diagram-flowchart",
1126 feature = "diagram-swimlane",
1127 feature = "diagram-agentflow"
1128))]
1129pub struct FlowchartComplexity {
1130 pub nodes: usize,
1131 pub edges: usize,
1132 pub subgraphs: usize,
1133 pub label_bytes: usize,
1134 pub subgraph_depth: usize,
1135}
1136
1137#[cfg(any(
1138 feature = "diagram-flowchart",
1139 feature = "diagram-swimlane",
1140 feature = "diagram-agentflow"
1141))]
1142impl FlowchartComplexity {
1143 pub fn from_model(model: &FlowchartModel) -> Self {
1144 Self {
1145 nodes: model.nodes.len().saturating_add(model.subgraphs.len()),
1146 edges: model.edges.len(),
1147 subgraphs: model.subgraphs.len(),
1148 label_bytes: flowchart_text_bytes(model),
1149 subgraph_depth: flowchart_subgraph_depth(model),
1150 }
1151 }
1152
1153 pub fn as_model_complexity(self) -> ModelComplexity {
1154 ModelComplexity::new(
1155 self.nodes.saturating_add(self.edges),
1156 self.label_bytes,
1157 self.subgraph_depth,
1158 )
1159 }
1160}
1161
1162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1164#[cfg(feature = "diagram-treemap")]
1165pub struct TreemapComplexity {
1166 pub nodes: usize,
1167 pub classes: usize,
1168 pub label_bytes: usize,
1169 pub nesting_depth: usize,
1170}
1171
1172#[cfg(feature = "diagram-treemap")]
1173impl TreemapComplexity {
1174 pub fn from_model(model: &TreemapDiagramRenderModel) -> Self {
1175 let mut nodes = 0usize;
1176 let mut classes = 0usize;
1177 let mut label_bytes = [
1178 model.acc_title.as_deref(),
1179 model.acc_descr.as_deref(),
1180 model.title.as_deref(),
1181 ]
1182 .into_iter()
1183 .flatten()
1184 .fold(0usize, |total, value| total.saturating_add(value.len()));
1185 let mut nesting_depth = 0usize;
1186
1187 let mut pending = vec![(&model.root, 0usize)];
1188 while let Some((node, depth)) = pending.pop() {
1189 nodes = nodes.saturating_add(1);
1190 nesting_depth = nesting_depth.max(depth);
1191 label_bytes = label_bytes
1192 .saturating_add(node.name.len())
1193 .saturating_add(node.class_selector.as_deref().map_or(0, str::len))
1194 .saturating_add(
1195 node.css_compiled_styles
1196 .as_deref()
1197 .unwrap_or(&[])
1198 .iter()
1199 .map(String::len)
1200 .sum::<usize>(),
1201 );
1202 if let Some(value) = node.value.as_ref() {
1203 label_bytes = label_bytes.saturating_add(json_text_bytes(value));
1204 nodes = nodes.saturating_add(json_item_count(value));
1205 }
1206 if let Some(children) = node.children.as_ref() {
1207 for child in children.iter().rev() {
1208 pending.push((child, depth.saturating_add(1)));
1209 }
1210 }
1211 }
1212
1213 for (class_name, class_def) in &model.classes {
1214 classes = classes.saturating_add(1);
1215 label_bytes = label_bytes
1216 .saturating_add(class_name.len())
1217 .saturating_add(class_def.id.len())
1218 .saturating_add(class_def.styles.iter().map(String::len).sum::<usize>())
1219 .saturating_add(class_def.text_styles.iter().map(String::len).sum::<usize>());
1220 }
1221
1222 Self {
1223 nodes,
1224 classes,
1225 label_bytes,
1226 nesting_depth,
1227 }
1228 }
1229
1230 pub fn as_model_complexity(self) -> ModelComplexity {
1231 ModelComplexity::new(
1232 self.nodes.saturating_add(self.classes),
1233 self.label_bytes,
1234 self.nesting_depth,
1235 )
1236 }
1237}
1238
1239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1241#[cfg(feature = "diagram-ishikawa")]
1242pub struct IshikawaComplexity {
1243 pub nodes: usize,
1244 pub label_bytes: usize,
1245 pub nesting_depth: usize,
1246}
1247
1248#[cfg(feature = "diagram-ishikawa")]
1249impl IshikawaComplexity {
1250 pub fn from_model(model: &IshikawaDiagramRenderModel) -> Self {
1251 let mut complexity = Self {
1252 nodes: 0,
1253 label_bytes: [
1254 model.acc_title.as_deref(),
1255 model.acc_descr.as_deref(),
1256 model.title.as_deref(),
1257 ]
1258 .into_iter()
1259 .flatten()
1260 .fold(0usize, |total, value| total.saturating_add(value.len())),
1261 nesting_depth: 0,
1262 };
1263 let Some(root) = model.root.as_ref() else {
1264 return complexity;
1265 };
1266
1267 let mut pending = vec![(root, 0usize)];
1268 while let Some((node, depth)) = pending.pop() {
1269 complexity.nodes = complexity.nodes.saturating_add(1);
1270 complexity.label_bytes = complexity.label_bytes.saturating_add(node.text.len());
1271 complexity.nesting_depth = complexity.nesting_depth.max(depth);
1272 for child in node.children.iter().rev() {
1273 pending.push((child, depth.saturating_add(1)));
1274 }
1275 }
1276 complexity
1277 }
1278
1279 pub fn as_model_complexity(self) -> ModelComplexity {
1280 ModelComplexity::new(self.nodes, self.label_bytes, self.nesting_depth)
1281 }
1282}
1283
1284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1285#[cfg(feature = "diagram-class")]
1286pub struct ClassComplexity {
1287 pub nodes: usize,
1288 pub edges: usize,
1289 pub namespaces: usize,
1290 pub label_bytes: usize,
1291 pub namespace_depth: usize,
1292}
1293
1294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1295#[cfg(feature = "diagram-mindmap")]
1296pub struct MindmapComplexity {
1297 pub nodes: usize,
1298 pub edges: usize,
1299 pub label_bytes: usize,
1300 pub nesting_depth: usize,
1301}
1302
1303#[cfg(feature = "diagram-mindmap")]
1304impl MindmapComplexity {
1305 pub fn from_model(model: &MindmapDiagramRenderModel) -> Self {
1306 let node_label_bytes = model.nodes.iter().fold(0usize, |total, node| {
1307 let strings = [
1308 node.id.as_str(),
1309 node.dom_id.as_str(),
1310 node.label.as_str(),
1311 node.label_type.as_str(),
1312 node.shape.as_str(),
1313 node.css_classes.as_str(),
1314 node.look.as_str(),
1315 node.node_id.as_str(),
1316 ];
1317 let total = strings.into_iter().fold(total, |subtotal, value| {
1318 subtotal.saturating_add(value.len())
1319 });
1320 let total = node.css_styles.iter().fold(total, |subtotal, value| {
1321 subtotal.saturating_add(value.len())
1322 });
1323 total.saturating_add(node.icon.as_deref().map_or(0, str::len))
1324 });
1325 let edge_label_bytes = model.edges.iter().fold(0usize, |total, edge| {
1326 [
1327 edge.id.as_str(),
1328 edge.start.as_str(),
1329 edge.end.as_str(),
1330 edge.edge_type.as_str(),
1331 edge.curve.as_str(),
1332 edge.thickness.as_str(),
1333 edge.look.as_str(),
1334 edge.classes.as_str(),
1335 ]
1336 .into_iter()
1337 .fold(total, |subtotal, value| {
1338 subtotal.saturating_add(value.len())
1339 })
1340 });
1341
1342 Self {
1343 nodes: model.nodes.len(),
1344 edges: model.edges.len(),
1345 label_bytes: node_label_bytes.saturating_add(edge_label_bytes),
1346 nesting_depth: model
1347 .nodes
1348 .iter()
1349 .filter_map(|node| usize::try_from(node.level).ok())
1350 .max()
1351 .unwrap_or(0),
1352 }
1353 }
1354
1355 pub fn as_model_complexity(self) -> ModelComplexity {
1356 ModelComplexity::new(
1357 self.nodes.saturating_add(self.edges),
1358 self.label_bytes,
1359 self.nesting_depth,
1360 )
1361 }
1362}
1363
1364#[cfg(feature = "diagram-class")]
1365impl ClassComplexity {
1366 pub fn from_model(model: &ClassDiagram) -> Self {
1367 let class_label_bytes = model
1368 .classes
1369 .values()
1370 .map(|node| {
1371 node.id
1372 .len()
1373 .saturating_add(node.label.len())
1374 .saturating_add(node.text.len())
1375 .saturating_add(node.type_param.len())
1376 .saturating_add(node.css_classes.len())
1377 .saturating_add(node.tooltip.as_deref().map(str::len).unwrap_or(0))
1378 .saturating_add(node.link.as_deref().map(str::len).unwrap_or(0))
1379 .saturating_add(
1380 node.members
1381 .iter()
1382 .chain(node.methods.iter())
1383 .map(|member| {
1384 member
1385 .display_text
1386 .len()
1387 .saturating_add(member.id.len())
1388 .saturating_add(member.parameters.len())
1389 .saturating_add(member.return_type.len())
1390 })
1391 .sum::<usize>(),
1392 )
1393 .saturating_add(node.annotations.iter().map(String::len).sum::<usize>())
1394 .saturating_add(node.styles.iter().map(String::len).sum::<usize>())
1395 })
1396 .sum::<usize>();
1397 let relation_label_bytes = model
1398 .relations
1399 .iter()
1400 .map(|rel| {
1401 rel.id
1402 .len()
1403 .saturating_add(rel.id1.len())
1404 .saturating_add(rel.id2.len())
1405 .saturating_add(rel.title.len())
1406 .saturating_add(rel.relation_title_1.as_deref().map_or(0, str::len))
1407 .saturating_add(rel.relation_title_2.as_deref().map_or(0, str::len))
1408 })
1409 .sum::<usize>();
1410 let note_label_bytes = model
1411 .notes
1412 .iter()
1413 .map(|note| {
1414 note.id
1415 .len()
1416 .saturating_add(note.text.len())
1417 .saturating_add(note.class_id.as_deref().map(str::len).unwrap_or(0))
1418 })
1419 .sum::<usize>();
1420 let interface_label_bytes = model
1421 .interfaces
1422 .iter()
1423 .map(|iface| {
1424 iface
1425 .id
1426 .len()
1427 .saturating_add(iface.label.len())
1428 .saturating_add(iface.class_id.len())
1429 })
1430 .sum::<usize>();
1431 let namespace_label_bytes = model
1432 .namespaces
1433 .values()
1434 .map(|namespace| {
1435 namespace
1436 .id
1437 .len()
1438 .saturating_add(namespace.label.len())
1439 .saturating_add(namespace.parent.as_deref().map(str::len).unwrap_or(0))
1440 })
1441 .sum::<usize>();
1442
1443 Self {
1444 nodes: model
1445 .classes
1446 .len()
1447 .saturating_add(model.notes.len())
1448 .saturating_add(model.interfaces.len())
1449 .saturating_add(model.namespaces.len()),
1450 edges: model.relations.len().saturating_add(
1451 model
1452 .notes
1453 .iter()
1454 .filter(|note| note.class_id.is_some())
1455 .count(),
1456 ),
1457 namespaces: model.namespaces.len(),
1458 label_bytes: class_label_bytes
1459 .saturating_add(relation_label_bytes)
1460 .saturating_add(note_label_bytes)
1461 .saturating_add(interface_label_bytes)
1462 .saturating_add(namespace_label_bytes),
1463 namespace_depth: class_namespace_depth(model),
1464 }
1465 }
1466
1467 pub fn as_model_complexity(self) -> ModelComplexity {
1468 ModelComplexity::new(
1469 self.nodes.saturating_add(self.edges),
1470 self.label_bytes,
1471 self.namespace_depth,
1472 )
1473 }
1474}
1475
1476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1477#[cfg(feature = "diagram-zenuml")]
1478pub struct ZenumlComplexity {
1479 pub participants: usize,
1480 pub groups: usize,
1481 pub statements: usize,
1482 pub fragments: usize,
1483 pub label_bytes: usize,
1484 pub nesting_depth: usize,
1485}
1486
1487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1488#[cfg(feature = "diagram-sequence")]
1489pub struct SequenceComplexity {
1490 pub messages: usize,
1491 pub block_depth: usize,
1492 pub items: usize,
1493 pub text_bytes: usize,
1494 pub nesting_depth: usize,
1495}
1496
1497#[cfg(feature = "diagram-sequence")]
1498impl SequenceComplexity {
1499 pub fn from_model(model: &SequenceDiagramRenderModel) -> Self {
1500 const ACTOR_PROPERTY_VALUE_DEPTH: usize = 4;
1501 const NOTE_ACTOR_VALUE_DEPTH: usize = 3;
1502
1503 let mut depth = 0usize;
1504 let mut block_depth = 0usize;
1505 for message in &model.messages {
1506 match message.message_type {
1507 10 | 12 | 15 | 19 | 22 | 27 | 30 | 32 => {
1508 depth = depth.saturating_add(1);
1509 block_depth = block_depth.max(depth);
1510 }
1511 11 | 14 | 16 | 21 | 23 | 29 | 31 => {
1512 depth = depth.saturating_sub(1);
1513 }
1514 _ => {}
1515 }
1516 }
1517 let common_text_bytes = [
1518 model.acc_title.as_deref(),
1519 model.acc_descr.as_deref(),
1520 model.title.as_deref(),
1521 ]
1522 .into_iter()
1523 .flatten()
1524 .fold(0usize, |total, value| total.saturating_add(value.len()));
1525 let actor_order_text_bytes = model
1526 .actor_order
1527 .iter()
1528 .fold(0usize, |total, value| total.saturating_add(value.len()));
1529 let mut arbitrary_json = JsonValueComplexity::default();
1530 let actor_text_bytes = model.actors.iter().fold(0usize, |total, (key, actor)| {
1531 let links = json_map_complexity(&actor.links, ACTOR_PROPERTY_VALUE_DEPTH);
1532 let properties = json_map_complexity(&actor.properties, ACTOR_PROPERTY_VALUE_DEPTH);
1533 arbitrary_json.merge(links);
1534 arbitrary_json.merge(properties);
1535 total
1536 .saturating_add(key.len())
1537 .saturating_add(actor.name.len())
1538 .saturating_add(actor.description.len())
1539 .saturating_add(actor.actor_type.len())
1540 .saturating_add(links.text_bytes)
1541 .saturating_add(properties.text_bytes)
1542 });
1543 let box_text_bytes = model.boxes.iter().fold(0usize, |total, sequence_box| {
1544 sequence_box.actor_keys.iter().fold(
1545 total
1546 .saturating_add(sequence_box.fill.len())
1547 .saturating_add(sequence_box.name.as_deref().map_or(0, str::len)),
1548 |subtotal, actor| subtotal.saturating_add(actor.len()),
1549 )
1550 });
1551 let message_text_bytes = model.messages.iter().fold(0usize, |total, message| {
1552 total
1553 .saturating_add(message.id.len())
1554 .saturating_add(message.from.as_deref().map_or(0, str::len))
1555 .saturating_add(message.to.as_deref().map_or(0, str::len))
1556 .saturating_add(message.message_text().len())
1557 });
1558 let note_text_bytes = model.notes.iter().fold(0usize, |total, note| {
1559 let actor = json_value_complexity(¬e.actor, NOTE_ACTOR_VALUE_DEPTH);
1560 arbitrary_json.merge(actor);
1561 total
1562 .saturating_add(actor.text_bytes)
1563 .saturating_add(note.message.len())
1564 });
1565 let lifecycle_text_bytes = model
1566 .created_actors
1567 .keys()
1568 .chain(model.destroyed_actors.keys())
1569 .fold(0usize, |total, value| total.saturating_add(value.len()));
1570 Self {
1571 messages: model.messages.len(),
1572 block_depth,
1573 items: model
1574 .actors
1575 .len()
1576 .saturating_add(model.boxes.len())
1577 .saturating_add(model.messages.len())
1578 .saturating_add(model.notes.len())
1579 .saturating_add(arbitrary_json.items),
1580 text_bytes: common_text_bytes
1581 .saturating_add(actor_order_text_bytes)
1582 .saturating_add(actor_text_bytes)
1583 .saturating_add(box_text_bytes)
1584 .saturating_add(message_text_bytes)
1585 .saturating_add(note_text_bytes)
1586 .saturating_add(lifecycle_text_bytes),
1587 nesting_depth: block_depth.max(arbitrary_json.nesting_depth),
1588 }
1589 }
1590
1591 pub fn as_model_complexity(self) -> ModelComplexity {
1592 ModelComplexity::new(self.items, self.text_bytes, self.nesting_depth)
1593 }
1594}
1595
1596#[cfg(feature = "diagram-zenuml")]
1597impl ZenumlComplexity {
1598 pub fn from_model(model: &ZenumlDiagramRenderModel) -> Self {
1599 let common_label_bytes = [model.title.as_deref(), model.starter.as_deref()]
1600 .into_iter()
1601 .flatten()
1602 .fold(0usize, |total, value| total.saturating_add(value.len()));
1603 let participant_label_bytes =
1604 model
1605 .participants
1606 .iter()
1607 .fold(0usize, |total, participant| {
1608 [
1609 Some(participant.name.as_str()),
1610 participant.label.as_deref(),
1611 participant.participant_type.as_deref(),
1612 participant.stereotype.as_deref(),
1613 participant.emoji.as_deref(),
1614 participant.width_source.as_deref(),
1615 participant.color.as_deref(),
1616 participant.group_id.as_deref(),
1617 ]
1618 .into_iter()
1619 .flatten()
1620 .fold(total, |subtotal, value| {
1621 subtotal.saturating_add(value.len())
1622 })
1623 });
1624 let group_label_bytes = model.groups.iter().fold(0usize, |total, group| {
1625 group.participant_names.iter().fold(
1626 total.saturating_add(group.id.as_deref().map_or(0, str::len)),
1627 |subtotal, name| subtotal.saturating_add(name.len()),
1628 )
1629 });
1630 let mut complexity = Self {
1631 participants: model.participants.len(),
1632 groups: model.groups.len(),
1633 statements: 0,
1634 fragments: 0,
1635 label_bytes: common_label_bytes
1636 .saturating_add(participant_label_bytes)
1637 .saturating_add(group_label_bytes),
1638 nesting_depth: 0,
1639 };
1640 let mut pending = vec![(model.statements.as_slice(), 0usize)];
1641 while let Some((statements, nesting_depth)) = pending.pop() {
1642 complexity.nesting_depth = complexity.nesting_depth.max(nesting_depth);
1643 for statement in statements {
1644 complexity.statements = complexity.statements.saturating_add(1);
1645 complexity.label_bytes = complexity
1646 .label_bytes
1647 .saturating_add(statement.comment.as_deref().map_or(0, str::len));
1648 match &statement.kind {
1649 ZenumlStatementKind::Message {
1650 explicit_from,
1651 resolved_from,
1652 resolved_to,
1653 label,
1654 assignment,
1655 body,
1656 ..
1657 } => {
1658 complexity.label_bytes = complexity
1659 .label_bytes
1660 .saturating_add(explicit_from.as_deref().map_or(0, str::len))
1661 .saturating_add(resolved_from.as_deref().map_or(0, str::len))
1662 .saturating_add(resolved_to.as_deref().map_or(0, str::len))
1663 .saturating_add(label.len())
1664 .saturating_add(assignment.as_deref().map_or(0, str::len));
1665 pending.push((body, nesting_depth.saturating_add(1)));
1666 }
1667 ZenumlStatementKind::Creation {
1668 resolved_from,
1669 resolved_to,
1670 constructor,
1671 parameters,
1672 assignment,
1673 label,
1674 body,
1675 ..
1676 } => {
1677 complexity.label_bytes = complexity
1678 .label_bytes
1679 .saturating_add(resolved_from.as_deref().map_or(0, str::len))
1680 .saturating_add(resolved_to.len())
1681 .saturating_add(constructor.len())
1682 .saturating_add(parameters.len())
1683 .saturating_add(assignment.as_deref().map_or(0, str::len))
1684 .saturating_add(label.len());
1685 pending.push((body, nesting_depth.saturating_add(1)));
1686 }
1687 ZenumlStatementKind::Return {
1688 explicit_from,
1689 resolved_from,
1690 explicit_to,
1691 resolved_to,
1692 label,
1693 } => {
1694 complexity.label_bytes = complexity
1695 .label_bytes
1696 .saturating_add(explicit_from.as_deref().map_or(0, str::len))
1697 .saturating_add(resolved_from.as_deref().map_or(0, str::len))
1698 .saturating_add(explicit_to.as_deref().map_or(0, str::len))
1699 .saturating_add(resolved_to.as_deref().map_or(0, str::len))
1700 .saturating_add(label.len());
1701 }
1702 ZenumlStatementKind::Fragment {
1703 label, sections, ..
1704 } => {
1705 complexity.fragments = complexity.fragments.saturating_add(1);
1706 complexity.label_bytes = complexity
1707 .label_bytes
1708 .saturating_add(label.as_deref().map_or(0, str::len));
1709 for section in sections {
1710 complexity.label_bytes = complexity
1711 .label_bytes
1712 .saturating_add(section.label.as_deref().map_or(0, str::len));
1713 pending.push((§ion.statements, nesting_depth.saturating_add(1)));
1714 }
1715 }
1716 ZenumlStatementKind::Reference {
1717 participants,
1718 label,
1719 } => {
1720 complexity.label_bytes = participants.iter().fold(
1721 complexity.label_bytes.saturating_add(label.len()),
1722 |total, participant| total.saturating_add(participant.len()),
1723 );
1724 }
1725 ZenumlStatementKind::Divider { label } => {
1726 complexity.label_bytes = complexity.label_bytes.saturating_add(label.len());
1727 }
1728 }
1729 }
1730 }
1731 complexity
1732 }
1733
1734 pub fn as_model_complexity(self) -> ModelComplexity {
1735 ModelComplexity::new(
1736 self.participants
1737 .saturating_add(self.groups)
1738 .saturating_add(self.statements),
1739 self.label_bytes,
1740 self.nesting_depth,
1741 )
1742 }
1743}
1744
1745#[cfg(any(
1746 feature = "diagram-flowchart",
1747 feature = "diagram-swimlane",
1748 feature = "diagram-agentflow"
1749))]
1750fn flowchart_text_bytes(model: &FlowchartModel) -> usize {
1751 let mut total = [
1752 Some(model.keyword.as_str()),
1753 model.acc_title.as_deref(),
1754 model.acc_descr.as_deref(),
1755 model.direction.as_deref(),
1756 ]
1757 .into_iter()
1758 .flatten()
1759 .fold(0usize, |subtotal, value| {
1760 subtotal.saturating_add(value.len())
1761 });
1762 for (id, styles) in &model.class_defs {
1763 total = styles
1764 .iter()
1765 .fold(total.saturating_add(id.len()), |subtotal, value| {
1766 subtotal.saturating_add(value.len())
1767 });
1768 }
1769 total = model.vertex_calls.iter().fold(total, |subtotal, value| {
1770 subtotal.saturating_add(value.len())
1771 });
1772 if let Some(defaults) = &model.edge_defaults {
1773 total = total.saturating_add(defaults.interpolate.as_deref().map_or(0, str::len));
1774 total = defaults.style.iter().fold(total, |subtotal, value| {
1775 subtotal.saturating_add(value.len())
1776 });
1777 }
1778 for node in &model.nodes {
1779 for value in [
1780 Some(node.id.as_str()),
1781 node.label.as_deref(),
1782 node.label_type.as_deref(),
1783 node.layout_shape.as_deref(),
1784 node.shape.as_deref(),
1785 node.icon.as_deref(),
1786 node.form.as_deref(),
1787 node.pos.as_deref(),
1788 node.img.as_deref(),
1789 node.constraint.as_deref(),
1790 node.link.as_deref(),
1791 node.link_target.as_deref(),
1792 ]
1793 .into_iter()
1794 .flatten()
1795 {
1796 total = total.saturating_add(value.len());
1797 }
1798 total = node
1799 .classes
1800 .iter()
1801 .chain(&node.styles)
1802 .fold(total, |subtotal, value| {
1803 subtotal.saturating_add(value.len())
1804 });
1805 }
1806 for edge in &model.edges {
1807 for value in [
1808 Some(edge.id.as_str()),
1809 Some(edge.from.as_str()),
1810 Some(edge.to.as_str()),
1811 edge.label.as_deref(),
1812 edge.label_type.as_deref(),
1813 edge.edge_type.as_deref(),
1814 Some(edge.arrow.as_str()),
1815 edge.stroke.as_deref(),
1816 edge.interpolate.as_deref(),
1817 edge.animation.as_deref(),
1818 ]
1819 .into_iter()
1820 .flatten()
1821 {
1822 total = total.saturating_add(value.len());
1823 }
1824 total = edge
1825 .classes
1826 .iter()
1827 .chain(&edge.style)
1828 .fold(total, |subtotal, value| {
1829 subtotal.saturating_add(value.len())
1830 });
1831 }
1832 for subgraph in &model.subgraphs {
1833 for value in [
1834 Some(subgraph.id.as_str()),
1835 Some(subgraph.title.as_str()),
1836 subgraph.dir.as_deref(),
1837 subgraph.label_type.as_deref(),
1838 ]
1839 .into_iter()
1840 .flatten()
1841 {
1842 total = total.saturating_add(value.len());
1843 }
1844 total = subgraph
1845 .classes
1846 .iter()
1847 .chain(&subgraph.styles)
1848 .chain(&subgraph.nodes)
1849 .fold(total, |subtotal, value| {
1850 subtotal.saturating_add(value.len())
1851 });
1852 }
1853 total = model.tooltips.iter().fold(total, |subtotal, (id, value)| {
1854 subtotal
1855 .saturating_add(id.len())
1856 .saturating_add(value.len())
1857 });
1858 total
1859}
1860
1861#[cfg(any(
1862 feature = "diagram-flowchart",
1863 feature = "diagram-swimlane",
1864 feature = "diagram-agentflow"
1865))]
1866fn flowchart_subgraph_depth(model: &FlowchartModel) -> usize {
1867 if model.subgraphs.is_empty() {
1868 return 0;
1869 }
1870 let indices = model
1871 .subgraphs
1872 .iter()
1873 .enumerate()
1874 .map(|(index, subgraph)| (subgraph.id.as_str(), index))
1875 .collect::<std::collections::HashMap<_, _>>();
1876 let mut children = vec![Vec::new(); model.subgraphs.len()];
1877 let mut incoming = vec![0usize; model.subgraphs.len()];
1878 for (parent, subgraph) in model.subgraphs.iter().enumerate() {
1879 for member in &subgraph.nodes {
1880 let Some(&child) = indices.get(member.as_str()) else {
1881 continue;
1882 };
1883 children[parent].push(child);
1884 incoming[child] = incoming[child].saturating_add(1);
1885 }
1886 }
1887 let mut queue = std::collections::VecDeque::new();
1888 let mut depths = vec![1usize; model.subgraphs.len()];
1889 for (index, incoming) in incoming.iter().enumerate() {
1890 if *incoming == 0 {
1891 queue.push_back(index);
1892 }
1893 }
1894 while let Some(parent) = queue.pop_front() {
1895 for &child in &children[parent] {
1896 depths[child] = depths[child].max(depths[parent].saturating_add(1));
1897 incoming[child] = incoming[child].saturating_sub(1);
1898 if incoming[child] == 0 {
1899 queue.push_back(child);
1900 }
1901 }
1902 }
1903 depths.into_iter().max().unwrap_or(0)
1904}
1905
1906#[cfg(feature = "diagram-class")]
1907fn class_namespace_depth(model: &ClassDiagram) -> usize {
1908 if model.namespaces.is_empty() {
1909 return 0;
1910 }
1911 let indices = model
1912 .namespaces
1913 .keys()
1914 .enumerate()
1915 .map(|(index, id)| (id.as_str(), index))
1916 .collect::<std::collections::HashMap<_, _>>();
1917 let parents = model
1918 .namespaces
1919 .values()
1920 .map(|namespace| {
1921 namespace
1922 .parent
1923 .as_deref()
1924 .and_then(|parent| indices.get(parent).copied())
1925 })
1926 .collect::<Vec<_>>();
1927 let mut depths = vec![0usize; parents.len()];
1928 let mut completed = vec![false; parents.len()];
1929 for start in 0..parents.len() {
1930 if completed[start] {
1931 continue;
1932 }
1933 let mut path = Vec::new();
1934 let mut positions = std::collections::HashMap::new();
1935 let mut cursor = Some(start);
1936 while let Some(index) = cursor {
1937 if completed[index] || positions.contains_key(&index) {
1938 break;
1939 }
1940 positions.insert(index, path.len());
1941 path.push(index);
1942 cursor = parents[index];
1943 }
1944 let mut depth = cursor
1945 .filter(|index| completed[*index])
1946 .map(|index| depths[index])
1947 .unwrap_or(0);
1948 for index in path.into_iter().rev() {
1949 depth = depth.saturating_add(1);
1950 depths[index] = depth;
1951 completed[index] = true;
1952 }
1953 }
1954 depths.into_iter().max().unwrap_or(0)
1955}
1956
1957#[cfg(feature = "diagram-kanban")]
1958fn kanban_nesting_depth(model: &KanbanDiagramRenderModel) -> usize {
1959 let indices = model
1960 .nodes
1961 .iter()
1962 .enumerate()
1963 .map(|(index, node)| (node.id.as_str(), index))
1964 .collect::<std::collections::HashMap<_, _>>();
1965 let parents = model
1966 .nodes
1967 .iter()
1968 .map(|node| {
1969 node.parent_id
1970 .as_deref()
1971 .and_then(|parent| indices.get(parent).copied())
1972 })
1973 .collect::<Vec<_>>();
1974 let mut depths = vec![0usize; parents.len()];
1975 let mut completed = vec![false; parents.len()];
1976 for start in 0..parents.len() {
1977 if completed[start] {
1978 continue;
1979 }
1980 let mut path = Vec::new();
1981 let mut positions = std::collections::HashMap::new();
1982 let mut cursor = Some(start);
1983 while let Some(index) = cursor {
1984 if completed[index] || positions.contains_key(&index) {
1985 break;
1986 }
1987 positions.insert(index, path.len());
1988 path.push(index);
1989 cursor = parents[index];
1990 }
1991 let mut depth = cursor
1992 .filter(|index| completed[*index])
1993 .map(|index| depths[index])
1994 .unwrap_or(0);
1995 for index in path.into_iter().rev() {
1996 depth = depth.saturating_add(1);
1997 depths[index] = depth;
1998 completed[index] = true;
1999 }
2000 }
2001 depths.into_iter().max().unwrap_or(0)
2002}
2003
2004#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2005#[cfg(any(feature = "diagram-sequence", feature = "diagram-treemap"))]
2006struct JsonValueComplexity {
2007 items: usize,
2008 text_bytes: usize,
2009 nesting_depth: usize,
2010}
2011
2012#[cfg(feature = "diagram-sequence")]
2013impl JsonValueComplexity {
2014 fn merge(&mut self, other: Self) {
2015 self.items = self.items.saturating_add(other.items);
2016 self.text_bytes = self.text_bytes.saturating_add(other.text_bytes);
2017 self.nesting_depth = self.nesting_depth.max(other.nesting_depth);
2018 }
2019}
2020
2021#[cfg(any(feature = "diagram-sequence", feature = "diagram-treemap"))]
2022enum JsonTraversalFrame<'a> {
2023 Value(&'a serde_json::Value, usize),
2024 Array(std::slice::Iter<'a, serde_json::Value>, usize),
2025 Object(serde_json::map::Iter<'a>, usize),
2026}
2027
2028#[cfg(feature = "diagram-sequence")]
2029fn json_map_complexity(
2030 map: &serde_json::Map<String, serde_json::Value>,
2031 value_depth: usize,
2032) -> JsonValueComplexity {
2033 let mut complexity = JsonValueComplexity {
2034 items: map.len(),
2035 text_bytes: map
2036 .keys()
2037 .fold(0usize, |total, key| total.saturating_add(key.len())),
2038 nesting_depth: 0,
2039 };
2040 for value in map.values() {
2041 complexity.merge(json_value_complexity(value, value_depth));
2042 }
2043 complexity
2044}
2045
2046#[cfg(any(feature = "diagram-sequence", feature = "diagram-treemap"))]
2048fn json_value_complexity(value: &serde_json::Value, initial_depth: usize) -> JsonValueComplexity {
2049 let mut complexity = JsonValueComplexity::default();
2050 let mut pending = vec![JsonTraversalFrame::Value(value, initial_depth)];
2051 while let Some(frame) = pending.pop() {
2052 match frame {
2053 JsonTraversalFrame::Value(value, depth) => {
2054 complexity.nesting_depth = complexity.nesting_depth.max(depth);
2055 match value {
2056 serde_json::Value::String(value) => {
2057 complexity.text_bytes = complexity.text_bytes.saturating_add(value.len());
2058 }
2059 serde_json::Value::Array(values) => {
2060 complexity.items = complexity.items.saturating_add(values.len());
2061 pending.push(JsonTraversalFrame::Array(
2062 values.iter(),
2063 depth.saturating_add(1),
2064 ));
2065 }
2066 serde_json::Value::Object(values) => {
2067 complexity.items = complexity.items.saturating_add(values.len());
2068 pending.push(JsonTraversalFrame::Object(
2069 values.iter(),
2070 depth.saturating_add(1),
2071 ));
2072 }
2073 serde_json::Value::Null
2074 | serde_json::Value::Bool(_)
2075 | serde_json::Value::Number(_) => {}
2076 }
2077 }
2078 JsonTraversalFrame::Array(mut values, depth) => {
2079 if let Some(value) = values.next() {
2080 pending.push(JsonTraversalFrame::Array(values, depth));
2081 pending.push(JsonTraversalFrame::Value(value, depth));
2082 }
2083 }
2084 JsonTraversalFrame::Object(mut values, depth) => {
2085 if let Some((key, value)) = values.next() {
2086 complexity.text_bytes = complexity.text_bytes.saturating_add(key.len());
2087 pending.push(JsonTraversalFrame::Object(values, depth));
2088 pending.push(JsonTraversalFrame::Value(value, depth));
2089 }
2090 }
2091 }
2092 }
2093 complexity
2094}
2095
2096#[cfg(feature = "diagram-treemap")]
2097fn json_text_bytes(value: &serde_json::Value) -> usize {
2098 json_value_complexity(value, 0).text_bytes
2099}
2100
2101#[cfg(feature = "diagram-treemap")]
2102fn json_item_count(value: &serde_json::Value) -> usize {
2103 json_value_complexity(value, 0).items
2104}
2105
2106#[cfg(test)]
2107mod tests {
2108 use super::*;
2109
2110 #[test]
2111 fn profile_values_are_single_source_for_input_limits() {
2112 assert_eq!(
2113 RESOURCE_PROFILE_DESCRIPTORS.len(),
2114 ResourceProfile::ALL.len()
2115 );
2116 assert_eq!(
2117 INPUT_RESOURCE_LIMIT_DESCRIPTORS.len(),
2118 INPUT_RESOURCE_LIMIT_COUNT
2119 );
2120 for profile in ResourceProfile::ALL {
2121 let policy = InputResourcePolicy::for_profile(profile);
2122 for limit in InputResourceLimitId::ALL {
2123 assert_eq!(policy.base_value(limit), policy.value(limit));
2124 }
2125 }
2126
2127 let interactive = InputResourcePolicy::for_profile(ResourceProfile::Interactive);
2128 for (limit, expected) in [
2129 (InputResourceLimitId::MaxModelItems, Some(32_000)),
2130 (InputResourceLimitId::MaxModelTextBytes, Some(2 * MIB)),
2131 (InputResourceLimitId::MaxModelNestingDepth, Some(256)),
2132 ] {
2133 assert_eq!(interactive.value(limit), expected, "{limit:?}");
2134 }
2135 }
2136
2137 #[test]
2138 fn constrained_policy_rejects_source_bytes() {
2139 let source_error = InputResourcePolicy::for_profile(ResourceProfile::Constrained)
2140 .apply_for_test(InputResourceLimitId::MaxSourceBytes, 4)
2141 .check_source_bytes("12345")
2142 .unwrap_err();
2143 assert_eq!(source_error.limit, "max_source_bytes");
2144 }
2145
2146 #[cfg(feature = "diagram-flowchart")]
2147 #[test]
2148 fn constrained_policy_rejects_flowchart_cardinality() {
2149 let parsed = crate::Engine::new()
2150 .parse_diagram_for_render_model_sync(
2151 "flowchart TD\nA --> B",
2152 crate::ParseOptions::strict(),
2153 )
2154 .unwrap()
2155 .unwrap();
2156 let model_error = InputResourcePolicy::for_profile(ResourceProfile::Constrained)
2157 .apply_for_test(InputResourceLimitId::MaxModelItems, 2)
2158 .check_render_model(parsed.model())
2159 .unwrap_err();
2160 assert_eq!(model_error.limit, "max_model_items");
2161 }
2162
2163 #[cfg(feature = "diagram-flowchart")]
2164 #[test]
2165 fn parsed_render_resource_checks_include_parser_owned_flowchart_label_sources() {
2166 let parsed = crate::Engine::new()
2167 .parse_diagram_for_render_model_sync(
2168 "flowchart LR\nA[\" entity \"] --> B\n",
2169 crate::ParseOptions::strict(),
2170 )
2171 .unwrap()
2172 .unwrap();
2173 let model_complexity = ModelComplexity::from_render_model(parsed.model());
2174 assert!(parsed.retained_render_context_bytes() > 0);
2175
2176 let policy = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2177 .with_limit(
2178 InputResourceLimitId::MaxModelTextBytes,
2179 model_complexity.text_bytes,
2180 )
2181 .unwrap();
2182 policy
2183 .check_render_model(parsed.model())
2184 .expect("the public model alone fits its exact text budget");
2185 let error = policy.check_parsed_render(&parsed).unwrap_err();
2186 assert_eq!(error.limit, "max_model_text_bytes");
2187 assert!(error.actual > model_complexity.text_bytes);
2188 }
2189
2190 #[cfg(feature = "diagram-sequence")]
2191 #[test]
2192 fn sequence_complexity_bounds_messages_and_nested_frames() {
2193 let parsed = crate::Engine::new()
2194 .parse_diagram_for_render_model_sync(
2195 "sequenceDiagram\nloop outer\nrect rgb(240,240,240)\nA->>B: hi\nend\nend",
2196 crate::ParseOptions::strict(),
2197 )
2198 .unwrap()
2199 .unwrap();
2200 let crate::RenderSemanticModel::Sequence(model) = parsed.model() else {
2201 panic!("expected Sequence model");
2202 };
2203 let complexity = SequenceComplexity::from_model(model);
2204 assert_eq!(complexity.block_depth, 2);
2205 assert_eq!(complexity.messages, model.messages.len());
2206
2207 let error = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2208 .with_limit(InputResourceLimitId::MaxModelNestingDepth, 1)
2209 .unwrap()
2210 .check_render_model(parsed.model())
2211 .unwrap_err();
2212 assert_eq!(error.limit, "max_model_nesting_depth");
2213 assert_eq!(error.actual, 2);
2214 assert_eq!(error.max, 1);
2215 }
2216
2217 #[cfg(feature = "diagram-sequence")]
2218 #[test]
2219 fn sequence_complexity_counts_arbitrary_json_items_and_depth_iteratively() {
2220 let parsed = crate::Engine::new()
2221 .parse_diagram_for_render_model_sync(
2222 "sequenceDiagram\nparticipant A",
2223 crate::ParseOptions::strict(),
2224 )
2225 .unwrap()
2226 .unwrap();
2227 let (_, mut render_model) = parsed.into_parts();
2228 let RenderSemanticModel::Sequence(model) = &mut render_model else {
2229 panic!("expected Sequence model");
2230 };
2231 let mut nested = serde_json::Value::Null;
2232 for _ in 0..300 {
2233 nested = serde_json::Value::Array(vec![nested]);
2234 }
2235 model
2236 .actors
2237 .get_mut("A")
2238 .unwrap()
2239 .properties
2240 .insert("payload".to_string(), nested);
2241
2242 let complexity = SequenceComplexity::from_model(model);
2243 assert_eq!(complexity.nesting_depth, 304);
2244 assert_eq!(complexity.items, 302);
2245
2246 let exact = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2247 .with_limit(
2248 InputResourceLimitId::MaxModelNestingDepth,
2249 complexity.nesting_depth,
2250 )
2251 .unwrap();
2252 exact
2253 .check_render_model(&render_model)
2254 .expect("the exact JSON depth boundary should pass");
2255
2256 let below = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2257 .with_limit(
2258 InputResourceLimitId::MaxModelNestingDepth,
2259 complexity.nesting_depth - 1,
2260 )
2261 .unwrap();
2262 let error = below
2263 .check_render_model(&render_model)
2264 .expect_err("the N-1 JSON depth boundary should fail");
2265 assert_eq!(error.limit, "max_model_nesting_depth");
2266 assert_eq!(error.actual, complexity.nesting_depth);
2267 assert_eq!(error.max, complexity.nesting_depth - 1);
2268 }
2269
2270 #[cfg(feature = "diagram-mindmap")]
2271 #[test]
2272 fn mindmap_model_limits_cover_pre_layout_cardinality_and_labels() {
2273 let constrained = InputResourcePolicy::for_profile(ResourceProfile::Constrained);
2274 assert_eq!(
2275 constrained.value(InputResourceLimitId::MaxModelItems),
2276 Some(16_000)
2277 );
2278
2279 let parsed = crate::Engine::new()
2280 .parse_diagram_for_render_model_sync(
2281 "mindmap\n Root\n First child\n Second child\n",
2282 crate::ParseOptions::strict(),
2283 )
2284 .unwrap()
2285 .unwrap();
2286 let RenderSemanticModel::Mindmap(model) = parsed.model() else {
2287 panic!("expected Mindmap model");
2288 };
2289 let complexity = MindmapComplexity::from_model(model);
2290 assert_eq!(complexity.nodes, 3);
2291 assert_eq!(complexity.edges, 2);
2292 assert!(complexity.label_bytes >= "RootFirst childSecond child".len());
2293
2294 for (id, max, expected_limit) in [
2295 (InputResourceLimitId::MaxModelItems, 4, "max_model_items"),
2296 (
2297 InputResourceLimitId::MaxModelTextBytes,
2298 1,
2299 "max_model_text_bytes",
2300 ),
2301 ] {
2302 let error = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2303 .with_limit(id, max)
2304 .unwrap()
2305 .check_render_model(parsed.model())
2306 .unwrap_err();
2307 assert_eq!(error.phase, InputResourceLimitPhase::Model);
2308 assert_eq!(error.limit, expected_limit);
2309 }
2310 }
2311
2312 #[cfg(any(
2313 feature = "diagram-kanban",
2314 feature = "diagram-radar",
2315 feature = "diagram-requirement"
2316 ))]
2317 #[test]
2318 fn generic_model_budget_covers_new_family_models_without_family_knobs() {
2319 let models = [
2320 #[cfg(feature = "diagram-kanban")]
2321 RenderSemanticModel::Kanban(KanbanDiagramRenderModel {
2322 nodes: vec![crate::diagrams::kanban::KanbanRenderNode::new(
2323 "todo", "Todo",
2324 )],
2325 }),
2326 #[cfg(feature = "diagram-radar")]
2327 RenderSemanticModel::Radar({
2328 let mut model = RadarDiagramRenderModel::default();
2329 model.axes = vec![crate::diagrams::radar::RadarRenderAxis {
2330 name: "speed".to_string(),
2331 label: "Speed".to_string(),
2332 }];
2333 model
2334 }),
2335 #[cfg(feature = "diagram-requirement")]
2336 RenderSemanticModel::Requirement(RequirementDiagramRenderModel {
2337 direction: "TB".to_string(),
2338 requirements: vec![crate::diagrams::requirement::RequirementRenderNode {
2339 name: "R1".to_string(),
2340 node_type: "requirement".to_string(),
2341 requirement_id: "1".to_string(),
2342 text: "Must render".to_string(),
2343 risk: "Low".to_string(),
2344 verify_method: "Test".to_string(),
2345 css_styles: Vec::new(),
2346 classes: Vec::new(),
2347 }],
2348 acc_title: None,
2349 acc_descr: None,
2350 elements: Vec::new(),
2351 relationships: Vec::new(),
2352 classes: Default::default(),
2353 }),
2354 ];
2355 for model in models {
2356 let error = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2357 .with_limit(InputResourceLimitId::MaxModelTextBytes, 1)
2358 .unwrap()
2359 .check_render_model(&model)
2360 .unwrap_err();
2361 assert_eq!(error.limit, "max_model_text_bytes");
2362 }
2363 }
2364
2365 #[cfg(feature = "diagram-kanban")]
2366 #[test]
2367 fn kanban_complexity_accounts_for_parent_chain_depth_without_quadratic_scans() {
2368 let root = KanbanDiagramRenderModel {
2369 nodes: ["section", "lane", "card"]
2370 .into_iter()
2371 .enumerate()
2372 .map(|(index, id)| {
2373 let mut node =
2374 crate::diagrams::kanban::KanbanRenderNode::new(id, format!("Node {id}"));
2375 if index > 0 {
2376 node.parent_id = Some(["section", "lane"][index - 1].to_string());
2377 }
2378 node
2379 })
2380 .collect(),
2381 };
2382
2383 assert_eq!(ModelComplexity::from_kanban(&root).nesting_depth, 3);
2384 }
2385
2386 #[cfg(feature = "diagram-treemap")]
2387 #[test]
2388 fn treemap_complexity_handles_deep_typed_trees_without_serde_recursion() {
2389 let mut node = crate::diagrams::treemap::TreemapNodeRenderModel {
2390 name: "leaf".to_string(),
2391 ..Default::default()
2392 };
2393 for index in (0..1_500).rev() {
2394 node = crate::diagrams::treemap::TreemapNodeRenderModel {
2395 name: format!("section{index}"),
2396 children: Some(vec![node]),
2397 ..Default::default()
2398 };
2399 }
2400 let model = TreemapDiagramRenderModel {
2401 root: node,
2402 ..Default::default()
2403 };
2404
2405 let complexity = TreemapComplexity::from_model(&model);
2406
2407 assert_eq!(complexity.nodes, 1_501);
2408 assert_eq!(complexity.nesting_depth, 1_500);
2409 assert!(complexity.label_bytes >= "leaf".len());
2410 }
2411
2412 #[cfg(feature = "diagram-ishikawa")]
2413 #[test]
2414 fn ishikawa_complexity_handles_deep_typed_trees_without_serde_recursion() {
2415 let mut node = crate::diagrams::ishikawa::IshikawaNodeRenderModel {
2416 text: "leaf".to_string(),
2417 children: Vec::new(),
2418 };
2419 for index in (0..1_500).rev() {
2420 node = crate::diagrams::ishikawa::IshikawaNodeRenderModel {
2421 text: format!("cause{index}"),
2422 children: vec![node],
2423 };
2424 }
2425 let model = IshikawaDiagramRenderModel {
2426 root: Some(node),
2427 ..Default::default()
2428 };
2429
2430 let complexity = IshikawaComplexity::from_model(&model);
2431
2432 assert_eq!(complexity.nodes, 1_501);
2433 assert_eq!(complexity.nesting_depth, 1_500);
2434 assert!(complexity.label_bytes >= "leaf".len());
2435 }
2436
2437 impl InputResourcePolicy {
2438 fn apply_for_test(mut self, id: InputResourceLimitId, value: usize) -> Self {
2439 self.apply_limit(id, value).unwrap();
2440 self
2441 }
2442 }
2443}