Skip to main content

canic_core/config/component_group/
mod.rs

1//! Module: config::component_group
2//!
3//! Responsibility: compile Component Group declarations into one canonical acyclic graph.
4//! Does not own: final deployment purpose resolution, placement, persistence, or runtime parentage.
5//! Boundary: validated checked-in declarations become bounded occurrence-preserving projections.
6
7mod label;
8#[cfg(test)]
9mod tests;
10
11use crate::{
12    config::schema::{ComponentGroupSpecConfig, ConfigModel},
13    ids::{
14        ComponentGroupMemberId, ComponentGroupMemberPath, ComponentGroupMemberPathError,
15        ComponentGroupSpecId, ComponentSpecId, FleetServiceId,
16    },
17};
18use std::collections::{BTreeMap, BTreeSet};
19
20use candid::CandidType;
21use serde::{Deserialize, Serialize};
22use thiserror::Error as ThisError;
23
24pub use label::{
25    ComponentDeploymentLabel, ComponentDeploymentLabelKey, ComponentDeploymentLabelParseError,
26    ComponentDeploymentLabelValue, MAX_COMPONENT_DEPLOYMENT_LABEL_KEY_BYTES,
27    MAX_COMPONENT_DEPLOYMENT_LABEL_VALUE_BYTES, MAX_COMPONENT_DEPLOYMENT_LABELS,
28};
29
30const COMPONENT_GROUP_GRAPH_DOMAIN: &[u8] = b"canic/component-group-graph/v3";
31const COMPONENT_GROUP_GRAPH_SCHEMA_VERSION: u32 = 3;
32
33/// Maximum Component Group declarations in one App.
34pub const MAX_COMPONENT_GROUP_SPECS: usize = 256;
35/// Maximum direct Component or included-group members in one declaration.
36pub const MAX_COMPONENT_GROUP_MEMBERS: usize = 256;
37/// Maximum direct members across the complete declaration graph.
38pub const MAX_COMPONENT_GROUP_DECLARED_MEMBERS: usize = 16_384;
39/// Maximum included-group edges across the complete declaration graph.
40pub const MAX_COMPONENT_GROUP_INCLUSIONS: usize = 4_096;
41/// Maximum flattened Component occurrences emitted by any one selected group.
42pub const MAX_COMPONENT_GROUP_FLATTENED_MEMBERS: usize = 4_096;
43/// Maximum canonical bytes for the complete Component Group declaration graph.
44pub const MAX_COMPONENT_GROUP_GRAPH_CANONICAL_BYTES: usize = 2_097_152;
45
46impl ConfigModel {
47    /// Compile checked-in Component Group declarations into canonical graph order.
48    pub fn compile_component_group_topology(
49        &self,
50    ) -> Result<ComponentGroupTopology, ComponentGroupTopologyError> {
51        ComponentGroupTopology::compile(self)
52    }
53}
54
55/// Canonical checked-in Component Group declaration graph.
56#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
57#[serde(deny_unknown_fields)]
58pub struct ComponentGroupTopology {
59    pub component_groups: Vec<ComponentGroupSpec>,
60}
61
62impl ComponentGroupTopology {
63    /// Compile bounded declarations and prove every group can flatten acyclically.
64    pub fn compile(config: &ConfigModel) -> Result<Self, ComponentGroupTopologyError> {
65        if config.component_groups.len() > MAX_COMPONENT_GROUP_SPECS {
66            return Err(ComponentGroupTopologyError::GroupBoundExceeded {
67                actual: config.component_groups.len(),
68                maximum: MAX_COMPONENT_GROUP_SPECS,
69            });
70        }
71
72        let mut declared_members = 0_usize;
73        let mut inclusions = 0_usize;
74        let mut component_groups = Vec::with_capacity(config.component_groups.len());
75        for (component_group, source) in &config.component_groups {
76            let member_count = checked_member_count(component_group, source)?;
77            declared_members = declared_members.checked_add(member_count).ok_or(
78                ComponentGroupTopologyError::DeclaredMemberBoundExceeded {
79                    actual: usize::MAX,
80                    maximum: MAX_COMPONENT_GROUP_DECLARED_MEMBERS,
81                },
82            )?;
83            if declared_members > MAX_COMPONENT_GROUP_DECLARED_MEMBERS {
84                return Err(ComponentGroupTopologyError::DeclaredMemberBoundExceeded {
85                    actual: declared_members,
86                    maximum: MAX_COMPONENT_GROUP_DECLARED_MEMBERS,
87                });
88            }
89            inclusions = inclusions.checked_add(source.groups.len()).ok_or(
90                ComponentGroupTopologyError::InclusionBoundExceeded {
91                    actual: usize::MAX,
92                    maximum: MAX_COMPONENT_GROUP_INCLUSIONS,
93                },
94            )?;
95            if inclusions > MAX_COMPONENT_GROUP_INCLUSIONS {
96                return Err(ComponentGroupTopologyError::InclusionBoundExceeded {
97                    actual: inclusions,
98                    maximum: MAX_COMPONENT_GROUP_INCLUSIONS,
99                });
100            }
101            component_groups.push(compile_group(config, component_group, source)?);
102        }
103
104        let topology = Self { component_groups };
105        topology.canonical_bytes()?;
106        Ok(topology)
107    }
108
109    /// Return one exact canonical declaration.
110    #[must_use]
111    pub fn get(&self, component_group: &ComponentGroupSpecId) -> Option<&ComponentGroupSpec> {
112        self.component_groups
113            .binary_search_by(|candidate| candidate.component_group.cmp(component_group))
114            .ok()
115            .map(|index| &self.component_groups[index])
116    }
117
118    /// Flatten one selected declaration occurrence-by-occurrence without Spec deduplication.
119    pub fn flatten(
120        &self,
121        component_group: &ComponentGroupSpecId,
122    ) -> Result<FlattenedComponentGroup, ComponentGroupTopologyError> {
123        self.validate_canonical_projection()?;
124        self.flatten_canonical(component_group)
125    }
126
127    fn flatten_canonical(
128        &self,
129        component_group: &ComponentGroupSpecId,
130    ) -> Result<FlattenedComponentGroup, ComponentGroupTopologyError> {
131        if self.get(component_group).is_none() {
132            return Err(ComponentGroupTopologyError::UnknownGroup {
133                component_group: component_group.clone(),
134            });
135        }
136
137        let mut components = Vec::new();
138        let mut member_path = Vec::new();
139        let mut active_groups = Vec::new();
140        let mut service_purposes = Vec::new();
141        let mut labels = BTreeMap::new();
142        self.flatten_into(
143            component_group,
144            &mut member_path,
145            &mut active_groups,
146            &mut service_purposes,
147            &mut labels,
148            &mut components,
149        )?;
150        Ok(FlattenedComponentGroup {
151            component_group: component_group.clone(),
152            components,
153        })
154    }
155
156    /// Return the exact canonical graph bytes used by later semantic digests.
157    pub fn canonical_bytes(&self) -> Result<Vec<u8>, ComponentGroupTopologyError> {
158        self.validate_canonical_projection()?;
159        for group in &self.component_groups {
160            self.flatten_canonical(&group.component_group)?;
161        }
162        let mut bytes = Vec::new();
163        encode_bytes(&mut bytes, COMPONENT_GROUP_GRAPH_DOMAIN);
164        bytes.extend_from_slice(&COMPONENT_GROUP_GRAPH_SCHEMA_VERSION.to_be_bytes());
165        encode_u64(&mut bytes, self.component_groups.len());
166        for group in &self.component_groups {
167            encode_text(&mut bytes, group.component_group.as_str());
168            encode_u64(&mut bytes, group.members.len());
169            for member in &group.members {
170                match member {
171                    ComponentGroupMember::Component {
172                        member,
173                        component_spec,
174                        kind,
175                        service_purpose,
176                        labels,
177                    } => {
178                        bytes.push(0);
179                        encode_text(&mut bytes, member.as_str());
180                        encode_text(&mut bytes, component_spec.as_str());
181                        encode_leaf_kind(&mut bytes, kind);
182                        encode_service_purpose(&mut bytes, service_purpose.as_ref());
183                        encode_labels(&mut bytes, labels);
184                    }
185                    ComponentGroupMember::Group {
186                        member,
187                        component_group,
188                        service_purpose,
189                        labels,
190                    } => {
191                        bytes.push(1);
192                        encode_text(&mut bytes, member.as_str());
193                        encode_text(&mut bytes, component_group.as_str());
194                        encode_service_purpose(&mut bytes, service_purpose.as_ref());
195                        encode_labels(&mut bytes, labels);
196                    }
197                }
198            }
199        }
200        if bytes.len() > MAX_COMPONENT_GROUP_GRAPH_CANONICAL_BYTES {
201            return Err(ComponentGroupTopologyError::CanonicalBytesBoundExceeded {
202                actual: bytes.len(),
203                maximum: MAX_COMPONENT_GROUP_GRAPH_CANONICAL_BYTES,
204            });
205        }
206        Ok(bytes)
207    }
208
209    fn validate_canonical_projection(&self) -> Result<(), ComponentGroupTopologyError> {
210        if self.component_groups.len() > MAX_COMPONENT_GROUP_SPECS {
211            return Err(ComponentGroupTopologyError::GroupBoundExceeded {
212                actual: self.component_groups.len(),
213                maximum: MAX_COMPONENT_GROUP_SPECS,
214            });
215        }
216
217        let mut declared_members = 0_usize;
218        let mut inclusions = 0_usize;
219        let mut previous_group: Option<&ComponentGroupSpecId> = None;
220        for group in &self.component_groups {
221            if previous_group.is_some_and(|previous| previous >= &group.component_group) {
222                return Err(ComponentGroupTopologyError::NonCanonicalGroupOrder {
223                    component_group: group.component_group.clone(),
224                });
225            }
226            previous_group = Some(&group.component_group);
227
228            if group.members.is_empty() {
229                return Err(ComponentGroupTopologyError::EmptyGroup {
230                    component_group: group.component_group.clone(),
231                });
232            }
233            if group.members.len() > MAX_COMPONENT_GROUP_MEMBERS {
234                return Err(ComponentGroupTopologyError::MemberBoundExceeded {
235                    component_group: group.component_group.clone(),
236                    actual: group.members.len(),
237                    maximum: MAX_COMPONENT_GROUP_MEMBERS,
238                });
239            }
240            declared_members = declared_members.checked_add(group.members.len()).ok_or(
241                ComponentGroupTopologyError::DeclaredMemberBoundExceeded {
242                    actual: usize::MAX,
243                    maximum: MAX_COMPONENT_GROUP_DECLARED_MEMBERS,
244                },
245            )?;
246            if declared_members > MAX_COMPONENT_GROUP_DECLARED_MEMBERS {
247                return Err(ComponentGroupTopologyError::DeclaredMemberBoundExceeded {
248                    actual: declared_members,
249                    maximum: MAX_COMPONENT_GROUP_DECLARED_MEMBERS,
250                });
251            }
252            let mut previous_member: Option<&ComponentGroupMemberId> = None;
253            for member in &group.members {
254                let member_id = member.member();
255                if previous_member.is_some_and(|previous| previous >= member_id) {
256                    return Err(ComponentGroupTopologyError::NonCanonicalMemberOrder {
257                        component_group: group.component_group.clone(),
258                        member: member_id.clone(),
259                    });
260                }
261                previous_member = Some(member_id);
262                validate_member_labels(&group.component_group, member)?;
263                if let ComponentGroupMember::Group {
264                    component_group: included,
265                    ..
266                } = member
267                {
268                    inclusions = inclusions.checked_add(1).ok_or(
269                        ComponentGroupTopologyError::InclusionBoundExceeded {
270                            actual: usize::MAX,
271                            maximum: MAX_COMPONENT_GROUP_INCLUSIONS,
272                        },
273                    )?;
274                    if inclusions > MAX_COMPONENT_GROUP_INCLUSIONS {
275                        return Err(ComponentGroupTopologyError::InclusionBoundExceeded {
276                            actual: inclusions,
277                            maximum: MAX_COMPONENT_GROUP_INCLUSIONS,
278                        });
279                    }
280                    if self.get(included).is_none() {
281                        return Err(ComponentGroupTopologyError::UnknownIncludedGroup {
282                            component_group: group.component_group.clone(),
283                            included: included.clone(),
284                        });
285                    }
286                }
287            }
288        }
289        Ok(())
290    }
291
292    fn flatten_into(
293        &self,
294        component_group: &ComponentGroupSpecId,
295        member_path: &mut Vec<ComponentGroupMemberId>,
296        active_groups: &mut Vec<ComponentGroupSpecId>,
297        service_purposes: &mut Vec<FleetServiceMemberPurpose>,
298        effective_labels: &mut BTreeMap<ComponentDeploymentLabelKey, ComponentDeploymentLabelValue>,
299        output: &mut Vec<FlattenedComponentGroupMember>,
300    ) -> Result<(), ComponentGroupTopologyError> {
301        if active_groups.contains(component_group) {
302            return Err(ComponentGroupTopologyError::InclusionCycle {
303                component_group: component_group.clone(),
304            });
305        }
306        let group =
307            self.get(component_group)
308                .ok_or_else(|| ComponentGroupTopologyError::UnknownGroup {
309                    component_group: component_group.clone(),
310                })?;
311        active_groups.push(component_group.clone());
312
313        for member in &group.members {
314            member_path.push(member.member().clone());
315            let output_start = output.len();
316            if let Some(purpose) = member.service_purpose() {
317                service_purposes.push(purpose);
318            }
319            let current_path =
320                ComponentGroupMemberPath::try_from(member_path.clone()).map_err(|source| {
321                    ComponentGroupTopologyError::InvalidMemberPath {
322                        component_group: active_groups[0].clone(),
323                        source,
324                    }
325                })?;
326            let added_label_keys = extend_effective_labels(
327                &active_groups[0],
328                &current_path,
329                member.labels(),
330                effective_labels,
331            )?;
332            match member {
333                ComponentGroupMember::Component {
334                    component_spec,
335                    kind,
336                    ..
337                } => {
338                    if output.len() >= MAX_COMPONENT_GROUP_FLATTENED_MEMBERS {
339                        return Err(ComponentGroupTopologyError::FlattenedMemberBoundExceeded {
340                            component_group: active_groups[0].clone(),
341                            actual: output.len() + 1,
342                            maximum: MAX_COMPONENT_GROUP_FLATTENED_MEMBERS,
343                        });
344                    }
345                    output.push(FlattenedComponentGroupMember {
346                        member_path: current_path,
347                        component_spec: component_spec.clone(),
348                        kind: kind.clone(),
349                        service_purpose_assignments: match kind {
350                            ComponentGroupLeafKind::Ordinary => Vec::new(),
351                            ComponentGroupLeafKind::FleetService { .. } => service_purposes.clone(),
352                        },
353                        labels: effective_labels
354                            .iter()
355                            .map(|(key, value)| ComponentDeploymentLabel {
356                                key: key.clone(),
357                                value: value.clone(),
358                            })
359                            .collect(),
360                    });
361                }
362                ComponentGroupMember::Group {
363                    component_group: included,
364                    ..
365                } => self.flatten_into(
366                    included,
367                    member_path,
368                    active_groups,
369                    service_purposes,
370                    effective_labels,
371                    output,
372                )?,
373            }
374            if member.service_purpose().is_some()
375                && !output[output_start..]
376                    .iter()
377                    .any(FlattenedComponentGroupMember::is_fleet_service)
378            {
379                return Err(
380                    ComponentGroupTopologyError::InapplicableServicePurposeAssignment {
381                        component_group: component_group.clone(),
382                        member: member.member().clone(),
383                    },
384                );
385            }
386            if member.service_purpose().is_some() {
387                service_purposes.pop();
388            }
389            for key in added_label_keys {
390                effective_labels.remove(&key);
391            }
392            member_path.pop();
393        }
394
395        active_groups.pop();
396        Ok(())
397    }
398}
399
400/// One canonical Component Group declaration.
401#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
402#[serde(deny_unknown_fields)]
403pub struct ComponentGroupSpec {
404    pub component_group: ComponentGroupSpecId,
405    pub members: Vec<ComponentGroupMember>,
406}
407
408/// One direct declaration member; included groups remain configuration-only edges.
409#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
410pub enum ComponentGroupMember {
411    Component {
412        member: ComponentGroupMemberId,
413        component_spec: ComponentSpecId,
414        kind: ComponentGroupLeafKind,
415        service_purpose: Option<FleetServiceMemberPurpose>,
416        labels: Vec<ComponentDeploymentLabel>,
417    },
418    Group {
419        member: ComponentGroupMemberId,
420        component_group: ComponentGroupSpecId,
421        service_purpose: Option<FleetServiceMemberPurpose>,
422        labels: Vec<ComponentDeploymentLabel>,
423    },
424}
425
426impl ComponentGroupMember {
427    #[must_use]
428    pub const fn member(&self) -> &ComponentGroupMemberId {
429        match self {
430            Self::Component { member, .. } | Self::Group { member, .. } => member,
431        }
432    }
433
434    #[must_use]
435    pub const fn service_purpose(&self) -> Option<FleetServiceMemberPurpose> {
436        match self {
437            Self::Component {
438                service_purpose, ..
439            }
440            | Self::Group {
441                service_purpose, ..
442            } => *service_purpose,
443        }
444    }
445
446    #[must_use]
447    pub fn labels(&self) -> &[ComponentDeploymentLabel] {
448        match self {
449            Self::Component { labels, .. } | Self::Group { labels, .. } => labels,
450        }
451    }
452}
453
454/// Typed declaration kind for one flattened Component Group leaf.
455#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
456pub enum ComponentGroupLeafKind {
457    Ordinary,
458    FleetService { service: FleetServiceId },
459}
460
461/// Exact semantic purpose assigned to one Fleet-service Component occurrence.
462#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
463pub enum FleetServiceMemberPurpose {
464    #[serde(rename = "authority")]
465    Authority,
466    #[serde(rename = "replica")]
467    Replica,
468    #[serde(rename = "pool_member")]
469    PoolMember,
470}
471
472/// Complete flattened direct-Component occurrences for one selected group.
473#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
474#[serde(deny_unknown_fields)]
475pub struct FlattenedComponentGroup {
476    pub component_group: ComponentGroupSpecId,
477    pub components: Vec<FlattenedComponentGroupMember>,
478}
479
480/// One distinct flattened Component occurrence identified by its full member path.
481#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
482#[serde(deny_unknown_fields)]
483pub struct FlattenedComponentGroupMember {
484    pub member_path: ComponentGroupMemberPath,
485    pub component_spec: ComponentSpecId,
486    pub kind: ComponentGroupLeafKind,
487    pub service_purpose_assignments: Vec<FleetServiceMemberPurpose>,
488    pub labels: Vec<ComponentDeploymentLabel>,
489}
490
491impl FlattenedComponentGroupMember {
492    pub(super) const fn is_fleet_service(&self) -> bool {
493        matches!(self.kind, ComponentGroupLeafKind::FleetService { .. })
494    }
495}
496
497/// Typed rejection for invalid Component Group declaration or canonical graph state.
498#[derive(Debug, ThisError)]
499pub enum ComponentGroupTopologyError {
500    #[error("Component Group count {actual} exceeds bound {maximum}")]
501    GroupBoundExceeded { actual: usize, maximum: usize },
502
503    #[error("Component Group '{component_group}' must declare at least one member")]
504    EmptyGroup {
505        component_group: ComponentGroupSpecId,
506    },
507
508    #[error("Component Group '{component_group}' member count {actual} exceeds bound {maximum}")]
509    MemberBoundExceeded {
510        component_group: ComponentGroupSpecId,
511        actual: usize,
512        maximum: usize,
513    },
514
515    #[error("Component Group declared-member count {actual} exceeds bound {maximum}")]
516    DeclaredMemberBoundExceeded { actual: usize, maximum: usize },
517
518    #[error("Component Group inclusion count {actual} exceeds bound {maximum}")]
519    InclusionBoundExceeded { actual: usize, maximum: usize },
520
521    #[error("Component Group '{component_group}' declares member '{member}' more than once")]
522    DuplicateMember {
523        component_group: ComponentGroupSpecId,
524        member: ComponentGroupMemberId,
525    },
526
527    #[error(
528        "Component Group '{component_group}' member '{member}' references unknown Component Spec '{component_spec}'"
529    )]
530    UnknownComponentSpec {
531        component_group: ComponentGroupSpecId,
532        member: ComponentGroupMemberId,
533        component_spec: ComponentSpecId,
534    },
535
536    #[error("Component Group '{component_group}' includes unknown Component Group '{included}'")]
537    UnknownIncludedGroup {
538        component_group: ComponentGroupSpecId,
539        included: ComponentGroupSpecId,
540    },
541
542    #[error("unknown Component Group '{component_group}'")]
543    UnknownGroup {
544        component_group: ComponentGroupSpecId,
545    },
546
547    #[error("Component Group inclusion cycle involves '{component_group}'")]
548    InclusionCycle {
549        component_group: ComponentGroupSpecId,
550    },
551
552    #[error(
553        "Component Group '{component_group}' flattened member count {actual} exceeds bound {maximum}"
554    )]
555    FlattenedMemberBoundExceeded {
556        component_group: ComponentGroupSpecId,
557        actual: usize,
558        maximum: usize,
559    },
560
561    #[error("Component Group '{component_group}' has an invalid flattened member path: {source}")]
562    InvalidMemberPath {
563        component_group: ComponentGroupSpecId,
564        #[source]
565        source: ComponentGroupMemberPathError,
566    },
567
568    #[error(
569        "Component Group '{component_group}' member '{member}' assigns Fleet-service purpose without a service-bearing leaf"
570    )]
571    InapplicableServicePurposeAssignment {
572        component_group: ComponentGroupSpecId,
573        member: ComponentGroupMemberId,
574    },
575
576    #[error(
577        "Component Group '{component_group}' member '{member}' has {actual} labels; maximum is {maximum}"
578    )]
579    LabelBoundExceeded {
580        component_group: ComponentGroupSpecId,
581        member: ComponentGroupMemberId,
582        actual: usize,
583        maximum: usize,
584    },
585
586    #[error(
587        "Component Group '{component_group}' member '{member}' label '{label}' is duplicated or not in canonical order"
588    )]
589    NonCanonicalLabelOrder {
590        component_group: ComponentGroupSpecId,
591        member: ComponentGroupMemberId,
592        label: ComponentDeploymentLabelKey,
593    },
594
595    #[error(
596        "Component Group '{component_group}' flattened member '{member_path:?}' repeats label key '{label}'"
597    )]
598    DuplicateEffectiveLabel {
599        component_group: ComponentGroupSpecId,
600        member_path: ComponentGroupMemberPath,
601        label: ComponentDeploymentLabelKey,
602    },
603
604    #[error(
605        "Component Group '{component_group}' flattened member '{member_path:?}' has {actual} effective labels; maximum is {maximum}"
606    )]
607    EffectiveLabelBoundExceeded {
608        component_group: ComponentGroupSpecId,
609        member_path: ComponentGroupMemberPath,
610        actual: usize,
611        maximum: usize,
612    },
613
614    #[error("Component Group graph canonical bytes {actual} exceed bound {maximum}")]
615    CanonicalBytesBoundExceeded { actual: usize, maximum: usize },
616
617    #[error("Component Group '{component_group}' is not in canonical order")]
618    NonCanonicalGroupOrder {
619        component_group: ComponentGroupSpecId,
620    },
621
622    #[error("Component Group '{component_group}' member '{member}' is not in canonical order")]
623    NonCanonicalMemberOrder {
624        component_group: ComponentGroupSpecId,
625        member: ComponentGroupMemberId,
626    },
627}
628
629fn checked_member_count(
630    component_group: &ComponentGroupSpecId,
631    source: &ComponentGroupSpecConfig,
632) -> Result<usize, ComponentGroupTopologyError> {
633    let count = source
634        .components
635        .len()
636        .checked_add(source.groups.len())
637        .ok_or_else(|| ComponentGroupTopologyError::MemberBoundExceeded {
638            component_group: component_group.clone(),
639            actual: usize::MAX,
640            maximum: MAX_COMPONENT_GROUP_MEMBERS,
641        })?;
642    if count == 0 {
643        return Err(ComponentGroupTopologyError::EmptyGroup {
644            component_group: component_group.clone(),
645        });
646    }
647    if count > MAX_COMPONENT_GROUP_MEMBERS {
648        return Err(ComponentGroupTopologyError::MemberBoundExceeded {
649            component_group: component_group.clone(),
650            actual: count,
651            maximum: MAX_COMPONENT_GROUP_MEMBERS,
652        });
653    }
654    Ok(count)
655}
656
657fn compile_group(
658    config: &ConfigModel,
659    component_group: &ComponentGroupSpecId,
660    source: &ComponentGroupSpecConfig,
661) -> Result<ComponentGroupSpec, ComponentGroupTopologyError> {
662    let mut seen = BTreeSet::new();
663    let mut members = Vec::with_capacity(source.components.len() + source.groups.len());
664    for (member, component) in &source.components {
665        if !seen.insert(member.clone()) {
666            return Err(ComponentGroupTopologyError::DuplicateMember {
667                component_group: component_group.clone(),
668                member: member.clone(),
669            });
670        }
671        if !config
672            .component_specs
673            .contains_key(&component.component_spec)
674        {
675            return Err(ComponentGroupTopologyError::UnknownComponentSpec {
676                component_group: component_group.clone(),
677                member: member.clone(),
678                component_spec: component.component_spec.clone(),
679            });
680        }
681        members.push(ComponentGroupMember::Component {
682            member: member.clone(),
683            component_spec: component.component_spec.clone(),
684            kind: component
685                .service
686                .clone()
687                .map_or(ComponentGroupLeafKind::Ordinary, |service| {
688                    ComponentGroupLeafKind::FleetService { service }
689                }),
690            service_purpose: component.service_purpose,
691            labels: source_labels(&component.labels),
692        });
693    }
694    for (member, included) in &source.groups {
695        if !seen.insert(member.clone()) {
696            return Err(ComponentGroupTopologyError::DuplicateMember {
697                component_group: component_group.clone(),
698                member: member.clone(),
699            });
700        }
701        if !config
702            .component_groups
703            .contains_key(&included.component_group)
704        {
705            return Err(ComponentGroupTopologyError::UnknownIncludedGroup {
706                component_group: component_group.clone(),
707                included: included.component_group.clone(),
708            });
709        }
710        members.push(ComponentGroupMember::Group {
711            member: member.clone(),
712            component_group: included.component_group.clone(),
713            service_purpose: included.service_purpose,
714            labels: source_labels(&included.labels),
715        });
716    }
717    members.sort_by(|left, right| left.member().cmp(right.member()));
718    Ok(ComponentGroupSpec {
719        component_group: component_group.clone(),
720        members,
721    })
722}
723
724fn encode_u64(bytes: &mut Vec<u8>, value: usize) {
725    let value = u64::try_from(value).expect("bounded Component Group length fits in u64");
726    bytes.extend_from_slice(&value.to_be_bytes());
727}
728
729fn encode_bytes(output: &mut Vec<u8>, value: &[u8]) {
730    encode_u64(output, value.len());
731    output.extend_from_slice(value);
732}
733
734fn encode_text(output: &mut Vec<u8>, value: &str) {
735    encode_bytes(output, value.as_bytes());
736}
737
738fn encode_leaf_kind(output: &mut Vec<u8>, kind: &ComponentGroupLeafKind) {
739    match kind {
740        ComponentGroupLeafKind::Ordinary => output.push(0),
741        ComponentGroupLeafKind::FleetService { service } => {
742            output.push(1);
743            encode_text(output, service.as_str());
744        }
745    }
746}
747
748fn encode_service_purpose(output: &mut Vec<u8>, purpose: Option<&FleetServiceMemberPurpose>) {
749    match purpose {
750        None => output.push(0),
751        Some(FleetServiceMemberPurpose::Authority) => output.extend_from_slice(&[1, 0]),
752        Some(FleetServiceMemberPurpose::Replica) => output.extend_from_slice(&[1, 1]),
753        Some(FleetServiceMemberPurpose::PoolMember) => output.extend_from_slice(&[1, 2]),
754    }
755}
756
757fn encode_labels(output: &mut Vec<u8>, labels: &[ComponentDeploymentLabel]) {
758    encode_u64(output, labels.len());
759    for label in labels {
760        encode_text(output, label.key.as_str());
761        encode_text(output, label.value.as_str());
762    }
763}
764
765pub(super) fn source_labels(
766    labels: &BTreeMap<ComponentDeploymentLabelKey, ComponentDeploymentLabelValue>,
767) -> Vec<ComponentDeploymentLabel> {
768    labels
769        .iter()
770        .map(|(key, value)| ComponentDeploymentLabel {
771            key: key.clone(),
772            value: value.clone(),
773        })
774        .collect()
775}
776
777fn validate_member_labels(
778    component_group: &ComponentGroupSpecId,
779    member: &ComponentGroupMember,
780) -> Result<(), ComponentGroupTopologyError> {
781    let labels = member.labels();
782    if labels.len() > MAX_COMPONENT_DEPLOYMENT_LABELS {
783        return Err(ComponentGroupTopologyError::LabelBoundExceeded {
784            component_group: component_group.clone(),
785            member: member.member().clone(),
786            actual: labels.len(),
787            maximum: MAX_COMPONENT_DEPLOYMENT_LABELS,
788        });
789    }
790    let mut previous: Option<&ComponentDeploymentLabelKey> = None;
791    for label in labels {
792        if previous.is_some_and(|key| key >= &label.key) {
793            return Err(ComponentGroupTopologyError::NonCanonicalLabelOrder {
794                component_group: component_group.clone(),
795                member: member.member().clone(),
796                label: label.key.clone(),
797            });
798        }
799        previous = Some(&label.key);
800    }
801    Ok(())
802}
803
804fn extend_effective_labels(
805    component_group: &ComponentGroupSpecId,
806    member_path: &ComponentGroupMemberPath,
807    member_labels: &[ComponentDeploymentLabel],
808    effective_labels: &mut BTreeMap<ComponentDeploymentLabelKey, ComponentDeploymentLabelValue>,
809) -> Result<Vec<ComponentDeploymentLabelKey>, ComponentGroupTopologyError> {
810    let mut added_label_keys = Vec::with_capacity(member_labels.len());
811    for label in member_labels {
812        match effective_labels.entry(label.key.clone()) {
813            std::collections::btree_map::Entry::Vacant(entry) => {
814                entry.insert(label.value.clone());
815                added_label_keys.push(label.key.clone());
816            }
817            std::collections::btree_map::Entry::Occupied(_) => {
818                return Err(ComponentGroupTopologyError::DuplicateEffectiveLabel {
819                    component_group: component_group.clone(),
820                    member_path: member_path.clone(),
821                    label: label.key.clone(),
822                });
823            }
824        }
825    }
826    if effective_labels.len() > MAX_COMPONENT_DEPLOYMENT_LABELS {
827        return Err(ComponentGroupTopologyError::EffectiveLabelBoundExceeded {
828            component_group: component_group.clone(),
829            member_path: member_path.clone(),
830            actual: effective_labels.len(),
831            maximum: MAX_COMPONENT_DEPLOYMENT_LABELS,
832        });
833    }
834    Ok(added_label_keys)
835}