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