Skip to main content

systemprompt_database/scope/
provider.rs

1//! The [`ConnectionScopeProvider`] trait and its setting/error types.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::sync::Arc;
7
8use systemprompt_models::RequestScope;
9
10#[derive(Debug, thiserror::Error)]
11pub enum ScopeError {
12    #[error("invalid scope setting key '{key}': must be a dotted custom-GUC name")]
13    InvalidKey { key: String },
14    #[error("scope provider failed: {0}")]
15    Provider(String),
16}
17
18/// One transaction-local setting to apply on a scoped transaction.
19///
20/// Injection-safe by construction: the key is validated to the custom-GUC
21/// grammar at construction, and both key and value are bound as parameters to
22/// `SELECT set_config($1, $2, true)` — never interpolated into SQL.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ScopeSetting {
25    key: String,
26    value: String,
27}
28
29impl ScopeSetting {
30    /// Build a setting, validating that `key` is a dotted custom-GUC name
31    /// (`app.current_org` style): dot-separated identifiers of ASCII
32    /// alphanumerics and underscores, not starting with a digit.
33    pub fn new(key: impl Into<String>, value: impl Into<String>) -> Result<Self, ScopeError> {
34        let key = key.into();
35        if !is_custom_guc_name(&key) {
36            return Err(ScopeError::InvalidKey { key });
37        }
38        Ok(Self {
39            key,
40            value: value.into(),
41        })
42    }
43
44    #[must_use]
45    pub fn key(&self) -> &str {
46        &self.key
47    }
48
49    #[must_use]
50    pub fn value(&self) -> &str {
51        &self.value
52    }
53}
54
55fn is_custom_guc_name(key: &str) -> bool {
56    let mut segments = 0;
57    for segment in key.split('.') {
58        let mut chars = segment.chars();
59        let Some(first) = chars.next() else {
60            return false;
61        };
62        if !(first.is_ascii_alphabetic() || first == '_') {
63            return false;
64        }
65        if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
66            return false;
67        }
68        segments += 1;
69    }
70    segments >= 2
71}
72
73/// Translates a [`RequestScope`] into transaction-local settings.
74///
75/// `#[async_trait]`: consumed as `Arc<dyn ConnectionScopeProvider>` collected
76/// from the inventory registry, so the trait must be dyn-compatible.
77#[async_trait::async_trait]
78pub trait ConnectionScopeProvider: Send + Sync {
79    /// Settings to apply after `BEGIN` on a scoped transaction. Return an
80    /// empty vec when the scope carries nothing this provider handles.
81    async fn scope_settings(&self, scope: &RequestScope) -> Result<Vec<ScopeSetting>, ScopeError>;
82}
83
84pub type SharedScopeProvider = Arc<dyn ConnectionScopeProvider>;