1use anyhow::{Context, Result, bail};
19use qk_taskgraph::Task;
20use serde_json::Value;
21
22pub const FIELDS: &[&str] = &["mode", "ignoreReads"];
25
26#[derive(Clone, Debug, Default, PartialEq, Eq)]
27pub struct Settings {
28 pub mode: Mode,
29 pub ignore_reads: Vec<String>,
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub enum Mode {
34 #[default]
36 On,
37 Record,
39}
40
41impl Settings {
42 pub fn of(task: &Task) -> Result<Option<Self>> {
45 let Some(value) = task.definition.extra.get("qk:ultracache") else {
46 return Ok(None);
47 };
48 let Value::Object(object) = value else {
49 bail!("qk:ultracache must be an object");
50 };
51 let mode = match object.get("mode").map(Value::as_str) {
52 None | Some(Some("on")) => Mode::On,
53 Some(Some("record")) => Mode::Record,
54 Some(Some("off")) => return Ok(None),
55 Some(Some(mode @ ("warn" | "error"))) => {
56 bail!(
57 "qk:ultracache.mode {mode:?} is not supported yet; use \"on\", \"record\" or \"off\""
58 )
59 }
60 Some(_) => bail!("qk:ultracache.mode must be \"on\", \"record\" or \"off\""),
61 };
62 let ignore_reads: Vec<String> = match object.get("ignoreReads") {
63 None => Vec::new(),
64 Some(value) => serde_json::from_value(value.clone())
65 .context("qk:ultracache.ignoreReads must be an array of globs")?,
66 };
67 for glob in &ignore_reads {
69 crate::glob::Pattern::new(glob, false).with_context(|| {
70 format!("qk:ultracache.ignoreReads has an invalid glob {glob:?}")
71 })?;
72 }
73 Ok(Some(Self { mode, ignore_reads }))
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 fn task(value: Value) -> Task {
82 let mut definition: qk_config::Target =
83 serde_json::from_value(serde_json::json!({})).unwrap();
84 definition.extra.insert("qk:ultracache".into(), value);
85 Task {
86 id: "app:build".into(),
87 project: "app".into(),
88 target: "build".into(),
89 configuration: None,
90 args: Vec::new(),
91 definition,
92 dependencies: Default::default(),
93 edges: Default::default(),
94 }
95 }
96
97 #[test]
98 fn reads_the_mode_and_ignored_reads() {
99 let on = Settings::of(&task(serde_json::json!({"ignoreReads": ["tmp/**"]})))
100 .unwrap()
101 .unwrap();
102 assert_eq!(on.ignore_reads, ["tmp/**"]);
103 assert_eq!(on.mode, Mode::On);
104 let record = Settings::of(&task(serde_json::json!({"mode": "record"})))
105 .unwrap()
106 .unwrap();
107 assert_eq!(record.mode, Mode::Record);
108 assert!(
109 Settings::of(&task(serde_json::json!({"mode": "off"})))
110 .unwrap()
111 .is_none()
112 );
113 for invalid in [
114 serde_json::json!(true),
115 serde_json::json!({"mode": "warn"}),
116 serde_json::json!({"mode": 1}),
117 serde_json::json!({"ignoreReads": "tmp/**"}),
118 serde_json::json!({"ignoreReads": ["src/[*.log"]}),
119 ] {
120 assert!(Settings::of(&task(invalid.clone())).is_err(), "{invalid}");
121 }
122 }
123}