use super::*;
#[path = "authored.rs"]
mod authored;
pub use authored::{AuthoredFace, authored_face};
#[path = "create.rs"]
mod create;
use create::create_module;
#[path = "delete.rs"]
mod delete;
pub use delete::delete_module;
#[path = "trash.rs"]
mod trash;
use trash::stash_face_source;
#[path = "face_values.rs"]
mod face_values;
use face_values::ModuleFaceValues;
#[path = "face_write.rs"]
mod face_write;
use face_write::{FaceWrite, apply_module_face_values};
#[path = "migration.rs"]
mod migration;
use migration::{migrate_kind_subtree, migrate_module_subtree};
#[path = "paths.rs"]
mod paths;
use paths::generated_paths;
pub struct AuthoringChange {
pub message: String,
pub source: PathBuf,
}
#[derive(Clone, Copy, Debug)]
pub struct NewModuleFace<'a> {
pub module: &'a str,
pub kind: &'a str,
pub preset: &'a str,
pub parts: &'a str,
pub name_zh: &'a str,
pub name_en: &'a str,
pub summary_zh: &'a str,
pub summary_en: &'a str,
pub exports: &'a str,
pub stable_name: &'a str,
pub parent: NodeId,
pub needs_registry: bool,
pub getting_from_other_registry: &'a str,
pub registration_rule: &'a str,
pub admission: &'a str,
pub handle_traits: &'a str,
pub handle_contracts: &'a str,
pub part_traits: &'a str,
pub part_contracts: &'a str,
pub requires: &'a str,
pub provides: &'a str,
pub runtime_checks: &'a str,
pub flow: &'a str,
pub flow_provider: &'a str,
}
#[derive(Clone, Copy, Debug)]
pub struct ModuleFacePatch<'a> {
pub module: &'a str,
pub kind: &'a str,
pub preset: &'a str,
pub parts: &'a str,
pub name_zh: &'a str,
pub name_en: &'a str,
pub summary_zh: &'a str,
pub summary_en: &'a str,
pub exports: &'a str,
pub stable_name: &'a str,
pub needs_registry: bool,
pub getting_from_other_registry: &'a str,
pub registration_rule: &'a str,
pub admission: &'a str,
pub handle_traits: &'a str,
pub handle_contracts: &'a str,
pub part_traits: &'a str,
pub part_contracts: &'a str,
pub requires: &'a str,
pub provides: &'a str,
pub runtime_checks: &'a str,
pub flow: &'a str,
pub flow_provider: &'a str,
}
pub fn add_module(registry: &Registry, spec: &str) -> Result<AuthoringChange, String> {
add_module_with_registration(registry, spec).map(|(change, _)| change)
}
pub fn add_module_with_registration(
registry: &Registry,
spec: &str,
) -> Result<(AuthoringChange, RegistrationSnapshot), String> {
let mut fields = spec.split_whitespace();
let name = fields
.next()
.ok_or_else(|| "usage: add <module-name> [parent-node]".to_owned())?;
validate_name(name)?;
let parent = fields
.next()
.map(str::parse::<NodeId>)
.transpose()
.map_err(|_| "parent must be a 32-digit node identity".to_owned())?
.unwrap_or(ROOT_NODE_ID);
if fields.next().is_some() {
return Err("usage: add <module-name> [parent-node]".to_owned());
}
create_module(registry, name, parent, None)
}
pub fn add_module_from_face(
registry: &Registry,
spec: &NewModuleFace<'_>,
) -> Result<(AuthoringChange, RegistrationSnapshot), String> {
let values = ModuleFaceValues::from_new(spec);
validate_name(values.module)?;
create_module(registry, values.module, spec.parent, Some(&values))
}
pub fn generated_snapshots() -> Result<Vec<RegistrationSnapshot>, String> {
generated_snapshots_from(&source_root())
}
pub fn generated_snapshots_from(root: &Path) -> Result<Vec<RegistrationSnapshot>, String> {
let mut sources = Vec::new();
collect_face_sources(root, &mut sources)?;
sources.sort();
sources
.into_iter()
.map(|source| {
FaceManifest::parse_source(&source)
.and_then(|face| face.to_snapshot())
.map_err(|error| format!("{}: {error}", source.display()))
})
.collect()
}
struct StdSourceTree;
impl nichlink::source::SourceTree for StdSourceTree {
fn is_directory(&self, path: &Path) -> bool {
path.is_dir()
}
fn entries(&self, path: &Path) -> Result<Vec<PathBuf>, String> {
fs::read_dir(path)
.map_err(|error| format!("cannot scan {}: {error}", path.display()))?
.map(|entry| {
entry
.map(|entry| entry.path())
.map_err(|error| format!("cannot scan {}: {error}", path.display()))
})
.collect()
}
fn read_text(&self, path: &Path) -> Result<String, String> {
fs::read_to_string(path).map_err(|error| format!("cannot read {}: {error}", path.display()))
}
}
fn collect_face_sources(directory: &Path, sources: &mut Vec<PathBuf>) -> Result<(), String> {
nichlink::source::collect_rust_sources(
&StdSourceTree,
directory,
nichlink::source::SourceWalk {
skip_target: false,
skip_registry_core: true,
skip_compile_error_demo: true,
},
|_, source| match source {
None => nichlink::source::Keep::NeedSource,
Some(text) if crate::syntax::is_face_source(text, GENERATED_MARKER) => {
nichlink::source::Keep::Yes
}
Some(_) => nichlink::source::Keep::No,
},
sources,
)
}
pub fn edit_module_face(
registry: &Registry,
id: NodeId,
patch: &ModuleFacePatch<'_>,
) -> Result<AuthoringChange, String> {
let (_, source) = generated_paths(registry, id)?;
let mut face = FaceManifest::parse_source(&source)?;
let values = ModuleFaceValues::from_patch(patch);
let old_module = face.values.get("module").cloned().unwrap_or_default();
let requested_module = values.module.trim();
validate_name(requested_module)?;
let module_changed = requested_module != old_module;
let original_kind = face.values.get("kind").cloned().unwrap_or_default();
let kind_changed = normalize_kind_name(values.kind.trim()) != original_kind;
apply_module_face_values(&mut face, &values, FaceWrite::Edit)?;
if !values.needs_registry
&& registry
.registry(id)
.is_some_and(|owned_registry| !owned_registry.is_empty())
{
return Err("cannot disable a Registry while it still owns child entries".to_owned());
}
if module_changed {
return migrate_module_subtree(registry, id, source, face, requested_module);
}
if kind_changed {
return migrate_kind_subtree(registry, id, source, face);
}
let authored = face.to_snapshot()?;
let existing = registry
.find(id)
.ok_or_else(|| format!("node `{id}` is not registered"))?;
let merged = existing.clone().merge_authored(authored);
registry
.validate_snapshot_replacement(id, merged)
.map_err(|error| format!("registration rejected:\n{error}"))?;
let old_source = fs::read_to_string(&source)
.map_err(|error| format!("cannot read {}: {error}", source.display()))?;
let rule = face.rule_source_path()?;
let old_rule = fs::read_to_string(&rule).ok();
let rendered = face.render_source()?;
let backup = if rendered == old_source {
None
} else {
Some(stash_face_source(&source, id, &old_source)?)
};
if let Err(error) = atomic_write(&source, &rendered) {
let _ = atomic_write(&source, &old_source);
return Err(error);
}
if face.owns_rule_source() {
let rule_source = face
.render_rule_source()?
.ok_or_else(|| "registry rule source was unexpectedly omitted".to_owned())?;
if let Err(error) = (|| {
fs::create_dir_all(rule.parent().expect("rule source has a parent"))
.map_err(|error| format!("cannot create registry rule directory: {error}"))?;
atomic_write(&rule, &rule_source)
})() {
let _ = atomic_write(&source, &old_source);
if let Some(old_rule) = old_rule {
let _ = atomic_write(&rule, &old_rule);
} else {
let _ = fs::remove_file(&rule);
}
return Err(error);
}
} else if old_rule.is_some() {
if let Err(error) = fs::remove_file(&rule) {
let _ = atomic_write(&source, &old_source);
if let Some(old_rule) = old_rule {
let _ = atomic_write(&rule, &old_rule);
}
return Err(format!(
"cannot remove registry rule {}: {error}",
rule.display()
));
}
}
Ok(AuthoringChange {
message: match &backup {
Some(backup) => format!(
"updated registration face {} (previous text kept at {})",
source.display(),
backup.display()
),
None => format!("updated registration face {}", source.display()),
},
source,
})
}