type_bridge_server/
schema_source.rs1use type_bridge_core_lib::_schema::TypeSchema;
2
3use crate::error::PipelineError;
4
5pub trait SchemaSource: Send + Sync {
30 fn load(&self) -> Result<TypeSchema, PipelineError>;
32}
33
34pub struct FileSchemaSource {
36 path: String,
37}
38
39impl FileSchemaSource {
40 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
60pub struct InMemorySchemaSource {
64 typeql: String,
65}
66
67impl InMemorySchemaSource {
68 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 #[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 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 #[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 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 #[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 #[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}