1use indexmap::{IndexMap, IndexSet};
2use serde::{Deserialize, Serialize};
3
4use crate::error::{Error, HyleResult};
5use crate::field::{Field, FieldType};
6use crate::query::{Manifest, Query};
7use crate::raw::{ModelRows, Outcome, Row, Source, value_to_lookup_key};
8use crate::view::{apply_view, derive_columns, Column};
9#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct Model {
12 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub label: Option<String>,
14 #[serde(default)]
15 pub fields: IndexMap<String, Field>,
16}
17
18impl Model {
19 pub fn new() -> Self {
20 Self::default()
21 }
22
23 pub fn with_label(label: impl Into<String>) -> Self {
24 Self {
25 label: Some(label.into()),
26 fields: IndexMap::new(),
27 }
28 }
29
30 pub fn field(mut self, name: impl Into<String>, field: Field) -> Self {
31 self.fields.insert(name.into(), field);
32 self
33 }
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct Blueprint {
39 #[serde(default)]
40 pub models: IndexMap<String, Model>,
41}
42
43#[derive(Debug, Serialize)]
45#[serde(rename_all = "camelCase")]
46pub struct ResolvedView {
47 pub outcome: Outcome,
48 pub rows: Vec<Row>,
49 pub is_single: bool,
50 pub columns: Vec<Column>,
51}
52
53impl Blueprint {
54 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn model(mut self, name: impl Into<String>, model: Model) -> Self {
59 self.models.insert(name.into(), model);
60 self
61 }
62
63 pub fn manifest(&self, query: Query) -> HyleResult<Manifest> {
64 let model = self
65 .models
66 .get(&query.model)
67 .ok_or_else(|| Error::UnknownModel(query.model.clone()))?;
68
69 let mut fields = if query.select.is_empty() {
70 model.fields.keys().cloned().collect::<Vec<_>>()
71 } else {
72 query.select.clone()
73 };
74
75 if fields.is_empty() {
76 return Err(Error::EmptySelection);
77 }
78
79 for field in &fields {
80 if !model.fields.contains_key(field) {
81 return Err(Error::UnknownField {
82 model: query.model.clone(),
83 field: field.clone(),
84 });
85 }
86 }
87
88 let id = query.where_.get("id").cloned();
89 let filter = query
90 .where_
91 .iter()
92 .filter(|(key, _)| key.as_str() != "id")
93 .map(|(key, value)| (key.clone(), value.clone()))
94 .collect::<IndexMap<_, _>>();
95
96 let explicit_filter_fields = query
97 .filters
98 .iter()
99 .flatten()
100 .cloned()
101 .collect::<IndexSet<_>>();
102
103 for field in &explicit_filter_fields {
104 if !model.fields.contains_key(field) {
105 return Err(Error::UnknownField {
106 model: query.model.clone(),
107 field: field.clone(),
108 });
109 }
110 }
111
112 let mut lookups = IndexSet::new();
113 let mut inlines = IndexSet::new();
114
115 for field_name in &fields {
116 let field = &model.fields[field_name];
117 collect_references(
118 self,
119 &query.model,
120 field_name,
121 &field.field_type,
122 explicit_filter_fields.contains(field_name),
123 &mut lookups,
124 &mut inlines,
125 )?;
126 }
127
128 for field_name in &explicit_filter_fields {
129 if fields.contains(field_name) {
130 continue;
131 }
132
133 let field = &model.fields[field_name];
134 collect_references(
135 self,
136 &query.model,
137 field_name,
138 &field.field_type,
139 true,
140 &mut lookups,
141 &mut inlines,
142 )?;
143 }
144
145 fields.shrink_to_fit();
146
147 Ok(Manifest {
148 base: query.model,
149 id,
150 fields,
151 filter,
152 lookups: lookups.into_iter().collect(),
153 inlines: inlines.into_iter().collect(),
154 page: query.page,
155 per_page: query.per_page,
156 sort: query.sort,
157 method: query.method,
158 filter_fields: query.filters,
159 })
160 }
161
162 pub fn resolve_query(&self, query: Query, source: &Source) -> HyleResult<(Manifest, Outcome, Vec<Row>)> {
164 let manifest = self.manifest(query)?;
165 let outcome = self.resolve(&manifest, source)?;
166 let rows = outcome.rows.rows();
167 Ok((manifest, outcome, rows))
168 }
169
170 pub fn resolve_and_view(&self, manifest: &Manifest, source: &Source) -> HyleResult<ResolvedView> {
176 let outcome = self.resolve(manifest, source)?;
177 let all_rows = outcome.rows.rows();
178 let rows = apply_view(all_rows, manifest);
179 let is_single = crate::raw::is_single(manifest, &outcome);
180 let columns = derive_columns(self, manifest)?;
181 Ok(ResolvedView { outcome, rows, is_single, columns })
182 }
183
184 pub fn resolve(&self, manifest: &Manifest, source: &Source) -> HyleResult<Outcome> {
185 let base = source
186 .get(&manifest.base)
187 .ok_or_else(|| Error::MissingBaseModel(manifest.base.clone()))?;
188
189 let mut lookups = IndexMap::new();
190
191 for model_name in manifest.lookups.iter().chain(manifest.inlines.iter()) {
192 if let Some(result) = source.get(model_name) {
193 lookups.insert(model_name.clone(), rows_by_id(result.rows()));
194 }
195 }
196
197 Ok(Outcome {
198 rows: base.result.clone(),
199 total: base.total,
200 lookups,
201 })
202 }
203}
204
205fn collect_references(
206 blueprint: &Blueprint,
207 source_model: &str,
208 source_field: &str,
209 field_type: &FieldType,
210 explicit_need: bool,
211 lookups: &mut IndexSet<String>,
212 inlines: &mut IndexSet<String>,
213) -> HyleResult<()> {
214 match field_type {
215 FieldType::Primitive { .. } => Ok(()),
216 FieldType::Reference { reference } => {
217 if !blueprint.models.contains_key(&reference.entity) {
218 return Err(Error::UnknownReference {
219 model: source_model.to_owned(),
220 field: source_field.to_owned(),
221 target: reference.entity.clone(),
222 });
223 }
224
225 if explicit_need {
226 lookups.insert(reference.entity.clone());
227 } else {
228 inlines.insert(reference.entity.clone());
229 }
230
231 Ok(())
232 }
233 FieldType::Array { item } => collect_references(
234 blueprint,
235 source_model,
236 source_field,
237 item,
238 explicit_need,
239 lookups,
240 inlines,
241 ),
242 FieldType::Shape { fields } => {
243 for (name, field) in fields {
244 collect_references(
245 blueprint,
246 source_model,
247 name,
248 &field.field_type,
249 explicit_need,
250 lookups,
251 inlines,
252 )?;
253 }
254 Ok(())
255 }
256 }
257}
258
259fn rows_by_id(rows: Vec<Row>) -> IndexMap<String, Row> {
260 rows.into_iter()
261 .filter_map(|row| {
262 let id = row.get("id").and_then(value_to_lookup_key)?;
263 Some((id.clone(), row))
264 })
265 .collect()
266}
267
268#[allow(dead_code)]
269fn _assert_rows_send_sync(_: ModelRows) {}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::field::{Field, Reference};
275 use crate::raw::ModelResult;
276 use serde_json::json;
277
278 fn simple_blueprint() -> Blueprint {
279 Blueprint::new()
280 .model(
281 "user",
282 Model::new()
283 .field("name", Field::string("Name"))
284 .field("email", Field::string("Email"))
285 .field("role", Field::reference("Role", "role")),
286 )
287 .model(
288 "role",
289 Model::new()
290 .field("name", Field::string("Name")),
291 )
292 }
293
294 fn user_source() -> Source {
295 let mut src = Source::new();
296 src.insert(
297 "user".into(),
298 ModelResult::many(vec![
299 indexmap::indexmap! {
300 "id".to_owned() => json!(1),
301 "name".to_owned() => json!("Alice"),
302 "email".to_owned()=> json!("alice@example.test"),
303 "role".to_owned() => json!("admin"),
304 },
305 ]),
306 );
307 src.insert(
308 "role".into(),
309 ModelResult::many(vec![
310 indexmap::indexmap! {
311 "id".to_owned() => json!("admin"),
312 "name".to_owned() => json!("Admin"),
313 },
314 ]),
315 );
316 src
317 }
318
319 #[test]
322 fn manifest_happy_path() {
323 let bp = simple_blueprint();
324 let q = Query::new("user").select(["name", "email"]);
325 let m = bp.manifest(q).unwrap();
326 assert_eq!(m.base, "user");
327 assert_eq!(m.fields, vec!["name", "email"]);
328 assert!(m.lookups.is_empty());
329 assert!(m.inlines.is_empty());
330 }
331
332 #[test]
333 fn manifest_empty_select_uses_all_fields() {
334 let bp = simple_blueprint();
335 let q = Query::new("user");
336 let m = bp.manifest(q).unwrap();
337 assert!(m.fields.contains(&"name".to_owned()));
338 assert!(m.fields.contains(&"role".to_owned()));
339 }
340
341 #[test]
342 fn manifest_unknown_model_errors() {
343 let bp = simple_blueprint();
344 let q = Query::new("ghost");
345 assert!(matches!(bp.manifest(q), Err(Error::UnknownModel(_))));
346 }
347
348 #[test]
349 fn manifest_unknown_field_errors() {
350 let bp = simple_blueprint();
351 let q = Query::new("user").select(["ghost"]);
352 assert!(matches!(bp.manifest(q), Err(Error::UnknownField { .. })));
353 }
354
355 #[test]
356 fn manifest_reference_field_goes_in_inlines() {
357 let bp = simple_blueprint();
358 let q = Query::new("user").select(["name", "role"]);
359 let m = bp.manifest(q).unwrap();
360 assert!(m.inlines.contains(&"role".to_owned()));
361 assert!(m.lookups.is_empty());
362 }
363
364 #[test]
365 fn manifest_reference_field_in_filter_goes_in_lookups() {
366 let bp = simple_blueprint();
367 let q = Query::new("user")
368 .select(["name", "role"])
369 .filter_layout([["role"]]);
370 let m = bp.manifest(q).unwrap();
371 assert!(m.lookups.contains(&"role".to_owned()));
372 assert!(!m.inlines.contains(&"role".to_owned()));
373 }
374
375 #[test]
376 fn manifest_unknown_reference_errors() {
377 let bp = Blueprint::new().model(
378 "user",
379 Model::new().field("dept", Field::reference("Dept", "department")),
380 );
381 let q = Query::new("user").select(["dept"]);
382 assert!(matches!(bp.manifest(q), Err(Error::UnknownReference { .. })));
383 }
384
385 #[test]
388 fn resolve_happy_path() {
389 let bp = simple_blueprint();
390 let q = Query::new("user").select(["name", "role"]);
391 let m = bp.manifest(q).unwrap();
392 let src = user_source();
393 let outcome = bp.resolve(&m, &src).unwrap();
394 assert_eq!(outcome.rows.rows().len(), 1);
395 assert!(outcome.lookups.contains_key("role"));
396 }
397
398 #[test]
399 fn resolve_missing_base_model_errors() {
400 let bp = simple_blueprint();
401 let manifest = Manifest {
402 base: "ghost".into(),
403 id: None,
404 fields: vec![],
405 filter: Default::default(),
406 lookups: vec![],
407 inlines: vec![],
408 page: None,
409 per_page: None,
410 sort: None,
411 method: None,
412 filter_fields: vec![],
413 };
414 let src = user_source();
415 assert!(matches!(bp.resolve(&manifest, &src), Err(Error::MissingBaseModel(_))));
416 }
417
418 #[test]
421 fn resolve_and_view_end_to_end() {
422 let bp = simple_blueprint();
423 let q = Query::new("user").select(["name", "email"]);
424 let m = bp.manifest(q).unwrap();
425 let src = user_source();
426 let view = bp.resolve_and_view(&m, &src).unwrap();
427 assert_eq!(view.rows.len(), 1);
428 assert_eq!(view.rows[0]["name"], json!("Alice"));
429 assert_eq!(view.columns.len(), 2);
430 assert!(!view.is_single);
431 }
432
433 #[test]
434 fn resolve_and_view_single_record() {
435 let bp = simple_blueprint();
436 let q = Query {
437 model: "user".into(),
438 select: vec!["name".into()],
439 method: Some("one".into()),
440 ..Default::default()
441 };
442 let m = bp.manifest(q).unwrap();
443 let src = user_source();
444 let view = bp.resolve_and_view(&m, &src).unwrap();
445 assert!(view.is_single);
446 }
447}