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, 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        // Verify getter/mutator APIs on SmartList
751        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        // Test removing and taking facets
770        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;