Skip to main content

nichlink_run_method/authoring/validation/
validation.rs

1//! Names and paths shared by file-backed authoring.
2//! 文件创作共用的名称与路径校验。
3//!
4//! The pure validators live in the kernel `authoring` module; this shim
5//! keeps the `AuthoringContext` and its environment fallback chain, and
6//! re-exports the kernel helpers so the historical
7//! `nichlink_run_method::authoring::validation` paths keep working.
8//! 纯校验函数位于 kernel 的 `authoring` 模块;本 shim 保留
9//! `AuthoringContext` 及其环境变量回落链,并重导出 kernel 辅助函数,
10//! 保证 `nichlink_run_method::authoring::validation` 历史路径继续可用。
11
12use std::cell::RefCell;
13use std::path::{Path, PathBuf};
14
15pub use nichlink::authoring::validation::*;
16
17/// Filesystem and identity scope for one authoring operation.
18/// 单次注册面创作操作使用的文件系统与身份上下文。
19#[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    /// Pair a package root with the namespace its faces are authored under.
41    /// 把一个包根与其注册面创作所用的命名空间配对。
42    ///
43    /// The values are inert until [`AuthoringContext::scope`] installs them, so
44    /// constructing one never touches process-global state.
45    /// 这些取值在 [`AuthoringContext::scope`] 安装之前不生效,因此构造本身不会触碰
46    /// 进程级状态。
47    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    /// Run an operation in this context without changing process environment.
55    /// 在此上下文中执行操作,不修改进程环境变量。
56    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
63/// The namespace this operation authors under: the active context first, then
64/// the environment, then the documented default.
65/// 本次操作创作所用的命名空间:先活动上下文,再环境变量,最后文档化的默认值。
66///
67/// The decision itself is `nichlink::lexicon::resolve_namespace`, shared with
68/// Studio and the MCP bridge; this function only supplies what it reads.
69/// 决策本身是 `nichlink::lexicon::resolve_namespace`,与 Studio 和 MCP 桥共用;本函数
70/// 只负责提供它读取的东西。
71pub(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
89/// Legacy layout kept readable so existing projects can migrate gradually.
90/// 保留旧布局读取能力,现有项目可以逐步迁移。
91pub(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
96/// The package this operation writes into: the active context first, then the
97/// shared `lexicon` rule over the environment and the working directory.
98/// 本次操作写入的包:先活动上下文,再对环境和当前目录套用共用的 `lexicon` 规则。
99pub(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}