Skip to main content

teaql_core/
web.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4use crate::{
5    BaseEntity, BaseEntityData, Entity, Record, SmartList, Value, compact_row_to_json_value,
6    record_to_json_value,
7};
8
9pub const STYLE_KEY: &str = "style";
10pub const ACTION_LIST_KEY: &str = "actionList";
11pub const WEB_RESPONSE_VERSION: &str = "1.001";
12
13#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct WebStyle {
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub background_color: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub color: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub class_names: Option<String>,
22}
23
24impl WebStyle {
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    pub fn with_background_color(color: impl Into<String>) -> Self {
30        Self::new().background_color(color)
31    }
32
33    pub fn with_font_color(color: impl Into<String>) -> Self {
34        Self::new().font_color(color)
35    }
36
37    pub fn with_class_names(class_names: impl Into<String>) -> Self {
38        Self::new().class_names(class_names)
39    }
40
41    pub fn background_color(mut self, color: impl Into<String>) -> Self {
42        self.background_color = Some(color.into());
43        self
44    }
45
46    pub fn font_color(mut self, color: impl Into<String>) -> Self {
47        self.color = Some(color.into());
48        self
49    }
50
51    pub fn class_names(mut self, class_names: impl Into<String>) -> Self {
52        self.class_names = Some(class_names.into());
53        self
54    }
55
56    pub fn to_json_value(&self) -> serde_json::Value {
57        serde_json::to_value(self).expect("WebStyle serialization cannot fail")
58    }
59
60    pub fn bind_base(&self, entity: &mut BaseEntityData) {
61        entity.put_dynamic(STYLE_KEY, self.to_json_value());
62    }
63
64    pub fn bind_entity<E>(&self, entity: &mut E)
65    where
66        E: BaseEntity,
67    {
68        entity.put_dynamic(STYLE_KEY, self.to_json_value());
69    }
70
71    pub fn bind_record(&self, record: &mut Record) {
72        record.insert(STYLE_KEY.to_owned(), Value::Json(self.to_json_value()));
73    }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct WebAction {
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub key: Option<String>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub name: Option<String>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub level: Option<String>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub execute: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub target: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub component: Option<String>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub warning_message: Option<String>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub role_for_list: Option<String>,
95    #[serde(rename = "requestURL", skip_serializing_if = "Option::is_none")]
96    pub request_url: Option<String>,
97}
98
99impl WebAction {
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    pub fn view_web_action() -> Self {
105        Self::new()
106            .name("VIEW DETAIL")
107            .level("view")
108            .execute("switchview")
109            .target("detail")
110    }
111
112    pub fn view_sub_list_action(
113        name: impl Into<String>,
114        list_view_name: impl Into<String>,
115        role_for_list: impl Into<String>,
116    ) -> Self {
117        Self::new()
118            .name(name)
119            .level("view")
120            .execute("gotoList")
121            .role_for_list(role_for_list)
122            .target(list_view_name)
123    }
124
125    pub fn simple_component_action(
126        name: impl Into<String>,
127        component_name: impl Into<String>,
128    ) -> Self {
129        Self::new().name(name).component(component_name)
130    }
131
132    pub fn modify_web_action(name: impl Into<String>, url: impl Into<String>) -> Self {
133        Self::modify_web_action_with_warning(name, url, None::<String>)
134    }
135
136    pub fn modify_web_action_with_warning(
137        name: impl Into<String>,
138        url: impl Into<String>,
139        warning_message: Option<impl Into<String>>,
140    ) -> Self {
141        let name = name.into();
142        Self::new()
143            .name(name.clone())
144            .key(name)
145            .level("modify")
146            .execute("switchview")
147            .target("modify")
148            .request_url(url)
149            .optional_warning_message(warning_message)
150    }
151
152    pub fn default_modify_web_action() -> Self {
153        Self::new()
154            .name("UPDATE")
155            .level("modify")
156            .execute("switchview")
157            .target("modify")
158    }
159
160    pub fn delete_web_action() -> Self {
161        Self::new()
162            .name("DELETE")
163            .level("delete")
164            .execute("switchview")
165            .target("deleteview")
166    }
167
168    pub fn delete_web_action_with_warning(
169        url: impl Into<String>,
170        warning_message: impl Into<String>,
171    ) -> Self {
172        Self::modify_web_action_with_warning("web.action.delete", url, Some(warning_message.into()))
173    }
174
175    pub fn audit_web_action(url: impl Into<String>, warning_message: impl Into<String>) -> Self {
176        Self::modify_web_action_with_warning("AUDIT", url, Some(warning_message.into()))
177    }
178
179    pub fn discard_web_action(url: impl Into<String>, warning_message: impl Into<String>) -> Self {
180        Self::modify_web_action_with_warning("DISCARD", url, Some(warning_message.into()))
181    }
182
183    pub fn goto_action(
184        name: impl Into<String>,
185        target: impl Into<String>,
186        url: impl Into<String>,
187    ) -> Self {
188        Self::new()
189            .name(name)
190            .level("modify")
191            .execute("gotoview")
192            .target(target)
193            .request_url(url)
194    }
195
196    pub fn switch_view_action(view_name: impl Into<String>, target: impl Into<String>) -> Self {
197        Self::new()
198            .name(view_name)
199            .level("modify")
200            .execute("switchview")
201            .target(target)
202    }
203
204    pub fn add_new_web_action(object_display_name: impl Into<String>) -> Self {
205        Self::new()
206            .name(format!("NEW {}", object_display_name.into()))
207            .level("modify")
208            .execute("switchview")
209            .target("addnew")
210    }
211
212    pub fn batch_upload_web_action() -> Self {
213        Self::new()
214            .name("BATCH UPLOAD")
215            .level("modify")
216            .execute("switchview")
217            .target("batchupload")
218    }
219
220    pub fn common_web_actions() -> Vec<Self> {
221        vec![Self::view_web_action(), Self::default_modify_web_action()]
222    }
223
224    pub fn key(mut self, key: impl Into<String>) -> Self {
225        self.key = Some(key.into());
226        self
227    }
228
229    pub fn name(mut self, name: impl Into<String>) -> Self {
230        self.name = Some(name.into());
231        self
232    }
233
234    pub fn level(mut self, level: impl Into<String>) -> Self {
235        self.level = Some(level.into());
236        self
237    }
238
239    pub fn execute(mut self, execute: impl Into<String>) -> Self {
240        self.execute = Some(execute.into());
241        self
242    }
243
244    pub fn target(mut self, target: impl Into<String>) -> Self {
245        self.target = Some(target.into());
246        self
247    }
248
249    pub fn component(mut self, component: impl Into<String>) -> Self {
250        self.component = Some(component.into());
251        self
252    }
253
254    pub fn warning_message(mut self, warning_message: impl Into<String>) -> Self {
255        self.warning_message = Some(warning_message.into());
256        self
257    }
258
259    pub fn optional_warning_message(mut self, warning_message: Option<impl Into<String>>) -> Self {
260        self.warning_message = warning_message.map(Into::into);
261        self
262    }
263
264    pub fn role_for_list(mut self, role_for_list: impl Into<String>) -> Self {
265        self.role_for_list = Some(role_for_list.into());
266        self
267    }
268
269    pub fn request_url(mut self, request_url: impl Into<String>) -> Self {
270        self.request_url = Some(request_url.into());
271        self
272    }
273
274    pub fn to_json_value(&self) -> serde_json::Value {
275        serde_json::to_value(self).expect("WebAction serialization cannot fail")
276    }
277
278    pub fn bind_base(&self, entity: &mut BaseEntityData) {
279        append_action(&mut entity.dynamic, self.to_json_value());
280    }
281
282    pub fn bind_entity<E>(&self, entity: &mut E)
283    where
284        E: BaseEntity,
285    {
286        append_action(&mut entity.base_mut().dynamic, self.to_json_value());
287    }
288
289    pub fn bind_record(&self, record: &mut Record) {
290        append_record_action(record, self.to_json_value());
291    }
292}
293
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct WebResponse {
297    pub data: Vec<serde_json::Value>,
298    pub result_code: i32,
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub status: Option<String>,
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub message: Option<String>,
303    pub record_count: u64,
304    pub version: String,
305    pub facets: BTreeMap<String, serde_json::Value>,
306}
307
308impl WebResponse {
309    pub fn success() -> Self {
310        Self {
311            data: Vec::new(),
312            result_code: 0,
313            status: Some("YES".to_owned()),
314            message: None,
315            record_count: 0,
316            version: WEB_RESPONSE_VERSION.to_owned(),
317            facets: BTreeMap::new(),
318        }
319    }
320
321    pub fn fail(message: impl Into<String>) -> Self {
322        Self {
323            data: Vec::new(),
324            result_code: 1,
325            status: Some("NO".to_owned()),
326            message: Some(message.into()),
327            record_count: 0,
328            version: WEB_RESPONSE_VERSION.to_owned(),
329            facets: BTreeMap::new(),
330        }
331    }
332
333    pub fn empty_list(message: impl Into<String>) -> Self {
334        Self {
335            data: Vec::new(),
336            result_code: 0,
337            status: None,
338            message: Some(message.into()),
339            record_count: 0,
340            version: WEB_RESPONSE_VERSION.to_owned(),
341            facets: BTreeMap::new(),
342        }
343    }
344
345    pub fn from_records(records: impl IntoIterator<Item = Record>) -> Self {
346        let data: Vec<_> = records
347            .into_iter()
348            .map(|record| record_to_json_value(&record))
349            .collect();
350        Self::success().with_data(data)
351    }
352
353    pub fn from_entity<E>(entity: &E) -> Self
354    where
355        E: Entity + Clone,
356    {
357        Self::from_records([entity.clone().into_values().into()])
358    }
359
360    pub fn from_entities<E>(entities: impl IntoIterator<Item = E>) -> Self
361    where
362        E: Entity,
363    {
364        Self::from_records(
365            entities
366                .into_iter()
367                .map(|entity| entity.into_values().into()),
368        )
369    }
370
371    pub fn from_smart_list<E>(mut smart_list: SmartList<E>) -> Self
372    where
373        E: Entity,
374    {
375        let total_count = smart_list.total_count_or_len();
376        let mut facets = BTreeMap::new();
377        for (key, facet_list) in smart_list.take_facets() {
378            let data: Vec<_> = facet_list
379                .data
380                .iter()
381                .map(compact_row_to_json_value)
382                .collect();
383            facets.insert(key, serde_json::Value::Array(data));
384        }
385        Self::from_entities(smart_list)
386            .with_record_count(total_count)
387            .with_facets(facets)
388    }
389
390    pub fn with_data(mut self, data: Vec<serde_json::Value>) -> Self {
391        self.record_count = data.len() as u64;
392        self.data = data;
393        self
394    }
395
396    pub fn with_record_count(mut self, record_count: u64) -> Self {
397        self.record_count = record_count;
398        self
399    }
400
401    pub fn with_facets(mut self, facets: BTreeMap<String, serde_json::Value>) -> Self {
402        self.facets = facets;
403        self
404    }
405
406    pub fn with_facets_option(
407        mut self,
408        facets: Option<BTreeMap<String, serde_json::Value>>,
409    ) -> Self {
410        self.facets = facets.unwrap_or_default();
411        self
412    }
413
414    pub fn push_json(mut self, value: impl Into<serde_json::Value>) -> Self {
415        self.data.push(value.into());
416        self.record_count = self.data.len() as u64;
417        self
418    }
419
420    pub fn to_json_value(&self) -> serde_json::Value {
421        serde_json::to_value(self).expect("WebResponse serialization cannot fail")
422    }
423}
424
425fn append_action(
426    dynamic: &mut std::collections::BTreeMap<String, Value>,
427    action: serde_json::Value,
428) {
429    match dynamic.get_mut(ACTION_LIST_KEY) {
430        Some(Value::Json(serde_json::Value::Array(actions))) => actions.push(action),
431        Some(existing) => {
432            let previous = std::mem::replace(existing, Value::Null);
433            *existing = Value::Json(serde_json::Value::Array(vec![
434                previous.to_json_value(),
435                action,
436            ]));
437        }
438        None => {
439            dynamic.insert(
440                ACTION_LIST_KEY.to_owned(),
441                Value::Json(serde_json::Value::Array(vec![action])),
442            );
443        }
444    }
445}
446
447fn append_record_action(record: &mut Record, action: serde_json::Value) {
448    match record.get_mut(ACTION_LIST_KEY) {
449        Some(Value::Json(serde_json::Value::Array(actions))) => actions.push(action),
450        Some(existing) => {
451            let previous = std::mem::replace(existing, Value::Null);
452            *existing = Value::Json(serde_json::Value::Array(vec![
453                previous.to_json_value(),
454                action,
455            ]));
456        }
457        None => {
458            record.insert(
459                ACTION_LIST_KEY.to_owned(),
460                Value::Json(serde_json::Value::Array(vec![action])),
461            );
462        }
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[test]
471    fn test_web_response_constructors_and_json_shape() {
472        let success_resp = WebResponse::success();
473        assert_eq!(success_resp.result_code, 0);
474        assert_eq!(success_resp.status.as_deref(), Some("YES"));
475        assert_eq!(success_resp.message, None);
476        assert_eq!(success_resp.version, WEB_RESPONSE_VERSION);
477        assert_eq!(success_resp.data.len(), 0);
478        assert_eq!(success_resp.record_count, 0);
479
480        let fail_resp = WebResponse::fail("Internal Error");
481        assert_eq!(fail_resp.result_code, 1);
482        assert_eq!(fail_resp.status.as_deref(), Some("NO"));
483        assert_eq!(fail_resp.message.as_deref(), Some("Internal Error"));
484        assert_eq!(fail_resp.data.len(), 0);
485        assert_eq!(fail_resp.record_count, 0);
486
487        let empty_list_resp = WebResponse::empty_list("No items found");
488        assert_eq!(empty_list_resp.result_code, 0);
489        assert_eq!(empty_list_resp.status, None);
490        assert_eq!(empty_list_resp.message.as_deref(), Some("No items found"));
491        assert_eq!(empty_list_resp.data.len(), 0);
492        assert_eq!(empty_list_resp.record_count, 0);
493
494        let json = success_resp.to_json_value();
495        assert!(json.is_object());
496        let obj = json.as_object().unwrap();
497        assert_eq!(obj.get("resultCode").unwrap().as_i64().unwrap(), 0);
498        assert_eq!(obj.get("status").unwrap().as_str().unwrap(), "YES");
499        assert!(!obj.contains_key("message")); // skip_serializing_if = "Option::is_none"
500        assert_eq!(
501            obj.get("version").unwrap().as_str().unwrap(),
502            WEB_RESPONSE_VERSION
503        );
504        assert_eq!(obj.get("recordCount").unwrap().as_i64().unwrap(), 0);
505        assert!(obj.get("data").unwrap().as_array().unwrap().is_empty());
506        assert!(obj.get("facets").unwrap().as_object().unwrap().is_empty());
507    }
508}