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    pub fn new(key: impl Into<String>, value: impl Into<String>) -> Result<Self, ScopeError> {
31        let key = key.into();
32        if !is_custom_guc_name(&key) {
33            return Err(ScopeError::InvalidKey { key });
34        }
35        Ok(Self {
36            key,
37            value: value.into(),
38        })
39    }
40
41    #[must_use]
42    pub fn key(&self) -> &str {
43        &self.key
44    }
45
46    #[must_use]
47    pub fn value(&self) -> &str {
48        &self.value
49    }
50}
51
52fn is_custom_guc_name(key: &str) -> bool {
53    let mut segments = 0;
54    for segment in key.split('.') {
55        let mut chars = segment.chars();
56        let Some(first) = chars.next() else {
57            return false;
58        };
59        if !(first.is_ascii_alphabetic() || first == '_') {
60            return false;
61        }
62        if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
63            return false;
64        }
65        segments += 1;
66    }
67    segments >= 2
68}
69
70/// Translates a [`RequestScope`] into transaction-local settings.
71///
72/// `#[async_trait]`: consumed as `Arc<dyn ConnectionScopeProvider>` collected
73/// from the inventory registry, so the trait must be dyn-compatible.
74#[async_trait::async_trait]
75pub trait ConnectionScopeProvider: Send + Sync {
76    async fn scope_settings(&self, scope: &RequestScope) -> Result<Vec<ScopeSetting>, ScopeError>;
77}
78
79pub type SharedScopeProvider = Arc<dyn ConnectionScopeProvider>;