Skip to main content

omena_cascade/
origin.rs

1//! Cascade-origin inputs and their mapping onto the existing priority ladder.
2
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeSet;
5use std::sync::OnceLock;
6
7use crate::CascadeLevel;
8
9#[derive(
10    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize,
11)]
12#[serde(rename_all = "camelCase")]
13pub enum CascadeOriginV0 {
14    UserAgent,
15    User,
16    #[default]
17    Author,
18    Inline,
19}
20
21impl CascadeOriginV0 {
22    pub const fn is_author(&self) -> bool {
23        matches!(self, Self::Author)
24    }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct CascadeOriginDriverV0 {
30    pub origin: CascadeOriginV0,
31    pub important: bool,
32    pub level: CascadeLevel,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
36#[serde(rename_all = "camelCase")]
37pub enum CascadeWinnerAxisV0 {
38    CascadeLevel,
39    LayerRank,
40    ScopeProximity,
41    Specificity,
42    SourceOrder,
43}
44
45#[derive(Debug, Deserialize)]
46#[serde(rename_all = "camelCase")]
47struct CascadeDriverCensusV0 {
48    schema_version: String,
49    product: String,
50    levels: Vec<CascadeDriverLevelV0>,
51    winner_axes: Vec<CascadeDriverAxisV0>,
52}
53
54#[derive(Debug, Deserialize)]
55#[serde(rename_all = "camelCase")]
56struct CascadeDriverLevelV0 {
57    level: String,
58    status: String,
59    driver_inputs: Vec<String>,
60    #[serde(default)]
61    follow_up: Option<String>,
62}
63
64#[derive(Debug, Deserialize)]
65#[serde(rename_all = "camelCase")]
66struct CascadeDriverAxisV0 {
67    axis: CascadeWinnerAxisV0,
68    status: String,
69}
70
71const CASCADE_DRIVER_CENSUS_JSON: &str = include_str!("../data/cascade-driver-census.json");
72static CASCADE_DRIVER_CENSUS: OnceLock<Result<CascadeDriverCensusV0, String>> = OnceLock::new();
73
74fn cascade_driver_census_v0() -> Option<&'static CascadeDriverCensusV0> {
75    CASCADE_DRIVER_CENSUS
76        .get_or_init(|| {
77            serde_json::from_str(CASCADE_DRIVER_CENSUS_JSON).map_err(|error| error.to_string())
78        })
79        .as_ref()
80        .ok()
81}
82
83pub fn cascade_driven_levels_v0() -> Vec<CascadeLevel> {
84    cascade_driver_census_v0()
85        .into_iter()
86        .flat_map(|census| census.levels.iter())
87        .filter(|entry| entry.status == "driven")
88        .filter_map(|entry| cascade_level_from_name_v0(entry.level.as_str()))
89        .collect()
90}
91
92pub fn cascade_driven_winner_axes_v0() -> Vec<CascadeWinnerAxisV0> {
93    cascade_driver_census_v0()
94        .into_iter()
95        .flat_map(|census| census.winner_axes.iter())
96        .filter(|entry| entry.status == "driven")
97        .map(|entry| entry.axis)
98        .collect()
99}
100
101pub fn cascade_driver_census_is_consistent_v0() -> bool {
102    let Some(census) = cascade_driver_census_v0() else {
103        return false;
104    };
105    let catalog = cascade_level_catalog_v0();
106    let levels = census
107        .levels
108        .iter()
109        .filter_map(|entry| cascade_level_from_name_v0(entry.level.as_str()))
110        .collect::<Vec<_>>();
111    let driven = census
112        .levels
113        .iter()
114        .filter(|entry| entry.status == "driven")
115        .filter_map(|entry| cascade_level_from_name_v0(entry.level.as_str()))
116        .collect::<Vec<_>>();
117    let expected_driven = cascade_origin_driver_catalog_v0()
118        .into_iter()
119        .map(|driver| driver.level)
120        .collect::<BTreeSet<_>>();
121    let all_levels_have_evidence = census.levels.iter().all(|entry| {
122        (entry.status == "driven" && !entry.driver_inputs.is_empty() && entry.follow_up.is_none())
123            || (entry.status == "deferred"
124                && entry.driver_inputs.is_empty()
125                && entry
126                    .follow_up
127                    .as_deref()
128                    .is_some_and(|value| !value.is_empty()))
129    });
130    let expected_axes = [
131        CascadeWinnerAxisV0::CascadeLevel,
132        CascadeWinnerAxisV0::LayerRank,
133        CascadeWinnerAxisV0::ScopeProximity,
134        CascadeWinnerAxisV0::Specificity,
135        CascadeWinnerAxisV0::SourceOrder,
136    ];
137    census.schema_version == "0"
138        && census.product == "omena-cascade.driver-census"
139        && levels == catalog
140        && driven.into_iter().collect::<BTreeSet<_>>() == expected_driven
141        && all_levels_have_evidence
142        && census.winner_axes.len() == expected_axes.len()
143        && census
144            .winner_axes
145            .iter()
146            .all(|entry| entry.status == "driven")
147        && census
148            .winner_axes
149            .iter()
150            .map(|entry| entry.axis)
151            .eq(expected_axes)
152}
153
154fn cascade_level_from_name_v0(name: &str) -> Option<CascadeLevel> {
155    cascade_level_catalog_v0()
156        .into_iter()
157        .find(|level| cascade_level_name_v0(*level) == name)
158}
159
160pub const fn cascade_level_for_origin(origin: CascadeOriginV0, important: bool) -> CascadeLevel {
161    match (origin, important) {
162        (CascadeOriginV0::UserAgent, false) => CascadeLevel::UserAgentNormal,
163        (CascadeOriginV0::User, false) => CascadeLevel::UserNormal,
164        (CascadeOriginV0::Author, false) => CascadeLevel::AuthorNormal,
165        (CascadeOriginV0::Inline, false) => CascadeLevel::InlineNormal,
166        (CascadeOriginV0::UserAgent, true) => CascadeLevel::UserAgentImportant,
167        (CascadeOriginV0::User, true) => CascadeLevel::UserImportant,
168        (CascadeOriginV0::Author | CascadeOriginV0::Inline, true) => CascadeLevel::AuthorImportant,
169    }
170}
171
172pub const fn cascade_level_catalog_v0() -> [CascadeLevel; 9] {
173    [
174        CascadeLevel::UserAgentNormal,
175        CascadeLevel::UserNormal,
176        CascadeLevel::AuthorNormal,
177        CascadeLevel::InlineNormal,
178        CascadeLevel::Animation,
179        CascadeLevel::AuthorImportant,
180        CascadeLevel::UserImportant,
181        CascadeLevel::UserAgentImportant,
182        CascadeLevel::Transition,
183    ]
184}
185
186pub const fn cascade_level_name_v0(level: CascadeLevel) -> &'static str {
187    match level {
188        CascadeLevel::UserAgentNormal => "userAgentNormal",
189        CascadeLevel::UserNormal => "userNormal",
190        CascadeLevel::AuthorNormal => "authorNormal",
191        CascadeLevel::InlineNormal => "inlineNormal",
192        CascadeLevel::Animation => "animation",
193        CascadeLevel::AuthorImportant => "authorImportant",
194        CascadeLevel::UserImportant => "userImportant",
195        CascadeLevel::UserAgentImportant => "userAgentImportant",
196        CascadeLevel::Transition => "transition",
197    }
198}
199
200pub const fn cascade_origin_driver_catalog_v0() -> [CascadeOriginDriverV0; 8] {
201    [
202        origin_driver(CascadeOriginV0::UserAgent, false),
203        origin_driver(CascadeOriginV0::User, false),
204        origin_driver(CascadeOriginV0::Author, false),
205        origin_driver(CascadeOriginV0::Inline, false),
206        origin_driver(CascadeOriginV0::Author, true),
207        origin_driver(CascadeOriginV0::Inline, true),
208        origin_driver(CascadeOriginV0::User, true),
209        origin_driver(CascadeOriginV0::UserAgent, true),
210    ]
211}
212
213const fn origin_driver(origin: CascadeOriginV0, important: bool) -> CascadeOriginDriverV0 {
214    CascadeOriginDriverV0 {
215        origin,
216        important,
217        level: cascade_level_for_origin(origin, important),
218    }
219}