Skip to main content

greentic_cap_types/
lib.rs

1//! Canonical capability data model for Greentic.
2
3#![forbid(unsafe_code)]
4
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7
8#[cfg(feature = "schemars")]
9use schemars::JsonSchema;
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// Capability metadata stored alongside offers, requirements, and consumes.
14pub type CapabilityMetadata = BTreeMap<String, serde_json::Value>;
15
16/// Error returned when parsing a capability identifier fails.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum CapabilityIdError {
19    /// The identifier does not use the `cap://` scheme.
20    MissingScheme,
21    /// The identifier has no content after `cap://`.
22    Empty,
23    /// The identifier contains an unsupported character.
24    InvalidCharacter { ch: char, index: usize },
25}
26
27impl fmt::Display for CapabilityIdError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::MissingScheme => write!(f, "capability ids must use the cap:// scheme"),
31            Self::Empty => write!(f, "capability ids must not be empty"),
32            Self::InvalidCharacter { ch, index } => {
33                write!(
34                    f,
35                    "capability id contains invalid character {ch:?} at index {index}"
36                )
37            }
38        }
39    }
40}
41
42impl std::error::Error for CapabilityIdError {}
43
44/// Canonical capability identifier.
45#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
47#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
48#[cfg_attr(feature = "schemars", derive(JsonSchema))]
49pub struct CapabilityId(String);
50
51impl CapabilityId {
52    /// Creates a validated capability identifier.
53    pub fn new(value: impl Into<String>) -> Result<Self, CapabilityIdError> {
54        let value = value.into();
55        Self::validate(&value)?;
56        Ok(Self(value))
57    }
58
59    /// Returns the inner capability identifier string.
60    pub fn as_str(&self) -> &str {
61        &self.0
62    }
63
64    fn validate(value: &str) -> Result<(), CapabilityIdError> {
65        if value.is_empty() {
66            return Err(CapabilityIdError::Empty);
67        }
68        if !value.starts_with("cap://") {
69            return Err(CapabilityIdError::MissingScheme);
70        }
71
72        let remainder = &value["cap://".len()..];
73        if remainder.is_empty() {
74            return Err(CapabilityIdError::Empty);
75        }
76
77        for (index, ch) in value.char_indices() {
78            if ch.is_ascii_alphanumeric() || matches!(ch, ':' | '/' | '-' | '_' | '.' | '+') {
79                continue;
80            }
81            return Err(CapabilityIdError::InvalidCharacter { ch, index });
82        }
83
84        Ok(())
85    }
86}
87
88impl TryFrom<String> for CapabilityId {
89    type Error = CapabilityIdError;
90
91    fn try_from(value: String) -> Result<Self, Self::Error> {
92        Self::validate(&value)?;
93        Ok(Self(value))
94    }
95}
96
97impl From<CapabilityId> for String {
98    fn from(value: CapabilityId) -> Self {
99        value.0
100    }
101}
102
103impl fmt::Display for CapabilityId {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        f.write_str(&self.0)
106    }
107}
108
109/// Optional provider reference for an offered capability.
110#[derive(Clone, Debug, PartialEq, Eq, Default)]
111#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
112#[cfg_attr(feature = "schemars", derive(JsonSchema))]
113pub struct CapabilityProviderRef {
114    /// Provider component identifier or component ref.
115    pub component_ref: String,
116    /// Provider operation used for this capability.
117    pub operation: String,
118    /// Optional map from logical contract operations to concrete component operations.
119    #[cfg_attr(
120        feature = "serde",
121        serde(default, skip_serializing_if = "Vec::is_empty")
122    )]
123    pub operation_map: Vec<CapabilityProviderOperationMap>,
124}
125
126/// Logical capability operation mapped to a concrete component operation.
127#[derive(Clone, Debug, PartialEq, Eq)]
128#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
129#[cfg_attr(feature = "schemars", derive(JsonSchema))]
130pub struct CapabilityProviderOperationMap {
131    /// Logical contract operation name.
132    pub contract_operation: String,
133    /// Concrete component operation name.
134    pub component_operation: String,
135    /// Input schema expected by the contract.
136    pub input_schema: serde_json::Value,
137    /// Output schema expected by the contract.
138    pub output_schema: serde_json::Value,
139}
140
141/// Self-description for a component used by pack capability compatibility checks.
142#[derive(Clone, Debug, PartialEq, Eq)]
143#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
144#[cfg_attr(feature = "schemars", derive(JsonSchema))]
145pub struct CapabilityComponentDescriptor {
146    /// Component identifier or component ref.
147    pub component_ref: String,
148    /// Component version string.
149    pub version: String,
150    /// Operations exposed by the component.
151    #[cfg_attr(feature = "serde", serde(default))]
152    pub operations: Vec<CapabilityComponentOperation>,
153    /// Capability ids declared by the component.
154    #[cfg_attr(feature = "serde", serde(default))]
155    pub capabilities: Vec<CapabilityId>,
156    /// Free-form metadata.
157    #[cfg_attr(
158        feature = "serde",
159        serde(default, skip_serializing_if = "BTreeMap::is_empty")
160    )]
161    pub metadata: CapabilityMetadata,
162}
163
164/// A single component operation in the self-description.
165#[derive(Clone, Debug, PartialEq, Eq)]
166#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
167#[cfg_attr(feature = "schemars", derive(JsonSchema))]
168pub struct CapabilityComponentOperation {
169    /// Operation name.
170    pub name: String,
171    /// Input schema for the operation.
172    pub input_schema: serde_json::Value,
173    /// Output schema for the operation.
174    pub output_schema: serde_json::Value,
175}
176
177/// Offers a capability from a provider.
178#[derive(Clone, Debug, PartialEq, Eq)]
179#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
180#[cfg_attr(feature = "schemars", derive(JsonSchema))]
181pub struct CapabilityOffer {
182    /// Stable offer identifier.
183    pub id: String,
184    /// Capability being offered.
185    pub capability: CapabilityId,
186    /// Provider implementation reference.
187    #[cfg_attr(
188        feature = "serde",
189        serde(default, skip_serializing_if = "Option::is_none")
190    )]
191    pub provider: Option<CapabilityProviderRef>,
192    /// Profiles that this offer belongs to.
193    #[cfg_attr(
194        feature = "serde",
195        serde(default, skip_serializing_if = "Vec::is_empty")
196    )]
197    pub profiles: Vec<String>,
198    /// Optional human-readable description.
199    #[cfg_attr(
200        feature = "serde",
201        serde(default, skip_serializing_if = "Option::is_none")
202    )]
203    pub description: Option<String>,
204    /// Free-form metadata.
205    #[cfg_attr(
206        feature = "serde",
207        serde(default, skip_serializing_if = "BTreeMap::is_empty")
208    )]
209    pub metadata: CapabilityMetadata,
210}
211
212impl CapabilityOffer {
213    /// Creates a new capability offer.
214    pub fn new(id: impl Into<String>, capability: CapabilityId) -> Self {
215        Self {
216            id: id.into(),
217            capability,
218            provider: None,
219            profiles: Vec::new(),
220            description: None,
221            metadata: BTreeMap::new(),
222        }
223    }
224}
225
226/// Describes a required capability.
227#[derive(Clone, Debug, PartialEq, Eq)]
228#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
229#[cfg_attr(feature = "schemars", derive(JsonSchema))]
230pub struct CapabilityRequirement {
231    /// Stable requirement identifier.
232    pub id: String,
233    /// Required capability.
234    pub capability: CapabilityId,
235    /// Profiles that this requirement belongs to.
236    #[cfg_attr(
237        feature = "serde",
238        serde(default, skip_serializing_if = "Vec::is_empty")
239    )]
240    pub profiles: Vec<String>,
241    /// Whether the requirement is optional.
242    #[cfg_attr(feature = "serde", serde(default))]
243    pub optional: bool,
244    /// Optional human-readable description.
245    #[cfg_attr(
246        feature = "serde",
247        serde(default, skip_serializing_if = "Option::is_none")
248    )]
249    pub description: Option<String>,
250    /// Free-form metadata.
251    #[cfg_attr(
252        feature = "serde",
253        serde(default, skip_serializing_if = "BTreeMap::is_empty")
254    )]
255    pub metadata: CapabilityMetadata,
256}
257
258impl CapabilityRequirement {
259    /// Creates a new capability requirement.
260    pub fn new(id: impl Into<String>, capability: CapabilityId) -> Self {
261        Self {
262            id: id.into(),
263            capability,
264            profiles: Vec::new(),
265            optional: false,
266            description: None,
267            metadata: BTreeMap::new(),
268        }
269    }
270}
271
272/// How a capability is consumed.
273#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
274#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
275#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
276#[cfg_attr(feature = "schemars", derive(JsonSchema))]
277pub enum CapabilityConsumeMode {
278    /// The consumer reads from the capability.
279    #[default]
280    Shared,
281    /// The consumer mutates the capability.
282    Exclusive,
283    /// The consumer requires ephemeral use.
284    Ephemeral,
285}
286
287/// Describes a capability that is consumed.
288#[derive(Clone, Debug, PartialEq, Eq)]
289#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
290#[cfg_attr(feature = "schemars", derive(JsonSchema))]
291pub struct CapabilityConsume {
292    /// Stable consume identifier.
293    pub id: String,
294    /// Consumed capability.
295    pub capability: CapabilityId,
296    /// Profiles that this consume belongs to.
297    #[cfg_attr(
298        feature = "serde",
299        serde(default, skip_serializing_if = "Vec::is_empty")
300    )]
301    pub profiles: Vec<String>,
302    /// How the capability is consumed.
303    #[cfg_attr(feature = "serde", serde(default))]
304    pub mode: CapabilityConsumeMode,
305    /// Optional human-readable description.
306    #[cfg_attr(
307        feature = "serde",
308        serde(default, skip_serializing_if = "Option::is_none")
309    )]
310    pub description: Option<String>,
311    /// Free-form metadata.
312    #[cfg_attr(
313        feature = "serde",
314        serde(default, skip_serializing_if = "BTreeMap::is_empty")
315    )]
316    pub metadata: CapabilityMetadata,
317}
318
319impl CapabilityConsume {
320    /// Creates a new capability consume declaration.
321    pub fn new(id: impl Into<String>, capability: CapabilityId) -> Self {
322        Self {
323            id: id.into(),
324            capability,
325            profiles: Vec::new(),
326            mode: CapabilityConsumeMode::Shared,
327            description: None,
328            metadata: BTreeMap::new(),
329        }
330    }
331}
332
333/// A named bundle of capability requirements and consumes.
334#[derive(Clone, Debug, PartialEq, Eq)]
335#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
336#[cfg_attr(feature = "schemars", derive(JsonSchema))]
337pub struct CapabilityProfile {
338    /// Profile identifier.
339    pub id: String,
340    /// Optional human-readable description.
341    #[cfg_attr(
342        feature = "serde",
343        serde(default, skip_serializing_if = "Option::is_none")
344    )]
345    pub description: Option<String>,
346    /// Requirements included in the profile.
347    #[cfg_attr(
348        feature = "serde",
349        serde(default, skip_serializing_if = "Vec::is_empty")
350    )]
351    pub requires: Vec<CapabilityRequirement>,
352    /// Consumes included in the profile.
353    #[cfg_attr(
354        feature = "serde",
355        serde(default, skip_serializing_if = "Vec::is_empty")
356    )]
357    pub consumes: Vec<CapabilityConsume>,
358}
359
360impl CapabilityProfile {
361    /// Creates a new profile.
362    pub fn new(id: impl Into<String>) -> Self {
363        Self {
364            id: id.into(),
365            description: None,
366            requires: Vec::new(),
367            consumes: Vec::new(),
368        }
369    }
370}
371
372/// Top-level capability declaration payload.
373#[derive(Clone, Debug, PartialEq, Eq)]
374#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
375#[cfg_attr(feature = "schemars", derive(JsonSchema))]
376pub struct CapabilityDeclaration {
377    /// Capabilities offered by the pack/component.
378    #[cfg_attr(
379        feature = "serde",
380        serde(default, skip_serializing_if = "Vec::is_empty")
381    )]
382    pub offers: Vec<CapabilityOffer>,
383    /// Top-level requirements.
384    #[cfg_attr(
385        feature = "serde",
386        serde(default, skip_serializing_if = "Vec::is_empty")
387    )]
388    pub requires: Vec<CapabilityRequirement>,
389    /// Top-level consumes.
390    #[cfg_attr(
391        feature = "serde",
392        serde(default, skip_serializing_if = "Vec::is_empty")
393    )]
394    pub consumes: Vec<CapabilityConsume>,
395    /// Optional named profiles.
396    #[cfg_attr(
397        feature = "serde",
398        serde(default, skip_serializing_if = "Vec::is_empty")
399    )]
400    pub profiles: Vec<CapabilityProfile>,
401}
402
403impl CapabilityDeclaration {
404    /// Creates an empty capability declaration.
405    pub fn new() -> Self {
406        Self {
407            offers: Vec::new(),
408            requires: Vec::new(),
409            consumes: Vec::new(),
410            profiles: Vec::new(),
411        }
412    }
413
414    /// Validates the declaration for duplicate ids and bad profile references.
415    pub fn validate(&self) -> Result<(), CapabilityValidationError> {
416        validate_profile_collection(&self.profiles)?;
417
418        let profile_ids: BTreeSet<&str> = self
419            .profiles
420            .iter()
421            .map(|profile| profile.id.as_str())
422            .collect();
423
424        validate_section(
425            "offers.id",
426            self.offers.iter().map(|offer| offer.id.as_str()),
427        )?;
428        validate_section(
429            "requires.id",
430            self.requires
431                .iter()
432                .map(|requirement| requirement.id.as_str()),
433        )?;
434        validate_section(
435            "consumes.id",
436            self.consumes.iter().map(|consume| consume.id.as_str()),
437        )?;
438
439        for offer in &self.offers {
440            validate_profile_refs("offers", &offer.id, &offer.profiles, &profile_ids)?;
441            validate_named_id("offers", &offer.id)?;
442        }
443
444        for requirement in &self.requires {
445            validate_requirement(requirement, &profile_ids)?;
446        }
447
448        for consume in &self.consumes {
449            validate_consume(consume, &profile_ids)?;
450        }
451
452        for profile in &self.profiles {
453            profile.validate(&profile_ids)?;
454        }
455
456        Ok(())
457    }
458}
459
460impl Default for CapabilityDeclaration {
461    fn default() -> Self {
462        Self::new()
463    }
464}
465
466impl CapabilityProfile {
467    fn validate(&self, known_profiles: &BTreeSet<&str>) -> Result<(), CapabilityValidationError> {
468        validate_named_id("profiles.id", &self.id)?;
469        for requirement in &self.requires {
470            validate_requirement(requirement, known_profiles)?;
471        }
472        for consume in &self.consumes {
473            validate_consume(consume, known_profiles)?;
474        }
475        Ok(())
476    }
477}
478
479/// Stable binding between a request and a selected offer.
480#[derive(Clone, Debug, PartialEq, Eq)]
481#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
482#[cfg_attr(feature = "schemars", derive(JsonSchema))]
483pub struct CapabilityBinding {
484    /// Binding kind.
485    pub kind: CapabilityBindingKind,
486    /// The request or consume item that was bound.
487    pub request_id: String,
488    /// The selected offer identifier.
489    pub offer_id: String,
490    /// The resolved capability.
491    pub capability: CapabilityId,
492    /// The selected provider reference, if known.
493    #[cfg_attr(
494        feature = "serde",
495        serde(default, skip_serializing_if = "Option::is_none")
496    )]
497    pub provider: Option<CapabilityProviderRef>,
498    /// Optional profile association for the binding.
499    #[cfg_attr(
500        feature = "serde",
501        serde(default, skip_serializing_if = "Option::is_none")
502    )]
503    pub profile: Option<String>,
504}
505
506impl CapabilityBinding {
507    /// Creates a binding for a requirement or consume.
508    pub fn new(
509        kind: CapabilityBindingKind,
510        request_id: impl Into<String>,
511        offer_id: impl Into<String>,
512        capability: CapabilityId,
513    ) -> Self {
514        Self {
515            kind,
516            request_id: request_id.into(),
517            offer_id: offer_id.into(),
518            capability,
519            provider: None,
520            profile: None,
521        }
522    }
523
524    /// Validates the binding identifiers.
525    pub fn validate(&self) -> Result<(), CapabilityValidationError> {
526        validate_named_id("bindings.request_id", &self.request_id)?;
527        validate_named_id("bindings.offer_id", &self.offer_id)?;
528        Ok(())
529    }
530}
531
532/// Binding origin.
533#[derive(Clone, Copy, Debug, PartialEq, Eq)]
534#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
535#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
536#[cfg_attr(feature = "schemars", derive(JsonSchema))]
537pub enum CapabilityBindingKind {
538    /// Binding for a required capability.
539    Requirement,
540    /// Binding for a consumed capability.
541    Consume,
542}
543
544/// Machine-readable capability resolution result.
545#[derive(Clone, Debug, PartialEq, Eq)]
546#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
547#[cfg_attr(feature = "schemars", derive(JsonSchema))]
548pub struct CapabilityResolution {
549    /// Original declaration that was resolved.
550    pub declaration: CapabilityDeclaration,
551    /// Stable bindings chosen by the resolver.
552    #[cfg_attr(
553        feature = "serde",
554        serde(default, skip_serializing_if = "Vec::is_empty")
555    )]
556    pub bindings: Vec<CapabilityBinding>,
557}
558
559impl CapabilityResolution {
560    /// Creates an empty resolution result for a declaration.
561    pub fn new(declaration: CapabilityDeclaration) -> Self {
562        Self {
563            declaration,
564            bindings: Vec::new(),
565        }
566    }
567
568    /// Validates the underlying declaration and the binding set.
569    pub fn validate(&self) -> Result<(), CapabilityValidationError> {
570        self.declaration.validate()?;
571        validate_section(
572            "bindings",
573            self.bindings
574                .iter()
575                .map(|binding| binding.request_id.as_str()),
576        )?;
577        for binding in &self.bindings {
578            binding.validate()?;
579        }
580        Ok(())
581    }
582}
583
584/// Validation error for capability declarations and resolutions.
585#[derive(Clone, Debug, PartialEq, Eq)]
586pub enum CapabilityValidationError {
587    /// Empty or malformed non-capability identifier.
588    InvalidIdentifier {
589        /// Logical collection name.
590        section: &'static str,
591        /// Offending identifier.
592        id: String,
593    },
594    /// Invalid capability identifier.
595    InvalidCapabilityId {
596        /// Context for the invalid field.
597        field: &'static str,
598        /// Offending value.
599        value: String,
600        /// Low-level parse error.
601        source: CapabilityIdError,
602    },
603    /// Duplicate identifier within a collection.
604    DuplicateId {
605        /// Logical collection name.
606        section: &'static str,
607        /// Offending identifier.
608        id: String,
609    },
610    /// Empty or malformed profile identifier.
611    InvalidProfileId {
612        /// Logical collection name.
613        section: &'static str,
614        /// Offending profile id.
615        id: String,
616    },
617    /// Reference to a non-existent profile.
618    UnknownProfileReference {
619        /// Logical collection name.
620        section: &'static str,
621        /// Offending profile reference.
622        reference: String,
623    },
624}
625
626impl fmt::Display for CapabilityValidationError {
627    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
628        match self {
629            Self::InvalidIdentifier { section, id } => {
630                write!(f, "{section} contains invalid identifier {id:?}")
631            }
632            Self::InvalidCapabilityId {
633                field,
634                value,
635                source,
636            } => {
637                write!(
638                    f,
639                    "{field} contains invalid capability id {value:?}: {source}"
640                )
641            }
642            Self::DuplicateId { section, id } => {
643                write!(f, "{section} contains duplicate identifier {id:?}")
644            }
645            Self::InvalidProfileId { section, id } => {
646                write!(f, "{section} contains invalid profile id {id:?}")
647            }
648            Self::UnknownProfileReference { section, reference } => {
649                write!(f, "{section} references unknown profile {reference:?}")
650            }
651        }
652    }
653}
654
655impl std::error::Error for CapabilityValidationError {}
656
657fn validate_id_field(field: &'static str, value: &str) -> Result<(), CapabilityValidationError> {
658    if value.trim().is_empty() {
659        return Err(CapabilityValidationError::InvalidIdentifier {
660            section: field,
661            id: value.to_owned(),
662        });
663    }
664    CapabilityId::validate(value).map_err(|source| CapabilityValidationError::InvalidCapabilityId {
665        field,
666        value: value.to_owned(),
667        source,
668    })
669}
670
671fn validate_named_id(section: &'static str, value: &str) -> Result<(), CapabilityValidationError> {
672    if value.trim().is_empty() || value.chars().any(char::is_whitespace) {
673        return Err(CapabilityValidationError::InvalidIdentifier {
674            section,
675            id: value.to_owned(),
676        });
677    }
678    Ok(())
679}
680
681fn validate_section<'a, I>(section: &'static str, ids: I) -> Result<(), CapabilityValidationError>
682where
683    I: IntoIterator<Item = &'a str>,
684{
685    let mut seen = BTreeSet::new();
686    for id in ids {
687        validate_named_id(section, id)?;
688        if !seen.insert(id.to_owned()) {
689            return Err(CapabilityValidationError::DuplicateId {
690                section,
691                id: id.to_owned(),
692            });
693        }
694    }
695    Ok(())
696}
697
698fn validate_profile_collection(
699    profiles: &[CapabilityProfile],
700) -> Result<(), CapabilityValidationError> {
701    let mut seen = BTreeSet::new();
702    for profile in profiles {
703        if profile.id.trim().is_empty() || profile.id.chars().any(char::is_whitespace) {
704            return Err(CapabilityValidationError::InvalidProfileId {
705                section: "profiles",
706                id: profile.id.clone(),
707            });
708        }
709        if !seen.insert(profile.id.clone()) {
710            return Err(CapabilityValidationError::DuplicateId {
711                section: "profiles",
712                id: profile.id.clone(),
713            });
714        }
715    }
716    Ok(())
717}
718
719fn validate_profile_refs(
720    section: &'static str,
721    owner: &str,
722    refs: &[String],
723    known_profiles: &BTreeSet<&str>,
724) -> Result<(), CapabilityValidationError> {
725    let mut seen = BTreeSet::new();
726    for reference in refs {
727        if reference.trim().is_empty() || reference.chars().any(char::is_whitespace) {
728            return Err(CapabilityValidationError::InvalidProfileId {
729                section,
730                id: reference.clone(),
731            });
732        }
733        if !seen.insert(reference.clone()) {
734            return Err(CapabilityValidationError::DuplicateId {
735                section,
736                id: format!("{owner}:{reference}"),
737            });
738        }
739        if !known_profiles.contains(reference.as_str()) {
740            return Err(CapabilityValidationError::UnknownProfileReference {
741                section,
742                reference: reference.clone(),
743            });
744        }
745    }
746    Ok(())
747}
748
749fn validate_requirement(
750    requirement: &CapabilityRequirement,
751    known_profiles: &BTreeSet<&str>,
752) -> Result<(), CapabilityValidationError> {
753    validate_id_field("requires.capability", requirement.capability.as_str())?;
754    validate_named_id("requires.id", &requirement.id)?;
755    validate_profile_refs(
756        "requires.profiles",
757        &requirement.id,
758        &requirement.profiles,
759        known_profiles,
760    )
761}
762
763fn validate_consume(
764    consume: &CapabilityConsume,
765    known_profiles: &BTreeSet<&str>,
766) -> Result<(), CapabilityValidationError> {
767    validate_id_field("consumes.capability", consume.capability.as_str())?;
768    validate_named_id("consumes.id", &consume.id)?;
769    validate_profile_refs(
770        "consumes.profiles",
771        &consume.id,
772        &consume.profiles,
773        known_profiles,
774    )
775}
776
777#[cfg(test)]
778mod tests {
779    use super::*;
780
781    fn cap(value: &str) -> CapabilityId {
782        CapabilityId::new(value).expect("valid capability id")
783    }
784
785    #[test]
786    fn capability_id_requires_cap_scheme() {
787        let err = CapabilityId::new("memory.short-term").unwrap_err();
788        assert_eq!(err, CapabilityIdError::MissingScheme);
789    }
790
791    #[test]
792    fn declaration_round_trips_json_and_cbor() {
793        let mut declaration = CapabilityDeclaration::new();
794
795        let mut offer = CapabilityOffer::new("offer.memory", cap("cap://memory.short-term"));
796        offer.profiles.push("memory-default".to_string());
797        offer.provider = Some(CapabilityProviderRef {
798            component_ref: "component:redis".to_string(),
799            operation: "provide".to_string(),
800            operation_map: Vec::new(),
801        });
802        declaration.offers.push(offer);
803
804        let mut requirement =
805            CapabilityRequirement::new("require.memory", cap("cap://memory.short-term"));
806        requirement.profiles.push("memory-default".to_string());
807        declaration.requires.push(requirement);
808
809        let mut consume = CapabilityConsume::new("consume.memory", cap("cap://memory.short-term"));
810        consume.profiles.push("memory-default".to_string());
811        declaration.consumes.push(consume);
812
813        declaration.profiles.push(CapabilityProfile {
814            id: "memory-default".to_string(),
815            description: Some("default memory profile".to_string()),
816            requires: vec![],
817            consumes: vec![],
818        });
819
820        declaration.validate().expect("valid declaration");
821
822        let json = serde_json::to_string_pretty(&declaration).expect("json encode");
823        let decoded_json: CapabilityDeclaration = serde_json::from_str(&json).expect("json decode");
824        assert_eq!(declaration, decoded_json);
825
826        let cbor = serde_cbor::to_vec(&declaration).expect("cbor encode");
827        let decoded_cbor: CapabilityDeclaration =
828            serde_cbor::from_slice(&cbor).expect("cbor decode");
829        assert_eq!(declaration, decoded_cbor);
830    }
831
832    #[test]
833    fn declaration_rejects_duplicate_offers() {
834        let mut declaration = CapabilityDeclaration::new();
835        declaration.offers.push(CapabilityOffer::new(
836            "offer.memory",
837            cap("cap://memory.short-term"),
838        ));
839        declaration.offers.push(CapabilityOffer::new(
840            "offer.memory",
841            cap("cap://memory.short-term"),
842        ));
843
844        let err = declaration.validate().unwrap_err();
845        assert_eq!(
846            err,
847            CapabilityValidationError::DuplicateId {
848                section: "offers.id",
849                id: "offer.memory".to_string(),
850            }
851        );
852    }
853
854    #[test]
855    fn declaration_rejects_unknown_profile_reference() {
856        let mut declaration = CapabilityDeclaration::new();
857        let mut offer = CapabilityOffer::new("offer.memory", cap("cap://memory.short-term"));
858        offer.profiles.push("memory-default".to_string());
859        declaration.offers.push(offer);
860
861        let err = declaration.validate().unwrap_err();
862        assert_eq!(
863            err,
864            CapabilityValidationError::UnknownProfileReference {
865                section: "offers",
866                reference: "memory-default".to_string(),
867            }
868        );
869    }
870
871    #[test]
872    fn declaration_rejects_malformed_profile_ids() {
873        let mut declaration = CapabilityDeclaration::new();
874        declaration.profiles.push(CapabilityProfile::new(" "));
875        let err = declaration.validate().unwrap_err();
876        assert_eq!(
877            err,
878            CapabilityValidationError::InvalidProfileId {
879                section: "profiles",
880                id: " ".to_string(),
881            }
882        );
883    }
884
885    #[test]
886    fn declaration_rejects_empty_requirement_ids() {
887        let mut declaration = CapabilityDeclaration::new();
888        declaration.requires.push(CapabilityRequirement::new(
889            "",
890            cap("cap://memory.short-term"),
891        ));
892        let err = declaration.validate().unwrap_err();
893        assert_eq!(
894            err,
895            CapabilityValidationError::InvalidIdentifier {
896                section: "requires.id",
897                id: String::new(),
898            }
899        );
900    }
901
902    #[test]
903    fn resolution_allows_multiple_requests_to_share_an_offer() {
904        let mut declaration = CapabilityDeclaration::new();
905        declaration.offers.push(CapabilityOffer::new(
906            "offer.memory",
907            cap("cap://memory.short-term"),
908        ));
909
910        declaration.requires.push(CapabilityRequirement::new(
911            "require.one",
912            cap("cap://memory.short-term"),
913        ));
914        declaration.requires.push(CapabilityRequirement::new(
915            "require.two",
916            cap("cap://memory.short-term"),
917        ));
918
919        let mut resolution = CapabilityResolution::new(declaration);
920        resolution.bindings.push(CapabilityBinding::new(
921            CapabilityBindingKind::Requirement,
922            "require.one",
923            "offer.memory",
924            cap("cap://memory.short-term"),
925        ));
926        resolution.bindings.push(CapabilityBinding::new(
927            CapabilityBindingKind::Requirement,
928            "require.two",
929            "offer.memory",
930            cap("cap://memory.short-term"),
931        ));
932
933        resolution.validate().expect("shared offer binding");
934    }
935
936    #[test]
937    fn provider_operation_map_is_serialized() {
938        let provider = CapabilityProviderRef {
939            component_ref: "component:redis".to_string(),
940            operation: "provide".to_string(),
941            operation_map: vec![CapabilityProviderOperationMap {
942                contract_operation: "read".to_string(),
943                component_operation: "provide".to_string(),
944                input_schema: serde_json::json!({"type": "string"}),
945                output_schema: serde_json::json!({"type": "string"}),
946            }],
947        };
948
949        let json = serde_json::to_value(&provider).expect("json");
950        assert!(json.get("operation_map").is_some());
951    }
952}