Skip to main content

type_bridge_server/
schema_source.rs

1use type_bridge_core_lib::_schema::TypeSchema;
2
3use crate::error::PipelineError;
4
5/// Source of a TypeDB schema for the query pipeline.
6///
7/// Implement this trait to load a schema from any source: a file on disk,
8/// an in-memory string, a remote URL, or even directly from TypeDB.
9///
10/// # Example
11///
12/// This example is ignored because the remote fetch function and its transport
13/// error mapping are intentionally supplied by the consuming application.
14///
15/// ```rust,ignore
16/// use type_bridge_server::{SchemaSource, PipelineError};
17/// use type_bridge_core_lib::_schema::TypeSchema;
18///
19/// struct RemoteSchemaSource { url: String }
20///
21/// impl SchemaSource for RemoteSchemaSource {
22///     fn load(&self) -> Result<TypeSchema, PipelineError> {
23///         let content = fetch_schema(&self.url)?;
24///         TypeSchema::from_typeql(&content)
25///             .map_err(|e| PipelineError::Schema(e.to_string()))
26///     }
27/// }
28/// ```
29pub trait SchemaSource: Send + Sync {
30    /// Load and parse the schema, returning a `TypeSchema`.
31    fn load(&self) -> Result<TypeSchema, PipelineError>;
32}
33
34/// Load a schema from a TypeQL file on disk.
35pub struct FileSchemaSource {
36    path: String,
37}
38
39impl FileSchemaSource {
40    /// Construct a source that reads TypeQL from `path` when loaded.
41    pub fn new(path: impl Into<String>) -> Self {
42        Self { path: path.into() }
43    }
44}
45
46impl SchemaSource for FileSchemaSource {
47    fn load(&self) -> Result<TypeSchema, PipelineError> {
48        let content = std::fs::read_to_string(&self.path).map_err(|e| {
49            PipelineError::Schema(format!("Failed to read schema file '{}': {}", self.path, e))
50        })?;
51        TypeSchema::from_typeql(&content).map_err(parse_schema_error)
52    }
53}
54
55#[cfg_attr(coverage_nightly, coverage(off))]
56fn parse_schema_error(e: impl std::fmt::Display) -> PipelineError {
57    PipelineError::Schema(format!("Failed to parse schema: {}", e))
58}
59
60/// Load a schema from an in-memory TypeQL string.
61///
62/// Useful for testing or when the schema is embedded in the application.
63pub struct InMemorySchemaSource {
64    typeql: String,
65}
66
67impl InMemorySchemaSource {
68    /// Construct a source from TypeQL already held in memory.
69    pub fn new(typeql: impl Into<String>) -> Self {
70        Self {
71            typeql: typeql.into(),
72        }
73    }
74}
75
76impl SchemaSource for InMemorySchemaSource {
77    fn load(&self) -> Result<TypeSchema, PipelineError> {
78        TypeSchema::from_typeql(&self.typeql).map_err(parse_schema_error)
79    }
80}
81
82#[cfg(test)]
83#[cfg_attr(coverage_nightly, coverage(off))]
84mod tests {
85    use super::*;
86
87    const VALID_SCHEMA: &str = r#"
88define
89    attribute name, value string;
90    entity person, owns name;
91"#;
92
93    const COMPLEX_SCHEMA: &str = r#"
94define
95    attribute name, value string;
96    attribute age, value long;
97    attribute email, value string;
98    entity person,
99        owns name @key,
100        owns age,
101        owns email;
102    entity employee sub person;
103"#;
104
105    // --- FileSchemaSource tests ---
106
107    #[test]
108    fn file_schema_source_valid() {
109        let dir = tempfile::tempdir().unwrap();
110        let path = dir.path().join("schema.tql");
111        std::fs::write(&path, VALID_SCHEMA).unwrap();
112
113        let source = FileSchemaSource::new(path.to_str().unwrap());
114        let schema = source.load().unwrap();
115        assert!(schema.entities.contains_key("person"));
116    }
117
118    #[test]
119    fn file_schema_source_missing_file() {
120        let source = FileSchemaSource::new("/nonexistent/schema.tql");
121        let err = source.load().unwrap_err();
122        assert!(
123            matches!(&err, PipelineError::Schema(msg) if msg.contains("Failed to read schema file"))
124        );
125    }
126
127    #[test]
128    fn file_schema_source_invalid_typeql() {
129        let dir = tempfile::tempdir().unwrap();
130        let path = dir.path().join("bad.tql");
131        std::fs::write(&path, "this is not valid typeql").unwrap();
132
133        let source = FileSchemaSource::new(path.to_str().unwrap());
134        let err = source.load().unwrap_err();
135        assert!(
136            matches!(&err, PipelineError::Schema(msg) if msg.contains("Failed to parse schema"))
137        );
138    }
139
140    #[test]
141    fn file_schema_source_empty_file() {
142        let dir = tempfile::tempdir().unwrap();
143        let path = dir.path().join("empty.tql");
144        std::fs::write(&path, "").unwrap();
145
146        let source = FileSchemaSource::new(path.to_str().unwrap());
147        // Empty input parses to an empty schema (no entities/relations/attributes)
148        let schema = source.load().unwrap();
149        assert!(schema.entities.is_empty());
150        assert!(schema.relations.is_empty());
151        assert!(schema.attributes.is_empty());
152    }
153
154    #[test]
155    fn file_schema_source_error_contains_path() {
156        let source = FileSchemaSource::new("/some/specific/path.tql");
157        let err = source.load().unwrap_err();
158        let msg = err.to_string();
159        assert!(
160            msg.contains("/some/specific/path.tql"),
161            "Error should contain the file path: {msg}"
162        );
163    }
164
165    // --- InMemorySchemaSource tests ---
166
167    #[test]
168    fn in_memory_schema_source_valid() {
169        let source = InMemorySchemaSource::new(VALID_SCHEMA);
170        let schema = source.load().unwrap();
171        assert!(schema.entities.contains_key("person"));
172    }
173
174    #[test]
175    fn in_memory_schema_source_invalid() {
176        let source = InMemorySchemaSource::new("not valid typeql at all");
177        let err = source.load().unwrap_err();
178        assert!(
179            matches!(&err, PipelineError::Schema(msg) if msg.contains("Failed to parse schema"))
180        );
181    }
182
183    #[test]
184    fn in_memory_schema_source_empty() {
185        let source = InMemorySchemaSource::new("");
186        // Empty input parses to an empty schema
187        let schema = source.load().unwrap();
188        assert!(schema.entities.is_empty());
189    }
190
191    #[test]
192    fn in_memory_schema_source_complex_schema() {
193        let source = InMemorySchemaSource::new(COMPLEX_SCHEMA);
194        let schema = source.load().unwrap();
195        assert!(schema.entities.contains_key("person"));
196        assert!(schema.entities.contains_key("employee"));
197        assert!(schema.attributes.contains_key("name"));
198        assert!(schema.attributes.contains_key("age"));
199        assert!(schema.attributes.contains_key("email"));
200    }
201
202    // --- Constructor tests ---
203
204    #[test]
205    fn file_schema_source_new_stores_path() {
206        let source = FileSchemaSource::new("/my/path.tql");
207        assert_eq!(source.path, "/my/path.tql");
208    }
209
210    #[test]
211    fn in_memory_schema_source_new_stores_typeql() {
212        let source = InMemorySchemaSource::new("define attribute x, value string;");
213        assert_eq!(source.typeql, "define attribute x, value string;");
214    }
215
216    // --- Trait object safety ---
217
218    #[test]
219    fn schema_source_trait_object_safety() {
220        let source: Box<dyn SchemaSource> = Box::new(InMemorySchemaSource::new(VALID_SCHEMA));
221        let schema = source.load().unwrap();
222        assert!(schema.entities.contains_key("person"));
223    }
224}