Skip to main content

greentic_types/pack/extensions/
capabilities.rs

1//! Extension payload for declarative capability offers.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6#[cfg(feature = "serde")]
7use ciborium::{de::from_reader, ser::into_writer};
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11/// Pack extension identifier for capability offers (v1).
12pub const EXT_CAPABILITIES_V1: &str = "greentic.ext.capabilities.v1";
13
14/// Capabilities extension payload (v1).
15#[derive(Clone, Debug, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17pub struct CapabilitiesExtensionV1 {
18    /// Schema version for the payload.
19    pub schema_version: u32,
20    /// Capability offers declared by the pack.
21    pub offers: Vec<CapabilityOfferV1>,
22}
23
24impl CapabilitiesExtensionV1 {
25    /// Creates a new capabilities payload.
26    pub fn new(offers: Vec<CapabilityOfferV1>) -> Self {
27        Self {
28            schema_version: 1,
29            offers,
30        }
31    }
32
33    /// Validates schema and setup invariants.
34    pub fn validate(&self) -> Result<(), CapabilitiesExtensionError> {
35        if self.schema_version != 1 {
36            return Err(CapabilitiesExtensionError::UnsupportedSchemaVersion(
37                self.schema_version,
38            ));
39        }
40        for offer in &self.offers {
41            if offer.requires_setup && offer.setup.is_none() {
42                return Err(CapabilitiesExtensionError::MissingSetup {
43                    offer_id: offer.offer_id.clone(),
44                });
45            }
46            if let Some(setup) = offer.setup.as_ref()
47                && setup.qa_ref.trim().is_empty()
48            {
49                return Err(CapabilitiesExtensionError::InvalidSetupQaRef {
50                    offer_id: offer.offer_id.clone(),
51                });
52            }
53        }
54        Ok(())
55    }
56
57    /// Converts payload to an extension value suitable for `ExtensionInline::Other`.
58    #[cfg(feature = "serde")]
59    pub fn to_extension_value(&self) -> Result<serde_json::Value, CapabilitiesExtensionError> {
60        serde_json::to_value(self)
61            .map_err(|err| CapabilitiesExtensionError::Serialize(err.to_string()))
62    }
63
64    /// Parses payload from an extension value.
65    #[cfg(feature = "serde")]
66    pub fn from_extension_value(
67        value: &serde_json::Value,
68    ) -> Result<Self, CapabilitiesExtensionError> {
69        let decoded: Self = serde_json::from_value(value.clone())
70            .map_err(|err| CapabilitiesExtensionError::Deserialize(err.to_string()))?;
71        decoded.validate()?;
72        Ok(decoded)
73    }
74}
75
76/// Single capability offer in the extension payload.
77#[derive(Clone, Debug, PartialEq, Eq)]
78#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
79pub struct CapabilityOfferV1 {
80    /// Stable offer identifier used for deterministic tie-breaks.
81    pub offer_id: String,
82    /// Capability identifier, e.g. `greentic.cap.memory.shortterm`.
83    pub cap_id: String,
84    /// Offer contract version, e.g. `v1` or semver.
85    pub version: String,
86    /// Provider operation reference.
87    pub provider: CapabilityProviderRefV1,
88    /// Optional scope restrictions.
89    #[cfg_attr(
90        feature = "serde",
91        serde(default, skip_serializing_if = "Option::is_none")
92    )]
93    pub scope: Option<CapabilityScopeV1>,
94    /// Deterministic selection priority (ascending).
95    #[cfg_attr(feature = "serde", serde(default))]
96    pub priority: i32,
97    /// Whether the offer needs setup before runtime use.
98    #[cfg_attr(feature = "serde", serde(default))]
99    pub requires_setup: bool,
100    /// Optional setup metadata.
101    #[cfg_attr(
102        feature = "serde",
103        serde(default, skip_serializing_if = "Option::is_none")
104    )]
105    pub setup: Option<CapabilitySetupV1>,
106    /// Optional hook applicability metadata.
107    #[cfg_attr(
108        feature = "serde",
109        serde(default, skip_serializing_if = "Option::is_none")
110    )]
111    pub applies_to: Option<CapabilityHookAppliesToV1>,
112}
113
114/// Provider operation target for a capability offer.
115#[derive(Clone, Debug, PartialEq, Eq)]
116#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
117pub struct CapabilityProviderRefV1 {
118    /// Component reference inside the pack.
119    pub component_ref: String,
120    /// Operation exported by the provider component.
121    pub op: String,
122}
123
124/// Optional scope constraints for a capability offer.
125#[derive(Clone, Debug, PartialEq, Eq, Default)]
126#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
127pub struct CapabilityScopeV1 {
128    /// Allowed env ids.
129    #[cfg_attr(
130        feature = "serde",
131        serde(default, skip_serializing_if = "Vec::is_empty")
132    )]
133    pub envs: Vec<String>,
134    /// Allowed tenant ids.
135    #[cfg_attr(
136        feature = "serde",
137        serde(default, skip_serializing_if = "Vec::is_empty")
138    )]
139    pub tenants: Vec<String>,
140    /// Allowed team ids.
141    #[cfg_attr(
142        feature = "serde",
143        serde(default, skip_serializing_if = "Vec::is_empty")
144    )]
145    pub teams: Vec<String>,
146}
147
148/// Setup metadata for capability offers requiring setup.
149#[derive(Clone, Debug, PartialEq, Eq)]
150#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
151pub struct CapabilitySetupV1 {
152    /// Pack-relative QA spec reference.
153    pub qa_ref: String,
154}
155
156/// Optional applicability constraints for hook capabilities.
157#[derive(Clone, Debug, PartialEq, Eq, Default)]
158#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
159pub struct CapabilityHookAppliesToV1 {
160    /// Exact operation names (v1).
161    #[cfg_attr(
162        feature = "serde",
163        serde(default, skip_serializing_if = "Vec::is_empty")
164    )]
165    pub op_names: Vec<String>,
166}
167
168/// Errors produced while encoding/decoding capabilities extension payloads.
169#[derive(Debug, thiserror::Error)]
170pub enum CapabilitiesExtensionError {
171    /// Serialization failed.
172    #[error("capabilities extension serialize failed: {0}")]
173    Serialize(String),
174    /// Deserialization failed.
175    #[error("capabilities extension deserialize failed: {0}")]
176    Deserialize(String),
177    /// Unsupported schema version.
178    #[error("unsupported capabilities extension schema_version {0}")]
179    UnsupportedSchemaVersion(u32),
180    /// Setup section is required when `requires_setup=true`.
181    #[error("capabilities extension offer `{offer_id}` requires setup but setup is missing")]
182    MissingSetup {
183        /// Offer identifier that violated setup requirements.
184        offer_id: String,
185    },
186    /// Setup QA reference must not be empty.
187    #[error("capabilities extension offer `{offer_id}` has empty setup.qa_ref")]
188    InvalidSetupQaRef {
189        /// Offer identifier that contains an invalid QA reference.
190        offer_id: String,
191    },
192    /// Extension payload missing inline data.
193    #[error("capabilities extension missing inline payload")]
194    MissingInline,
195    /// Extension payload used an unexpected inline type.
196    #[error("capabilities extension inline payload has unexpected type")]
197    UnexpectedInline,
198}
199
200/// Serializes capabilities extension payload to CBOR bytes.
201#[cfg(feature = "serde")]
202pub fn encode_capabilities_extension_v1_to_cbor_bytes(
203    payload: &CapabilitiesExtensionV1,
204) -> Result<Vec<u8>, CapabilitiesExtensionError> {
205    let mut buf = Vec::new();
206    into_writer(payload, &mut buf)
207        .map_err(|err| CapabilitiesExtensionError::Serialize(err.to_string()))?;
208    Ok(buf)
209}
210
211/// Deserializes capabilities extension payload from CBOR bytes.
212#[cfg(feature = "serde")]
213pub fn decode_capabilities_extension_v1_from_cbor_bytes(
214    bytes: &[u8],
215) -> Result<CapabilitiesExtensionV1, CapabilitiesExtensionError> {
216    let decoded: CapabilitiesExtensionV1 = from_reader(bytes)
217        .map_err(|err| CapabilitiesExtensionError::Deserialize(err.to_string()))?;
218    decoded.validate()?;
219    Ok(decoded)
220}