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