Skip to main content

systemprompt_provider_contracts/config_plane/
set.rs

1//! [`ConfigPlaneSet`]: the registered [`ConfigPlane`]s in dependency order,
2//! the authority each one runs under, and the only way to obtain a
3//! [`PlaneWriter`].
4//!
5//! [`ConfigPlaneSet::new`] refuses duplicate names, a `depends_on` naming an
6//! unregistered plane and a dependency cycle, and orders the planes so every
7//! plane follows the planes it depends on (registration order breaks ties).
8//! The reconciler walks [`ConfigPlaneSet::planes`] in that order.
9//!
10//! A console or API writer edits a plane's rows only while holding a
11//! [`PlaneWriter`] for it, and [`ConfigPlaneSet::writer`] mints one only when
12//! the plane is not file-managed: a plane whose declaration exists under
13//! `file` authority answers [`ConfigPlaneError::FileManaged`], because the
14//! reconciler would overwrite the edit on its next pass. A plane whose file is
15//! absent is never file-managed, whatever the profile says.
16//!
17//! Copyright (c) systemprompt.io — Business Source License 1.1.
18//! See <https://systemprompt.io> for licensing details.
19
20use std::collections::{BTreeMap, HashSet};
21use std::fmt;
22use std::path::{Path, PathBuf};
23use std::sync::Arc;
24
25use serde::{Deserialize, Serialize};
26
27use super::{ConfigPlane, ConfigPlaneError, ConfigPlaneResult};
28
29/// The authority a profile names for one plane under
30/// `config_planes.authority`.
31#[derive(
32    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema,
33)]
34#[serde(rename_all = "lowercase")]
35pub enum PlaneAuthority {
36    #[default]
37    File,
38    Db,
39}
40
41impl PlaneAuthority {
42    #[must_use]
43    pub const fn as_str(self) -> &'static str {
44        match self {
45            Self::File => "file",
46            Self::Db => "db",
47        }
48    }
49}
50
51/// The authority a plane actually runs under once the declaration file's
52/// presence is taken into account.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum EffectiveAuthority {
56    File,
57    Db,
58    Undeclared,
59}
60
61impl EffectiveAuthority {
62    #[must_use]
63    pub const fn as_str(self) -> &'static str {
64        match self {
65            Self::File => "file",
66            Self::Db => "db",
67            Self::Undeclared => "undeclared",
68        }
69    }
70
71    #[must_use]
72    pub const fn is_file_managed(self) -> bool {
73        matches!(self, Self::File)
74    }
75}
76
77/// Proof that the holder may write a plane's rows directly.
78///
79/// It has no public constructor: [`ConfigPlaneSet::writer`] is the only
80/// source, so a write path that takes a `&PlaneWriter` cannot run against a
81/// file-managed plane.
82pub struct PlaneWriter<'a> {
83    plane: &'a dyn ConfigPlane,
84    authority: EffectiveAuthority,
85}
86
87impl<'a> PlaneWriter<'a> {
88    #[must_use]
89    pub fn plane(&self) -> &'a dyn ConfigPlane {
90        self.plane
91    }
92
93    #[must_use]
94    pub fn name(&self) -> &'static str {
95        self.plane.name()
96    }
97
98    #[must_use]
99    pub const fn authority(&self) -> EffectiveAuthority {
100        self.authority
101    }
102}
103
104impl fmt::Debug for PlaneWriter<'_> {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        f.debug_struct("PlaneWriter")
107            .field("plane", &self.plane.name())
108            .field("authority", &self.authority)
109            .finish()
110    }
111}
112
113#[derive(Clone)]
114pub struct ConfigPlaneSet {
115    planes: Vec<Arc<dyn ConfigPlane>>,
116    authority: BTreeMap<String, PlaneAuthority>,
117    services_root: PathBuf,
118}
119
120impl fmt::Debug for ConfigPlaneSet {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        f.debug_struct("ConfigPlaneSet")
123            .field("planes", &self.names())
124            .field("authority", &self.authority)
125            .field("services_root", &self.services_root)
126            .finish()
127    }
128}
129
130impl ConfigPlaneSet {
131    pub fn new(
132        planes: Vec<Arc<dyn ConfigPlane>>,
133        authority: BTreeMap<String, PlaneAuthority>,
134        services_root: PathBuf,
135    ) -> ConfigPlaneResult<Self> {
136        Ok(Self {
137            planes: dependency_order(planes)?,
138            authority,
139            services_root,
140        })
141    }
142
143    #[must_use]
144    pub fn planes(&self) -> &[Arc<dyn ConfigPlane>] {
145        &self.planes
146    }
147
148    #[must_use]
149    pub fn names(&self) -> Vec<&'static str> {
150        self.planes.iter().map(|p| p.name()).collect()
151    }
152
153    #[must_use]
154    pub fn services_root(&self) -> &Path {
155        &self.services_root
156    }
157
158    pub fn find(&self, name: &str) -> ConfigPlaneResult<&Arc<dyn ConfigPlane>> {
159        self.planes
160            .iter()
161            .find(|p| p.name() == name)
162            .ok_or_else(|| ConfigPlaneError::UnknownPlane(name.to_owned()))
163    }
164
165    #[must_use]
166    pub fn declared_exists(&self, plane: &dyn ConfigPlane) -> bool {
167        self.services_root.join(plane.declared_path()).exists()
168    }
169
170    #[must_use]
171    pub fn authority_of(&self, plane: &dyn ConfigPlane) -> EffectiveAuthority {
172        match self
173            .authority
174            .get(plane.name())
175            .copied()
176            .unwrap_or_default()
177        {
178            PlaneAuthority::Db => EffectiveAuthority::Db,
179            PlaneAuthority::File if self.declared_exists(plane) => EffectiveAuthority::File,
180            PlaneAuthority::File => EffectiveAuthority::Undeclared,
181        }
182    }
183
184    pub fn authority(&self, name: &str) -> ConfigPlaneResult<EffectiveAuthority> {
185        Ok(self.authority_of(self.find(name)?.as_ref()))
186    }
187
188    pub fn writer(&self, name: &str) -> ConfigPlaneResult<PlaneWriter<'_>> {
189        let plane = self.find(name)?;
190        let authority = self.authority_of(plane.as_ref());
191        if authority.is_file_managed() {
192            return Err(ConfigPlaneError::FileManaged {
193                plane: name.to_owned(),
194                path: plane.declared_path().to_owned(),
195            });
196        }
197        Ok(PlaneWriter {
198            plane: plane.as_ref(),
199            authority,
200        })
201    }
202
203    #[must_use]
204    pub fn unregistered_authority_keys(&self) -> Vec<&str> {
205        self.authority
206            .keys()
207            .map(String::as_str)
208            .filter(|key| !self.planes.iter().any(|p| p.name() == *key))
209            .collect()
210    }
211}
212
213fn dependency_order(
214    planes: Vec<Arc<dyn ConfigPlane>>,
215) -> ConfigPlaneResult<Vec<Arc<dyn ConfigPlane>>> {
216    let mut seen = HashSet::new();
217    for plane in &planes {
218        if !seen.insert(plane.name()) {
219            return Err(ConfigPlaneError::DuplicatePlane(plane.name().to_owned()));
220        }
221    }
222    for plane in &planes {
223        if let Some(missing) = plane.depends_on().iter().find(|dep| !seen.contains(*dep)) {
224            return Err(ConfigPlaneError::UnknownDependency {
225                plane: plane.name().to_owned(),
226                dependency: (*missing).to_owned(),
227            });
228        }
229    }
230    let mut pending: Vec<Option<Arc<dyn ConfigPlane>>> = planes.into_iter().map(Some).collect();
231    let mut placed: HashSet<&'static str> = HashSet::new();
232    let mut ordered = Vec::with_capacity(pending.len());
233    while ordered.len() < pending.len() {
234        let ready = pending.iter().position(|slot| {
235            slot.as_ref()
236                .is_some_and(|p| p.depends_on().iter().all(|dep| placed.contains(dep)))
237        });
238        let Some(index) = ready else {
239            let stuck = pending.iter().flatten().next().map_or("", |p| p.name());
240            return Err(ConfigPlaneError::DependencyCycle(stuck.to_owned()));
241        };
242        if let Some(plane) = pending.get_mut(index).and_then(Option::take) {
243            placed.insert(plane.name());
244            ordered.push(plane);
245        }
246    }
247    Ok(ordered)
248}