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