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