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_json(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) => model.model_complexity(),
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) => model.model_complexity(),
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 {
595 let mut counter = ModelComplexitySerializer::default();
596 if model.serialize(&mut counter).is_err() {
597 return Self::new(usize::MAX, usize::MAX, usize::MAX);
598 }
599 counter.finish()
600 }
601
602 pub fn from_json(model: &serde_json::Value) -> Self {
604 let complexity = json_value_complexity(model, 0);
605 Self::new(
606 complexity.items.max(1),
607 complexity.text_bytes,
608 complexity.container_depth,
609 )
610 }
611
612 #[cfg(feature = "diagram-kanban")]
613 pub fn from_kanban(model: &KanbanDiagramRenderModel) -> Self {
614 let text_bytes = model.nodes.iter().fold(0usize, |total, node| {
615 [
616 Some(node.id.as_str()),
617 Some(node.label.as_str()),
618 node.parent_id.as_deref(),
619 node.ticket.as_deref(),
620 node.priority.as_deref(),
621 node.assigned.as_deref(),
622 node.icon.as_deref(),
623 ]
624 .into_iter()
625 .flatten()
626 .fold(total, |subtotal, value| {
627 subtotal.saturating_add(value.len())
628 })
629 });
630 let nesting_depth = kanban_nesting_depth(model);
631 Self::new(model.nodes.len(), text_bytes, nesting_depth)
632 }
633
634 #[cfg(feature = "diagram-radar")]
635 pub fn from_radar(model: &RadarDiagramRenderModel) -> Self {
636 let common_text_bytes = [
637 model.title.as_deref(),
638 model.acc_title.as_deref(),
639 model.acc_descr.as_deref(),
640 Some(model.options.graticule.as_str()),
641 ]
642 .into_iter()
643 .flatten()
644 .fold(0usize, |total, value| total.saturating_add(value.len()));
645 let axis_text_bytes = model.axes.iter().fold(0usize, |total, axis| {
646 total
647 .saturating_add(axis.name.len())
648 .saturating_add(axis.label.len())
649 });
650 let (curve_items, curve_text_bytes) =
651 model
652 .curves
653 .iter()
654 .fold((0usize, 0usize), |(items, text_bytes), curve| {
655 (
656 items.saturating_add(curve.entries.len()),
657 text_bytes
658 .saturating_add(curve.name.len())
659 .saturating_add(curve.label.len()),
660 )
661 });
662 Self::new(
663 model
664 .axes
665 .len()
666 .saturating_add(model.curves.len())
667 .saturating_add(curve_items),
668 common_text_bytes
669 .saturating_add(axis_text_bytes)
670 .saturating_add(curve_text_bytes),
671 0,
672 )
673 }
674
675 #[cfg(feature = "diagram-requirement")]
676 pub fn from_requirement(model: &RequirementDiagramRenderModel) -> Self {
677 let common_text_bytes = [
678 model.acc_title.as_deref(),
679 model.acc_descr.as_deref(),
680 Some(model.direction.as_str()),
681 ]
682 .into_iter()
683 .flatten()
684 .fold(0usize, |total, value| total.saturating_add(value.len()));
685 let requirement_text_bytes = model.requirements.iter().fold(0usize, |total, node| {
686 let total = [
687 node.name.as_str(),
688 node.node_type.as_str(),
689 node.requirement_id.as_str(),
690 node.text.as_str(),
691 node.risk.as_str(),
692 node.verify_method.as_str(),
693 ]
694 .into_iter()
695 .fold(total, |subtotal, value| {
696 subtotal.saturating_add(value.len())
697 });
698 node.css_styles
699 .iter()
700 .chain(&node.classes)
701 .fold(total, |subtotal, value| {
702 subtotal.saturating_add(value.len())
703 })
704 });
705 let element_text_bytes = model.elements.iter().fold(0usize, |total, node| {
706 let total = [
707 node.name.as_str(),
708 node.element_type.as_str(),
709 node.doc_ref.as_str(),
710 ]
711 .into_iter()
712 .fold(total, |subtotal, value| {
713 subtotal.saturating_add(value.len())
714 });
715 node.css_styles
716 .iter()
717 .chain(&node.classes)
718 .fold(total, |subtotal, value| {
719 subtotal.saturating_add(value.len())
720 })
721 });
722 let relationship_text_bytes =
723 model
724 .relationships
725 .iter()
726 .fold(0usize, |total, relationship| {
727 [
728 relationship.rel_type.as_str(),
729 relationship.src.as_str(),
730 relationship.dst.as_str(),
731 ]
732 .into_iter()
733 .fold(total, |subtotal, value| {
734 subtotal.saturating_add(value.len())
735 })
736 });
737 let class_text_bytes = model.classes.iter().fold(0usize, |total, (key, class)| {
738 class.styles.iter().chain(&class.text_styles).fold(
739 total
740 .saturating_add(key.len())
741 .saturating_add(class.id.len()),
742 |subtotal, value| subtotal.saturating_add(value.len()),
743 )
744 });
745 Self::new(
746 model
747 .requirements
748 .len()
749 .saturating_add(model.elements.len())
750 .saturating_add(model.relationships.len())
751 .saturating_add(model.classes.len()),
752 common_text_bytes
753 .saturating_add(requirement_text_bytes)
754 .saturating_add(element_text_bytes)
755 .saturating_add(relationship_text_bytes)
756 .saturating_add(class_text_bytes),
757 0,
758 )
759 }
760}
761
762#[derive(Debug, Default)]
763struct ModelComplexitySerializer {
764 items: usize,
765 text_bytes: usize,
766 current_depth: usize,
767 max_depth: usize,
768}
769
770impl ModelComplexitySerializer {
771 fn enter(&mut self) {
772 self.current_depth = self.current_depth.saturating_add(1);
773 self.max_depth = self.max_depth.max(self.current_depth);
774 }
775
776 fn leave(&mut self) {
777 self.current_depth = self.current_depth.saturating_sub(1);
778 }
779
780 fn finish(self) -> ModelComplexity {
781 ModelComplexity::new(
782 self.items.max(1),
783 self.text_bytes,
784 self.max_depth.saturating_sub(1),
785 )
786 }
787}
788
789#[derive(Debug)]
790struct ModelComplexitySerializationError;
791
792impl std::fmt::Display for ModelComplexitySerializationError {
793 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
794 formatter.write_str("model complexity serialization failed")
795 }
796}
797
798impl std::error::Error for ModelComplexitySerializationError {}
799
800impl serde::ser::Error for ModelComplexitySerializationError {
801 fn custom<T: std::fmt::Display>(_message: T) -> Self {
802 Self
803 }
804}
805
806struct ModelCompound<'a> {
807 counter: &'a mut ModelComplexitySerializer,
808 count_items: bool,
809}
810
811impl<'a> ModelCompound<'a> {
812 fn new(counter: &'a mut ModelComplexitySerializer, count_items: bool) -> Self {
813 counter.enter();
814 Self {
815 counter,
816 count_items,
817 }
818 }
819
820 fn serialize_value<T: Serialize + ?Sized>(
821 &mut self,
822 value: &T,
823 ) -> Result<(), ModelComplexitySerializationError> {
824 if self.count_items {
825 self.counter.items = self.counter.items.saturating_add(1);
826 }
827 value.serialize(&mut *self.counter)
828 }
829}
830
831impl Drop for ModelCompound<'_> {
832 fn drop(&mut self) {
833 self.counter.leave();
834 }
835}
836
837impl SerializeSeq for ModelCompound<'_> {
838 type Ok = ();
839 type Error = ModelComplexitySerializationError;
840
841 fn serialize_element<T: Serialize + ?Sized>(
842 &mut self,
843 value: &T,
844 ) -> Result<Self::Ok, Self::Error> {
845 self.serialize_value(value)
846 }
847
848 fn end(self) -> Result<Self::Ok, Self::Error> {
849 Ok(())
850 }
851}
852
853impl SerializeTuple for ModelCompound<'_> {
854 type Ok = ();
855 type Error = ModelComplexitySerializationError;
856
857 fn serialize_element<T: Serialize + ?Sized>(
858 &mut self,
859 value: &T,
860 ) -> Result<Self::Ok, Self::Error> {
861 self.serialize_value(value)
862 }
863
864 fn end(self) -> Result<Self::Ok, Self::Error> {
865 Ok(())
866 }
867}
868
869impl SerializeTupleStruct for ModelCompound<'_> {
870 type Ok = ();
871 type Error = ModelComplexitySerializationError;
872
873 fn serialize_field<T: Serialize + ?Sized>(
874 &mut self,
875 value: &T,
876 ) -> Result<Self::Ok, Self::Error> {
877 self.serialize_value(value)
878 }
879
880 fn end(self) -> Result<Self::Ok, Self::Error> {
881 Ok(())
882 }
883}
884
885impl SerializeTupleVariant for ModelCompound<'_> {
886 type Ok = ();
887 type Error = ModelComplexitySerializationError;
888
889 fn serialize_field<T: Serialize + ?Sized>(
890 &mut self,
891 value: &T,
892 ) -> Result<Self::Ok, Self::Error> {
893 self.serialize_value(value)
894 }
895
896 fn end(self) -> Result<Self::Ok, Self::Error> {
897 Ok(())
898 }
899}
900
901impl SerializeMap for ModelCompound<'_> {
902 type Ok = ();
903 type Error = ModelComplexitySerializationError;
904
905 fn serialize_key<T: Serialize + ?Sized>(&mut self, key: &T) -> Result<Self::Ok, Self::Error> {
906 self.serialize_value(key)
907 }
908
909 fn serialize_value<T: Serialize + ?Sized>(
910 &mut self,
911 value: &T,
912 ) -> Result<Self::Ok, Self::Error> {
913 value.serialize(&mut *self.counter)
914 }
915
916 fn end(self) -> Result<Self::Ok, Self::Error> {
917 Ok(())
918 }
919}
920
921impl SerializeStruct for ModelCompound<'_> {
922 type Ok = ();
923 type Error = ModelComplexitySerializationError;
924
925 fn serialize_field<T: Serialize + ?Sized>(
926 &mut self,
927 _key: &'static str,
928 value: &T,
929 ) -> Result<Self::Ok, Self::Error> {
930 value.serialize(&mut *self.counter)
931 }
932
933 fn end(self) -> Result<Self::Ok, Self::Error> {
934 Ok(())
935 }
936}
937
938impl SerializeStructVariant for ModelCompound<'_> {
939 type Ok = ();
940 type Error = ModelComplexitySerializationError;
941
942 fn serialize_field<T: Serialize + ?Sized>(
943 &mut self,
944 _key: &'static str,
945 value: &T,
946 ) -> Result<Self::Ok, Self::Error> {
947 value.serialize(&mut *self.counter)
948 }
949
950 fn end(self) -> Result<Self::Ok, Self::Error> {
951 Ok(())
952 }
953}
954
955impl<'a> serde::Serializer for &'a mut ModelComplexitySerializer {
956 type Ok = ();
957 type Error = ModelComplexitySerializationError;
958 type SerializeSeq = ModelCompound<'a>;
959 type SerializeTuple = ModelCompound<'a>;
960 type SerializeTupleStruct = ModelCompound<'a>;
961 type SerializeTupleVariant = ModelCompound<'a>;
962 type SerializeMap = ModelCompound<'a>;
963 type SerializeStruct = ModelCompound<'a>;
964 type SerializeStructVariant = ModelCompound<'a>;
965
966 fn serialize_bool(self, _value: bool) -> Result<Self::Ok, Self::Error> {
967 Ok(())
968 }
969
970 fn serialize_i8(self, _value: i8) -> Result<Self::Ok, Self::Error> {
971 Ok(())
972 }
973
974 fn serialize_i16(self, _value: i16) -> Result<Self::Ok, Self::Error> {
975 Ok(())
976 }
977
978 fn serialize_i32(self, _value: i32) -> Result<Self::Ok, Self::Error> {
979 Ok(())
980 }
981
982 fn serialize_i64(self, _value: i64) -> Result<Self::Ok, Self::Error> {
983 Ok(())
984 }
985
986 fn serialize_i128(self, _value: i128) -> Result<Self::Ok, Self::Error> {
987 Ok(())
988 }
989
990 fn serialize_u8(self, _value: u8) -> Result<Self::Ok, Self::Error> {
991 Ok(())
992 }
993
994 fn serialize_u16(self, _value: u16) -> Result<Self::Ok, Self::Error> {
995 Ok(())
996 }
997
998 fn serialize_u32(self, _value: u32) -> Result<Self::Ok, Self::Error> {
999 Ok(())
1000 }
1001
1002 fn serialize_u64(self, _value: u64) -> Result<Self::Ok, Self::Error> {
1003 Ok(())
1004 }
1005
1006 fn serialize_u128(self, _value: u128) -> Result<Self::Ok, Self::Error> {
1007 Ok(())
1008 }
1009
1010 fn serialize_f32(self, _value: f32) -> Result<Self::Ok, Self::Error> {
1011 Ok(())
1012 }
1013
1014 fn serialize_f64(self, _value: f64) -> Result<Self::Ok, Self::Error> {
1015 Ok(())
1016 }
1017
1018 fn serialize_char(self, value: char) -> Result<Self::Ok, Self::Error> {
1019 self.text_bytes = self.text_bytes.saturating_add(value.len_utf8());
1020 Ok(())
1021 }
1022
1023 fn serialize_str(self, value: &str) -> Result<Self::Ok, Self::Error> {
1024 self.text_bytes = self.text_bytes.saturating_add(value.len());
1025 Ok(())
1026 }
1027
1028 fn serialize_bytes(self, value: &[u8]) -> Result<Self::Ok, Self::Error> {
1029 self.items = self.items.saturating_add(value.len());
1030 Ok(())
1031 }
1032
1033 fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
1034 Ok(())
1035 }
1036
1037 fn serialize_some<T: Serialize + ?Sized>(self, value: &T) -> Result<Self::Ok, Self::Error> {
1038 value.serialize(self)
1039 }
1040
1041 fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
1042 Ok(())
1043 }
1044
1045 fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
1046 Ok(())
1047 }
1048
1049 fn serialize_unit_variant(
1050 self,
1051 _name: &'static str,
1052 _variant_index: u32,
1053 _variant: &'static str,
1054 ) -> Result<Self::Ok, Self::Error> {
1055 Ok(())
1056 }
1057
1058 fn serialize_newtype_struct<T: Serialize + ?Sized>(
1059 self,
1060 _name: &'static str,
1061 value: &T,
1062 ) -> Result<Self::Ok, Self::Error> {
1063 value.serialize(self)
1064 }
1065
1066 fn serialize_newtype_variant<T: Serialize + ?Sized>(
1067 self,
1068 _name: &'static str,
1069 _variant_index: u32,
1070 _variant: &'static str,
1071 value: &T,
1072 ) -> Result<Self::Ok, Self::Error> {
1073 value.serialize(self)
1074 }
1075
1076 fn serialize_seq(self, _length: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
1077 Ok(ModelCompound::new(self, true))
1078 }
1079
1080 fn serialize_tuple(self, _length: usize) -> Result<Self::SerializeTuple, Self::Error> {
1081 Ok(ModelCompound::new(self, true))
1082 }
1083
1084 fn serialize_tuple_struct(
1085 self,
1086 _name: &'static str,
1087 _length: usize,
1088 ) -> Result<Self::SerializeTupleStruct, Self::Error> {
1089 Ok(ModelCompound::new(self, true))
1090 }
1091
1092 fn serialize_tuple_variant(
1093 self,
1094 _name: &'static str,
1095 _variant_index: u32,
1096 _variant: &'static str,
1097 _length: usize,
1098 ) -> Result<Self::SerializeTupleVariant, Self::Error> {
1099 Ok(ModelCompound::new(self, true))
1100 }
1101
1102 fn serialize_map(self, _length: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
1103 Ok(ModelCompound::new(self, true))
1104 }
1105
1106 fn serialize_struct(
1107 self,
1108 _name: &'static str,
1109 _length: usize,
1110 ) -> Result<Self::SerializeStruct, Self::Error> {
1111 Ok(ModelCompound::new(self, false))
1112 }
1113
1114 fn serialize_struct_variant(
1115 self,
1116 _name: &'static str,
1117 _variant_index: u32,
1118 _variant: &'static str,
1119 _length: usize,
1120 ) -> Result<Self::SerializeStructVariant, Self::Error> {
1121 Ok(ModelCompound::new(self, false))
1122 }
1123
1124 fn collect_str<T: std::fmt::Display + ?Sized>(
1125 self,
1126 value: &T,
1127 ) -> Result<Self::Ok, Self::Error> {
1128 self.text_bytes = self.text_bytes.saturating_add(value.to_string().len());
1129 Ok(())
1130 }
1131
1132 fn is_human_readable(&self) -> bool {
1133 true
1134 }
1135}
1136
1137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1138#[cfg(any(
1139 feature = "diagram-flowchart",
1140 feature = "diagram-swimlane",
1141 feature = "diagram-agentflow"
1142))]
1143pub struct FlowchartComplexity {
1144 pub nodes: usize,
1145 pub edges: usize,
1146 pub subgraphs: usize,
1147 pub label_bytes: usize,
1148 pub subgraph_depth: usize,
1149}
1150
1151#[cfg(any(
1152 feature = "diagram-flowchart",
1153 feature = "diagram-swimlane",
1154 feature = "diagram-agentflow"
1155))]
1156impl FlowchartComplexity {
1157 pub fn from_model(model: &FlowchartModel) -> Self {
1158 Self {
1159 nodes: model.nodes.len().saturating_add(model.subgraphs.len()),
1160 edges: model.edges.len(),
1161 subgraphs: model.subgraphs.len(),
1162 label_bytes: flowchart_text_bytes(model),
1163 subgraph_depth: flowchart_subgraph_depth(model),
1164 }
1165 }
1166
1167 pub fn as_model_complexity(self) -> ModelComplexity {
1168 ModelComplexity::new(
1169 self.nodes.saturating_add(self.edges),
1170 self.label_bytes,
1171 self.subgraph_depth,
1172 )
1173 }
1174}
1175
1176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1178#[cfg(feature = "diagram-treemap")]
1179pub struct TreemapComplexity {
1180 pub nodes: usize,
1181 pub classes: usize,
1182 pub label_bytes: usize,
1183 pub nesting_depth: usize,
1184}
1185
1186#[cfg(feature = "diagram-treemap")]
1187impl TreemapComplexity {
1188 pub fn from_model(model: &TreemapDiagramRenderModel) -> Self {
1189 let mut nodes = 0usize;
1190 let mut classes = 0usize;
1191 let mut label_bytes = [
1192 model.acc_title.as_deref(),
1193 model.acc_descr.as_deref(),
1194 model.title.as_deref(),
1195 ]
1196 .into_iter()
1197 .flatten()
1198 .fold(0usize, |total, value| total.saturating_add(value.len()));
1199 let mut nesting_depth = 0usize;
1200
1201 let mut pending = vec![(&model.root, 0usize)];
1202 let mut visited = vec![false; model.nodes.len()];
1203 while let Some((node, depth)) = pending.pop() {
1204 nodes = nodes.saturating_add(1);
1205 nesting_depth = nesting_depth.max(depth);
1206 label_bytes = label_bytes
1207 .saturating_add(node.name.len())
1208 .saturating_add(node.class_selector.as_deref().map_or(0, str::len))
1209 .saturating_add(
1210 node.css_compiled_styles
1211 .as_deref()
1212 .unwrap_or(&[])
1213 .iter()
1214 .map(String::len)
1215 .sum::<usize>(),
1216 );
1217 if let Some(value) = node.value.as_ref() {
1218 label_bytes = label_bytes.saturating_add(json_text_bytes(value));
1219 nodes = nodes.saturating_add(json_item_count(value));
1220 }
1221 if let Some(children) = node.children.as_ref() {
1222 for child in children.iter().rev() {
1223 if let Some(record) = model.nodes.get(*child)
1224 && !visited[*child]
1225 {
1226 visited[*child] = true;
1227 pending.push((record, depth.saturating_add(1)));
1228 }
1229 }
1230 }
1231 }
1232
1233 for (class_name, class_def) in &model.classes {
1234 classes = classes.saturating_add(1);
1235 label_bytes = label_bytes
1236 .saturating_add(class_name.len())
1237 .saturating_add(class_def.id.len())
1238 .saturating_add(class_def.styles.iter().map(String::len).sum::<usize>())
1239 .saturating_add(class_def.text_styles.iter().map(String::len).sum::<usize>());
1240 }
1241
1242 Self {
1243 nodes,
1244 classes,
1245 label_bytes,
1246 nesting_depth,
1247 }
1248 }
1249
1250 pub fn as_model_complexity(self) -> ModelComplexity {
1251 ModelComplexity::new(
1252 self.nodes.saturating_add(self.classes),
1253 self.label_bytes,
1254 self.nesting_depth,
1255 )
1256 }
1257}
1258
1259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1261#[cfg(feature = "diagram-ishikawa")]
1262pub struct IshikawaComplexity {
1263 pub nodes: usize,
1264 pub label_bytes: usize,
1265 pub nesting_depth: usize,
1266}
1267
1268#[cfg(feature = "diagram-ishikawa")]
1269impl IshikawaComplexity {
1270 pub fn from_model(model: &IshikawaDiagramRenderModel) -> Self {
1271 let mut complexity = Self {
1272 nodes: 0,
1273 label_bytes: [
1274 model.acc_title.as_deref(),
1275 model.acc_descr.as_deref(),
1276 model.title.as_deref(),
1277 ]
1278 .into_iter()
1279 .flatten()
1280 .fold(0usize, |total, value| total.saturating_add(value.len())),
1281 nesting_depth: 0,
1282 };
1283 let Some(root) = model.root.and_then(|id| model.nodes.get(id)) else {
1284 return complexity;
1285 };
1286
1287 let mut pending = vec![(root, 0usize)];
1288 let mut visited = vec![false; model.nodes.len()];
1289 if let Some(root) = model.root {
1290 visited[root] = true;
1291 }
1292 while let Some((node, depth)) = pending.pop() {
1293 complexity.nodes = complexity.nodes.saturating_add(1);
1294 complexity.label_bytes = complexity.label_bytes.saturating_add(node.text.len());
1295 complexity.nesting_depth = complexity.nesting_depth.max(depth);
1296 for child in node.children.iter().rev() {
1297 if let Some(record) = model.nodes.get(*child)
1298 && !visited[*child]
1299 {
1300 visited[*child] = true;
1301 pending.push((record, depth.saturating_add(1)));
1302 }
1303 }
1304 }
1305 complexity
1306 }
1307
1308 pub fn as_model_complexity(self) -> ModelComplexity {
1309 ModelComplexity::new(self.nodes, self.label_bytes, self.nesting_depth)
1310 }
1311}
1312
1313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1314#[cfg(feature = "diagram-class")]
1315pub struct ClassComplexity {
1316 pub nodes: usize,
1317 pub edges: usize,
1318 pub namespaces: usize,
1319 pub label_bytes: usize,
1320 pub namespace_depth: usize,
1321}
1322
1323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1324#[cfg(feature = "diagram-mindmap")]
1325pub struct MindmapComplexity {
1326 pub nodes: usize,
1327 pub edges: usize,
1328 pub label_bytes: usize,
1329 pub nesting_depth: usize,
1330}
1331
1332#[cfg(feature = "diagram-mindmap")]
1333impl MindmapComplexity {
1334 pub fn from_model(model: &MindmapDiagramRenderModel) -> Self {
1335 let node_label_bytes = model.nodes.iter().fold(0usize, |total, node| {
1336 let strings = [
1337 node.id.as_str(),
1338 node.dom_id.as_str(),
1339 node.label.as_str(),
1340 node.label_type.as_str(),
1341 node.shape.as_str(),
1342 node.css_classes.as_str(),
1343 node.look.as_str(),
1344 node.node_id.as_str(),
1345 ];
1346 let total = strings.into_iter().fold(total, |subtotal, value| {
1347 subtotal.saturating_add(value.len())
1348 });
1349 let total = node.css_styles.iter().fold(total, |subtotal, value| {
1350 subtotal.saturating_add(value.len())
1351 });
1352 total.saturating_add(node.icon.as_deref().map_or(0, str::len))
1353 });
1354 let edge_label_bytes = model.edges.iter().fold(0usize, |total, edge| {
1355 [
1356 edge.id.as_str(),
1357 edge.start.as_str(),
1358 edge.end.as_str(),
1359 edge.edge_type.as_str(),
1360 edge.curve.as_str(),
1361 edge.thickness.as_str(),
1362 edge.look.as_str(),
1363 edge.classes.as_str(),
1364 ]
1365 .into_iter()
1366 .fold(total, |subtotal, value| {
1367 subtotal.saturating_add(value.len())
1368 })
1369 });
1370
1371 Self {
1372 nodes: model.nodes.len(),
1373 edges: model.edges.len(),
1374 label_bytes: node_label_bytes.saturating_add(edge_label_bytes),
1375 nesting_depth: model
1376 .nodes
1377 .iter()
1378 .filter_map(|node| usize::try_from(node.level).ok())
1379 .max()
1380 .unwrap_or(0),
1381 }
1382 }
1383
1384 pub fn as_model_complexity(self) -> ModelComplexity {
1385 ModelComplexity::new(
1386 self.nodes.saturating_add(self.edges),
1387 self.label_bytes,
1388 self.nesting_depth,
1389 )
1390 }
1391}
1392
1393#[cfg(feature = "diagram-class")]
1394impl ClassComplexity {
1395 pub fn from_model(model: &ClassDiagram) -> Self {
1396 let class_label_bytes = model
1397 .classes
1398 .values()
1399 .map(|node| {
1400 node.id
1401 .len()
1402 .saturating_add(node.label.len())
1403 .saturating_add(node.text.len())
1404 .saturating_add(node.type_param.len())
1405 .saturating_add(node.css_classes.len())
1406 .saturating_add(node.tooltip.as_deref().map(str::len).unwrap_or(0))
1407 .saturating_add(node.link.as_deref().map(str::len).unwrap_or(0))
1408 .saturating_add(
1409 node.members
1410 .iter()
1411 .chain(node.methods.iter())
1412 .map(|member| {
1413 member
1414 .display_text
1415 .len()
1416 .saturating_add(member.id.len())
1417 .saturating_add(member.parameters.len())
1418 .saturating_add(member.return_type.len())
1419 })
1420 .sum::<usize>(),
1421 )
1422 .saturating_add(node.annotations.iter().map(String::len).sum::<usize>())
1423 .saturating_add(node.styles.iter().map(String::len).sum::<usize>())
1424 })
1425 .sum::<usize>();
1426 let relation_label_bytes = model
1427 .relations
1428 .iter()
1429 .map(|rel| {
1430 rel.id
1431 .len()
1432 .saturating_add(rel.id1.len())
1433 .saturating_add(rel.id2.len())
1434 .saturating_add(rel.title.len())
1435 .saturating_add(rel.relation_title_1.as_deref().map_or(0, str::len))
1436 .saturating_add(rel.relation_title_2.as_deref().map_or(0, str::len))
1437 })
1438 .sum::<usize>();
1439 let note_label_bytes = model
1440 .notes
1441 .iter()
1442 .map(|note| {
1443 note.id
1444 .len()
1445 .saturating_add(note.text.len())
1446 .saturating_add(note.class_id.as_deref().map(str::len).unwrap_or(0))
1447 })
1448 .sum::<usize>();
1449 let interface_label_bytes = model
1450 .interfaces
1451 .iter()
1452 .map(|iface| {
1453 iface
1454 .id
1455 .len()
1456 .saturating_add(iface.label.len())
1457 .saturating_add(iface.class_id.len())
1458 })
1459 .sum::<usize>();
1460 let namespace_label_bytes = model
1461 .namespaces
1462 .values()
1463 .map(|namespace| {
1464 namespace
1465 .id
1466 .len()
1467 .saturating_add(namespace.label.len())
1468 .saturating_add(namespace.parent.as_deref().map(str::len).unwrap_or(0))
1469 })
1470 .sum::<usize>();
1471
1472 Self {
1473 nodes: model
1474 .classes
1475 .len()
1476 .saturating_add(model.notes.len())
1477 .saturating_add(model.interfaces.len())
1478 .saturating_add(model.namespaces.len()),
1479 edges: model.relations.len().saturating_add(
1480 model
1481 .notes
1482 .iter()
1483 .filter(|note| note.class_id.is_some())
1484 .count(),
1485 ),
1486 namespaces: model.namespaces.len(),
1487 label_bytes: class_label_bytes
1488 .saturating_add(relation_label_bytes)
1489 .saturating_add(note_label_bytes)
1490 .saturating_add(interface_label_bytes)
1491 .saturating_add(namespace_label_bytes),
1492 namespace_depth: class_namespace_depth(model),
1493 }
1494 }
1495
1496 pub fn as_model_complexity(self) -> ModelComplexity {
1497 ModelComplexity::new(
1498 self.nodes.saturating_add(self.edges),
1499 self.label_bytes,
1500 self.namespace_depth,
1501 )
1502 }
1503}
1504
1505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1506#[cfg(feature = "diagram-zenuml")]
1507pub struct ZenumlComplexity {
1508 pub participants: usize,
1509 pub groups: usize,
1510 pub statements: usize,
1511 pub fragments: usize,
1512 pub label_bytes: usize,
1513 pub nesting_depth: usize,
1514}
1515
1516#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1517#[cfg(feature = "diagram-sequence")]
1518pub struct SequenceComplexity {
1519 pub messages: usize,
1520 pub block_depth: usize,
1521 pub items: usize,
1522 pub text_bytes: usize,
1523 pub nesting_depth: usize,
1524}
1525
1526#[cfg(feature = "diagram-sequence")]
1527impl SequenceComplexity {
1528 pub fn from_model(model: &SequenceDiagramRenderModel) -> Self {
1529 const ACTOR_PROPERTY_VALUE_DEPTH: usize = 4;
1530 const NOTE_ACTOR_VALUE_DEPTH: usize = 3;
1531
1532 let mut depth = 0usize;
1533 let mut block_depth = 0usize;
1534 for message in &model.messages {
1535 match message.message_type {
1536 10 | 12 | 15 | 19 | 22 | 27 | 30 | 32 => {
1537 depth = depth.saturating_add(1);
1538 block_depth = block_depth.max(depth);
1539 }
1540 11 | 14 | 16 | 21 | 23 | 29 | 31 => {
1541 depth = depth.saturating_sub(1);
1542 }
1543 _ => {}
1544 }
1545 }
1546 let common_text_bytes = [
1547 model.acc_title.as_deref(),
1548 model.acc_descr.as_deref(),
1549 model.title.as_deref(),
1550 ]
1551 .into_iter()
1552 .flatten()
1553 .fold(0usize, |total, value| total.saturating_add(value.len()));
1554 let actor_order_text_bytes = model
1555 .actor_order
1556 .iter()
1557 .fold(0usize, |total, value| total.saturating_add(value.len()));
1558 let mut arbitrary_json = JsonValueComplexity::default();
1559 let actor_text_bytes = model.actors.iter().fold(0usize, |total, (key, actor)| {
1560 let links = json_map_complexity(&actor.links, ACTOR_PROPERTY_VALUE_DEPTH);
1561 let properties = json_map_complexity(&actor.properties, ACTOR_PROPERTY_VALUE_DEPTH);
1562 arbitrary_json.merge(links);
1563 arbitrary_json.merge(properties);
1564 total
1565 .saturating_add(key.len())
1566 .saturating_add(actor.name.len())
1567 .saturating_add(actor.description.len())
1568 .saturating_add(actor.actor_type.len())
1569 .saturating_add(links.text_bytes)
1570 .saturating_add(properties.text_bytes)
1571 });
1572 let box_text_bytes = model.boxes.iter().fold(0usize, |total, sequence_box| {
1573 sequence_box.actor_keys.iter().fold(
1574 total
1575 .saturating_add(sequence_box.fill.len())
1576 .saturating_add(sequence_box.name.as_deref().map_or(0, str::len)),
1577 |subtotal, actor| subtotal.saturating_add(actor.len()),
1578 )
1579 });
1580 let message_text_bytes = model.messages.iter().fold(0usize, |total, message| {
1581 total
1582 .saturating_add(message.id.len())
1583 .saturating_add(message.from.as_deref().map_or(0, str::len))
1584 .saturating_add(message.to.as_deref().map_or(0, str::len))
1585 .saturating_add(message.message_text().len())
1586 });
1587 let note_text_bytes = model.notes.iter().fold(0usize, |total, note| {
1588 let actor = json_value_complexity(¬e.actor, NOTE_ACTOR_VALUE_DEPTH);
1589 arbitrary_json.merge(actor);
1590 total
1591 .saturating_add(actor.text_bytes)
1592 .saturating_add(note.message.len())
1593 });
1594 let lifecycle_text_bytes = model
1595 .created_actors
1596 .keys()
1597 .chain(model.destroyed_actors.keys())
1598 .fold(0usize, |total, value| total.saturating_add(value.len()));
1599 Self {
1600 messages: model.messages.len(),
1601 block_depth,
1602 items: model
1603 .actors
1604 .len()
1605 .saturating_add(model.boxes.len())
1606 .saturating_add(model.messages.len())
1607 .saturating_add(model.notes.len())
1608 .saturating_add(arbitrary_json.items),
1609 text_bytes: common_text_bytes
1610 .saturating_add(actor_order_text_bytes)
1611 .saturating_add(actor_text_bytes)
1612 .saturating_add(box_text_bytes)
1613 .saturating_add(message_text_bytes)
1614 .saturating_add(note_text_bytes)
1615 .saturating_add(lifecycle_text_bytes),
1616 nesting_depth: block_depth.max(arbitrary_json.nesting_depth),
1617 }
1618 }
1619
1620 pub fn as_model_complexity(self) -> ModelComplexity {
1621 ModelComplexity::new(self.items, self.text_bytes, self.nesting_depth)
1622 }
1623}
1624
1625#[cfg(feature = "diagram-zenuml")]
1626impl ZenumlComplexity {
1627 pub fn from_model(model: &ZenumlDiagramRenderModel) -> Self {
1628 let common_label_bytes = [model.title.as_deref(), model.starter.as_deref()]
1629 .into_iter()
1630 .flatten()
1631 .fold(0usize, |total, value| total.saturating_add(value.len()));
1632 let participant_label_bytes =
1633 model
1634 .participants
1635 .iter()
1636 .fold(0usize, |total, participant| {
1637 [
1638 Some(participant.name.as_str()),
1639 participant.label.as_deref(),
1640 participant.participant_type.as_deref(),
1641 participant.stereotype.as_deref(),
1642 participant.emoji.as_deref(),
1643 participant.width_source.as_deref(),
1644 participant.color.as_deref(),
1645 participant.group_id.as_deref(),
1646 ]
1647 .into_iter()
1648 .flatten()
1649 .fold(total, |subtotal, value| {
1650 subtotal.saturating_add(value.len())
1651 })
1652 });
1653 let group_label_bytes = model.groups.iter().fold(0usize, |total, group| {
1654 group.participant_names.iter().fold(
1655 total.saturating_add(group.id.as_deref().map_or(0, str::len)),
1656 |subtotal, name| subtotal.saturating_add(name.len()),
1657 )
1658 });
1659 let mut complexity = Self {
1660 participants: model.participants.len(),
1661 groups: model.groups.len(),
1662 statements: 0,
1663 fragments: 0,
1664 label_bytes: common_label_bytes
1665 .saturating_add(participant_label_bytes)
1666 .saturating_add(group_label_bytes),
1667 nesting_depth: 0,
1668 };
1669 let mut pending = vec![(model.statements.as_slice(), 0usize)];
1670 while let Some((statements, nesting_depth)) = pending.pop() {
1671 complexity.nesting_depth = complexity.nesting_depth.max(nesting_depth);
1672 for statement in statements {
1673 complexity.statements = complexity.statements.saturating_add(1);
1674 complexity.label_bytes = complexity
1675 .label_bytes
1676 .saturating_add(statement.comment.as_deref().map_or(0, str::len));
1677 match &statement.kind {
1678 ZenumlStatementKind::Message {
1679 explicit_from,
1680 resolved_from,
1681 resolved_to,
1682 label,
1683 assignment,
1684 body,
1685 ..
1686 } => {
1687 complexity.label_bytes = complexity
1688 .label_bytes
1689 .saturating_add(explicit_from.as_deref().map_or(0, str::len))
1690 .saturating_add(resolved_from.as_deref().map_or(0, str::len))
1691 .saturating_add(resolved_to.as_deref().map_or(0, str::len))
1692 .saturating_add(label.len())
1693 .saturating_add(assignment.as_deref().map_or(0, str::len));
1694 pending.push((body, nesting_depth.saturating_add(1)));
1695 }
1696 ZenumlStatementKind::Creation {
1697 resolved_from,
1698 resolved_to,
1699 constructor,
1700 parameters,
1701 assignment,
1702 label,
1703 body,
1704 ..
1705 } => {
1706 complexity.label_bytes = complexity
1707 .label_bytes
1708 .saturating_add(resolved_from.as_deref().map_or(0, str::len))
1709 .saturating_add(resolved_to.len())
1710 .saturating_add(constructor.len())
1711 .saturating_add(parameters.len())
1712 .saturating_add(assignment.as_deref().map_or(0, str::len))
1713 .saturating_add(label.len());
1714 pending.push((body, nesting_depth.saturating_add(1)));
1715 }
1716 ZenumlStatementKind::Return {
1717 explicit_from,
1718 resolved_from,
1719 explicit_to,
1720 resolved_to,
1721 label,
1722 } => {
1723 complexity.label_bytes = complexity
1724 .label_bytes
1725 .saturating_add(explicit_from.as_deref().map_or(0, str::len))
1726 .saturating_add(resolved_from.as_deref().map_or(0, str::len))
1727 .saturating_add(explicit_to.as_deref().map_or(0, str::len))
1728 .saturating_add(resolved_to.as_deref().map_or(0, str::len))
1729 .saturating_add(label.len());
1730 }
1731 ZenumlStatementKind::Fragment {
1732 label, sections, ..
1733 } => {
1734 complexity.fragments = complexity.fragments.saturating_add(1);
1735 complexity.label_bytes = complexity
1736 .label_bytes
1737 .saturating_add(label.as_deref().map_or(0, str::len));
1738 for section in sections {
1739 complexity.label_bytes = complexity
1740 .label_bytes
1741 .saturating_add(section.label.as_deref().map_or(0, str::len));
1742 pending.push((§ion.statements, nesting_depth.saturating_add(1)));
1743 }
1744 }
1745 ZenumlStatementKind::Reference {
1746 participants,
1747 label,
1748 } => {
1749 complexity.label_bytes = participants.iter().fold(
1750 complexity.label_bytes.saturating_add(label.len()),
1751 |total, participant| total.saturating_add(participant.len()),
1752 );
1753 }
1754 ZenumlStatementKind::Divider { label } => {
1755 complexity.label_bytes = complexity.label_bytes.saturating_add(label.len());
1756 }
1757 }
1758 }
1759 }
1760 complexity
1761 }
1762
1763 pub fn as_model_complexity(self) -> ModelComplexity {
1764 ModelComplexity::new(
1765 self.participants
1766 .saturating_add(self.groups)
1767 .saturating_add(self.statements),
1768 self.label_bytes,
1769 self.nesting_depth,
1770 )
1771 }
1772}
1773
1774#[cfg(any(
1775 feature = "diagram-flowchart",
1776 feature = "diagram-swimlane",
1777 feature = "diagram-agentflow"
1778))]
1779fn flowchart_text_bytes(model: &FlowchartModel) -> usize {
1780 let mut total = [
1781 Some(model.keyword.as_str()),
1782 model.acc_title.as_deref(),
1783 model.acc_descr.as_deref(),
1784 model.direction.as_deref(),
1785 ]
1786 .into_iter()
1787 .flatten()
1788 .fold(0usize, |subtotal, value| {
1789 subtotal.saturating_add(value.len())
1790 });
1791 for (id, styles) in &model.class_defs {
1792 total = styles
1793 .iter()
1794 .fold(total.saturating_add(id.len()), |subtotal, value| {
1795 subtotal.saturating_add(value.len())
1796 });
1797 }
1798 total = model.vertex_calls.iter().fold(total, |subtotal, value| {
1799 subtotal.saturating_add(value.len())
1800 });
1801 if let Some(defaults) = &model.edge_defaults {
1802 total = total.saturating_add(defaults.interpolate.as_deref().map_or(0, str::len));
1803 total = defaults.style.iter().fold(total, |subtotal, value| {
1804 subtotal.saturating_add(value.len())
1805 });
1806 }
1807 for node in &model.nodes {
1808 for value in [
1809 Some(node.id.as_str()),
1810 node.label.as_deref(),
1811 node.label_type.as_deref(),
1812 node.layout_shape.as_deref(),
1813 node.shape.as_deref(),
1814 node.icon.as_deref(),
1815 node.form.as_deref(),
1816 node.pos.as_deref(),
1817 node.img.as_deref(),
1818 node.constraint.as_deref(),
1819 node.link.as_deref(),
1820 node.link_target.as_deref(),
1821 ]
1822 .into_iter()
1823 .flatten()
1824 {
1825 total = total.saturating_add(value.len());
1826 }
1827 total = node
1828 .classes
1829 .iter()
1830 .chain(&node.styles)
1831 .fold(total, |subtotal, value| {
1832 subtotal.saturating_add(value.len())
1833 });
1834 }
1835 for edge in &model.edges {
1836 for value in [
1837 Some(edge.id.as_str()),
1838 Some(edge.from.as_str()),
1839 Some(edge.to.as_str()),
1840 edge.label.as_deref(),
1841 edge.label_type.as_deref(),
1842 edge.edge_type.as_deref(),
1843 Some(edge.arrow.as_str()),
1844 edge.stroke.as_deref(),
1845 edge.interpolate.as_deref(),
1846 edge.animation.as_deref(),
1847 ]
1848 .into_iter()
1849 .flatten()
1850 {
1851 total = total.saturating_add(value.len());
1852 }
1853 total = edge
1854 .classes
1855 .iter()
1856 .chain(&edge.style)
1857 .fold(total, |subtotal, value| {
1858 subtotal.saturating_add(value.len())
1859 });
1860 }
1861 for subgraph in &model.subgraphs {
1862 for value in [
1863 Some(subgraph.id.as_str()),
1864 Some(subgraph.title.as_str()),
1865 subgraph.dir.as_deref(),
1866 subgraph.label_type.as_deref(),
1867 ]
1868 .into_iter()
1869 .flatten()
1870 {
1871 total = total.saturating_add(value.len());
1872 }
1873 total = subgraph
1874 .classes
1875 .iter()
1876 .chain(&subgraph.styles)
1877 .chain(&subgraph.nodes)
1878 .fold(total, |subtotal, value| {
1879 subtotal.saturating_add(value.len())
1880 });
1881 }
1882 total = model.tooltips.iter().fold(total, |subtotal, (id, value)| {
1883 subtotal
1884 .saturating_add(id.len())
1885 .saturating_add(value.len())
1886 });
1887 total
1888}
1889
1890#[cfg(any(
1891 feature = "diagram-flowchart",
1892 feature = "diagram-swimlane",
1893 feature = "diagram-agentflow"
1894))]
1895fn flowchart_subgraph_depth(model: &FlowchartModel) -> usize {
1896 if model.subgraphs.is_empty() {
1897 return 0;
1898 }
1899 let indices = model
1900 .subgraphs
1901 .iter()
1902 .enumerate()
1903 .map(|(index, subgraph)| (subgraph.id.as_str(), index))
1904 .collect::<std::collections::HashMap<_, _>>();
1905 let mut children = vec![Vec::new(); model.subgraphs.len()];
1906 let mut incoming = vec![0usize; model.subgraphs.len()];
1907 for (parent, subgraph) in model.subgraphs.iter().enumerate() {
1908 for member in &subgraph.nodes {
1909 let Some(&child) = indices.get(member.as_str()) else {
1910 continue;
1911 };
1912 children[parent].push(child);
1913 incoming[child] = incoming[child].saturating_add(1);
1914 }
1915 }
1916 let mut queue = std::collections::VecDeque::new();
1917 let mut depths = vec![1usize; model.subgraphs.len()];
1918 for (index, incoming) in incoming.iter().enumerate() {
1919 if *incoming == 0 {
1920 queue.push_back(index);
1921 }
1922 }
1923 while let Some(parent) = queue.pop_front() {
1924 for &child in &children[parent] {
1925 depths[child] = depths[child].max(depths[parent].saturating_add(1));
1926 incoming[child] = incoming[child].saturating_sub(1);
1927 if incoming[child] == 0 {
1928 queue.push_back(child);
1929 }
1930 }
1931 }
1932 depths.into_iter().max().unwrap_or(0)
1933}
1934
1935#[cfg(feature = "diagram-class")]
1936fn class_namespace_depth(model: &ClassDiagram) -> usize {
1937 if model.namespaces.is_empty() {
1938 return 0;
1939 }
1940 let indices = model
1941 .namespaces
1942 .keys()
1943 .enumerate()
1944 .map(|(index, id)| (id.as_str(), index))
1945 .collect::<std::collections::HashMap<_, _>>();
1946 let parents = model
1947 .namespaces
1948 .values()
1949 .map(|namespace| {
1950 namespace
1951 .parent
1952 .as_deref()
1953 .and_then(|parent| indices.get(parent).copied())
1954 })
1955 .collect::<Vec<_>>();
1956 let mut depths = vec![0usize; parents.len()];
1957 let mut completed = vec![false; parents.len()];
1958 for start in 0..parents.len() {
1959 if completed[start] {
1960 continue;
1961 }
1962 let mut path = Vec::new();
1963 let mut positions = std::collections::HashMap::new();
1964 let mut cursor = Some(start);
1965 while let Some(index) = cursor {
1966 if completed[index] || positions.contains_key(&index) {
1967 break;
1968 }
1969 positions.insert(index, path.len());
1970 path.push(index);
1971 cursor = parents[index];
1972 }
1973 let mut depth = cursor
1974 .filter(|index| completed[*index])
1975 .map(|index| depths[index])
1976 .unwrap_or(0);
1977 for index in path.into_iter().rev() {
1978 depth = depth.saturating_add(1);
1979 depths[index] = depth;
1980 completed[index] = true;
1981 }
1982 }
1983 depths.into_iter().max().unwrap_or(0)
1984}
1985
1986#[cfg(feature = "diagram-kanban")]
1987fn kanban_nesting_depth(model: &KanbanDiagramRenderModel) -> usize {
1988 let indices = model
1989 .nodes
1990 .iter()
1991 .enumerate()
1992 .map(|(index, node)| (node.id.as_str(), index))
1993 .collect::<std::collections::HashMap<_, _>>();
1994 let parents = model
1995 .nodes
1996 .iter()
1997 .map(|node| {
1998 node.parent_id
1999 .as_deref()
2000 .and_then(|parent| indices.get(parent).copied())
2001 })
2002 .collect::<Vec<_>>();
2003 let mut depths = vec![0usize; parents.len()];
2004 let mut completed = vec![false; parents.len()];
2005 for start in 0..parents.len() {
2006 if completed[start] {
2007 continue;
2008 }
2009 let mut path = Vec::new();
2010 let mut positions = std::collections::HashMap::new();
2011 let mut cursor = Some(start);
2012 while let Some(index) = cursor {
2013 if completed[index] || positions.contains_key(&index) {
2014 break;
2015 }
2016 positions.insert(index, path.len());
2017 path.push(index);
2018 cursor = parents[index];
2019 }
2020 let mut depth = cursor
2021 .filter(|index| completed[*index])
2022 .map(|index| depths[index])
2023 .unwrap_or(0);
2024 for index in path.into_iter().rev() {
2025 depth = depth.saturating_add(1);
2026 depths[index] = depth;
2027 completed[index] = true;
2028 }
2029 }
2030 depths.into_iter().max().unwrap_or(0)
2031}
2032
2033#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2034struct JsonValueComplexity {
2035 items: usize,
2036 text_bytes: usize,
2037 nesting_depth: usize,
2038 container_depth: usize,
2039}
2040
2041#[cfg(feature = "diagram-sequence")]
2042impl JsonValueComplexity {
2043 fn merge(&mut self, other: Self) {
2044 self.items = self.items.saturating_add(other.items);
2045 self.text_bytes = self.text_bytes.saturating_add(other.text_bytes);
2046 self.nesting_depth = self.nesting_depth.max(other.nesting_depth);
2047 self.container_depth = self.container_depth.max(other.container_depth);
2048 }
2049}
2050
2051enum JsonTraversalFrame<'a> {
2052 Value(&'a serde_json::Value, usize),
2053 Array(std::slice::Iter<'a, serde_json::Value>, usize),
2054 Object(serde_json::map::Iter<'a>, usize),
2055}
2056
2057#[cfg(feature = "diagram-sequence")]
2058fn json_map_complexity(
2059 map: &serde_json::Map<String, serde_json::Value>,
2060 value_depth: usize,
2061) -> JsonValueComplexity {
2062 let mut complexity = JsonValueComplexity {
2063 items: map.len(),
2064 text_bytes: map
2065 .keys()
2066 .fold(0usize, |total, key| total.saturating_add(key.len())),
2067 nesting_depth: 0,
2068 container_depth: 0,
2069 };
2070 for value in map.values() {
2071 complexity.merge(json_value_complexity(value, value_depth));
2072 }
2073 complexity
2074}
2075
2076fn json_value_complexity(value: &serde_json::Value, initial_depth: usize) -> JsonValueComplexity {
2078 let mut complexity = JsonValueComplexity::default();
2079 let mut pending = vec![JsonTraversalFrame::Value(value, initial_depth)];
2080 while let Some(frame) = pending.pop() {
2081 match frame {
2082 JsonTraversalFrame::Value(value, depth) => {
2083 complexity.nesting_depth = complexity.nesting_depth.max(depth);
2084 match value {
2085 serde_json::Value::String(value) => {
2086 complexity.text_bytes = complexity.text_bytes.saturating_add(value.len());
2087 }
2088 serde_json::Value::Array(values) => {
2089 complexity.container_depth = complexity.container_depth.max(depth);
2090 complexity.items = complexity.items.saturating_add(values.len());
2091 pending.push(JsonTraversalFrame::Array(
2092 values.iter(),
2093 depth.saturating_add(1),
2094 ));
2095 }
2096 serde_json::Value::Object(values) => {
2097 complexity.container_depth = complexity.container_depth.max(depth);
2098 complexity.items = complexity.items.saturating_add(values.len());
2099 pending.push(JsonTraversalFrame::Object(
2100 values.iter(),
2101 depth.saturating_add(1),
2102 ));
2103 }
2104 serde_json::Value::Null
2105 | serde_json::Value::Bool(_)
2106 | serde_json::Value::Number(_) => {}
2107 }
2108 }
2109 JsonTraversalFrame::Array(mut values, depth) => {
2110 if let Some(value) = values.next() {
2111 pending.push(JsonTraversalFrame::Array(values, depth));
2112 pending.push(JsonTraversalFrame::Value(value, depth));
2113 }
2114 }
2115 JsonTraversalFrame::Object(mut values, depth) => {
2116 if let Some((key, value)) = values.next() {
2117 complexity.text_bytes = complexity.text_bytes.saturating_add(key.len());
2118 pending.push(JsonTraversalFrame::Object(values, depth));
2119 pending.push(JsonTraversalFrame::Value(value, depth));
2120 }
2121 }
2122 }
2123 }
2124 complexity
2125}
2126
2127#[cfg(feature = "diagram-treemap")]
2128fn json_text_bytes(value: &serde_json::Value) -> usize {
2129 json_value_complexity(value, 0).text_bytes
2130}
2131
2132#[cfg(feature = "diagram-treemap")]
2133fn json_item_count(value: &serde_json::Value) -> usize {
2134 json_value_complexity(value, 0).items
2135}
2136
2137#[cfg(test)]
2138mod tests {
2139 use super::*;
2140
2141 #[test]
2142 fn iterative_json_complexity_matches_existing_serialized_shape() {
2143 for value in [
2144 serde_json::json!(null),
2145 serde_json::json!([]),
2146 serde_json::json!({"empty": [], "nested": {"name": ["value", {}]}}),
2147 serde_json::json!([1, "text", [[], {}]]),
2148 ] {
2149 assert_eq!(
2150 ModelComplexity::from_json(&value),
2151 ModelComplexity::from_serializable(&value),
2152 );
2153 }
2154 }
2155
2156 #[test]
2157 fn generic_complexity_rejects_managed_json_depth_without_panicking() {
2158 let mut value = serde_json::Value::Null;
2159 for _ in 0..129 {
2160 value = serde_json::Value::Array(vec![value]);
2161 }
2162 let managed = crate::ManagedSemanticJson::from_value(value);
2163 assert_eq!(
2164 ModelComplexity::from_serializable(&managed),
2165 ModelComplexity::new(usize::MAX, usize::MAX, usize::MAX)
2166 );
2167 }
2168
2169 #[test]
2170 fn custom_json_complexity_preserves_deep_payload_and_model_name() {
2171 let mut value = serde_json::json!("leaf");
2172 for _ in 0..5_000 {
2173 value = serde_json::Value::Array(vec![value]);
2174 }
2175 let model =
2176 RenderSemanticModel::CustomJson(crate::CustomJsonRenderModel::new("custom", value));
2177 assert_eq!(
2178 ModelComplexity::from_render_model(&model),
2179 ModelComplexity::new(5_000, 10, 4_999),
2180 );
2181 drop(model);
2182 }
2183
2184 #[test]
2185 fn profile_values_are_single_source_for_input_limits() {
2186 assert_eq!(
2187 RESOURCE_PROFILE_DESCRIPTORS.len(),
2188 ResourceProfile::ALL.len()
2189 );
2190 assert_eq!(
2191 INPUT_RESOURCE_LIMIT_DESCRIPTORS.len(),
2192 INPUT_RESOURCE_LIMIT_COUNT
2193 );
2194 for profile in ResourceProfile::ALL {
2195 let policy = InputResourcePolicy::for_profile(profile);
2196 for limit in InputResourceLimitId::ALL {
2197 assert_eq!(policy.base_value(limit), policy.value(limit));
2198 }
2199 }
2200
2201 let interactive = InputResourcePolicy::for_profile(ResourceProfile::Interactive);
2202 for (limit, expected) in [
2203 (InputResourceLimitId::MaxModelItems, Some(32_000)),
2204 (InputResourceLimitId::MaxModelTextBytes, Some(2 * MIB)),
2205 (InputResourceLimitId::MaxModelNestingDepth, Some(256)),
2206 ] {
2207 assert_eq!(interactive.value(limit), expected, "{limit:?}");
2208 }
2209 }
2210
2211 #[test]
2212 fn constrained_policy_rejects_source_bytes() {
2213 let source_error = InputResourcePolicy::for_profile(ResourceProfile::Constrained)
2214 .apply_for_test(InputResourceLimitId::MaxSourceBytes, 4)
2215 .check_source_bytes("12345")
2216 .unwrap_err();
2217 assert_eq!(source_error.limit, "max_source_bytes");
2218 }
2219
2220 #[cfg(feature = "diagram-flowchart")]
2221 #[test]
2222 fn constrained_policy_rejects_flowchart_cardinality() {
2223 let parsed = crate::Engine::new()
2224 .parse_diagram_for_render_model_sync(
2225 "flowchart TD\nA --> B",
2226 crate::ParseOptions::strict(),
2227 )
2228 .unwrap()
2229 .unwrap();
2230 let model_error = InputResourcePolicy::for_profile(ResourceProfile::Constrained)
2231 .apply_for_test(InputResourceLimitId::MaxModelItems, 2)
2232 .check_render_model(parsed.model())
2233 .unwrap_err();
2234 assert_eq!(model_error.limit, "max_model_items");
2235 }
2236
2237 #[cfg(feature = "diagram-flowchart")]
2238 #[test]
2239 fn parsed_render_resource_checks_include_parser_owned_flowchart_label_sources() {
2240 let parsed = crate::Engine::new()
2241 .parse_diagram_for_render_model_sync(
2242 "flowchart LR\nA[\" entity \"] --> B\n",
2243 crate::ParseOptions::strict(),
2244 )
2245 .unwrap()
2246 .unwrap();
2247 let model_complexity = ModelComplexity::from_render_model(parsed.model());
2248 assert!(parsed.retained_render_context_bytes() > 0);
2249
2250 let policy = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2251 .with_limit(
2252 InputResourceLimitId::MaxModelTextBytes,
2253 model_complexity.text_bytes,
2254 )
2255 .unwrap();
2256 policy
2257 .check_render_model(parsed.model())
2258 .expect("the public model alone fits its exact text budget");
2259 let error = policy.check_parsed_render(&parsed).unwrap_err();
2260 assert_eq!(error.limit, "max_model_text_bytes");
2261 assert!(error.actual > model_complexity.text_bytes);
2262 }
2263
2264 #[cfg(feature = "diagram-sequence")]
2265 #[test]
2266 fn sequence_complexity_bounds_messages_and_nested_frames() {
2267 let parsed = crate::Engine::new()
2268 .parse_diagram_for_render_model_sync(
2269 "sequenceDiagram\nloop outer\nrect rgb(240,240,240)\nA->>B: hi\nend\nend",
2270 crate::ParseOptions::strict(),
2271 )
2272 .unwrap()
2273 .unwrap();
2274 let crate::RenderSemanticModel::Sequence(model) = parsed.model() else {
2275 panic!("expected Sequence model");
2276 };
2277 let complexity = SequenceComplexity::from_model(model);
2278 assert_eq!(complexity.block_depth, 2);
2279 assert_eq!(complexity.messages, model.messages.len());
2280
2281 let error = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2282 .with_limit(InputResourceLimitId::MaxModelNestingDepth, 1)
2283 .unwrap()
2284 .check_render_model(parsed.model())
2285 .unwrap_err();
2286 assert_eq!(error.limit, "max_model_nesting_depth");
2287 assert_eq!(error.actual, 2);
2288 assert_eq!(error.max, 1);
2289 }
2290
2291 #[cfg(feature = "diagram-sequence")]
2292 #[test]
2293 fn sequence_complexity_counts_arbitrary_json_items_and_depth_iteratively() {
2294 let parsed = crate::Engine::new()
2295 .parse_diagram_for_render_model_sync(
2296 "sequenceDiagram\nparticipant A",
2297 crate::ParseOptions::strict(),
2298 )
2299 .unwrap()
2300 .unwrap();
2301 let (_, mut render_model) = parsed.into_parts();
2302 let RenderSemanticModel::Sequence(model) = &mut render_model else {
2303 panic!("expected Sequence model");
2304 };
2305 let mut nested = serde_json::Value::Null;
2306 for _ in 0..300 {
2307 nested = serde_json::Value::Array(vec![nested]);
2308 }
2309 model
2310 .actors
2311 .get_mut("A")
2312 .unwrap()
2313 .properties
2314 .insert("payload".to_string(), nested);
2315
2316 let complexity = SequenceComplexity::from_model(model);
2317 assert_eq!(complexity.nesting_depth, 304);
2318 assert_eq!(complexity.items, 302);
2319
2320 let exact = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2321 .with_limit(
2322 InputResourceLimitId::MaxModelNestingDepth,
2323 complexity.nesting_depth,
2324 )
2325 .unwrap();
2326 exact
2327 .check_render_model(&render_model)
2328 .expect("the exact JSON depth boundary should pass");
2329
2330 let below = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2331 .with_limit(
2332 InputResourceLimitId::MaxModelNestingDepth,
2333 complexity.nesting_depth - 1,
2334 )
2335 .unwrap();
2336 let error = below
2337 .check_render_model(&render_model)
2338 .expect_err("the N-1 JSON depth boundary should fail");
2339 assert_eq!(error.limit, "max_model_nesting_depth");
2340 assert_eq!(error.actual, complexity.nesting_depth);
2341 assert_eq!(error.max, complexity.nesting_depth - 1);
2342 }
2343
2344 #[cfg(feature = "diagram-mindmap")]
2345 #[test]
2346 fn mindmap_model_limits_cover_pre_layout_cardinality_and_labels() {
2347 let constrained = InputResourcePolicy::for_profile(ResourceProfile::Constrained);
2348 assert_eq!(
2349 constrained.value(InputResourceLimitId::MaxModelItems),
2350 Some(16_000)
2351 );
2352
2353 let parsed = crate::Engine::new()
2354 .parse_diagram_for_render_model_sync(
2355 "mindmap\n Root\n First child\n Second child\n",
2356 crate::ParseOptions::strict(),
2357 )
2358 .unwrap()
2359 .unwrap();
2360 let RenderSemanticModel::Mindmap(model) = parsed.model() else {
2361 panic!("expected Mindmap model");
2362 };
2363 let complexity = MindmapComplexity::from_model(model);
2364 assert_eq!(complexity.nodes, 3);
2365 assert_eq!(complexity.edges, 2);
2366 assert!(complexity.label_bytes >= "RootFirst childSecond child".len());
2367
2368 for (id, max, expected_limit) in [
2369 (InputResourceLimitId::MaxModelItems, 4, "max_model_items"),
2370 (
2371 InputResourceLimitId::MaxModelTextBytes,
2372 1,
2373 "max_model_text_bytes",
2374 ),
2375 ] {
2376 let error = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2377 .with_limit(id, max)
2378 .unwrap()
2379 .check_render_model(parsed.model())
2380 .unwrap_err();
2381 assert_eq!(error.phase, InputResourceLimitPhase::Model);
2382 assert_eq!(error.limit, expected_limit);
2383 }
2384 }
2385
2386 #[cfg(any(
2387 feature = "diagram-kanban",
2388 feature = "diagram-radar",
2389 feature = "diagram-requirement"
2390 ))]
2391 #[test]
2392 fn generic_model_budget_covers_new_family_models_without_family_knobs() {
2393 let models = [
2394 #[cfg(feature = "diagram-kanban")]
2395 RenderSemanticModel::Kanban(KanbanDiagramRenderModel {
2396 nodes: vec![crate::diagrams::kanban::KanbanRenderNode::new(
2397 "todo", "Todo",
2398 )],
2399 }),
2400 #[cfg(feature = "diagram-radar")]
2401 RenderSemanticModel::Radar({
2402 let mut model = RadarDiagramRenderModel::default();
2403 model.axes = vec![crate::diagrams::radar::RadarRenderAxis {
2404 name: "speed".to_string(),
2405 label: "Speed".to_string(),
2406 }];
2407 model
2408 }),
2409 #[cfg(feature = "diagram-requirement")]
2410 RenderSemanticModel::Requirement(RequirementDiagramRenderModel {
2411 direction: "TB".to_string(),
2412 requirements: vec![crate::diagrams::requirement::RequirementRenderNode {
2413 name: "R1".to_string(),
2414 node_type: "requirement".to_string(),
2415 requirement_id: "1".to_string(),
2416 text: "Must render".to_string(),
2417 risk: "Low".to_string(),
2418 verify_method: "Test".to_string(),
2419 css_styles: Vec::new(),
2420 classes: Vec::new(),
2421 }],
2422 acc_title: None,
2423 acc_descr: None,
2424 elements: Vec::new(),
2425 relationships: Vec::new(),
2426 classes: Default::default(),
2427 }),
2428 ];
2429 for model in models {
2430 let error = InputResourcePolicy::for_profile(ResourceProfile::UnboundedForTrustedInput)
2431 .with_limit(InputResourceLimitId::MaxModelTextBytes, 1)
2432 .unwrap()
2433 .check_render_model(&model)
2434 .unwrap_err();
2435 assert_eq!(error.limit, "max_model_text_bytes");
2436 }
2437 }
2438
2439 #[cfg(feature = "diagram-kanban")]
2440 #[test]
2441 fn kanban_complexity_accounts_for_parent_chain_depth_without_quadratic_scans() {
2442 let root = KanbanDiagramRenderModel {
2443 nodes: ["section", "lane", "card"]
2444 .into_iter()
2445 .enumerate()
2446 .map(|(index, id)| {
2447 let mut node =
2448 crate::diagrams::kanban::KanbanRenderNode::new(id, format!("Node {id}"));
2449 if index > 0 {
2450 node.parent_id = Some(["section", "lane"][index - 1].to_string());
2451 }
2452 node
2453 })
2454 .collect(),
2455 };
2456
2457 assert_eq!(ModelComplexity::from_kanban(&root).nesting_depth, 3);
2458 }
2459
2460 #[cfg(feature = "diagram-treemap")]
2461 #[test]
2462 fn treemap_complexity_handles_deep_typed_trees_without_serde_recursion() {
2463 let mut nodes = vec![crate::diagrams::treemap::TreemapNodeRenderModel {
2464 name: "leaf".to_string(),
2465 ..Default::default()
2466 }];
2467 for index in (0..1_500).rev() {
2468 nodes.push(crate::diagrams::treemap::TreemapNodeRenderModel {
2469 name: format!("section{index}"),
2470 children: Some(vec![nodes.len() - 1]),
2471 ..Default::default()
2472 });
2473 }
2474 let model = TreemapDiagramRenderModel {
2475 root: nodes.pop().unwrap(),
2476 nodes,
2477 ..Default::default()
2478 };
2479
2480 let complexity = TreemapComplexity::from_model(&model);
2481
2482 assert_eq!(complexity.nodes, 1_501);
2483 assert_eq!(complexity.nesting_depth, 1_500);
2484 assert!(complexity.label_bytes >= "leaf".len());
2485 }
2486
2487 #[cfg(feature = "diagram-ishikawa")]
2488 #[test]
2489 fn ishikawa_complexity_handles_deep_typed_trees_without_serde_recursion() {
2490 let mut nodes = vec![crate::diagrams::ishikawa::IshikawaNodeRenderModel {
2491 text: "leaf".to_string(),
2492 children: Vec::new(),
2493 }];
2494 for index in (0..1_500).rev() {
2495 nodes.push(crate::diagrams::ishikawa::IshikawaNodeRenderModel {
2496 text: format!("cause{index}"),
2497 children: vec![nodes.len() - 1],
2498 });
2499 }
2500 let model = IshikawaDiagramRenderModel {
2501 root: Some(nodes.len() - 1),
2502 nodes,
2503 ..Default::default()
2504 };
2505
2506 let complexity = IshikawaComplexity::from_model(&model);
2507
2508 assert_eq!(complexity.nodes, 1_501);
2509 assert_eq!(complexity.nesting_depth, 1_500);
2510 assert!(complexity.label_bytes >= "leaf".len());
2511 }
2512
2513 impl InputResourcePolicy {
2514 fn apply_for_test(mut self, id: InputResourceLimitId, value: usize) -> Self {
2515 self.apply_limit(id, value).unwrap();
2516 self
2517 }
2518 }
2519}