Skip to main content

codex_config/
cloud_config_bundle.rs

1//! Cloud config bundle domain model and shared in-memory loader.
2//!
3//! The backend bundle groups cloud-delivered config and requirements fragments
4//! by source bucket. `CloudConfigBundleLayers` converts those raw buckets into
5//! layer entries while preserving each bucket's insertion semantics.
6
7use crate::CloudConfigFragment;
8use crate::ConfigLayerEntry;
9use crate::RequirementSource;
10use crate::RequirementsLayerEntry;
11use crate::cloud_config_layers::CloudConfigLayerError;
12use crate::cloud_config_layers::cloud_config_layers_from_fragments_strict;
13use crate::cloud_config_layers_from_fragments;
14use codex_utils_absolute_path::AbsolutePathBuf;
15use futures::future::BoxFuture;
16use futures::future::FutureExt;
17use futures::future::Shared;
18use serde::Deserialize;
19use serde::Serialize;
20use std::fmt;
21use std::future::Future;
22use thiserror::Error;
23
24#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
25pub struct CloudConfigBundle {
26    pub config_toml: CloudConfigTomlBundle,
27    pub requirements_toml: CloudRequirementsTomlBundle,
28}
29
30impl CloudConfigBundle {
31    pub fn is_empty(&self) -> bool {
32        let CloudConfigBundle {
33            config_toml,
34            requirements_toml,
35        } = self;
36        let CloudConfigTomlBundle {
37            enterprise_managed: config_enterprise_managed,
38        } = config_toml;
39        let CloudRequirementsTomlBundle {
40            enterprise_managed: requirements_enterprise_managed,
41        } = requirements_toml;
42
43        config_enterprise_managed.is_empty() && requirements_enterprise_managed.is_empty()
44    }
45}
46
47#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
48pub struct CloudConfigTomlBundle {
49    pub enterprise_managed: Vec<CloudConfigFragment>,
50}
51
52#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
53pub struct CloudRequirementsTomlBundle {
54    pub enterprise_managed: Vec<CloudRequirementsFragment>,
55}
56
57#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
58pub struct CloudRequirementsFragment {
59    pub id: String,
60    pub name: String,
61    pub contents: String,
62}
63
64/// Cloud config bundle converted into semantic layer buckets.
65///
66/// This is not a final config stack. Callers still decide where each bucket is
67/// inserted relative to local/system/user layers.
68#[derive(Clone, Debug)]
69pub struct CloudConfigBundleLayers {
70    /// Enterprise-managed config layers in `ConfigLayerStack` order.
71    pub enterprise_managed_config: Vec<ConfigLayerEntry>,
72    /// Enterprise-managed requirements layers in requirements layer merge order.
73    pub enterprise_managed_requirements: Vec<RequirementsLayerEntry>,
74}
75
76impl CloudConfigBundleLayers {
77    pub fn from_bundle(
78        bundle: CloudConfigBundle,
79        base_dir: &AbsolutePathBuf,
80    ) -> Result<Self, CloudConfigLayerError> {
81        Self::from_bundle_impl(bundle, base_dir, /*strict_config*/ false)
82    }
83
84    pub fn from_bundle_strict_config(
85        bundle: CloudConfigBundle,
86        base_dir: &AbsolutePathBuf,
87    ) -> Result<Self, CloudConfigLayerError> {
88        Self::from_bundle_impl(bundle, base_dir, /*strict_config*/ true)
89    }
90
91    fn from_bundle_impl(
92        bundle: CloudConfigBundle,
93        base_dir: &AbsolutePathBuf,
94        strict_config: bool,
95    ) -> Result<Self, CloudConfigLayerError> {
96        // Keep this destructuring exhaustive so adding a new bundle bucket forces
97        // an explicit choice about how it becomes layer data.
98        let CloudConfigBundle {
99            config_toml:
100                CloudConfigTomlBundle {
101                    enterprise_managed: config_enterprise_managed,
102                },
103            requirements_toml:
104                CloudRequirementsTomlBundle {
105                    enterprise_managed: requirements_enterprise_managed,
106                },
107        } = bundle;
108
109        let enterprise_managed_config = if strict_config {
110            cloud_config_layers_from_fragments_strict(config_enterprise_managed, base_dir)?
111        } else {
112            cloud_config_layers_from_fragments(config_enterprise_managed, base_dir)?
113        };
114
115        let mut enterprise_managed_requirements = requirements_enterprise_managed
116            .into_iter()
117            .map(|fragment| {
118                RequirementsLayerEntry::from_toml(
119                    RequirementSource::EnterpriseManaged {
120                        id: fragment.id,
121                        name: fragment.name,
122                    },
123                    fragment.contents,
124                )
125                .with_base_dir(base_dir.clone())
126            })
127            .collect::<Vec<_>>();
128        // Bundle fragments arrive highest-priority first, while requirements
129        // layers are merged lowest-priority to highest-priority.
130        enterprise_managed_requirements.reverse();
131
132        Ok(Self {
133            enterprise_managed_config,
134            enterprise_managed_requirements,
135        })
136    }
137}
138
139#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub enum CloudConfigBundleLoadErrorCode {
141    Auth,
142    Timeout,
143    RequestFailed,
144    InvalidBundle,
145    Internal,
146}
147
148#[derive(Clone, Debug, Eq, Error, PartialEq)]
149#[error("{message}")]
150pub struct CloudConfigBundleLoadError {
151    code: CloudConfigBundleLoadErrorCode,
152    message: String,
153    status_code: Option<u16>,
154}
155
156impl CloudConfigBundleLoadError {
157    pub fn new(
158        code: CloudConfigBundleLoadErrorCode,
159        status_code: Option<u16>,
160        message: impl Into<String>,
161    ) -> Self {
162        Self {
163            code,
164            message: message.into(),
165            status_code,
166        }
167    }
168
169    pub fn code(&self) -> CloudConfigBundleLoadErrorCode {
170        self.code
171    }
172
173    pub fn status_code(&self) -> Option<u16> {
174        self.status_code
175    }
176}
177
178#[derive(Clone)]
179pub struct CloudConfigBundleLoader {
180    fut: Shared<BoxFuture<'static, Result<Option<CloudConfigBundle>, CloudConfigBundleLoadError>>>,
181}
182
183impl CloudConfigBundleLoader {
184    pub fn new<F>(fut: F) -> Self
185    where
186        F: Future<Output = Result<Option<CloudConfigBundle>, CloudConfigBundleLoadError>>
187            + Send
188            + 'static,
189    {
190        Self {
191            fut: fut.boxed().shared(),
192        }
193    }
194
195    pub async fn get(&self) -> Result<Option<CloudConfigBundle>, CloudConfigBundleLoadError> {
196        self.fut.clone().await
197    }
198}
199
200impl fmt::Debug for CloudConfigBundleLoader {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        f.debug_struct("CloudConfigBundleLoader").finish()
203    }
204}
205
206impl Default for CloudConfigBundleLoader {
207    fn default() -> Self {
208        Self::new(async { Ok(None) })
209    }
210}
211
212#[cfg(test)]
213#[path = "cloud_config_bundle_tests.rs"]
214mod tests;