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