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