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 = ignored_reads(task)?;
63 Ok(Some(Self { mode, ignore_reads }))
64 }
65}
66
67pub fn ignored_reads(task: &Task) -> Result<Vec<String>> {
71 let Some(Value::Object(object)) = task.definition.extra.get("qk:ultracache") else {
72 return Ok(Vec::new());
73 };
74 let ignore_reads: Vec<String> = match object.get("ignoreReads") {
75 None => Vec::new(),
76 Some(value) => serde_json::from_value(value.clone())
77 .context("qk:ultracache.ignoreReads must be an array of globs")?,
78 };
79 for glob in &ignore_reads {
81 crate::glob::Pattern::new(glob, false)
82 .with_context(|| format!("qk:ultracache.ignoreReads has an invalid glob {glob:?}"))?;
83 }
84 Ok(ignore_reads)
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 fn task(value: Value) -> Task {
92 let mut definition: qk_config::Target =
93 serde_json::from_value(serde_json::json!({})).unwrap();
94 definition.extra.insert("qk:ultracache".into(), value);
95 Task {
96 id: "app:build".into(),
97 project: "app".into(),
98 target: "build".into(),
99 configuration: None,
100 args: Vec::new(),
101 definition,
102 dependencies: Default::default(),
103 edges: Default::default(),
104 }
105 }
106
107 #[test]
108 fn reads_the_mode_and_ignored_reads() {
109 let on = Settings::of(&task(serde_json::json!({"ignoreReads": ["tmp/**"]})))
110 .unwrap()
111 .unwrap();
112 assert_eq!(on.ignore_reads, ["tmp/**"]);
113 assert_eq!(on.mode, Mode::On);
114 let record = Settings::of(&task(serde_json::json!({"mode": "record"})))
115 .unwrap()
116 .unwrap();
117 assert_eq!(record.mode, Mode::Record);
118 assert!(
119 Settings::of(&task(serde_json::json!({"mode": "off"})))
120 .unwrap()
121 .is_none()
122 );
123 let off = task(serde_json::json!({"mode": "off", "ignoreReads": ["tmp/**"]}));
125 assert_eq!(ignored_reads(&off).unwrap(), ["tmp/**"]);
126 for invalid in [
127 serde_json::json!(true),
128 serde_json::json!({"mode": "warn"}),
129 serde_json::json!({"mode": 1}),
130 serde_json::json!({"ignoreReads": "tmp/**"}),
131 serde_json::json!({"ignoreReads": ["src/[*.log"]}),
132 ] {
133 assert!(Settings::of(&task(invalid.clone())).is_err(), "{invalid}");
134 }
135 }
136}