1use crate::schema_api::{Schema, SchemaConnection};
4use std::collections::HashMap;
5use std::sync::OnceLock;
6
7#[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
19pub 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
56pub struct SchemaMetadataInit(pub fn() -> &'static SchemaMetadata);
58
59inventory::collect!(SchemaMetadataInit);
60
61pub struct SchemaConnectionsOverlayInit(pub fn() -> (&'static str, &'static [SchemaConnection]));
63
64inventory::collect!(SchemaConnectionsOverlayInit);
65
66pub 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#[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}