1#![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
13pub type CapabilityMetadata = BTreeMap<String, serde_json::Value>;
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum CapabilityIdError {
19 MissingScheme,
21 Empty,
23 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#[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 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 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#[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 pub component_ref: String,
116 pub operation: String,
118 #[cfg_attr(
120 feature = "serde",
121 serde(default, skip_serializing_if = "Vec::is_empty")
122 )]
123 pub operation_map: Vec<CapabilityProviderOperationMap>,
124}
125
126#[derive(Clone, Debug, PartialEq, Eq)]
128#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
129#[cfg_attr(feature = "schemars", derive(JsonSchema))]
130pub struct CapabilityProviderOperationMap {
131 pub contract_operation: String,
133 pub component_operation: String,
135 pub input_schema: serde_json::Value,
137 pub output_schema: serde_json::Value,
139}
140
141#[derive(Clone, Debug, PartialEq, Eq)]
143#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
144#[cfg_attr(feature = "schemars", derive(JsonSchema))]
145pub struct CapabilityComponentDescriptor {
146 pub component_ref: String,
148 pub version: String,
150 #[cfg_attr(feature = "serde", serde(default))]
152 pub operations: Vec<CapabilityComponentOperation>,
153 #[cfg_attr(feature = "serde", serde(default))]
155 pub capabilities: Vec<CapabilityId>,
156 #[cfg_attr(
158 feature = "serde",
159 serde(default, skip_serializing_if = "BTreeMap::is_empty")
160 )]
161 pub metadata: CapabilityMetadata,
162}
163
164#[derive(Clone, Debug, PartialEq, Eq)]
166#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
167#[cfg_attr(feature = "schemars", derive(JsonSchema))]
168pub struct CapabilityComponentOperation {
169 pub name: String,
171 pub input_schema: serde_json::Value,
173 pub output_schema: serde_json::Value,
175}
176
177#[derive(Clone, Debug, PartialEq, Eq)]
179#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
180#[cfg_attr(feature = "schemars", derive(JsonSchema))]
181pub struct CapabilityOffer {
182 pub id: String,
184 pub capability: CapabilityId,
186 #[cfg_attr(
188 feature = "serde",
189 serde(default, skip_serializing_if = "Option::is_none")
190 )]
191 pub provider: Option<CapabilityProviderRef>,
192 #[cfg_attr(
194 feature = "serde",
195 serde(default, skip_serializing_if = "Vec::is_empty")
196 )]
197 pub profiles: Vec<String>,
198 #[cfg_attr(
200 feature = "serde",
201 serde(default, skip_serializing_if = "Option::is_none")
202 )]
203 pub description: Option<String>,
204 #[cfg_attr(
206 feature = "serde",
207 serde(default, skip_serializing_if = "BTreeMap::is_empty")
208 )]
209 pub metadata: CapabilityMetadata,
210}
211
212impl CapabilityOffer {
213 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#[derive(Clone, Debug, PartialEq, Eq)]
228#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
229#[cfg_attr(feature = "schemars", derive(JsonSchema))]
230pub struct CapabilityRequirement {
231 pub id: String,
233 pub capability: CapabilityId,
235 #[cfg_attr(
237 feature = "serde",
238 serde(default, skip_serializing_if = "Vec::is_empty")
239 )]
240 pub profiles: Vec<String>,
241 #[cfg_attr(feature = "serde", serde(default))]
243 pub optional: bool,
244 #[cfg_attr(
246 feature = "serde",
247 serde(default, skip_serializing_if = "Option::is_none")
248 )]
249 pub description: Option<String>,
250 #[cfg_attr(
252 feature = "serde",
253 serde(default, skip_serializing_if = "BTreeMap::is_empty")
254 )]
255 pub metadata: CapabilityMetadata,
256}
257
258impl CapabilityRequirement {
259 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#[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 #[default]
280 Shared,
281 Exclusive,
283 Ephemeral,
285}
286
287#[derive(Clone, Debug, PartialEq, Eq)]
289#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
290#[cfg_attr(feature = "schemars", derive(JsonSchema))]
291pub struct CapabilityConsume {
292 pub id: String,
294 pub capability: CapabilityId,
296 #[cfg_attr(
298 feature = "serde",
299 serde(default, skip_serializing_if = "Vec::is_empty")
300 )]
301 pub profiles: Vec<String>,
302 #[cfg_attr(feature = "serde", serde(default))]
304 pub mode: CapabilityConsumeMode,
305 #[cfg_attr(
307 feature = "serde",
308 serde(default, skip_serializing_if = "Option::is_none")
309 )]
310 pub description: Option<String>,
311 #[cfg_attr(
313 feature = "serde",
314 serde(default, skip_serializing_if = "BTreeMap::is_empty")
315 )]
316 pub metadata: CapabilityMetadata,
317}
318
319impl CapabilityConsume {
320 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#[derive(Clone, Debug, PartialEq, Eq)]
335#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
336#[cfg_attr(feature = "schemars", derive(JsonSchema))]
337pub struct CapabilityProfile {
338 pub id: String,
340 #[cfg_attr(
342 feature = "serde",
343 serde(default, skip_serializing_if = "Option::is_none")
344 )]
345 pub description: Option<String>,
346 #[cfg_attr(
348 feature = "serde",
349 serde(default, skip_serializing_if = "Vec::is_empty")
350 )]
351 pub requires: Vec<CapabilityRequirement>,
352 #[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 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#[derive(Clone, Debug, PartialEq, Eq)]
374#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
375#[cfg_attr(feature = "schemars", derive(JsonSchema))]
376pub struct CapabilityDeclaration {
377 #[cfg_attr(
379 feature = "serde",
380 serde(default, skip_serializing_if = "Vec::is_empty")
381 )]
382 pub offers: Vec<CapabilityOffer>,
383 #[cfg_attr(
385 feature = "serde",
386 serde(default, skip_serializing_if = "Vec::is_empty")
387 )]
388 pub requires: Vec<CapabilityRequirement>,
389 #[cfg_attr(
391 feature = "serde",
392 serde(default, skip_serializing_if = "Vec::is_empty")
393 )]
394 pub consumes: Vec<CapabilityConsume>,
395 #[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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
481#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
482#[cfg_attr(feature = "schemars", derive(JsonSchema))]
483pub struct CapabilityBinding {
484 pub kind: CapabilityBindingKind,
486 pub request_id: String,
488 pub offer_id: String,
490 pub capability: CapabilityId,
492 #[cfg_attr(
494 feature = "serde",
495 serde(default, skip_serializing_if = "Option::is_none")
496 )]
497 pub provider: Option<CapabilityProviderRef>,
498 #[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 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 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#[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 Requirement,
540 Consume,
542}
543
544#[derive(Clone, Debug, PartialEq, Eq)]
546#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
547#[cfg_attr(feature = "schemars", derive(JsonSchema))]
548pub struct CapabilityResolution {
549 pub declaration: CapabilityDeclaration,
551 #[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 pub fn new(declaration: CapabilityDeclaration) -> Self {
562 Self {
563 declaration,
564 bindings: Vec::new(),
565 }
566 }
567
568 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#[derive(Clone, Debug, PartialEq, Eq)]
586pub enum CapabilityValidationError {
587 InvalidIdentifier {
589 section: &'static str,
591 id: String,
593 },
594 InvalidCapabilityId {
596 field: &'static str,
598 value: String,
600 source: CapabilityIdError,
602 },
603 DuplicateId {
605 section: &'static str,
607 id: String,
609 },
610 InvalidProfileId {
612 section: &'static str,
614 id: String,
616 },
617 UnknownProfileReference {
619 section: &'static str,
621 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}