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)
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
56pub 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 #[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 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 #[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 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 #[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 #[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}