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 #[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
58pub struct SchemaMetadataInit(pub fn() -> &'static SchemaMetadata);
60
61inventory::collect!(SchemaMetadataInit);
62
63pub struct SchemaConnectionsOverlayInit(pub fn() -> (&'static str, &'static [SchemaConnection]));
65
66inventory::collect!(SchemaConnectionsOverlayInit);
67
68pub 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#[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 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}