use std::path::Path;
use super::super::FaceManifest;
use crate::authoring::GENERATED_MARKER;
use crate::authoring::parse::*;
use crate::authoring::validation::{normalized_path, rule_path_for_source, rust_string};
impl FaceManifest {
pub(crate) fn render_source(&self) -> Result<String, String> {
let value = |key| self.values.get(key).map(String::as_str).unwrap_or("");
if !value(nichlink::lexicon::FACE_FIELD_PLUGIN).is_empty() {
return Err(
"this face declares `plugin:`, which the editor cannot rewrite yet; \
edit that line in the file by hand"
.to_owned(),
);
}
let kind = value("kind");
let preset = if value("preset").is_empty() {
"NoPreset"
} else {
value("preset")
};
let parts = if value("parts").is_empty() {
"NoParts"
} else {
value("parts")
};
let module_doc = if value("needs_registry") == "true" {
format!(
"//! {kind} face and its recursively requested registry.\n//! {kind} 注册面及其递归申请的注册机。"
)
} else {
format!("//! {kind} registration face.\n//! {kind} 注册面。")
};
let handle_doc = format!(
"/// Registration-only marker for the {kind} face.\n/// 仅用于 {kind} 注册面的 handle 标记,不代表运行时 object 实现。"
);
let registration_rule = if self.owns_rule_source() {
self.rule_module_path()?
} else {
"crate::RegistrationRule::ANY".to_owned()
};
let parent = if value("parent_source") == "<root>" {
"crate::root_node_id(env!(\"CARGO_PKG_NAME\"))".to_owned()
} else {
let source = Path::new(value("parent_source"));
let module = source
.parent()
.map_or_else(|| source.to_string_lossy().into_owned(), normalized_path);
format!("crate::{}::NODE_ID", module.replace('/', "::"))
};
let object_macro = if value("parent_source") == "<root>" {
"root_object".to_owned()
} else {
let source = Path::new(value("parent_source"));
let module = source
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("registry");
format!("{module}_object")
};
let admission = render_admission(value("admission"))?;
let exports = value("exports")
.split(',')
.map(str::trim)
.filter(|item| !item.is_empty())
.map(|item| format!("\"{}\"", rust_string(item)))
.collect::<Vec<_>>()
.join(", ");
let exports_decl = if exports.is_empty() {
String::new()
} else {
format!(" exports: [{exports}],\n")
};
let handle_traits = render_face_list("handle_traits", value("handle_traits"));
let handle_contracts = render_path_list(value("handle_contracts"));
let handle_contracts_decl = if handle_contracts.is_empty() {
String::new()
} else {
format!(" handle_contracts: [{handle_contracts}],\n")
};
let part_traits = render_face_list("part_traits", value("part_traits"));
let part_contracts = render_path_list(value("part_contracts"));
let part_contracts_decl = if part_contracts.is_empty() {
String::new()
} else {
format!(" part_contracts: [{part_contracts}],\n")
};
let requirements = render_requirements(value("requires"));
let provides = render_literal_list(value("provides"));
let runtime_checks = render_expression_list(value("runtime_checks"))?;
let flow = render_flow_expression(value("flow"))?;
let flow_provider = render_flow_provider(value("flow_provider"))?;
let getting = render_optional_source(value("getting_from_other_registry"))?;
let registry_rule_path = if value("registry_rule_path").is_empty() {
rule_path_for_source(value("source"))
} else {
value("registry_rule_path").to_owned()
};
let stable_decl = if value("stable_name").is_empty() {
String::new()
} else {
format!(
"\x20 stable_name: \"{}\",\n",
rust_string(value("stable_name"))
)
};
let name_zh = if value("name_zh").is_empty() {
kind
} else {
value("name_zh")
};
let name_en = if value("name_en").is_empty() {
kind
} else {
value("name_en")
};
let name_decl = if name_zh == kind && name_en == kind {
String::new()
} else {
format!(
" name: {{ zh: \"{}\", en: \"{}\" }},\n",
rust_string(name_zh),
rust_string(name_en)
)
};
let summary_decl = if value("summary_zh").is_empty() && value("summary_en").is_empty() {
String::new()
} else {
format!(
" summary: {{ zh: \"{}\", en: \"{}\" }},\n",
rust_string(value("summary_zh")),
rust_string(value("summary_en"))
)
};
let preset_decl = if !is_default_type(preset, "NoPreset") {
format!(" preset: {preset},\n")
} else {
String::new()
};
let parts_decl = if !is_default_type(parts, "NoParts") {
format!(" parts: {parts},\n")
} else {
String::new()
};
let custom_shape =
!is_default_type(preset, "NoPreset") || !is_default_type(parts, "NoParts");
let preset_decl = if custom_shape && preset_decl.is_empty() {
format!(" preset: {preset},\n")
} else {
preset_decl
};
let parts_decl = if custom_shape && parts_decl.is_empty() {
format!(" parts: {parts},\n")
} else {
parts_decl
};
let needs_decl = if value("needs_registry") == "true" {
" needs_registry: true,\n".to_owned()
} else {
String::new()
};
let parent_decl = format!(" parent: {parent},\n");
let canonical_rule_path = rule_path_for_source(value("source"));
let registry_fields = if self.owns_rule_source() {
let canonical = registry_rule_path == canonical_rule_path;
let rule_path = if canonical {
String::new()
} else {
format!(
" registry_rule_path: \"{}\",\n",
rust_string(®istry_rule_path)
)
};
let derived = canonical && self.derives_rule_from_the_sibling();
let rule = if derived {
String::new()
} else {
format!(" registry_rule: {registration_rule},\n")
};
format!("{rule_path}{rule}")
} else {
String::new()
};
let getting_decl = if !getting.is_empty() && getting != "None" {
format!(" getting_from_other_registry: {getting},\n")
} else {
String::new()
};
let admission_decl = if value("admission").is_empty() || value("admission") == "ANY" {
String::new()
} else {
format!(" admission: {admission},\n")
};
let requirements_decl = if !requirements.is_empty() {
format!(" requires: [{requirements}],\n")
} else {
String::new()
};
let provides_decl = if !provides.is_empty() {
format!(" provides: [{provides}],\n")
} else {
String::new()
};
let runtime_decl = if !runtime_checks.is_empty() {
format!(" runtime_checks: [{runtime_checks}],\n")
} else {
String::new()
};
let source = format!(
"{module_doc}\n\nuse crate::{{NoParts, NoPreset}};\n\n{handle_doc}\npub struct {kind};\n\ncrate::{object_macro}! {{\n kind: {kind},\n{preset_decl}{parts_decl}{name_decl}{summary_decl}{exports_decl}{stable_decl}{needs_decl}{parent_decl}{getting_decl}{registry_fields}{admission_decl}{handle_traits}{handle_contracts_decl}{part_traits}{part_contracts_decl}{requirements_decl}{provides_decl}{flow}{flow_provider}{runtime_decl}}}\n"
);
Ok(format!("{GENERATED_MARKER}\n{source}"))
}
}
fn is_default_type(value: &str, default: &str) -> bool {
let value = value.trim();
value == default || value.rsplit("::").next() == Some(default)
}
#[cfg(test)]
mod plugin_preservation_tests {
use crate::authoring::manifest::parse;
#[test]
fn a_face_with_a_plugin_refuses_a_silent_rewrite() {
let root = std::env::temp_dir().join("nichlink-plugin-face-fixture");
let face = root.join("widget/widget.rs");
std::fs::create_dir_all(face.parent().expect("fixture parent")).expect("fixture dir");
std::fs::write(
&face,
"// generated-by=NichLink\n\
crate::root_object! {\n\
kind: Widget,\n\
parent: crate::ROOT_NODE_ID,\n\
plugin: crate::PluginSpec::new(\"widget\"),\n\
}\n",
)
.expect("fixture face");
let manifest = parse::source(&face).expect("face manifest");
let error = manifest
.render_source()
.expect_err("a plugin field must refuse the rewrite")
.to_owned();
assert!(error.contains("plugin:"), "{error}");
let _ = std::fs::remove_dir_all(&root);
}
}