use serde::{Deserialize, Deserializer, Serialize};
pub mod slot {
pub const HOME: i32 = 0;
pub const CONTENT: i32 = 100;
pub const MEDIA: i32 = 200;
pub const APPLICATION: i32 = 500;
pub const PEOPLE: i32 = 700;
pub const TOOLS: i32 = 800;
pub const SYSTEM: i32 = 900;
pub const LAST: i32 = i32::MAX;
pub const NAMES: [(&str, i32); 8] = [
("home", HOME),
("content", CONTENT),
("media", MEDIA),
("application", APPLICATION),
("people", PEOPLE),
("tools", TOOLS),
("system", SYSTEM),
("last", LAST),
];
pub fn parse(name: &str) -> Option<i32> {
NAMES
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(name.trim()))
.map(|(_, v)| *v)
}
pub fn name(value: i32) -> String {
NAMES
.iter()
.find(|(_, v)| *v == value)
.map(|(n, _)| n.to_string())
.unwrap_or_else(|| value.to_string())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
#[non_exhaustive]
pub struct Position {
#[serde(deserialize_with = "deserialize_slot")]
pub slot: i32,
pub order: i32,
pub after: Option<String>,
pub before: Option<String>,
}
impl Default for Position {
fn default() -> Self {
Self {
slot: slot::APPLICATION,
order: 0,
after: None,
before: None,
}
}
}
impl Position {
pub fn new() -> Self {
Self::default()
}
pub fn slot(mut self, slot: i32) -> Self {
self.slot = slot;
self
}
pub fn order(mut self, order: i32) -> Self {
self.order = order;
self
}
pub fn after(mut self, key: impl Into<String>) -> Self {
self.after = Some(key.into());
self
}
pub fn before(mut self, key: impl Into<String>) -> Self {
self.before = Some(key.into());
self
}
}
pub fn deserialize_slot<'de, D>(deserializer: D) -> Result<i32, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Number(i32),
Name(String),
}
match Raw::deserialize(deserializer)? {
Raw::Number(n) => Ok(n),
Raw::Name(name) => slot::parse(&name).ok_or_else(|| {
serde::de::Error::custom(format!(
"unknown slot {name:?}; one of {}",
slot::NAMES
.iter()
.map(|(n, _)| *n)
.collect::<Vec<_>>()
.join(", ")
))
}),
}
}
pub fn deserialize_slot_opt<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Number(i32),
Name(String),
}
match Option::<Raw>::deserialize(deserializer)? {
None => Ok(None),
Some(Raw::Number(n)) => Ok(Some(n)),
Some(Raw::Name(name)) => slot::parse(&name)
.map(Some)
.ok_or_else(|| serde::de::Error::custom(format!("unknown slot {name:?}"))),
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Move {
#[serde(rename = "in")]
pub hierarchy: String,
#[serde(deserialize_with = "deserialize_slot_opt")]
pub slot: Option<i32>,
pub order: Option<i32>,
pub category: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Policy {
Fixed,
#[default]
Adjustable,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Hierarchy {
pub key: String,
pub policy: Policy,
pub operator_adjustable: bool,
pub pinned_first: Vec<String>,
pub pinned_last: Vec<String>,
}
impl Hierarchy {
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
policy: Policy::Adjustable,
operator_adjustable: false,
pinned_first: Vec::new(),
pinned_last: Vec::new(),
}
}
pub fn policy(mut self, policy: Policy) -> Self {
self.policy = policy;
self
}
pub fn operator_adjustable(mut self) -> Self {
self.operator_adjustable = true;
self
}
pub fn pin_first(mut self, key: impl Into<String>) -> Self {
self.pinned_first.push(key.into());
self
}
pub fn pin_last(mut self, key: impl Into<String>) -> Self {
self.pinned_last.push(key.into());
self
}
fn pinned(&self, key: &str) -> bool {
self.pinned_first.iter().any(|k| k == key) || self.pinned_last.iter().any(|k| k == key)
}
}
pub trait Arranged {
fn key(&self) -> &str;
fn position(&self) -> &Position;
fn label(&self) -> &str;
}
#[derive(Clone, Debug, Default)]
pub struct Overrides<'a> {
pub deployment: Option<&'a [String]>,
pub operator: Option<&'a [String]>,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ArrangeError {
#[error("`{key}` sits {relation} `{anchor}`, which is no sibling")]
UnknownAnchor {
key: String,
relation: &'static str,
anchor: String,
},
}
pub fn arrange<T: Arranged>(
hierarchy: &Hierarchy,
mut items: Vec<T>,
overrides: Overrides<'_>,
) -> Result<Vec<T>, ArrangeError> {
items.sort_by(|a, b| {
let (pa, pb) = (a.position(), b.position());
pa.slot
.cmp(&pb.slot)
.then_with(|| pa.order.cmp(&pb.order))
.then_with(|| a.label().cmp(b.label()))
});
anchor(&mut items)?;
if hierarchy.policy == Policy::Adjustable {
if let Some(sequence) = overrides.deployment {
reorder(hierarchy, &mut items, sequence);
}
if hierarchy.operator_adjustable {
if let Some(sequence) = overrides.operator {
reorder(hierarchy, &mut items, sequence);
}
}
} else if overrides.deployment.is_some() || overrides.operator.is_some() {
tracing::info!(hierarchy = %hierarchy.key, "arrangement is fixed; an override was ignored");
}
pin(hierarchy, &mut items);
Ok(items)
}
fn anchor<T: Arranged>(items: &mut Vec<T>) -> Result<(), ArrangeError> {
let keys: Vec<String> = items.iter().map(|i| i.key().to_string()).collect();
for key in keys {
let Some(at) = items.iter().position(|i| i.key() == key) else {
continue;
};
let (anchor_key, after) = match (&items[at].position().after, &items[at].position().before)
{
(Some(a), _) => (a.clone(), true),
(None, Some(b)) => (b.clone(), false),
(None, None) => continue,
};
let Some(target) = items.iter().position(|i| i.key() == anchor_key) else {
return Err(ArrangeError::UnknownAnchor {
key,
relation: if after { "after" } else { "before" },
anchor: anchor_key,
});
};
let moved = items.remove(at);
let target = items
.iter()
.position(|i| i.key() == anchor_key)
.unwrap_or(target);
let to = if after { target + 1 } else { target };
items.insert(to.min(items.len()), moved);
}
Ok(())
}
pub fn reorder<T: Arranged>(hierarchy: &Hierarchy, items: &mut Vec<T>, sequence: &[String]) {
let mut rest: Vec<T> = std::mem::take(items);
let mut ordered: Vec<T> = Vec::with_capacity(rest.len());
for key in sequence {
if hierarchy.pinned(key) {
continue;
}
if let Some(at) = rest.iter().position(|i| i.key() == key) {
ordered.push(rest.remove(at));
}
}
ordered.extend(rest);
*items = ordered;
}
fn pin<T: Arranged>(hierarchy: &Hierarchy, items: &mut Vec<T>) {
let mut rest: Vec<T> = std::mem::take(items);
let mut first: Vec<T> = Vec::new();
for key in &hierarchy.pinned_first {
if let Some(at) = rest.iter().position(|i| i.key() == key) {
first.push(rest.remove(at));
}
}
let mut last: Vec<T> = Vec::new();
for key in &hierarchy.pinned_last {
if let Some(at) = rest.iter().position(|i| i.key() == key) {
last.push(rest.remove(at));
}
}
first.extend(rest);
first.extend(last);
*items = first;
}
pub fn parse_sequence(text: &str) -> Vec<String> {
text.split(',')
.map(str::trim)
.filter(|k| !k.is_empty())
.map(str::to_string)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
struct Entry {
key: &'static str,
label: &'static str,
position: Position,
}
impl Arranged for Entry {
fn key(&self) -> &str {
self.key
}
fn position(&self) -> &Position {
&self.position
}
fn label(&self) -> &str {
self.label
}
}
fn entry(key: &'static str, position: Position) -> Entry {
Entry {
key,
label: key,
position,
}
}
fn keys(items: &[Entry]) -> Vec<&str> {
items.iter().map(|e| e.key).collect()
}
fn menu() -> Hierarchy {
Hierarchy::new("backend.nav")
.pin_first("dashboard")
.pin_last("settings")
.operator_adjustable()
}
#[test]
fn slots_order_the_bands_and_names_parse() {
let items = vec![
entry("acme.shop", Position::new()),
entry("settings", Position::new().slot(slot::SYSTEM)),
entry("media", Position::new().slot(slot::MEDIA)),
entry("dashboard", Position::new().slot(slot::HOME)),
entry("acme.blog", Position::new().order(-1)),
];
let out = arrange(&menu(), items, Overrides::default()).unwrap();
assert_eq!(
keys(&out),
["dashboard", "media", "acme.blog", "acme.shop", "settings"]
);
assert_eq!(slot::parse("Media"), Some(slot::MEDIA));
assert_eq!(slot::parse("nope"), None);
assert_eq!(slot::name(slot::TOOLS), "tools");
let p: Position = serde_json::from_str(r#"{"slot":"people","order":2}"#).unwrap();
assert_eq!((p.slot, p.order), (slot::PEOPLE, 2));
let p: Position = serde_json::from_str(r#"{"slot":150}"#).unwrap();
assert_eq!(p.slot, 150);
assert!(serde_json::from_str::<Position>(r#"{"slot":"attic"}"#).is_err());
}
#[test]
fn an_anchor_holds_across_bands_and_a_missing_one_is_refused() {
let items = vec![
entry("media", Position::new().slot(slot::MEDIA)),
entry("acme.shop", Position::new()),
entry(
"acme.reports",
Position::new().slot(slot::TOOLS).before("media"),
),
];
let out = arrange(&Hierarchy::new("h"), items, Overrides::default()).unwrap();
assert_eq!(keys(&out), ["acme.reports", "media", "acme.shop"]);
let items = vec![entry("a", Position::new().after("ghost"))];
assert_eq!(
arrange(&Hierarchy::new("h"), items, Overrides::default()).unwrap_err(),
ArrangeError::UnknownAnchor {
key: "a".into(),
relation: "after",
anchor: "ghost".into()
}
);
}
#[test]
fn overrides_rearrange_an_adjustable_hierarchy_and_never_the_pins() {
let items = || {
vec![
entry("dashboard", Position::new().slot(slot::HOME)),
entry("media", Position::new().slot(slot::MEDIA)),
entry("acme.shop", Position::new()),
entry("acme.blog", Position::new()),
entry("settings", Position::new().slot(slot::SYSTEM)),
]
};
let deployment = parse_sequence("acme.shop, media");
let operator = parse_sequence("settings,acme.blog,gone,dashboard");
let out = arrange(
&menu(),
items(),
Overrides {
deployment: Some(&deployment),
operator: None,
},
)
.unwrap();
assert_eq!(
keys(&out),
["dashboard", "acme.shop", "media", "acme.blog", "settings"]
);
let out = arrange(
&menu(),
items(),
Overrides {
deployment: Some(&deployment),
operator: Some(&operator),
},
)
.unwrap();
assert_eq!(
keys(&out),
["dashboard", "acme.blog", "acme.shop", "media", "settings"],
"the operator's order wins, the ends stay"
);
let fixed = Hierarchy::new("h").policy(Policy::Fixed);
let out = arrange(
&fixed,
items(),
Overrides {
deployment: Some(&deployment),
operator: Some(&operator),
},
)
.unwrap();
assert_eq!(
keys(&out),
["dashboard", "media", "acme.blog", "acme.shop", "settings"],
"a fixed hierarchy ignores every override"
);
let out = arrange(
&Hierarchy::new("h"),
items(),
Overrides {
deployment: None,
operator: Some(&operator),
},
)
.unwrap();
assert_eq!(
keys(&out),
["dashboard", "media", "acme.blog", "acme.shop", "settings"],
"an operator's order needs the hierarchy to allow it"
);
}
}