Skip to main content

teaql_core/
web.rs

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