Skip to main content

valence_core/
schema.rs

1//! Schema metadata registration hooks for `valence_schema!`.
2
3use crate::schema_api::{Schema, SchemaConnection};
4use std::collections::HashMap;
5use std::sync::OnceLock;
6
7/// Metadata for a schema definition discovered at runtime.
8#[derive(Debug, Clone)]
9pub struct SchemaMetadata {
10    pub table_name: &'static str,
11    pub version: &'static str,
12    pub description: Option<&'static str>,
13    pub privacy_read: &'static str,
14    pub privacy_write: &'static str,
15    pub databases: &'static [String],
16    pub schema: &'static Schema,
17}
18
19/// Alias used by `valence_schema!` codegen.
20pub type SchemaMetadataStruct = SchemaMetadata;
21
22impl SchemaMetadata {
23    pub fn new(
24        table_name: &'static str,
25        version: &'static str,
26        description: Option<&'static str>,
27        privacy_read: &'static str,
28        privacy_write: &'static str,
29        databases: &'static [String],
30        schema: &'static Schema,
31    ) -> Self {
32        Self {
33            table_name,
34            version,
35            description,
36            privacy_read,
37            privacy_write,
38            databases,
39            schema,
40        }
41    }
42
43    pub fn from_schema(schema: &'static Schema) -> Self {
44        Self {
45            table_name: schema.name.as_str(),
46            version: schema.version.as_str(),
47            description: schema.meta.description.as_deref(),
48            privacy_read: schema.privacy.read.as_str(),
49            privacy_write: schema.privacy.write.as_str(),
50            databases: schema.databases.as_slice(),
51            schema,
52        }
53    }
54}
55
56/// Lazy initializer submitted via `inventory::submit!`.
57pub struct SchemaMetadataInit(pub fn() -> &'static SchemaMetadata);
58
59inventory::collect!(SchemaMetadataInit);
60
61/// Codegen-submitted trait-merged connections for deletion graph and tooling.
62pub struct SchemaConnectionsOverlayInit(pub fn() -> (&'static str, &'static [SchemaConnection]));
63
64inventory::collect!(SchemaConnectionsOverlayInit);
65
66/// Connections for `table_name`, preferring macro-registered schema connections.
67pub fn schema_connections_for_table(
68    meta: &SchemaMetadata,
69) -> std::borrow::Cow<'static, [SchemaConnection]> {
70    if !meta.schema.connections.is_empty() {
71        return std::borrow::Cow::Borrowed(meta.schema.connections.as_slice());
72    }
73    for init in inventory::iter::<SchemaConnectionsOverlayInit> {
74        let (table, conns) = (init.0)();
75        if table == meta.table_name {
76            return std::borrow::Cow::Borrowed(conns);
77        }
78    }
79    std::borrow::Cow::Borrowed(&[])
80}
81
82/// Owned registry for schema metadata.
83#[derive(Debug)]
84pub struct SchemaRegistry {
85    inner: HashMap<String, &'static SchemaMetadata>,
86}
87
88impl SchemaRegistry {
89    pub fn new() -> Self {
90        Self {
91            inner: HashMap::new(),
92        }
93    }
94
95    pub fn auto_discover() -> Self {
96        let mut registry = Self::new();
97        for init in inventory::iter::<SchemaMetadataInit> {
98            let metadata = (init.0)();
99            registry
100                .inner
101                .insert(metadata.table_name.to_string(), metadata);
102        }
103        registry
104    }
105
106    pub fn set_global(registry: SchemaRegistry) {
107        GLOBAL_REGISTRY
108            .set(registry)
109            .expect("SchemaRegistry::set_global called more than once");
110    }
111
112    pub fn global() -> &'static SchemaRegistry {
113        GLOBAL_REGISTRY.get_or_init(SchemaRegistry::auto_discover)
114    }
115
116    pub fn register(&mut self, metadata: &'static SchemaMetadata) {
117        self.inner.insert(metadata.table_name.to_string(), metadata);
118    }
119
120    pub fn register_schema(&mut self, schema: &'static Schema) {
121        let metadata = SchemaMetadata::from_schema(schema);
122        self.register(Box::leak(Box::new(metadata)));
123    }
124
125    pub fn get_schema(&self, table_name: &str) -> Option<&'static SchemaMetadata> {
126        self.inner.get(table_name).copied()
127    }
128
129    pub fn get_full_schema(&self, table_name: &str) -> Option<&'static Schema> {
130        self.get_schema(table_name).map(|meta| meta.schema)
131    }
132
133    pub fn list_schemas(&self) -> Vec<&str> {
134        let mut keys: Vec<&str> = self.inner.keys().map(String::as_str).collect();
135        keys.sort_unstable();
136        keys
137    }
138
139    pub fn has_schema(&self, table_name: &str) -> bool {
140        self.inner.contains_key(table_name)
141    }
142}
143
144static GLOBAL_REGISTRY: OnceLock<SchemaRegistry> = OnceLock::new();
145
146impl Default for SchemaRegistry {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::evaluator::DEFAULT_IN_MEMORY;
156    use crate::schema_api::{SchemaField, SchemaMeta, SchemaPrivacy};
157
158    fn build_schema(name: &str) -> &'static Schema {
159        Box::leak(Box::new(Schema {
160            name: name.to_string(),
161            version: "1.0.0".to_string(),
162            databases: vec!["default".to_string()],
163            database_evaluator: &DEFAULT_IN_MEMORY,
164            privacy: SchemaPrivacy {
165                read: "public".to_string(),
166                write: "service".to_string(),
167            },
168            policies: None,
169            fields: vec![SchemaField {
170                name: "id".to_string(),
171                field_type: "string".to_string(),
172                primary: true,
173                nullable: false,
174                indexed: false,
175                unique: false,
176                default: None,
177                fk: None,
178                validations: Vec::new(),
179                policies: None,
180                encrypted: false,
181                enum_variants: Vec::new(),
182                enum_type: None,
183            }],
184            edges: Vec::new(),
185            connections: Vec::new(),
186            side_effects: Vec::new(),
187            iters: Vec::new(),
188            composite_key: Vec::new(),
189            traits: Vec::new(),
190            ttl: None,
191            ownership: None,
192            meta: SchemaMeta {
193                retention: "365 days".to_string(),
194                row_count: 0,
195                owner: "system".to_string(),
196                description: None,
197            },
198        }))
199    }
200
201    #[test]
202    fn register_and_list() {
203        let mut registry = SchemaRegistry::new();
204        let schema = build_schema("fixture");
205        registry.register(Box::leak(Box::new(SchemaMetadata::from_schema(schema))));
206        assert!(registry.has_schema("fixture"));
207        assert_eq!(registry.list_schemas(), vec!["fixture"]);
208    }
209}