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