Skip to main content

merman_core/
resources.rs

1//! Backend-independent Mermaid source and semantic-model resource policy.
2
3use 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    // Policy defaults, not Mermaid syntax limits. They deliberately preserve the
233    // aggregate headroom of the previous per-family cardinality budgets.
234    [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    /// Checks only the public semantic-model projection.
357    ///
358    /// Use [`Self::check_parsed_render`] for a parsed built-in render operation so parser-owned
359    /// render context is included in the text budget.
360    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    /// Counts the stable serialized model shape used by JSON-returning transport operations.
590    ///
591    /// A model whose serializer refuses the generic depth boundary receives saturated complexity
592    /// so resource policy rejects it without panicking. Use [`Self::from_json`] for an exact count
593    /// of a managed JSON value at arbitrary depth.
594    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    /// Counts JSON's serialized shape without invoking a recursive serializer.
603    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/// Computes Treemap model complexity iteratively without serializing a user-controlled node tree.
1177#[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/// Computes Ishikawa model complexity iteratively without growing the stack with cause depth.
1260#[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(&note.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((&section.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
2076/// Counts an arbitrary JSON subtree without recursive calls or width-proportional scratch space.
2077fn 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[\"&nbsp;entity&nbsp;\"] --> 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}