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