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)
45            .map_err(|e| PipelineError::Schema(format!("Failed to read schema file '{}': {}", self.path, e)))?;
46        TypeSchema::from_typeql(&content)
47            .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 { typeql: typeql.into() }
66    }
67}
68
69impl SchemaSource for InMemorySchemaSource {
70    fn load(&self) -> Result<TypeSchema, PipelineError> {
71        TypeSchema::from_typeql(&self.typeql)
72            .map_err(parse_schema_error)
73    }
74}
75
76#[cfg(test)]
77#[cfg_attr(coverage_nightly, coverage(off))]
78mod tests {
79    use super::*;
80
81    const VALID_SCHEMA: &str = r#"
82define
83    attribute name, value string;
84    entity person, owns name;
85"#;
86
87    const COMPLEX_SCHEMA: &str = r#"
88define
89    attribute name, value string;
90    attribute age, value long;
91    attribute email, value string;
92    entity person,
93        owns name @key,
94        owns age,
95        owns email;
96    entity employee sub person;
97"#;
98
99    // --- FileSchemaSource tests ---
100
101    #[test]
102    fn file_schema_source_valid() {
103        let dir = tempfile::tempdir().unwrap();
104        let path = dir.path().join("schema.tql");
105        std::fs::write(&path, VALID_SCHEMA).unwrap();
106
107        let source = FileSchemaSource::new(path.to_str().unwrap());
108        let schema = source.load().unwrap();
109        assert!(schema.entities.contains_key("person"));
110    }
111
112    #[test]
113    fn file_schema_source_missing_file() {
114        let source = FileSchemaSource::new("/nonexistent/schema.tql");
115        let err = source.load().unwrap_err();
116        assert!(matches!(&err, PipelineError::Schema(msg) if msg.contains("Failed to read schema file")));
117    }
118
119    #[test]
120    fn file_schema_source_invalid_typeql() {
121        let dir = tempfile::tempdir().unwrap();
122        let path = dir.path().join("bad.tql");
123        std::fs::write(&path, "this is not valid typeql").unwrap();
124
125        let source = FileSchemaSource::new(path.to_str().unwrap());
126        let err = source.load().unwrap_err();
127        assert!(matches!(&err, PipelineError::Schema(msg) if msg.contains("Failed to parse schema")));
128    }
129
130    #[test]
131    fn file_schema_source_empty_file() {
132        let dir = tempfile::tempdir().unwrap();
133        let path = dir.path().join("empty.tql");
134        std::fs::write(&path, "").unwrap();
135
136        let source = FileSchemaSource::new(path.to_str().unwrap());
137        // Empty input parses to an empty schema (no entities/relations/attributes)
138        let schema = source.load().unwrap();
139        assert!(schema.entities.is_empty());
140        assert!(schema.relations.is_empty());
141        assert!(schema.attributes.is_empty());
142    }
143
144    #[test]
145    fn file_schema_source_error_contains_path() {
146        let source = FileSchemaSource::new("/some/specific/path.tql");
147        let err = source.load().unwrap_err();
148        let msg = err.to_string();
149        assert!(msg.contains("/some/specific/path.tql"), "Error should contain the file path: {msg}");
150    }
151
152    // --- InMemorySchemaSource tests ---
153
154    #[test]
155    fn in_memory_schema_source_valid() {
156        let source = InMemorySchemaSource::new(VALID_SCHEMA);
157        let schema = source.load().unwrap();
158        assert!(schema.entities.contains_key("person"));
159    }
160
161    #[test]
162    fn in_memory_schema_source_invalid() {
163        let source = InMemorySchemaSource::new("not valid typeql at all");
164        let err = source.load().unwrap_err();
165        assert!(matches!(&err, PipelineError::Schema(msg) if msg.contains("Failed to parse schema")));
166    }
167
168    #[test]
169    fn in_memory_schema_source_empty() {
170        let source = InMemorySchemaSource::new("");
171        // Empty input parses to an empty schema
172        let schema = source.load().unwrap();
173        assert!(schema.entities.is_empty());
174    }
175
176    #[test]
177    fn in_memory_schema_source_complex_schema() {
178        let source = InMemorySchemaSource::new(COMPLEX_SCHEMA);
179        let schema = source.load().unwrap();
180        assert!(schema.entities.contains_key("person"));
181        assert!(schema.entities.contains_key("employee"));
182        assert!(schema.attributes.contains_key("name"));
183        assert!(schema.attributes.contains_key("age"));
184        assert!(schema.attributes.contains_key("email"));
185    }
186
187    // --- Constructor tests ---
188
189    #[test]
190    fn file_schema_source_new_stores_path() {
191        let source = FileSchemaSource::new("/my/path.tql");
192        assert_eq!(source.path, "/my/path.tql");
193    }
194
195    #[test]
196    fn in_memory_schema_source_new_stores_typeql() {
197        let source = InMemorySchemaSource::new("define attribute x, value string;");
198        assert_eq!(source.typeql, "define attribute x, value string;");
199    }
200
201    // --- Trait object safety ---
202
203    #[test]
204    fn schema_source_trait_object_safety() {
205        let source: Box<dyn SchemaSource> = Box::new(InMemorySchemaSource::new(VALID_SCHEMA));
206        let schema = source.load().unwrap();
207        assert!(schema.entities.contains_key("person"));
208    }
209}