Skip to main content

teaql_core/
lib.rs

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, NamedExpr, ObjectGroupBy, OrderBy,
35    PARTITION_RANK_PROPERTY, RawSqlProjection, Record, RelationAggregate, RelationLoad,
36    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        // Verify getter/mutator APIs on SmartList
750        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        // Test removing and taking facets
769        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;