1extern crate self as teaql_core;
2
3mod entity;
4mod entity_graph;
5mod expr;
6mod list;
7mod meta;
8mod mutation;
9mod naming;
10mod query;
11mod safe_expression;
12mod trace;
13mod value;
14mod web;
15mod xls;
16
17pub use entity::{
18 BaseEntity, BaseEntityData, Entity, EntityDescriptorStore, EntityError, IdentifiableEntity,
19 TeaqlEntity, VersionedEntity,
20};
21pub use entity_graph::{EntityGraph, EntityGraphBuilder, EntityGraphNode, EntityGraphOperation};
22pub use expr::{BinaryOp, Expr, ExprFunction};
23pub use list::SmartList;
24pub use meta::{EntityDescriptor, PropertyDescriptor, RelationDescriptor};
25pub use mutation::{
26 BatchInsertCommand, BatchUpdateCommand, DeleteCommand, InsertCommand, MutationKind,
27 RecoverCommand, UpdateCommand,
28};
29pub use naming::default_table_name;
30pub use query::{
31 Aggregate, AggregateFunction, AggregationCacheOptions, NamedExpr, ObjectGroupBy, OrderBy,
32 RawSqlProjection, Record, RelationAggregate, RelationLoad, SelectQuery, Slice, SortDirection,
33 record_to_json_value,
34};
35pub use safe_expression::{SafeExpression, TeaqlEmpty};
36pub use trace::TraceNode;
37pub use value::{DataType, Decimal, Value};
38pub use web::{ACTION_LIST_KEY, STYLE_KEY, WEB_RESPONSE_VERSION, WebAction, WebResponse, WebStyle};
39pub use xls::{XlsBlock, XlsBlockBuildContext, XlsPage, XlsWorkbook};
40
41#[cfg(test)]
42mod tests {
43 use std::collections::BTreeMap;
44
45 use super::*;
46 use chrono::{NaiveDate, TimeZone, Utc};
47 use teaql_macros::TeaqlEntity;
48
49 #[derive(Default)]
50 struct TestStore {
51 descriptors: Vec<EntityDescriptor>,
52 }
53
54 impl EntityDescriptorStore for TestStore {
55 fn register_descriptor(&mut self, descriptor: EntityDescriptor) {
56 self.descriptors.push(descriptor);
57 }
58 }
59
60 #[allow(dead_code)]
61 #[derive(Clone, TeaqlEntity)]
62 #[teaql(entity = "Order", table = "orders")]
63 struct OrderRow {
64 #[teaql(id)]
65 id: u64,
66 #[teaql(version)]
67 version: i64,
68 #[teaql(column = "display_name")]
69 name: String,
70 }
71
72 #[allow(dead_code)]
73 #[derive(Debug, TeaqlEntity)]
74 #[teaql(entity = "TypedNumber", table = "typed_number")]
75 struct TypedNumberRow {
76 #[teaql(id)]
77 id: u64,
78 signed: i32,
79 unsigned: u32,
80 amount: Decimal,
81 }
82
83 #[test]
84 fn derive_entity_descriptor() {
85 let descriptor = OrderRow::entity_descriptor();
86 assert_eq!(descriptor.name, "Order");
87 assert_eq!(descriptor.table_name, "orders");
88 assert_eq!(
89 descriptor.id_property().map(|p| p.name.as_str()),
90 Some("id")
91 );
92 assert_eq!(
93 descriptor
94 .property_by_name("name")
95 .map(|p| p.column_name.as_str()),
96 Some("display_name")
97 );
98 }
99
100 #[test]
101 fn derive_maps_checked_integer_and_decimal_fields() {
102 let descriptor = TypedNumberRow::entity_descriptor();
103 assert_eq!(
104 descriptor.property_by_name("amount").map(|p| p.data_type),
105 Some(DataType::Decimal)
106 );
107
108 let row = TypedNumberRow::from_record(Record::from([
109 ("id".to_owned(), Value::I64(7)),
110 ("signed".to_owned(), Value::I64(2_147_483_647)),
111 ("unsigned".to_owned(), Value::U64(4_294_967_295)),
112 ("amount".to_owned(), Value::Decimal(Decimal::new(12345, 2))),
113 ]))
114 .unwrap();
115 assert_eq!(row.id, 7);
116 assert_eq!(row.signed, i32::MAX);
117 assert_eq!(row.unsigned, u32::MAX);
118 assert_eq!(row.amount, Decimal::new(12345, 2));
119
120 let signed_overflow = TypedNumberRow::from_record(Record::from([
121 ("id".to_owned(), Value::U64(1)),
122 ("signed".to_owned(), Value::I64(i64::from(i32::MAX) + 1)),
123 ("unsigned".to_owned(), Value::U64(1)),
124 ("amount".to_owned(), Value::Decimal(Decimal::ONE)),
125 ]));
126 assert!(
127 signed_overflow
128 .unwrap_err()
129 .message
130 .contains("out of i32 range")
131 );
132
133 let unsigned_negative = TypedNumberRow::from_record(Record::from([
134 ("id".to_owned(), Value::U64(1)),
135 ("signed".to_owned(), Value::I64(1)),
136 ("unsigned".to_owned(), Value::I64(-1)),
137 ("amount".to_owned(), Value::Decimal(Decimal::ONE)),
138 ]));
139 assert!(
140 unsigned_negative
141 .unwrap_err()
142 .message
143 .contains("out of u32 range")
144 );
145 }
146
147 #[test]
148 fn derive_allows_partial_projected_records() {
149 let row = OrderRow::from_record(Record::from([(
150 "name".to_owned(),
151 Value::Text("projected".to_owned()),
152 )]))
153 .unwrap();
154 assert_eq!(row.id, 0);
155 assert_eq!(row.version, 0);
156 assert_eq!(row.name, "projected");
157
158 let nulls = OrderRow::from_record(Record::from([
159 ("id".to_owned(), Value::Null),
160 ("version".to_owned(), Value::Null),
161 ("name".to_owned(), Value::Null),
162 ]))
163 .unwrap();
164 assert_eq!(nulls.id, 0);
165 assert_eq!(nulls.version, 0);
166 assert_eq!(nulls.name, "");
167
168 match OrderRow::from_record(Record::from([("name".to_owned(), Value::U64(1))])) {
169 Ok(_) => panic!("wrong field type should fail"),
170 Err(err) => assert!(err.message.contains("invalid field name")),
171 }
172 }
173
174 #[allow(dead_code)]
175 #[derive(TeaqlEntity)]
176 #[teaql(entity = "Product", table = "product")]
177 struct ProductRow {
178 #[teaql(id)]
179 id: u64,
180 name: String,
181 }
182
183 #[allow(dead_code)]
184 #[derive(TeaqlEntity)]
185 #[teaql(entity = "OrderLine", table = "orderline")]
186 struct OrderLineRow {
187 #[teaql(id)]
188 id: u64,
189 #[teaql(column = "order_id")]
190 order_id: u64,
191 #[teaql(relation(
192 target = "Product",
193 local_key = "product_id",
194 foreign_key = "id",
195 attach = false,
196 delete_missing = false
197 ))]
198 product: Option<ProductRow>,
199 }
200
201 #[test]
202 fn derive_relation_descriptor_and_register() {
203 let descriptor = OrderLineRow::entity_descriptor();
204 let relation = descriptor.relation_by_name("product").unwrap();
205 assert_eq!(relation.target_entity, "Product");
206 assert_eq!(relation.local_key, "product_id");
207 assert_eq!(relation.foreign_key, "id");
208 assert!(!relation.attach);
209 assert!(!relation.delete_missing);
210
211 let mut store = TestStore::default();
212 OrderLineRow::register_into(&mut store);
213 assert_eq!(store.descriptors.len(), 1);
214 assert_eq!(store.descriptors[0].name, "OrderLine");
215 }
216
217 #[test]
218 fn register_entities_macro_registers_multiple_descriptors() {
219 let mut store = TestStore::default();
220 crate::register_entities!(&mut store, OrderRow, OrderLineRow);
221
222 assert_eq!(store.descriptors.len(), 2);
223 assert_eq!(store.descriptors[0].name, "Order");
224 assert_eq!(store.descriptors[1].name, "OrderLine");
225 }
226
227 #[allow(dead_code)]
228 #[derive(TeaqlEntity)]
229 struct DefaultTableNameRow {
230 #[teaql(id)]
231 id: u64,
232 }
233
234 #[allow(dead_code)]
235 #[derive(TeaqlEntity)]
236 struct TypedValueRow {
237 #[teaql(id)]
238 id: u64,
239 payload: serde_json::Value,
240 birthday: NaiveDate,
241 happened_at: chrono::DateTime<Utc>,
242 }
243
244 #[allow(dead_code)]
245 #[derive(TeaqlEntity)]
246 #[teaql(entity = "OrderAggregate", table = "order_aggregate")]
247 struct OrderAggregateRow {
248 #[teaql(id)]
249 id: u64,
250 #[teaql(dynamic)]
251 dynamic: BTreeMap<String, Value>,
252 }
253
254 #[test]
255 fn default_table_name_matches_java_sql_repository_rule() {
256 assert_eq!(default_table_name("Order"), "order_data");
257 assert_eq!(default_table_name("OrderLine"), "order_line_data");
258 assert_eq!(EntityDescriptor::new("Order").table_name, "order_data");
259 assert_eq!(
260 EntityDescriptor::new("OrderLine").table_name,
261 "order_line_data"
262 );
263 assert_eq!(
264 DefaultTableNameRow::entity_descriptor().table_name,
265 "default_table_name_row_data"
266 );
267 }
268
269 #[test]
270 fn derive_maps_json_date_and_timestamp_types() {
271 let descriptor = TypedValueRow::entity_descriptor();
272 assert_eq!(
273 descriptor.property_by_name("payload").map(|p| p.data_type),
274 Some(DataType::Json)
275 );
276 assert_eq!(
277 descriptor.property_by_name("birthday").map(|p| p.data_type),
278 Some(DataType::Date)
279 );
280 assert_eq!(
281 descriptor
282 .property_by_name("happened_at")
283 .map(|p| p.data_type),
284 Some(DataType::Timestamp)
285 );
286
287 let birthday = NaiveDate::from_ymd_opt(2024, 2, 3).unwrap();
288 let happened_at = Utc.with_ymd_and_hms(2024, 2, 3, 4, 5, 6).unwrap();
289 assert_eq!(
290 Value::from(serde_json::json!({"a": 1})),
291 Value::Json(serde_json::json!({"a": 1}))
292 );
293 assert_eq!(Value::from(birthday), Value::Date(birthday));
294 assert_eq!(Value::from(happened_at), Value::Timestamp(happened_at));
295 }
296
297 #[test]
298 fn query_builders_cover_filters_sort_aggregates_and_relations() {
299 let query = SelectQuery::new("Order")
300 .projects(["id", "name"])
301 .filter(Expr::gte("version", 1_i64))
302 .and_filter(Expr::not_in_list(
303 "name",
304 vec![Value::from("archived"), Value::from("deleted")],
305 ))
306 .and_filter(Expr::in_large(
307 "id",
308 vec![Value::from(1_u64), Value::from(2_u64)],
309 ))
310 .and_filter(Expr::contain("name", "rob"))
311 .and_filter(Expr::sound_like("name", "Robert"))
312 .and_filter(Expr::compare_columns(
313 "updated_at",
314 BinaryOp::Gte,
315 "created_at",
316 ))
317 .or_filter(Expr::is_null("name"))
318 .project_expr("nameSound", Expr::soundex(Expr::column("name")))
319 .order_desc("id")
320 .order_gbk_asc("name")
321 .group_by("name")
322 .count("total")
323 .sum("version", "versionSum")
324 .stddev("version", "versionStddev")
325 .enable_aggregation_cache_for(1_000)
326 .propagate_aggregation_cache(2_000)
327 .having(Expr::gt("total", 1_i64))
328 .relation("lines")
329 .relation_query(
330 "customer",
331 SelectQuery::new("Customer")
332 .project("name")
333 .filter(Expr::eq("status", "active")),
334 )
335 .page(20, 10);
336
337 assert_eq!(query.projection, vec!["id", "name"]);
338 assert_eq!(
339 query.expr_projection,
340 vec![NamedExpr::new(
341 "nameSound",
342 Expr::soundex(Expr::column("name"))
343 )]
344 );
345 assert_eq!(
346 query.order_by,
347 vec![OrderBy::desc("id"), OrderBy::asc_gbk("name")]
348 );
349 assert_eq!(query.group_by, vec!["name"]);
350 assert_eq!(
351 query.aggregates,
352 vec![
353 Aggregate::count("total"),
354 Aggregate::sum("version", "versionSum"),
355 Aggregate::stddev("version", "versionStddev")
356 ]
357 );
358 assert_eq!(
359 query.aggregation_cache,
360 Some(AggregationCacheOptions {
361 enabled: true,
362 cache_expired_millis: 1_000,
363 propagate: true,
364 propagate_cache_expired_millis: 2_000,
365 })
366 );
367 assert_eq!(query.having, Some(Expr::gt("total", 1_i64)));
368 assert_eq!(
369 query.relations,
370 vec![
371 RelationLoad::new("lines"),
372 RelationLoad::with_query(
373 "customer",
374 SelectQuery::new("Customer")
375 .project("name")
376 .filter(Expr::eq("status", "active")),
377 )
378 ]
379 );
380 assert_eq!(
381 query.slice,
382 Some(Slice {
383 limit: Some(10),
384 offset: 20
385 })
386 );
387 assert!(matches!(query.filter, Some(Expr::Or(_))));
388 }
389
390 #[test]
391 fn compare_columns_builds_property_to_property_filter() {
392 assert_eq!(
393 Expr::compare_columns("updated_at", BinaryOp::Gte, "created_at"),
394 Expr::Binary {
395 left: Box::new(Expr::Column("updated_at".to_owned())),
396 op: BinaryOp::Gte,
397 right: Box::new(Expr::Column("created_at".to_owned())),
398 }
399 );
400 }
401
402 #[test]
403 fn sound_like_builds_soundex_equality() {
404 assert_eq!(
405 Expr::sound_like("name", "Robert"),
406 Expr::binary(
407 Expr::soundex(Expr::column("name")),
408 BinaryOp::Eq,
409 Expr::soundex(Expr::value("Robert"))
410 )
411 );
412 }
413
414 #[test]
415 fn java_style_string_match_builders_expand_like_patterns() {
416 assert_eq!(Expr::contain("name", "tea"), Expr::like("name", "%tea%"));
417 assert_eq!(
418 Expr::not_contain("name", "tea"),
419 Expr::not_like("name", "%tea%")
420 );
421 assert_eq!(Expr::begin_with("name", "tea"), Expr::like("name", "tea%"));
422 assert_eq!(
423 Expr::not_begin_with("name", "tea"),
424 Expr::not_like("name", "tea%")
425 );
426 assert_eq!(Expr::end_with("name", "tea"), Expr::like("name", "%tea"));
427 assert_eq!(
428 Expr::not_end_with("name", "tea"),
429 Expr::not_like("name", "%tea")
430 );
431 }
432
433 #[test]
434 fn large_in_builders_use_large_binary_ops() {
435 assert_eq!(
436 Expr::in_large("id", vec![Value::from(1_u64)]),
437 Expr::binary(
438 Expr::column("id"),
439 BinaryOp::InLarge,
440 Expr::value(Value::List(vec![Value::from(1_u64)]))
441 )
442 );
443 assert_eq!(
444 Expr::not_in_large("id", vec![Value::from(1_u64)]),
445 Expr::binary(
446 Expr::column("id"),
447 BinaryOp::NotInLarge,
448 Expr::value(Value::List(vec![Value::from(1_u64)]))
449 )
450 );
451 }
452
453 #[test]
454 fn subquery_builder_projects_requested_field() {
455 let query = SelectQuery::new("OrderLine").filter(Expr::eq("name", "line-1"));
456 let expr = Expr::in_subquery("id", OrderLineRow::entity_descriptor(), query, "order_id");
457
458 let Expr::SubQuery {
459 left,
460 op,
461 entity,
462 query,
463 } = expr
464 else {
465 panic!("expected subquery expression");
466 };
467 assert_eq!(*left, Expr::column("id"));
468 assert_eq!(op, BinaryOp::In);
469 assert_eq!(entity.name, "OrderLine");
470 assert_eq!(query.projection, vec!["order_id"]);
471 }
472
473 #[test]
474 fn smart_list_supports_entity_ids_versions_and_records() {
475 let rows = SmartList::from(vec![
476 OrderRow {
477 id: 1,
478 version: 2,
479 name: String::from("a"),
480 },
481 OrderRow {
482 id: 3,
483 version: 4,
484 name: String::from("b"),
485 },
486 ]);
487
488 assert_eq!(rows.ids(), vec![Value::U64(1), Value::U64(3)]);
489 assert_eq!(rows.versions(), vec![2, 4]);
490
491 let records = rows.into_records();
492 assert_eq!(records.len(), 2);
493 assert_eq!(records.data[0].get("id"), Some(&Value::U64(1)));
494 assert_eq!(records.data[1].get("version"), Some(&Value::I64(4)));
495 }
496
497 #[test]
498 fn smart_list_supports_java_style_collection_helpers() {
499 let mut rows = SmartList::empty()
500 .with_total_count(10)
501 .with_aggregation("count", 2_u64)
502 .with_summary("label", "orders");
503 rows.push(OrderRow {
504 id: 1,
505 version: 2,
506 name: String::from("a"),
507 });
508 rows.extend(vec![OrderRow {
509 id: 3,
510 version: 4,
511 name: String::from("b"),
512 }]);
513
514 assert_eq!(rows.total_count_or_len(), 10);
515 assert_eq!(rows.get(1).map(|row| row.name.as_str()), Some("b"));
516 assert_eq!(rows.last().map(|row| row.id), Some(3));
517 assert_eq!(rows.aggregation("count"), Some(&Value::U64(2)));
518 assert_eq!(
519 rows.summary("label"),
520 Some(&Value::Text(String::from("orders")))
521 );
522 assert_eq!(rows.aggregation_json(), serde_json::json!({"count": 2}));
523 assert_eq!(rows.summary_json(), serde_json::json!({"label": "orders"}));
524
525 let names = rows.to_list(|row| row.name.clone());
526 assert_eq!(names, vec![String::from("a"), String::from("b")]);
527 let ids = rows.to_set(|row| row.id);
528 assert_eq!(ids.into_iter().collect::<Vec<_>>(), vec![1, 3]);
529
530 let by_id = rows.map_by_id();
531 assert_eq!(by_id.get("u:1").map(|row| row.name.as_str()), Some("a"));
532 assert_eq!(by_id.get("u:3").map(|row| row.name.as_str()), Some("b"));
533
534 let identity = rows.identity_map(|row| row.name.clone());
535 assert_eq!(identity.get("a").map(|row| row.id), Some(1));
536 let grouped = rows.group_by(|row| row.version % 2);
537 assert_eq!(grouped.get(&0).map(Vec::len), Some(2));
538
539 rows.merge_by(
540 vec![
541 OrderRow {
542 id: 3,
543 version: 5,
544 name: String::from("b2"),
545 },
546 OrderRow {
547 id: 4,
548 version: 1,
549 name: String::from("c"),
550 },
551 ],
552 |row| row.id,
553 );
554 assert_eq!(rows.len(), 3);
555 assert_eq!(rows.map_by_id().get("u:3").map(|row| row.version), Some(5));
556
557 rows.retain(|row| row.id != 1);
558 assert_eq!(rows.ids(), vec![Value::U64(3), Value::U64(4)]);
559 assert_eq!((&rows).into_iter().count(), 2);
560 assert_eq!(rows[0].name, "b2");
561 }
562
563 #[derive(Clone)]
564 struct SafeExpressionEntity {
565 base: BaseEntityData,
566 name: String,
567 lines: SmartList<OrderRow>,
568 }
569
570 impl TeaqlEntity for SafeExpressionEntity {
571 fn entity_descriptor() -> EntityDescriptor {
572 EntityDescriptor::new("SafeExpressionEntity")
573 }
574 }
575
576 impl Entity for SafeExpressionEntity {
577 fn from_record(_record: Record) -> Result<Self, EntityError> {
578 unimplemented!("test helper does not need record mapping")
579 }
580
581 fn into_record(self) -> Record {
582 Record::new()
583 }
584 }
585
586 impl BaseEntity for SafeExpressionEntity {
587 fn base(&self) -> &BaseEntityData {
588 &self.base
589 }
590
591 fn base_mut(&mut self) -> &mut BaseEntityData {
592 &mut self.base
593 }
594 }
595
596 #[test]
597 fn safe_expression_supports_null_safe_chaining_and_defaults() {
598 let entity = SafeExpressionEntity {
599 base: BaseEntityData::new().with_id(7).with_version(3),
600 name: "demo".to_owned(),
601 lines: SmartList::from(vec![OrderRow {
602 id: 11,
603 version: 1,
604 name: "line".to_owned(),
605 }]),
606 };
607
608 let expr = SafeExpression::value(entity);
609 assert_eq!(expr.clone().entity_id().eval(), Some(7));
610 assert_eq!(expr.clone().entity_version().eval(), Some(3));
611 assert_eq!(
612 expr.clone()
613 .apply(|entity| entity.name)
614 .or_else("x".to_owned()),
615 "demo"
616 );
617 assert_eq!(
618 expr.clone()
619 .apply(|entity| entity.lines)
620 .first()
621 .apply(|line| line.id)
622 .eval(),
623 Some(11)
624 );
625 assert!(
626 expr.clone()
627 .apply(|entity| entity.lines)
628 .get(4)
629 .apply(|line| line.id)
630 .is_null()
631 );
632 assert_eq!(
633 expr.clone().apply(|entity| entity.lines).size().or_else(0),
634 1
635 );
636 }
637
638 #[test]
639 fn safe_expression_exposes_java_style_empty_and_callbacks() {
640 let empty = SafeExpression::value(String::new());
641 assert!(empty.is_empty());
642 assert_eq!(empty.or_else("fallback".to_owned()), String::new());
643
644 let missing = SafeExpression::new((), |_| None::<String>);
645 assert!(missing.is_null());
646 assert_eq!(missing.or_else("fallback".to_owned()), "fallback");
647
648 let mut saw_null = false;
649 missing.when_is_null(|| {
650 saw_null = true;
651 });
652 assert!(saw_null);
653
654 let value = SafeExpression::value("teaql".to_owned());
655 let mut captured = String::new();
656 value.when_not_empty(|text| {
657 captured = text;
658 });
659 assert_eq!(captured, "teaql");
660 }
661
662 #[test]
663 fn web_style_and_action_bind_frontend_metadata() {
664 let mut base = BaseEntityData::new();
665 WebStyle::with_background_color("#ffeecc")
666 .font_color("#111111")
667 .bind_base(&mut base);
668 WebAction::view_web_action().bind_base(&mut base);
669 WebAction::modify_web_action("EDIT", "/orders/1/edit").bind_base(&mut base);
670
671 assert_eq!(
672 base.dynamic(STYLE_KEY)
673 .map(Value::to_json_value)
674 .and_then(|value| value.get("backgroundColor").cloned()),
675 Some(serde_json::json!("#ffeecc"))
676 );
677 assert_eq!(
678 base.dynamic(STYLE_KEY)
679 .map(Value::to_json_value)
680 .and_then(|value| value.get("color").cloned()),
681 Some(serde_json::json!("#111111"))
682 );
683
684 let actions_value = base
685 .dynamic(ACTION_LIST_KEY)
686 .map(Value::to_json_value)
687 .unwrap();
688 let actions = actions_value.as_array().unwrap();
689 assert_eq!(actions.len(), 2);
690 assert_eq!(actions[0]["execute"], serde_json::json!("switchview"));
691 assert_eq!(actions[0]["target"], serde_json::json!("detail"));
692 assert_eq!(actions[1]["name"], serde_json::json!("EDIT"));
693 assert_eq!(
694 actions[1]["requestURL"],
695 serde_json::json!("/orders/1/edit")
696 );
697 }
698
699 #[test]
700 fn web_response_wraps_entity_and_list_payloads() {
701 let entity = OrderRow {
702 id: 7,
703 version: 2,
704 name: "order".to_owned(),
705 };
706 let response = WebResponse::from_entity(&entity);
707 assert_eq!(response.result_code, 0);
708 assert_eq!(response.status.as_deref(), Some("YES"));
709 assert_eq!(response.record_count, 1);
710 assert_eq!(response.version, WEB_RESPONSE_VERSION);
711 assert_eq!(response.data[0]["id"], serde_json::json!(7));
712
713 let list = SmartList::from(vec![entity]).with_total_count(99);
714 let response = WebResponse::from_smart_list(list);
715 assert_eq!(response.record_count, 99);
716 assert_eq!(response.data.len(), 1);
717
718 let failed = WebResponse::fail("bad request").to_json_value();
719 assert_eq!(failed["status"], serde_json::json!("NO"));
720 assert_eq!(failed["message"], serde_json::json!("bad request"));
721 assert_eq!(failed["version"], serde_json::json!("1.001"));
722 }
723
724 #[test]
725 fn web_response_includes_facets() {
726 let entity = OrderRow {
727 id: 7,
728 version: 2,
729 name: "order".to_owned(),
730 };
731 let mut facet_record = Record::new();
732 facet_record.insert("status".to_owned(), Value::Text("PENDING".to_owned()));
733 facet_record.insert("count".to_owned(), Value::I64(5));
734
735 let facet_list = SmartList::from(vec![facet_record]);
736
737 let mut list = SmartList::from(vec![entity])
738 .with_total_count(99)
739 .with_facet("status", facet_list);
740
741 assert!(list.facets().contains_key("status"));
743 assert_eq!(list.facet("status").unwrap().len(), 1);
744 assert!(list.facet_mut("status").is_some());
745 assert!(list.facets_mut().contains_key("status"));
746
747 let response = WebResponse::from_smart_list(list);
748 assert_eq!(response.record_count, 99);
749 assert_eq!(response.data.len(), 1);
750
751 let json = response.to_json_value();
752 assert!(json.get("facets").is_some());
753 let facets_map = json["facets"].as_object().unwrap();
754 assert!(facets_map.contains_key("status"));
755 let status_facet = facets_map["status"].as_array().unwrap();
756 assert_eq!(status_facet.len(), 1);
757 assert_eq!(status_facet[0]["status"], serde_json::json!("PENDING"));
758 assert_eq!(status_facet[0]["count"], serde_json::json!(5));
759
760 let mut list2 = SmartList::new(vec![OrderRow {
762 id: 8,
763 version: 1,
764 name: "other".to_owned(),
765 }])
766 .with_facet("status", SmartList::empty());
767
768 let removed = list2.remove_facet("status");
769 assert!(removed.is_some());
770 assert!(list2.facet("status").is_none());
771
772 list2.add_facet("status", SmartList::empty());
773 let taken = list2.take_facets();
774 assert!(taken.contains_key("status"));
775 assert!(list2.facets().is_empty());
776 }
777
778 #[test]
779 fn xls_block_context_matches_java_navigation_model() {
780 let context = XlsBlockBuildContext::new("orders", 2, 3);
781 let header = context
782 .to_block("Order No")
783 .add_property("bold", true)
784 .span(2, 1);
785 let next = context.next().to_block("Amount");
786 let next_line = context.next_line().to_block("SO-1");
787 let new_line = context.new_line().to_block("reset-left");
788
789 assert_eq!(header.page, "orders");
790 assert_eq!(
791 (header.left, header.top, header.right, header.bottom),
792 (2, 3, 3, 3)
793 );
794 assert_eq!(header.width(), 2);
795 assert_eq!(header.height(), 1);
796 assert!(header.contains(3, 3));
797 assert!(!header.contains(4, 3));
798 assert_eq!((next.left, next.top), (3, 3));
799 assert_eq!((next_line.left, next_line.top), (2, 4));
800 assert_eq!((new_line.left, new_line.top), (0, 4));
801 assert_eq!(
802 header.properties.get("bold"),
803 Some(&serde_json::json!(true))
804 );
805 }
806
807 #[test]
808 fn xls_workbook_groups_pages_and_blocks_as_json_payload() {
809 let style = XlsBlock::new("orders", 0, 0, serde_json::Value::Null)
810 .add_property("backgroundColor", "#ffeecc");
811 let title = XlsBlock::new("orders", 0, 0, "Orders")
812 .style(style)
813 .span(3, 1);
814 let page = XlsPage::new("orders").add_block(title);
815 let workbook = XlsWorkbook::new().add_page(page);
816
817 assert!(workbook.page("orders").is_some());
818 assert_eq!(
819 workbook
820 .page("orders")
821 .and_then(|page| page.block_at(1, 0))
822 .map(|block| block.value.clone()),
823 Some(serde_json::json!("Orders"))
824 );
825
826 let json = workbook.to_json_value();
827 assert_eq!(json["pages"][0]["name"], serde_json::json!("orders"));
828 assert_eq!(json["pages"][0]["blocks"][0]["right"], serde_json::json!(2));
829 assert_eq!(
830 json["pages"][0]["blocks"][0]["styleReferBlock"]["properties"]["backgroundColor"],
831 serde_json::json!("#ffeecc")
832 );
833 }
834
835 #[test]
836 fn dynamic_properties_roundtrip_into_json() {
837 let aggregate = OrderAggregateRow::from_record(Record::from([
838 (String::from("id"), Value::U64(7)),
839 (String::from("lineCount"), Value::I64(3)),
840 (String::from("amount"), Value::F64(18.5)),
841 (
842 String::from("detail"),
843 Value::Object(Record::from([(String::from("status"), Value::from("ok"))])),
844 ),
845 ]))
846 .unwrap();
847
848 assert_eq!(aggregate.dynamic.get("lineCount"), Some(&Value::I64(3)));
849 assert_eq!(aggregate.dynamic.get("amount"), Some(&Value::F64(18.5)));
850 assert_eq!(
851 aggregate.dynamic.get("detail"),
852 Some(&Value::Object(Record::from([(
853 String::from("status"),
854 Value::Text(String::from("ok")),
855 )])))
856 );
857
858 let json = aggregate.into_json();
859 assert_eq!(json["id"], serde_json::json!(7));
860 assert_eq!(json["lineCount"], serde_json::json!(3));
861 assert_eq!(json["amount"], serde_json::json!(18.5));
862 assert_eq!(json["detail"], serde_json::json!({"status": "ok"}));
863 }
864
865 #[test]
866 fn base_entity_data_roundtrips_record_and_dynamic_properties() {
867 let mut base = BaseEntityData::new()
868 .with_id(11)
869 .with_version(3)
870 .with_dynamic("lineCount", 5)
871 .with_dynamic("detail", serde_json::json!({"status": "ok"}));
872 assert_eq!(base.dynamic("lineCount"), Some(&Value::I64(5)));
873 assert_eq!(base.dynamic_i64("lineCount"), Some(5));
874 base.put_dynamic("amount", 18.5);
875 assert_eq!(base.dynamic_f64("amount"), Some(18.5));
876
877 let record = base.to_record();
878 assert_eq!(record.get("id"), Some(&Value::U64(11)));
879 assert_eq!(record.get("version"), Some(&Value::I64(3)));
880 assert_eq!(record.get("lineCount"), Some(&Value::I64(5)));
881 assert_eq!(
882 record.get("detail"),
883 Some(&Value::Json(serde_json::json!({"status": "ok"})))
884 );
885
886 let restored = BaseEntityData::from_record(&record).unwrap();
887 assert_eq!(restored.id, 11);
888 assert_eq!(restored.version, 3);
889 assert_eq!(restored.dynamic("amount"), Some(&Value::F64(18.5)));
890 assert_eq!(restored.dynamic_f64("amount"), Some(18.5));
891 }
892}