warble_mdl_context/
raw_source.rs1use serde::Deserialize;
13
14use warble::{ContextLoader, DimensionInfo, LineageGraph, MetricInfo, ModelInfo};
15
16#[derive(Debug, Deserialize)]
20struct RawSchema {
21 #[allow(dead_code)]
22 source: String,
23 #[serde(default)]
24 tables: Vec<RawTable>,
25}
26
27#[derive(Debug, Deserialize)]
28struct RawTable {
29 #[allow(dead_code)]
30 name: String,
31 #[serde(default)]
32 columns: Vec<RawColumn>,
33}
34
35#[derive(Debug, Deserialize)]
36struct RawColumn {
37 #[allow(dead_code)]
38 name: String,
39 #[serde(rename = "type")]
40 #[allow(dead_code)]
41 col_type: String,
42}
43
44pub struct RawSources {
47 pub schema_json: String,
48 pub has_docs: bool,
50}
51
52pub struct RawSourceContext {
56 parseable: bool,
57 source_introspectable: Option<bool>,
58 raw_docs_readable: Option<bool>,
59 lineage: LineageGraph,
60}
61
62impl RawSourceContext {
63 pub fn from_sources(sources: &RawSources) -> Self {
66 match serde_json::from_str::<RawSchema>(&sources.schema_json) {
67 Ok(schema) => {
68 let introspectable = schema.tables.iter().any(|t| !t.columns.is_empty());
69 RawSourceContext {
70 parseable: true,
71 source_introspectable: Some(introspectable),
72 raw_docs_readable: Some(sources.has_docs),
73 lineage: LineageGraph::default(),
74 }
75 }
76 Err(_) => Self::unparseable(),
77 }
78 }
79
80 pub fn unparseable() -> Self {
85 RawSourceContext {
86 parseable: false,
87 source_introspectable: None,
88 raw_docs_readable: None,
89 lineage: LineageGraph::default(),
90 }
91 }
92}
93
94impl ContextLoader for RawSourceContext {
95 fn is_parseable(&self) -> bool {
96 self.parseable
97 }
98 fn metrics(&self) -> &[MetricInfo] {
99 &[]
100 }
101 fn dimensions(&self) -> &[DimensionInfo] {
102 &[]
103 }
104 fn time_dimensions(&self) -> &[DimensionInfo] {
105 &[]
106 }
107 fn models(&self) -> &[ModelInfo] {
108 &[]
109 }
110 fn lineage(&self) -> &LineageGraph {
111 &self.lineage
112 }
113 fn source_introspectable(&self) -> Option<bool> {
114 self.source_introspectable
115 }
116 fn raw_docs_readable(&self) -> Option<bool> {
117 self.raw_docs_readable
118 }
119}
120
121#[cfg(not(target_arch = "wasm32"))]
128pub fn read_raw_dir(dir: &std::path::Path) -> std::io::Result<Option<RawSources>> {
129 use std::fs;
130
131 let schema_path = dir.join("schema.json");
132 if !schema_path.is_file() {
133 return Ok(None);
134 }
135 let schema_json = fs::read_to_string(&schema_path)?;
136
137 let docs_dir = dir.join("docs");
138 let has_docs = docs_dir.is_dir()
139 && fs::read_dir(&docs_dir)?
140 .filter_map(Result::ok)
141 .any(|e| e.path().is_file());
142
143 Ok(Some(RawSources {
144 schema_json,
145 has_docs,
146 }))
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn good_sources(has_docs: bool) -> RawSources {
154 RawSources {
155 schema_json: r#"{
156 "source": "synthetic_shop",
157 "description": "a raw source",
158 "tables": [
159 { "name": "raw_orders", "columns": [
160 { "name": "order_id", "type": "INTEGER" },
161 { "name": "amount", "type": "DECIMAL" }
162 ] }
163 ]
164 }"#
165 .to_string(),
166 has_docs,
167 }
168 }
169
170 #[test]
171 fn good_schema_is_introspectable_and_reflects_docs() {
172 let ctx = RawSourceContext::from_sources(&good_sources(true));
173 assert!(ctx.is_parseable());
174 assert_eq!(ctx.source_introspectable(), Some(true));
175 assert_eq!(ctx.raw_docs_readable(), Some(true));
176
177 let ctx_no_docs = RawSourceContext::from_sources(&good_sources(false));
178 assert_eq!(ctx_no_docs.raw_docs_readable(), Some(false));
179 }
180
181 #[test]
182 fn empty_tables_are_not_introspectable() {
183 let sources = RawSources {
184 schema_json: r#"{"source":"empty_src","tables":[]}"#.to_string(),
185 has_docs: false,
186 };
187 let ctx = RawSourceContext::from_sources(&sources);
188 assert!(ctx.is_parseable(), "an empty-but-valid schema still parses");
189 assert_eq!(ctx.source_introspectable(), Some(false));
190 }
191
192 #[test]
193 fn unparseable_schema_yields_unparseable_context() {
194 let sources = RawSources {
195 schema_json: "not json at all".to_string(),
196 has_docs: true,
197 };
198 let ctx = RawSourceContext::from_sources(&sources);
199 assert!(!ctx.is_parseable());
200 assert_eq!(ctx.source_introspectable(), None);
201 assert_eq!(ctx.raw_docs_readable(), None);
202 }
203
204 #[test]
205 fn raw_source_context_can_answer_raw_shape_predicates() {
206 let ctx = RawSourceContext::from_sources(&good_sources(true));
207 assert!(ctx.can_answer("source_introspectable"));
208 assert!(ctx.can_answer("raw_docs_readable"));
209 }
210
211 #[test]
212 fn mdl_context_cannot_answer_raw_shape_predicates() {
213 use crate::MdlContext;
216 use wren_core_base::mdl::manifest::Manifest;
217
218 let json = r#"{
219 "catalog":"wren","schema":"public",
220 "models":[],"relationships":[],"cubes":[],"views":[]
221 }"#;
222 let manifest: Manifest = serde_json::from_str(json).unwrap();
223 let ctx = MdlContext::from_manifest(&manifest);
224 assert_eq!(ctx.source_introspectable(), None);
225 assert_eq!(ctx.raw_docs_readable(), None);
226 assert!(!ctx.can_answer("source_introspectable"));
227 assert!(!ctx.can_answer("raw_docs_readable"));
228 }
229}