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