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")); 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}