fallow_config/
config_inputs.rs1use std::ffi::OsString;
9use std::path::{Path, PathBuf};
10
11use crate::{FallowConfig, external_plugin_source_files};
12
13#[derive(Clone, Debug, Default, PartialEq, Eq)]
17pub struct ConfigInputs {
18 root: PathBuf,
19 plugin_paths: Vec<String>,
20 rule_pack_paths: Vec<String>,
21 auto_discover_dirs: Vec<PathBuf>,
22}
23
24#[derive(Clone, Debug, Default, PartialEq, Eq)]
27pub struct ConfigInputsSnapshot {
28 files: Vec<(PathBuf, Option<Vec<u8>>)>,
30 dirs: Vec<(PathBuf, Option<Vec<OsString>>)>,
33}
34
35impl ConfigInputs {
36 #[must_use]
40 pub fn new(root: &Path, config: &FallowConfig) -> Self {
41 Self {
42 root: root.to_path_buf(),
43 plugin_paths: config.plugins.clone(),
44 rule_pack_paths: config.rule_packs.clone(),
45 auto_discover_dirs: config.boundaries.auto_discover_dirs(root),
46 }
47 }
48
49 #[must_use]
51 pub fn snapshot(&self) -> ConfigInputsSnapshot {
52 let files = external_plugin_source_files(&self.root, &self.plugin_paths)
53 .into_iter()
54 .chain(self.rule_pack_paths.iter().map(|path| self.root.join(path)))
55 .map(|path| {
56 let content = std::fs::read(&path).ok();
57 (path, content)
58 })
59 .collect();
60 let dirs = self
61 .auto_discover_dirs
62 .iter()
63 .map(|dir| (dir.clone(), child_dir_names(dir)))
64 .collect();
65 ConfigInputsSnapshot { files, dirs }
66 }
67}
68
69fn child_dir_names(dir: &Path) -> Option<Vec<OsString>> {
70 let entries = std::fs::read_dir(dir).ok()?;
71 let mut names: Vec<OsString> = entries
72 .filter_map(Result::ok)
73 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
74 .map(|entry| entry.file_name())
75 .collect();
76 names.sort();
77 Some(names)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 fn inputs(root: &Path, config: &str) -> ConfigInputs {
85 ConfigInputs::new(root, &serde_json::from_str(config).unwrap())
86 }
87
88 #[test]
89 fn a_plugin_edit_or_a_new_plugin_file_changes_the_snapshot() {
90 let dir = tempfile::tempdir().unwrap();
91 let root = dir.path();
92 std::fs::create_dir_all(root.join(".fallow/plugins")).unwrap();
93 std::fs::write(root.join(".fallow/plugins/a.json"), r#"{"name":"a"}"#).unwrap();
94 let inputs = inputs(root, "{}");
95 let before = inputs.snapshot();
96 assert_eq!(before, inputs.snapshot());
97
98 std::fs::write(root.join(".fallow/plugins/a.json"), r#"{"name":"b"}"#).unwrap();
99 let edited = inputs.snapshot();
100 assert_ne!(before, edited, "an edit to a plugin file");
101
102 std::fs::write(root.join("fallow-plugin-c.toml"), "name = \"c\"").unwrap();
103 assert_ne!(edited, inputs.snapshot(), "a new root plugin file");
104 }
105
106 #[test]
107 fn configured_plugins_and_rule_packs_are_inputs_also_before_they_exist() {
108 let dir = tempfile::tempdir().unwrap();
109 let root = dir.path();
110 let inputs = inputs(
111 root,
112 r#"{"plugins":["tools/plugin.json"],"rulePacks":["policy.json"]}"#,
113 );
114 let before = inputs.snapshot();
115
116 std::fs::create_dir_all(root.join("tools")).unwrap();
117 std::fs::write(root.join("tools/plugin.json"), r#"{"name":"a"}"#).unwrap();
118 let with_plugin = inputs.snapshot();
119 assert_ne!(before, with_plugin, "a configured plugin file appeared");
120
121 std::fs::write(root.join("policy.json"), "{}").unwrap();
122 assert_ne!(with_plugin, inputs.snapshot(), "a rule pack appeared");
123 }
124
125 #[test]
126 fn a_new_child_of_an_auto_discover_dir_changes_the_snapshot() {
127 let dir = tempfile::tempdir().unwrap();
128 let root = dir.path();
129 std::fs::create_dir_all(root.join("src/features/auth")).unwrap();
130 let inputs = inputs(
131 root,
132 r#"{"boundaries":{"zones":[{"name":"features","patterns":[],"autoDiscover":["./src/features/"]}],"rules":[]}}"#,
133 );
134 let before = inputs.snapshot();
135
136 std::fs::write(root.join("src/features/index.ts"), "").unwrap();
137 assert_eq!(before, inputs.snapshot(), "a file is not a zone");
138 std::fs::create_dir_all(root.join("src/features/billing")).unwrap();
139 assert_ne!(before, inputs.snapshot(), "a new zone directory");
140 }
141
142 #[test]
143 fn a_preset_auto_discover_dir_is_an_input() {
144 let dir = tempfile::tempdir().unwrap();
145 let root = dir.path();
146 std::fs::create_dir_all(root.join("src/features/auth")).unwrap();
147 let inputs = inputs(root, r#"{"boundaries":{"preset":"bulletproof"}}"#);
148 let before = inputs.snapshot();
149
150 std::fs::create_dir_all(root.join("src/features/billing")).unwrap();
151 assert_ne!(before, inputs.snapshot(), "a new zone directory");
152 }
153}