nichlink_run_method/authoring/external_graft/
plan.rs1use std::fs;
15use std::path::PathBuf;
16use std::time::{SystemTime, UNIX_EPOCH};
17
18use crate::runtime::{LoadedGraft, graft_record_root, load_graft_record, load_graft_records};
19use crate::{GraftPlanDocument, NodeId, Registry};
20use nichlink::lexicon;
21
22use super::super::filesystem::atomic_write;
23use super::super::validation::package_root;
24
25pub fn external_graft_root() -> PathBuf {
33 graft_record_root(&package_root())
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct ExternalGraftPlanFile {
40 pub selector: String,
43 pub document: GraftPlanDocument,
46 pub root: PathBuf,
49}
50
51impl ExternalGraftPlanFile {
52 pub fn plan_path(&self) -> PathBuf {
55 self.root.join(lexicon::GRAFT_PLAN_FILE)
56 }
57
58 pub fn target(&self) -> NodeId {
61 self.document.target
62 }
63
64 pub fn target_path(&self) -> &str {
67 &self.document.target_path
68 }
69
70 pub fn graft(&self) -> &str {
73 &self.document.graft
74 }
75
76 pub fn full(&self) -> bool {
79 self.document.full
80 }
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct ExternalGraftPlanEntry {
91 pub selector: String,
94 pub root: PathBuf,
97 pub document: Result<GraftPlanDocument, String>,
100}
101
102impl ExternalGraftPlanEntry {
103 pub fn plan_path(&self) -> PathBuf {
106 self.root.join(lexicon::GRAFT_PLAN_FILE)
107 }
108
109 pub fn document(&self) -> Result<&GraftPlanDocument, &str> {
112 self.document.as_ref().map_err(String::as_str)
113 }
114}
115
116fn checked_selector(selector: &str) -> Result<&str, String> {
119 let selector = selector.trim();
120 crate::validate_graft_selector(selector)?;
121 Ok(selector)
122}
123
124pub fn external_graft_directory(selector: &str) -> Result<PathBuf, String> {
134 let selector = checked_selector(selector)?;
135 let root = external_graft_root().join(selector);
136 if !root.is_dir() {
137 return Err(format!(
138 "external graft `{selector}` does not exist at {}",
139 root.display()
140 ));
141 }
142 Ok(root)
143}
144
145pub fn create_external_graft(
148 registry: &Registry,
149 target: NodeId,
150 graft: impl Into<String>,
151 full: bool,
152) -> Result<ExternalGraftPlanFile, String> {
153 let target_path = registry
154 .path_for(target)
155 .ok_or_else(|| format!("graft target `{target}` is not registered"))?;
156 let selector = checked_selector(&graft.into())?.to_owned();
157
158 let root = external_graft_root().join(&selector);
159 if root.exists() {
160 return Err(format!(
161 "external graft `{selector}` already exists at {}",
162 root.join(lexicon::GRAFT_PLAN_FILE).display()
163 ));
164 }
165 let document = GraftPlanDocument::new(target, target_path, selector.clone(), full);
166 fs::create_dir_all(&root)
167 .map_err(|error| format!("cannot create external graft directory: {error}"))?;
168 if let Err(error) = atomic_write(&root.join(lexicon::GRAFT_PLAN_FILE), &document.render()) {
169 let _ = fs::remove_dir_all(&root);
170 return Err(format!("cannot write external graft plan: {error}"));
171 }
172
173 Ok(ExternalGraftPlanFile {
174 selector,
175 document,
176 root,
177 })
178}
179
180pub fn read_external_graft(selector: &str) -> Result<ExternalGraftPlanFile, String> {
187 let selector = checked_selector(selector)?;
188 let document = load_graft_record(&package_root(), selector)?;
189 Ok(ExternalGraftPlanFile {
190 selector: selector.to_owned(),
191 document,
192 root: external_graft_root().join(selector),
193 })
194}
195
196pub fn list_external_grafts() -> Result<Vec<ExternalGraftPlanEntry>, String> {
199 let root = external_graft_root();
200 let loaded = load_graft_records(&package_root())?;
201 Ok(loaded
202 .into_iter()
203 .map(|entry| match entry {
204 LoadedGraft::Record(record) => ExternalGraftPlanEntry {
205 root: root.join(&record.selector),
206 selector: record.selector.clone(),
207 document: Ok(record.document),
208 },
209 LoadedGraft::Unreadable { selector, reason } => ExternalGraftPlanEntry {
210 root: root.join(&selector),
211 selector,
212 document: Err(reason),
213 },
214 })
215 .collect())
216}
217
218pub fn rewrite_external_graft(selector: &str, full: bool) -> Result<ExternalGraftPlanFile, String> {
221 let mut plan = read_external_graft(selector)?;
222 if plan.document.full == full {
223 return Ok(plan);
224 }
225 plan.document.full = full;
226 let path = plan.plan_path();
227 atomic_write(&path, &plan.document.render())?;
228 Ok(plan)
229}
230
231pub fn remove_external_graft(selector: &str) -> Result<PathBuf, String> {
240 let selector = checked_selector(selector)?;
241 let root = external_graft_directory(selector)?;
242 let stamp = SystemTime::now()
243 .duration_since(UNIX_EPOCH)
244 .map_err(|error| format!("clock error: {error}"))?
245 .as_nanos();
246 let trash = package_root()
247 .join(lexicon::NICHLINK_DIR)
248 .join("trash")
249 .join(lexicon::EXTERNAL_GRAFT_DIR)
250 .join(format!("{selector}-{stamp}"));
251 fs::create_dir_all(trash.parent().expect("trash has a parent"))
252 .map_err(|error| format!("cannot create NichLink trash: {error}"))?;
253 fs::rename(&root, &trash).map_err(|error| {
254 format!(
255 "cannot move {} to {}: {error}",
256 root.display(),
257 trash.display()
258 )
259 })?;
260 Ok(trash)
261}
262
263#[cfg(test)]
264#[path = "plan_tests.rs"]
265mod plan_tests;