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