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