1use serde::{Deserialize, Deserializer, Serialize};
11
12pub mod slot {
17 pub const HOME: i32 = 0;
19 pub const CONTENT: i32 = 100;
21 pub const MEDIA: i32 = 200;
23 pub const APPLICATION: i32 = 500;
25 pub const PEOPLE: i32 = 700;
27 pub const TOOLS: i32 = 800;
29 pub const SYSTEM: i32 = 900;
31 pub const LAST: i32 = i32::MAX;
33
34 pub const NAMES: [(&str, i32); 8] = [
36 ("home", HOME),
37 ("content", CONTENT),
38 ("media", MEDIA),
39 ("application", APPLICATION),
40 ("people", PEOPLE),
41 ("tools", TOOLS),
42 ("system", SYSTEM),
43 ("last", LAST),
44 ];
45
46 pub fn parse(name: &str) -> Option<i32> {
48 NAMES
49 .iter()
50 .find(|(n, _)| n.eq_ignore_ascii_case(name.trim()))
51 .map(|(_, v)| *v)
52 }
53
54 pub fn name(value: i32) -> String {
56 NAMES
57 .iter()
58 .find(|(_, v)| *v == value)
59 .map(|(n, _)| n.to_string())
60 .unwrap_or_else(|| value.to_string())
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(default, deny_unknown_fields)]
67#[non_exhaustive]
68pub struct Position {
69 #[serde(deserialize_with = "deserialize_slot")]
71 pub slot: i32,
72 pub order: i32,
74 pub after: Option<String>,
76 pub before: Option<String>,
78}
79
80impl Default for Position {
81 fn default() -> Self {
82 Self {
83 slot: slot::APPLICATION,
84 order: 0,
85 after: None,
86 before: None,
87 }
88 }
89}
90
91impl Position {
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 pub fn slot(mut self, slot: i32) -> Self {
97 self.slot = slot;
98 self
99 }
100
101 pub fn order(mut self, order: i32) -> Self {
102 self.order = order;
103 self
104 }
105
106 pub fn after(mut self, key: impl Into<String>) -> Self {
107 self.after = Some(key.into());
108 self
109 }
110
111 pub fn before(mut self, key: impl Into<String>) -> Self {
112 self.before = Some(key.into());
113 self
114 }
115}
116
117pub fn deserialize_slot<'de, D>(deserializer: D) -> Result<i32, D::Error>
119where
120 D: Deserializer<'de>,
121{
122 #[derive(Deserialize)]
123 #[serde(untagged)]
124 enum Raw {
125 Number(i32),
126 Name(String),
127 }
128 match Raw::deserialize(deserializer)? {
129 Raw::Number(n) => Ok(n),
130 Raw::Name(name) => slot::parse(&name).ok_or_else(|| {
131 serde::de::Error::custom(format!(
132 "unknown slot {name:?}; one of {}",
133 slot::NAMES
134 .iter()
135 .map(|(n, _)| *n)
136 .collect::<Vec<_>>()
137 .join(", ")
138 ))
139 }),
140 }
141}
142
143pub fn deserialize_slot_opt<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
145where
146 D: Deserializer<'de>,
147{
148 #[derive(Deserialize)]
149 #[serde(untagged)]
150 enum Raw {
151 Number(i32),
152 Name(String),
153 }
154 match Option::<Raw>::deserialize(deserializer)? {
155 None => Ok(None),
156 Some(Raw::Number(n)) => Ok(Some(n)),
157 Some(Raw::Name(name)) => slot::parse(&name)
158 .map(Some)
159 .ok_or_else(|| serde::de::Error::custom(format!("unknown slot {name:?}"))),
160 }
161}
162
163#[derive(Debug, Clone, Default, Deserialize)]
173#[serde(deny_unknown_fields, default)]
174#[non_exhaustive]
175pub struct Move {
176 #[serde(rename = "in")]
178 pub hierarchy: String,
179 #[serde(deserialize_with = "deserialize_slot_opt")]
180 pub slot: Option<i32>,
181 pub order: Option<i32>,
182 pub category: Option<String>,
184}
185
186#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
189pub enum Policy {
190 Fixed,
192 #[default]
195 Adjustable,
196}
197
198#[derive(Clone, Debug)]
201#[non_exhaustive]
202pub struct Hierarchy {
203 pub key: String,
205 pub policy: Policy,
206 pub operator_adjustable: bool,
209 pub pinned_first: Vec<String>,
211 pub pinned_last: Vec<String>,
213}
214
215impl Hierarchy {
216 pub fn new(key: impl Into<String>) -> Self {
217 Self {
218 key: key.into(),
219 policy: Policy::Adjustable,
220 operator_adjustable: false,
221 pinned_first: Vec::new(),
222 pinned_last: Vec::new(),
223 }
224 }
225
226 pub fn policy(mut self, policy: Policy) -> Self {
227 self.policy = policy;
228 self
229 }
230
231 pub fn operator_adjustable(mut self) -> Self {
233 self.operator_adjustable = true;
234 self
235 }
236
237 pub fn pin_first(mut self, key: impl Into<String>) -> Self {
238 self.pinned_first.push(key.into());
239 self
240 }
241
242 pub fn pin_last(mut self, key: impl Into<String>) -> Self {
243 self.pinned_last.push(key.into());
244 self
245 }
246
247 fn pinned(&self, key: &str) -> bool {
248 self.pinned_first.iter().any(|k| k == key) || self.pinned_last.iter().any(|k| k == key)
249 }
250}
251
252pub trait Arranged {
254 fn key(&self) -> &str;
256 fn position(&self) -> &Position;
257 fn label(&self) -> &str;
259}
260
261#[derive(Clone, Debug, Default)]
266pub struct Overrides<'a> {
267 pub deployment: Option<&'a [String]>,
268 pub operator: Option<&'a [String]>,
269}
270
271#[derive(Debug, thiserror::Error, PartialEq, Eq)]
272pub enum ArrangeError {
273 #[error("`{key}` sits {relation} `{anchor}`, which is no sibling")]
274 UnknownAnchor {
275 key: String,
276 relation: &'static str,
277 anchor: String,
278 },
279}
280
281pub fn arrange<T: Arranged>(
285 hierarchy: &Hierarchy,
286 mut items: Vec<T>,
287 overrides: Overrides<'_>,
288) -> Result<Vec<T>, ArrangeError> {
289 items.sort_by(|a, b| {
290 let (pa, pb) = (a.position(), b.position());
291 pa.slot
292 .cmp(&pb.slot)
293 .then_with(|| pa.order.cmp(&pb.order))
294 .then_with(|| a.label().cmp(b.label()))
295 });
296 anchor(&mut items)?;
297 if hierarchy.policy == Policy::Adjustable {
298 if let Some(sequence) = overrides.deployment {
299 reorder(hierarchy, &mut items, sequence);
300 }
301 if hierarchy.operator_adjustable {
302 if let Some(sequence) = overrides.operator {
303 reorder(hierarchy, &mut items, sequence);
304 }
305 }
306 } else if overrides.deployment.is_some() || overrides.operator.is_some() {
307 tracing::info!(hierarchy = %hierarchy.key, "arrangement is fixed; an override was ignored");
308 }
309 pin(hierarchy, &mut items);
310 Ok(items)
311}
312
313fn anchor<T: Arranged>(items: &mut Vec<T>) -> Result<(), ArrangeError> {
317 let keys: Vec<String> = items.iter().map(|i| i.key().to_string()).collect();
318 for key in keys {
319 let Some(at) = items.iter().position(|i| i.key() == key) else {
320 continue;
321 };
322 let (anchor_key, after) = match (&items[at].position().after, &items[at].position().before)
323 {
324 (Some(a), _) => (a.clone(), true),
325 (None, Some(b)) => (b.clone(), false),
326 (None, None) => continue,
327 };
328 let Some(target) = items.iter().position(|i| i.key() == anchor_key) else {
329 return Err(ArrangeError::UnknownAnchor {
330 key,
331 relation: if after { "after" } else { "before" },
332 anchor: anchor_key,
333 });
334 };
335 let moved = items.remove(at);
336 let target = items
337 .iter()
338 .position(|i| i.key() == anchor_key)
339 .unwrap_or(target);
340 let to = if after { target + 1 } else { target };
341 items.insert(to.min(items.len()), moved);
342 }
343 Ok(())
344}
345
346pub fn reorder<T: Arranged>(hierarchy: &Hierarchy, items: &mut Vec<T>, sequence: &[String]) {
349 let mut rest: Vec<T> = std::mem::take(items);
350 let mut ordered: Vec<T> = Vec::with_capacity(rest.len());
351 for key in sequence {
352 if hierarchy.pinned(key) {
353 continue;
354 }
355 if let Some(at) = rest.iter().position(|i| i.key() == key) {
356 ordered.push(rest.remove(at));
357 }
358 }
359 ordered.extend(rest);
360 *items = ordered;
361}
362
363fn pin<T: Arranged>(hierarchy: &Hierarchy, items: &mut Vec<T>) {
365 let mut rest: Vec<T> = std::mem::take(items);
366 let mut first: Vec<T> = Vec::new();
367 for key in &hierarchy.pinned_first {
368 if let Some(at) = rest.iter().position(|i| i.key() == key) {
369 first.push(rest.remove(at));
370 }
371 }
372 let mut last: Vec<T> = Vec::new();
373 for key in &hierarchy.pinned_last {
374 if let Some(at) = rest.iter().position(|i| i.key() == key) {
375 last.push(rest.remove(at));
376 }
377 }
378 first.extend(rest);
379 first.extend(last);
380 *items = first;
381}
382
383pub fn parse_sequence(text: &str) -> Vec<String> {
386 text.split(',')
387 .map(str::trim)
388 .filter(|k| !k.is_empty())
389 .map(str::to_string)
390 .collect()
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[derive(Debug, PartialEq)]
398 struct Entry {
399 key: &'static str,
400 label: &'static str,
401 position: Position,
402 }
403
404 impl Arranged for Entry {
405 fn key(&self) -> &str {
406 self.key
407 }
408 fn position(&self) -> &Position {
409 &self.position
410 }
411 fn label(&self) -> &str {
412 self.label
413 }
414 }
415
416 fn entry(key: &'static str, position: Position) -> Entry {
417 Entry {
418 key,
419 label: key,
420 position,
421 }
422 }
423
424 fn keys(items: &[Entry]) -> Vec<&str> {
425 items.iter().map(|e| e.key).collect()
426 }
427
428 fn menu() -> Hierarchy {
429 Hierarchy::new("backend.nav")
430 .pin_first("dashboard")
431 .pin_last("settings")
432 .operator_adjustable()
433 }
434
435 #[test]
436 fn slots_order_the_bands_and_names_parse() {
437 let items = vec![
438 entry("acme.shop", Position::new()),
439 entry("settings", Position::new().slot(slot::SYSTEM)),
440 entry("media", Position::new().slot(slot::MEDIA)),
441 entry("dashboard", Position::new().slot(slot::HOME)),
442 entry("acme.blog", Position::new().order(-1)),
443 ];
444 let out = arrange(&menu(), items, Overrides::default()).unwrap();
445 assert_eq!(
446 keys(&out),
447 ["dashboard", "media", "acme.blog", "acme.shop", "settings"]
448 );
449 assert_eq!(slot::parse("Media"), Some(slot::MEDIA));
450 assert_eq!(slot::parse("nope"), None);
451 assert_eq!(slot::name(slot::TOOLS), "tools");
452 let p: Position = serde_json::from_str(r#"{"slot":"people","order":2}"#).unwrap();
453 assert_eq!((p.slot, p.order), (slot::PEOPLE, 2));
454 let p: Position = serde_json::from_str(r#"{"slot":150}"#).unwrap();
455 assert_eq!(p.slot, 150);
456 assert!(serde_json::from_str::<Position>(r#"{"slot":"attic"}"#).is_err());
457 }
458
459 #[test]
460 fn an_anchor_holds_across_bands_and_a_missing_one_is_refused() {
461 let items = vec![
462 entry("media", Position::new().slot(slot::MEDIA)),
463 entry("acme.shop", Position::new()),
464 entry(
465 "acme.reports",
466 Position::new().slot(slot::TOOLS).before("media"),
467 ),
468 ];
469 let out = arrange(&Hierarchy::new("h"), items, Overrides::default()).unwrap();
470 assert_eq!(keys(&out), ["acme.reports", "media", "acme.shop"]);
471 let items = vec![entry("a", Position::new().after("ghost"))];
472 assert_eq!(
473 arrange(&Hierarchy::new("h"), items, Overrides::default()).unwrap_err(),
474 ArrangeError::UnknownAnchor {
475 key: "a".into(),
476 relation: "after",
477 anchor: "ghost".into()
478 }
479 );
480 }
481
482 #[test]
483 fn overrides_rearrange_an_adjustable_hierarchy_and_never_the_pins() {
484 let items = || {
485 vec![
486 entry("dashboard", Position::new().slot(slot::HOME)),
487 entry("media", Position::new().slot(slot::MEDIA)),
488 entry("acme.shop", Position::new()),
489 entry("acme.blog", Position::new()),
490 entry("settings", Position::new().slot(slot::SYSTEM)),
491 ]
492 };
493 let deployment = parse_sequence("acme.shop, media");
494 let operator = parse_sequence("settings,acme.blog,gone,dashboard");
495 let out = arrange(
496 &menu(),
497 items(),
498 Overrides {
499 deployment: Some(&deployment),
500 operator: None,
501 },
502 )
503 .unwrap();
504 assert_eq!(
505 keys(&out),
506 ["dashboard", "acme.shop", "media", "acme.blog", "settings"]
507 );
508 let out = arrange(
509 &menu(),
510 items(),
511 Overrides {
512 deployment: Some(&deployment),
513 operator: Some(&operator),
514 },
515 )
516 .unwrap();
517 assert_eq!(
518 keys(&out),
519 ["dashboard", "acme.blog", "acme.shop", "media", "settings"],
520 "the operator's order wins, the ends stay"
521 );
522 let fixed = Hierarchy::new("h").policy(Policy::Fixed);
523 let out = arrange(
524 &fixed,
525 items(),
526 Overrides {
527 deployment: Some(&deployment),
528 operator: Some(&operator),
529 },
530 )
531 .unwrap();
532 assert_eq!(
533 keys(&out),
534 ["dashboard", "media", "acme.blog", "acme.shop", "settings"],
535 "a fixed hierarchy ignores every override"
536 );
537 let out = arrange(
538 &Hierarchy::new("h"),
539 items(),
540 Overrides {
541 deployment: None,
542 operator: Some(&operator),
543 },
544 )
545 .unwrap();
546 assert_eq!(
547 keys(&out),
548 ["dashboard", "media", "acme.blog", "acme.shop", "settings"],
549 "an operator's order needs the hierarchy to allow it"
550 );
551 }
552}