nichlink_run_method/authoring/validation/
validation.rs1use std::cell::RefCell;
13use std::path::{Path, PathBuf};
14
15pub use nichlink::authoring::validation::*;
16
17#[derive(Clone, Debug)]
20pub struct AuthoringContext {
21 package_root: PathBuf,
22 namespace: String,
23}
24
25thread_local! {
26 static ACTIVE_CONTEXT: RefCell<Option<AuthoringContext>> = const { RefCell::new(None) };
27}
28
29struct ContextRestore(Option<AuthoringContext>);
30
31impl Drop for ContextRestore {
32 fn drop(&mut self) {
33 ACTIVE_CONTEXT.with(|active| {
34 *active.borrow_mut() = self.0.take();
35 });
36 }
37}
38
39impl AuthoringContext {
40 pub fn new(package_root: impl Into<PathBuf>, namespace: impl Into<String>) -> Self {
48 Self {
49 package_root: package_root.into(),
50 namespace: namespace.into(),
51 }
52 }
53
54 pub fn scope<T>(&self, operation: impl FnOnce() -> T) -> T {
57 let previous = ACTIVE_CONTEXT.with(|active| active.replace(Some(self.clone())));
58 let _restore = ContextRestore(previous);
59 operation()
60 }
61}
62
63pub(super) fn authoring_namespace() -> String {
72 ACTIVE_CONTEXT
73 .with(|active| {
74 active
75 .borrow()
76 .as_ref()
77 .map(|context| context.namespace.clone())
78 })
79 .unwrap_or_else(|| {
80 nichlink::lexicon::resolve_namespace(
81 std::env::var(nichlink::lexicon::NAMESPACE_ENV)
82 .ok()
83 .as_deref(),
84 )
85 .to_owned()
86 })
87}
88
89pub(super) fn legacy_rule_path_for_source(source: &str) -> String {
92 let directory = Path::new(source).parent().unwrap_or_else(|| Path::new(""));
93 format!("src/{}/registry/rules/rules.rs", normalized_path(directory))
94}
95
96pub(super) fn package_root() -> PathBuf {
100 if let Some(root) = ACTIVE_CONTEXT.with(|active| {
101 active
102 .borrow()
103 .as_ref()
104 .map(|context| context.package_root.clone())
105 }) {
106 return root;
107 }
108 let configured = std::env::var_os(nichlink::lexicon::PACKAGE_ROOT_ENV).map(PathBuf::from);
109 let current = std::env::current_dir().ok();
110 nichlink::lexicon::resolve_package_root(
111 configured.as_deref(),
112 current.as_deref(),
113 current
114 .as_ref()
115 .is_some_and(|directory| directory.join("Cargo.toml").is_file()),
116 Path::new(env!("CARGO_MANIFEST_DIR")),
117 )
118}
119
120pub(super) fn source_root() -> PathBuf {
121 package_root().join("src")
122}
123
124#[cfg(test)]
125mod tests {
126 use super::{AuthoringContext, authoring_namespace, package_root};
127 use std::path::Path;
128
129 #[test]
130 fn nested_authoring_contexts_restore_the_previous_project() {
131 let outer = AuthoringContext::new("/tmp/nichlink-outer", "outer");
132 let inner = AuthoringContext::new("/tmp/nichlink-inner", "inner");
133
134 outer.scope(|| {
135 assert_eq!(package_root(), Path::new("/tmp/nichlink-outer"));
136 assert_eq!(authoring_namespace(), "outer");
137 inner.scope(|| {
138 assert_eq!(package_root(), Path::new("/tmp/nichlink-inner"));
139 assert_eq!(authoring_namespace(), "inner");
140 });
141 assert_eq!(package_root(), Path::new("/tmp/nichlink-outer"));
142 assert_eq!(authoring_namespace(), "outer");
143 });
144 }
145
146 #[test]
147 fn panicking_authoring_context_still_restores_its_parent() {
148 let outer = AuthoringContext::new("/tmp/nichlink-outer", "outer");
149 let inner = AuthoringContext::new("/tmp/nichlink-inner", "inner");
150
151 outer.scope(|| {
152 let result = std::panic::catch_unwind(|| inner.scope(|| panic!("expected panic")));
153 assert!(result.is_err());
154 assert_eq!(package_root(), Path::new("/tmp/nichlink-outer"));
155 assert_eq!(authoring_namespace(), "outer");
156 });
157 }
158}