type_bridge_server/
schema_source.rs1use type_bridge_core_lib::schema::TypeSchema;
2
3use crate::error::PipelineError;
4
5pub trait SchemaSource: Send + Sync {
27 fn load(&self) -> Result<TypeSchema, PipelineError>;
29}
30
31pub 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
56pub 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 #[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 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 #[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 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 #[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 #[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}