rustio_admin/view_layer/
spec.rs1use serde::{Deserialize, Serialize};
8
9use super::compose::CellComposition;
10use super::modes::ViewMode;
11use super::roles::{FieldRole, SemanticClass};
12
13pub const VIEW_SPEC_VERSION: u32 = 1;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct FieldViewSpec {
23 pub field_name: String,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub label: Option<String>,
29 pub role: FieldRole,
31 #[serde(default)]
33 pub priority: i32,
34 #[serde(default)]
36 pub sortable: bool,
37 #[serde(default)]
39 pub filterable: bool,
40 #[serde(default)]
43 pub default_filter: bool,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub width: Option<String>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub semantic_class: Option<SemanticClass>,
50}
51
52impl FieldViewSpec {
53 pub fn new(field_name: impl Into<String>, role: FieldRole) -> Self {
56 FieldViewSpec {
57 field_name: field_name.into(),
58 label: None,
59 role,
60 priority: 0,
61 sortable: false,
62 filterable: false,
63 default_filter: false,
64 width: None,
65 semantic_class: None,
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct ViewSpec {
76 pub model: String,
78 pub default_mode: ViewMode,
80 pub allowed_modes: Vec<ViewMode>,
82 pub fields: Vec<FieldViewSpec>,
84 #[serde(default)]
86 pub compositions: Vec<CellComposition>,
87 #[serde(default)]
90 pub default_filters: Vec<String>,
91 #[serde(default = "default_version")]
93 pub version: u32,
94}
95
96fn default_version() -> u32 {
97 VIEW_SPEC_VERSION
98}
99
100impl ViewSpec {
101 pub fn list_fields(&self) -> Vec<&FieldViewSpec> {
105 let mut visible: Vec<&FieldViewSpec> = self
106 .fields
107 .iter()
108 .filter(|f| f.role.shows_in_list())
109 .collect();
110 visible.sort_by_key(|f| f.priority);
112 visible
113 }
114
115 pub fn primary_field(&self) -> Option<&FieldViewSpec> {
118 self.fields.iter().find(|f| f.role == FieldRole::Primary)
119 }
120
121 pub fn redacted_fields(&self) -> Vec<&str> {
125 self.fields
126 .iter()
127 .filter(|f| !f.role.reaches_template())
128 .map(|f| f.field_name.as_str())
129 .collect()
130 }
131
132 pub fn resolve_mode(&self, requested: Option<&str>) -> ViewMode {
136 let wanted = requested.and_then(ViewMode::from_slug);
137 match wanted {
138 Some(mode) if self.allowed_modes.contains(&mode) => mode,
139 _ => self.default_mode,
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn sample_spec() -> ViewSpec {
149 ViewSpec {
150 model: "customer".into(),
151 default_mode: ViewMode::List,
152 allowed_modes: vec![ViewMode::List, ViewMode::Table, ViewMode::Cards],
153 fields: vec![
154 FieldViewSpec::new("full_name", FieldRole::Primary),
155 {
156 let mut f = FieldViewSpec::new("status", FieldRole::Badge);
157 f.semantic_class = Some(SemanticClass::Success);
158 f.filterable = true;
159 f.default_filter = true;
160 f
161 },
162 FieldViewSpec::new("password_hash", FieldRole::Hidden),
163 ],
164 compositions: vec![],
165 default_filters: vec!["status".into()],
166 version: VIEW_SPEC_VERSION,
167 }
168 }
169
170 #[test]
171 fn roundtrips_through_json() {
172 let spec = sample_spec();
173 let json = serde_json::to_string(&spec).unwrap();
174 let back: ViewSpec = serde_json::from_str(&json).unwrap();
175 assert_eq!(spec, back);
176 }
177
178 #[test]
179 fn enums_serialize_as_snake_case() {
180 let json = serde_json::to_string(&sample_spec()).unwrap();
181 assert!(json.contains("\"primary\""));
182 assert!(json.contains("\"badge\""));
183 assert!(json.contains("\"list\""));
184 assert!(json.contains("\"success\""));
185 }
186
187 #[test]
188 fn version_defaults_when_missing() {
189 let json = r#"{
191 "model": "thing",
192 "default_mode": "table",
193 "allowed_modes": ["table"],
194 "fields": []
195 }"#;
196 let spec: ViewSpec = serde_json::from_str(json).unwrap();
197 assert_eq!(spec.version, VIEW_SPEC_VERSION);
198 }
199
200 #[test]
201 fn list_fields_skip_hidden_and_sort_by_priority() {
202 let mut spec = sample_spec();
203 spec.fields[0].priority = 10;
204 spec.fields[1].priority = 1;
205 let listed: Vec<&str> = spec
206 .list_fields()
207 .iter()
208 .map(|f| f.field_name.as_str())
209 .collect();
210 assert_eq!(listed, vec!["status", "full_name"]);
211 assert!(!listed.contains(&"password_hash"));
212 }
213
214 #[test]
215 fn resolve_mode_rejects_disallowed() {
216 let spec = sample_spec();
217 assert_eq!(spec.resolve_mode(Some("compact")), ViewMode::List);
219 assert_eq!(spec.resolve_mode(Some("cards")), ViewMode::Cards);
220 assert_eq!(spec.resolve_mode(None), ViewMode::List);
221 }
222}