use crate::diagnostic::{DiagnosticError, codes, err};
use serde_json::{Map, Value};
use std::borrow::Cow;
use std::collections::{BTreeMap, HashSet};
use std::path::{Path, PathBuf};
pub(crate) fn root_metaparameter_env(
value: &Value,
api_meta: &BTreeMap<String, i64>,
) -> BTreeMap<String, i64> {
let mut env: BTreeMap<String, i64> = BTreeMap::new();
if let Some(decls) = value.get("metaparameters").and_then(|v| v.as_object()) {
for (name, decl) in decls {
if let Some(d) = decl.get("default")
&& d.as_i64().is_some()
&& !d.is_boolean()
{
env.insert(name.clone(), d.as_i64().expect("checked integer default"));
}
}
}
for (k, v) in api_meta {
env.insert(k.clone(), *v);
}
env
}
pub fn resolve_subsystem_refs_raw(
value: &mut Value,
base_path: &Path,
) -> Result<(), DiagnosticError> {
let staged = resolve_subsystem_refs_with_metaparameters(value, base_path, &BTreeMap::new())?;
apply_staged_index_sets(value, staged, &BTreeMap::new())
}
pub(crate) fn apply_staged_index_sets(
value: &mut Value,
staged: Map<String, Value>,
env: &BTreeMap<String, i64>,
) -> Result<(), DiagnosticError> {
if staged.is_empty() {
return Ok(());
}
let Some(obj) = value.as_object_mut() else {
return Ok(());
};
let mut registry: Map<String, Value> = obj
.get("index_sets")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default();
merge_subsystem_index_sets(&mut registry, &staged, "a ยง4.7 mount", env)?;
obj.insert("index_sets".to_string(), Value::Object(registry));
Ok(())
}
pub fn resolve_subsystem_refs(
file: &mut crate::EsmFile,
base_path: &Path,
) -> Result<(), DiagnosticError> {
let mut value = serde_json::to_value(&*file).map_err(|e| {
err(
codes::UNRESOLVED_SUBSYSTEM_REF,
format!("could not render the document as JSON for ref resolution: {e}"),
)
})?;
resolve_subsystem_refs_raw(&mut value, base_path)?;
*file = serde_json::from_value(value).map_err(|e| {
err(
codes::UNRESOLVED_SUBSYSTEM_REF,
format!("resolved document did not deserialize back into an EsmFile: {e}"),
)
})?;
Ok(())
}
pub fn resolve_subsystem_refs_with_metaparameters(
value: &mut Value,
base_path: &Path,
api_meta: &BTreeMap<String, i64>,
) -> Result<Map<String, Value>, DiagnosticError> {
let root_meta = root_metaparameter_env(value, api_meta);
let mut visited = HashSet::new();
walk_top_level(
value,
base_path,
&mut visited,
&root_meta,
api_meta,
true,
&root_meta,
DEFAULT_ELEMENT_TYPE,
)
}
fn show_index_set(decl: &Value) -> String {
let Some(obj) = decl.as_object() else {
return decl.to_string();
};
let parts: Vec<String> = [
"kind", "size", "members", "of", "offsets", "values", "from_faq",
]
.iter()
.filter_map(|k| obj.get(*k).map(|v| format!("{k}={v}")))
.collect();
if parts.is_empty() {
decl.to_string()
} else {
parts.join(", ")
}
}
fn merge_subsystem_index_sets(
registry: &mut Map<String, Value>,
loaded: &Map<String, Value>,
ref_str: &str,
env: &BTreeMap<String, i64>,
) -> Result<(), DiagnosticError> {
for (n, raw) in loaded {
let owned = crate::template_imports::fold_mount_contribution(raw, env);
let decl = &owned;
if let Some(existing) = registry.get(n) {
if existing != decl {
return Err(err(
codes::SUBSYSTEM_INDEX_SET_CONFLICT,
format!(
"index set '{n}' from subsystem ref '{ref_str}' ({}) collides with a \
non-deep-equal declaration already in the importing document's registry \
({}) โ contributed by the document's own `index_sets` or by an earlier \
mount. A referenced subsystem file's top-level index_sets merge into the \
importing document's registry; deep-equal redeclaration is idempotent, a \
size/kind disagreement is a load-time error (esm-spec ยง4.7). If the two \
are genuinely different axes that happen to share a name, rename one at \
its mount edge with `index_set_rename` (esm-spec ยง4.7 \"Mount-edge \
index-set renaming\"), e.g. \
{{\"ref\": \"{ref_str}\", \"index_set_rename\": {{\"{n}\": \"{n}_2\"}}}}.",
show_index_set(decl),
show_index_set(existing),
),
));
}
} else {
registry.insert(n.clone(), decl.clone());
}
}
Ok(())
}
struct RefNoun {
remote_plural: &'static str,
cycle_singular: &'static str,
item: &'static str,
code: &'static str,
}
const SUBSYSTEM_REF: RefNoun = RefNoun {
remote_plural: "subsystem refs",
cycle_singular: "subsystem reference",
item: "ref",
code: codes::UNRESOLVED_SUBSYSTEM_REF,
};
const AMBIGUOUS_SUBSYSTEM_REF: &str = codes::AMBIGUOUS_SUBSYSTEM_REF;
const TOPLEVEL_MODEL_REF: RefNoun = RefNoun {
remote_plural: "top-level model refs",
cycle_singular: "top-level model reference",
item: "top-level model ref",
code: codes::TOPLEVEL_MODEL_REF_UNRESOLVED,
};
fn load_ref_document(
ref_str: &str,
base: &Path,
visited: &mut HashSet<PathBuf>,
noun: &RefNoun,
) -> Result<(PathBuf, Value), DiagnosticError> {
let expanded = expand_env_refs(ref_str);
let ref_str: &str = &expanded;
if ref_str.starts_with("http://") || ref_str.starts_with("https://") {
return Err(err(
noun.code,
format!(
"Remote {} are not supported in the Rust loader; \
download {ref_str:?} to a local file first",
noun.remote_plural
),
));
}
let canonical = base.join(ref_str).canonicalize().map_err(|e| {
err(
noun.code,
format!("failed to resolve {} {ref_str:?}: {e}", noun.item),
)
})?;
if visited.contains(&canonical) {
return Err(err(
noun.code,
format!(
"circular {} detected: {}",
noun.cycle_singular,
canonical.display()
),
));
}
visited.insert(canonical.clone());
let loaded = (|| -> Result<Value, DiagnosticError> {
let content = std::fs::read_to_string(&canonical).map_err(|e| {
err(
noun.code,
format!(
"failed to read {} {}: {}",
noun.item,
canonical.display(),
e
),
)
})?;
let mut parsed: Value = serde_json::from_str(&content).map_err(|e| {
err(
noun.code,
format!(
"failed to parse {} {}: {}",
noun.item,
canonical.display(),
e
),
)
})?;
crate::parse::prepare_document_ops(&mut parsed)
.map_err(|e| err(noun.code, format!("{}: {e}", canonical.display())))?;
Ok(parsed)
})();
match loaded {
Ok(parsed) => Ok((canonical, parsed)),
Err(e) => {
visited.remove(&canonical);
Err(e)
}
}
}
fn index_set_names(value: &Value) -> std::collections::BTreeSet<String> {
value
.get("index_sets")
.and_then(|v| v.as_object())
.map(|o| o.keys().cloned().collect())
.unwrap_or_default()
}
fn refuse_toplevel_reaction_system_refs(obj: &Map<String, Value>) -> Result<(), DiagnosticError> {
let Some(rs) = obj.get("reaction_systems").and_then(|v| v.as_object()) else {
return Ok(());
};
for (name, entry) in rs {
let Some(entry) = entry.as_object() else {
continue;
};
if let Some(ref_str) = entry.get("ref").and_then(|v| v.as_str())
&& !entry.contains_key("species")
{
return Err(err(
codes::MOUNT_FORM_UNSUPPORTED,
format!(
"reaction_systems.{name}: a top-level `reaction_systems.<k>` `{{ref}}` mount \
(ref '{ref_str}') is not supported by this binding. Inline the reaction \
system, or mount it at a `subsystems.<k>` `{{ref}}` edge (esm-spec ยง4.7 \
\"Two mount forms, one mechanism\")"
),
));
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn walk_top_level(
value: &mut Value,
base_path: &Path,
visited: &mut HashSet<PathBuf>,
parent_meta: &BTreeMap<String, i64>,
api_meta: &BTreeMap<String, i64>,
defer: bool,
caller_env: &BTreeMap<String, i64>,
enclosing_precision: &str,
) -> Result<Map<String, Value>, DiagnosticError> {
let precision = effective_element_type(value, enclosing_precision);
let merge_env = {
let mut env = root_metaparameter_env(value, api_meta);
let declared: Vec<String> = value
.get("metaparameters")
.and_then(|v| v.as_object())
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
for (k, v) in caller_env {
if declared.contains(k) {
env.insert(k.clone(), *v);
}
}
env
};
let obj = match value.as_object_mut() {
Some(o) => o,
None => return Ok(Map::new()),
};
refuse_toplevel_reaction_system_refs(obj)?;
let mut staged: Map<String, Value> = Map::new();
inline_toplevel_model_refs(
obj,
base_path,
visited,
parent_meta,
api_meta,
if defer { Some(&mut staged) } else { None },
&merge_env,
&precision,
)?;
let mut registry: Map<String, Value> = if defer {
staged
} else {
obj.get("index_sets")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default()
};
if let Some(map) = obj.get_mut("models").and_then(|v| v.as_object_mut()) {
for (_name, system) in map.iter_mut() {
walk_subsystems(
system,
base_path,
visited,
Some(&mut registry),
parent_meta,
api_meta,
&merge_env,
&precision,
)?;
}
}
if let Some(map) = obj
.get_mut("reaction_systems")
.and_then(|v| v.as_object_mut())
{
for (_name, system) in map.iter_mut() {
walk_subsystems(
system,
base_path,
visited,
None,
parent_meta,
api_meta,
&merge_env,
&precision,
)?;
}
}
if defer {
return Ok(registry);
}
if !registry.is_empty() {
obj.insert("index_sets".to_string(), Value::Object(registry));
}
Ok(Map::new())
}
#[allow(clippy::too_many_arguments)]
fn inline_toplevel_model_refs(
obj: &mut Map<String, Value>,
base_path: &Path,
visited: &mut HashSet<PathBuf>,
parent_meta: &BTreeMap<String, i64>,
api_meta: &BTreeMap<String, i64>,
mut staged: Option<&mut Map<String, Value>>,
merge_env: &BTreeMap<String, i64>,
precision: &str,
) -> Result<(), DiagnosticError> {
let edge_names: Vec<String> = match obj.get("models").and_then(|v| v.as_object()) {
Some(models) => models
.iter()
.filter(|(_, m)| {
m.is_object() && m.get("ref").is_some() && m.get("variables").is_none()
})
.map(|(k, _)| k.clone())
.collect(),
None => return Ok(()),
};
if edge_names.is_empty() {
return Ok(());
}
for name in edge_names {
let entry = obj
.get_mut("models")
.and_then(|v| v.as_object_mut())
.and_then(|m| m.remove(&name))
.expect("edge entry present");
let entry_obj = entry.as_object().expect("edge entry is an object");
let ref_str = entry_obj
.get("ref")
.and_then(|v| v.as_str())
.ok_or_else(|| {
err(
TOPLEVEL_MODEL_REF.code,
"top-level model ref must be a string",
)
})?;
let mount_noun = format!("top-level model ref '{ref_str}'");
let (canonical, mut comp) =
load_ref_document(ref_str, base_path, visited, &TOPLEVEL_MODEL_REF)?;
let leaf_precision = declared_element_type(&comp);
let leaf_dir = canonical.parent().unwrap_or(base_path).to_path_buf();
let build: Result<Value, DiagnosticError> = (|| {
crate::lower_expression_templates::reject_expression_templates_pre_v04(&comp)?;
crate::template_imports::reject_template_imports_pre_v08(&comp)?;
if crate::template_imports::is_template_library_doc(&comp) {
return Err(err(
codes::SUBSYSTEM_REF_IS_TEMPLATE_LIBRARY,
format!(
"Model ref '{ref_str}' targets a template-library file ({}); \
libraries are imported via expression_template_imports \
(esm-spec 9.7.1)",
canonical.display()
),
));
}
if crate::coupling_imports::is_coupling_library_doc(&comp) {
return Err(err(
codes::SUBSYSTEM_REF_IS_COUPLING_LIBRARY,
format!(
"Model ref '{ref_str}' targets a coupling-library file ({}); \
libraries are imported via a coupling_import coupling entry \
(esm-spec 10.9)",
canonical.display()
),
));
}
let leaf_declared: Vec<String> = comp
.get("metaparameters")
.and_then(|v| v.as_object())
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
let mut bindings: BTreeMap<String, i64> = api_meta
.iter()
.filter(|(k, _)| leaf_declared.contains(k))
.map(|(k, v)| (k.clone(), *v))
.collect();
bindings.extend(read_edge_bindings(entry_obj, parent_meta, &mount_noun)?);
let leaf_env = root_metaparameter_env(&comp, &bindings);
let staged_nested =
walk_top_level(
&mut comp, &leaf_dir, visited, &leaf_env, api_meta, true, &bindings,
precision,
)?;
let injected: Vec<Value> = entry_obj
.get("expression_template_imports")
.and_then(|v| v.as_array())
.map(|a| {
let mut a = a.clone();
for e in a.iter_mut() {
absolutize_nested_refs(e, base_path);
}
a
})
.unwrap_or_default();
crate::template_imports::apply_scope_injections(&mut comp, &injected)?;
let leaf_mount_declared =
crate::template_imports::collect_mount_declared_metaparameters(&comp, &leaf_dir);
let mut resolved = crate::template_imports::resolve_template_machinery_scoped(
&comp,
&leaf_dir,
&bindings,
&leaf_mount_declared,
true,
)?
.unwrap_or_else(|| std::mem::take(&mut comp));
crate::lower_expression_templates::lower_expression_templates(&mut resolved)?;
crate::lower_expression_templates::expand(&mut resolved)?;
comp = resolved;
lower_mounted_enums_at_edge(&mut comp, &mount_noun)?;
let before_nested = index_set_names(&comp);
apply_staged_index_sets(&mut comp, staged_nested, &leaf_env)?;
let nested_contributed: std::collections::BTreeSet<String> = index_set_names(&comp)
.difference(&before_nested)
.cloned()
.collect();
crate::template_imports::apply_mount_index_set_rename(
&mut comp,
entry_obj,
&format!("top-level model ref '{ref_str}'"),
&nested_contributed,
)?;
let sel = entry_obj.get("model").and_then(|v| v.as_str());
let mut model = extract_toplevel_model(&comp, sel, ref_str, &canonical)?;
if let Some(model_obj) = model.as_object_mut() {
model_obj.remove("tests");
}
carry_mounted_element_type(
&mut model,
leaf_precision.as_deref(),
precision,
&mount_noun,
)?;
absolutize_nested_refs(&mut model, &leaf_dir);
Ok(model)
})();
visited.remove(&canonical);
let model = build?;
obj.get_mut("models")
.and_then(|v| v.as_object_mut())
.expect("models map present")
.insert(name, model);
crate::data_source_urls::resolve_data_source_urls(&mut comp, &leaf_dir)
.map_err(|e| err(e.code, e.message))?;
if let Some(loaded) = comp.get("index_sets").and_then(|v| v.as_object()).cloned()
&& !loaded.is_empty()
{
if let Some(staged) = staged.as_deref_mut() {
merge_subsystem_index_sets(staged, &loaded, ref_str, merge_env)?;
} else {
let registry = obj
.entry("index_sets".to_string())
.or_insert_with(|| Value::Object(Map::new()));
if let Some(registry) = registry.as_object_mut() {
merge_subsystem_index_sets(registry, &loaded, ref_str, merge_env)?;
}
}
}
for blk in ["function_tables", "data_sources"] {
let Some(src) = comp.get(blk).and_then(|v| v.as_object()) else {
continue;
};
if src.is_empty() {
continue;
}
let src = src.clone();
let dst = obj
.entry(blk.to_string())
.or_insert_with(|| Value::Object(Map::new()));
if let Some(dst) = dst.as_object_mut() {
for (k, v) in src {
dst.entry(k).or_insert(v);
}
}
}
}
Ok(())
}
fn lower_mounted_enums_at_edge(doc: &mut Value, mount_noun: &str) -> Result<(), DiagnosticError> {
crate::lower_enums::lower_mounted_document_enums(doc).map_err(|e| {
err(
e.code,
format!(
"{mount_noun}: {} โ an `enum` op in a mounted file resolves against that \
file's own `enums` block (esm-spec ยง9.3)",
e.message
),
)
})
}
fn extract_toplevel_model(
comp: &Value,
sel: Option<&str>,
ref_str: &str,
source: &Path,
) -> Result<Value, DiagnosticError> {
let models = comp
.as_object()
.and_then(|o| o.get("models"))
.and_then(|v| v.as_object())
.ok_or_else(|| {
err(
TOPLEVEL_MODEL_REF.code,
format!(
"top-level model ref '{ref_str}' resolves to a file with no models block ({})",
source.display()
),
)
})?;
match sel {
Some(name) => models.get(name).cloned().ok_or_else(|| {
let mut avail: Vec<&String> = models.keys().collect();
avail.sort();
err(
TOPLEVEL_MODEL_REF.code,
format!(
"top-level model ref '{ref_str}' has no model '{name}' (available: {})",
avail
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
),
)
}),
None => {
if models.len() == 1 {
Ok(models.values().next().cloned().expect("one model"))
} else {
let mut avail: Vec<&String> = models.keys().collect();
avail.sort();
Err(err(
TOPLEVEL_MODEL_REF.code,
format!(
"top-level model ref '{ref_str}' resolves to {} models; add a \"model\" \
selector to choose one (available: {})",
models.len(),
avail
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
),
))
}
}
}
}
fn absolutize_nested_refs(value: &mut Value, base_dir: &Path) {
match value {
Value::Object(map) => {
if let Some(Value::String(r)) = map.get("ref") {
let r = expand_env_refs(r).into_owned();
let is_abs =
r.starts_with('/') || r.starts_with("http://") || r.starts_with("https://");
if !is_abs {
let joined = base_dir.join(&r);
let abs = joined
.canonicalize()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| joined.to_string_lossy().into_owned());
map.insert("ref".to_string(), Value::String(abs));
}
}
for v in map.values_mut() {
absolutize_nested_refs(v, base_dir);
}
}
Value::Array(arr) => {
for v in arr.iter_mut() {
absolutize_nested_refs(v, base_dir);
}
}
_ => {}
}
}
#[allow(clippy::too_many_arguments)]
fn walk_subsystems(
value: &mut Value,
base_path: &Path,
visited: &mut HashSet<PathBuf>,
mut registry: Option<&mut Map<String, Value>>,
parent_meta: &BTreeMap<String, i64>,
api_meta: &BTreeMap<String, i64>,
merge_env: &BTreeMap<String, i64>,
precision: &str,
) -> Result<(), DiagnosticError> {
let obj = match value.as_object_mut() {
Some(o) => o,
None => return Ok(()),
};
let subs_val = match obj.get_mut("subsystems") {
Some(v) => v,
None => return Ok(()),
};
let subs = match subs_val.as_object_mut() {
Some(m) => m,
None => return Ok(()),
};
let names: Vec<String> = subs.keys().cloned().collect();
for name in names {
let entry = subs.remove(&name).unwrap_or(Value::Null);
let resolved = resolve_value(
entry,
base_path,
visited,
registry.as_deref_mut(),
parent_meta,
api_meta,
merge_env,
precision,
)?;
subs.insert(name, resolved);
}
Ok(())
}
fn read_edge_bindings(
obj: &serde_json::Map<String, Value>,
parent_meta: &std::collections::BTreeMap<String, i64>,
mount_noun: &str,
) -> Result<std::collections::BTreeMap<String, i64>, DiagnosticError> {
let mut out = std::collections::BTreeMap::new();
let Some(bindings) = obj.get("bindings") else {
return Ok(out);
};
let Some(bindings_obj) = bindings.as_object() else {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!(
"{mount_noun} `bindings` must be an object of \
metaparameter expressions (esm-spec 9.7.6)"
),
));
};
for (k, v) in bindings_obj {
let ctx = format!("{mount_noun}, binding '{k}'");
crate::template_imports::require_meta_expr(v, &ctx)?;
let folded = crate::template_imports::eval_meta_expr(v, parent_meta, &ctx)?;
out.insert(k.clone(), folded);
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn resolve_value(
value: Value,
base_path: &Path,
visited: &mut HashSet<PathBuf>,
registry: Option<&mut Map<String, Value>>,
parent_meta: &BTreeMap<String, i64>,
api_meta: &BTreeMap<String, i64>,
merge_env: &BTreeMap<String, i64>,
precision: &str,
) -> Result<Value, DiagnosticError> {
if let Some(obj) = value.as_object()
&& let Some(ref_val) = obj.get("ref")
{
let ref_str = ref_val
.as_str()
.ok_or_else(|| err(SUBSYSTEM_REF.code, "subsystem ref must be a string"))?;
let (canonical, mut parsed) =
load_ref_document(ref_str, base_path, visited, &SUBSYSTEM_REF)?;
let leaf_precision = declared_element_type(&parsed);
if crate::template_imports::is_template_library_doc(&parsed) {
visited.remove(&canonical);
return Err(err(
codes::SUBSYSTEM_REF_IS_TEMPLATE_LIBRARY,
format!(
"Subsystem ref '{ref_str}' targets a \
template-library file ({}); libraries are imported via \
expression_template_imports (esm-spec 9.7.1)",
canonical.display()
),
));
}
if crate::coupling_imports::is_coupling_library_doc(&parsed) {
visited.remove(&canonical);
return Err(err(
codes::SUBSYSTEM_REF_IS_COUPLING_LIBRARY,
format!(
"Subsystem ref '{ref_str}' targets a \
coupling-library file ({}); libraries are imported via a coupling_import \
coupling entry (esm-spec 10.9)",
canonical.display()
),
));
}
let parent_dir = canonical.parent().unwrap_or(base_path).to_path_buf();
let edge_result: Result<(), DiagnosticError> = (|| {
crate::lower_expression_templates::reject_expression_templates_pre_v04(&parsed)?;
crate::template_imports::reject_template_imports_pre_v08(&parsed)?;
let leaf_declared: Vec<String> = parsed
.get("metaparameters")
.and_then(|v| v.as_object())
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
let mut bindings: BTreeMap<String, i64> = api_meta
.iter()
.filter(|(k, _)| leaf_declared.contains(k))
.map(|(k, v)| (k.clone(), *v))
.collect();
bindings.extend(read_edge_bindings(obj, parent_meta, "subsystem ref")?);
let leaf_env = root_metaparameter_env(&parsed, &bindings);
let staged_nested = walk_top_level(
&mut parsed,
&parent_dir,
visited,
&leaf_env,
api_meta,
true,
&bindings,
precision,
)?;
let injected: Vec<Value> = obj
.get("expression_template_imports")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
crate::template_imports::apply_scope_injections(&mut parsed, &injected)?;
let leaf_mount_declared =
crate::template_imports::collect_mount_declared_metaparameters(
&parsed,
&parent_dir,
);
let mut resolved = crate::template_imports::resolve_template_machinery_scoped(
&parsed,
&parent_dir,
&bindings,
&leaf_mount_declared,
true,
)?
.unwrap_or_else(|| std::mem::take(&mut parsed));
crate::lower_expression_templates::lower_expression_templates(&mut resolved)?;
crate::lower_expression_templates::expand(&mut resolved)?;
parsed = resolved;
lower_mounted_enums_at_edge(&mut parsed, &format!("subsystem ref '{ref_str}'"))?;
let before_nested = index_set_names(&parsed);
apply_staged_index_sets(&mut parsed, staged_nested, &leaf_env)?;
let nested_contributed: std::collections::BTreeSet<String> = index_set_names(&parsed)
.difference(&before_nested)
.cloned()
.collect();
crate::template_imports::apply_mount_index_set_rename(
&mut parsed,
obj,
&format!("subsystem ref '{ref_str}'"),
&nested_contributed,
)?;
Ok(())
})();
if let Err(e) = edge_result {
visited.remove(&canonical);
return Err(e);
}
let nested_result = (|| -> Result<(), DiagnosticError> {
if let Some(reg) = registry
&& let Some(loaded) = parsed.get("index_sets").and_then(|v| v.as_object())
{
merge_subsystem_index_sets(reg, &loaded.clone(), ref_str, merge_env)?;
}
Ok(())
})();
visited.remove(&canonical);
nested_result?;
let mut system = extract_single_system(parsed, &canonical)?;
carry_mounted_element_type(
&mut system,
leaf_precision.as_deref(),
precision,
&format!("subsystem ref '{ref_str}'"),
)?;
return Ok(system);
}
let mut value = value;
walk_subsystems(
&mut value,
base_path,
visited,
registry,
parent_meta,
api_meta,
merge_env,
precision,
)?;
Ok(value)
}
const DEFAULT_ELEMENT_TYPE: &str = "Float64";
fn declared_element_type(doc: &Value) -> Option<String> {
doc.get("domain")
.and_then(|d| d.get("element_type"))
.and_then(Value::as_str)
.map(str::to_string)
}
fn effective_element_type(doc: &Value, enclosing: &str) -> String {
declared_element_type(doc).unwrap_or_else(|| enclosing.to_string())
}
fn carry_mounted_element_type(
component: &mut Value,
leaf: Option<&str>,
enclosing: &str,
mount_noun: &str,
) -> Result<(), DiagnosticError> {
let Some(leaf) = leaf else {
return Ok(());
};
if leaf == enclosing {
return Ok(());
}
stamp_element_type(component, leaf, enclosing, mount_noun, "")
}
fn stamp_element_type(
system: &mut Value,
leaf: &str,
enclosing: &str,
mount_noun: &str,
path: &str,
) -> Result<(), DiagnosticError> {
let Some(obj) = system.as_object_mut() else {
return Ok(());
};
if obj.contains_key("species") || obj.contains_key("reactions") {
let at = if path.is_empty() {
String::new()
} else {
format!(" (subsystem '{path}')")
};
return Err(err(
codes::ELEMENT_TYPE_UNCARRIED,
format!(
"{mount_noun}: the mounted document declares `domain.element_type: \"{leaf}\"` \
but the document mounting it evaluates in {enclosing}, and the component \
carries a reaction system{at} whose species and parameters cannot declare a \
per-variable `element_type`; the mount cannot keep the leaf's precision \
(esm-spec ยง4.7 \"Working precision crosses a mount\"). Declare the same \
`domain.element_type` on the mounting document, or remove it from the leaf"
),
));
}
if let Some(vars) = obj.get_mut("variables").and_then(Value::as_object_mut) {
for var in vars.values_mut() {
if let Some(v) = var.as_object_mut()
&& !v.contains_key("element_type")
{
v.insert("element_type".to_string(), Value::String(leaf.to_string()));
}
}
}
if let Some(subs) = obj.get_mut("subsystems").and_then(Value::as_object_mut) {
for (name, sub) in subs.iter_mut() {
let sub_path = if path.is_empty() {
name.clone()
} else {
format!("{path}.{name}")
};
stamp_element_type(sub, leaf, enclosing, mount_noun, &sub_path)?;
}
}
Ok(())
}
fn extract_single_system(value: Value, source: &Path) -> Result<Value, DiagnosticError> {
let obj = value.as_object().ok_or_else(|| {
err(
SUBSYSTEM_REF.code,
format!("ref {} did not parse to a JSON object", source.display()),
)
})?;
let systems: Vec<&Value> = ["models", "reaction_systems"]
.iter()
.filter_map(|key| obj.get(*key).and_then(|v| v.as_object()))
.flat_map(|m| m.values())
.collect();
match systems.as_slice() {
[only] => Ok((*only).clone()),
_ => Err(err(
AMBIGUOUS_SUBSYSTEM_REF,
format!(
"Subsystem reference '{}' resolves to a file containing multiple top-level \
systems; exactly one is required",
source.display()
),
)),
}
}
pub(crate) fn expand_env_refs(ref_str: &str) -> Cow<'_, str> {
if !ref_str.contains("${") {
return Cow::Borrowed(ref_str);
}
let mut out = String::with_capacity(ref_str.len());
let mut rest = ref_str;
while let Some(start) = rest.find("${") {
out.push_str(&rest[..start]);
let after = &rest[start + 2..];
match after.find('}') {
Some(end) if is_env_var_name(&after[..end]) => {
match std::env::var(&after[..end]) {
Ok(value) => out.push_str(&value),
Err(_) => out.push_str(&rest[start..start + 3 + end]),
}
rest = &after[end + 1..];
}
_ => {
out.push_str("${");
rest = after;
}
}
}
out.push_str(rest);
Cow::Owned(out)
}
fn is_env_var_name(name: &str) -> bool {
let mut chars = name.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::TempDir;
#[test]
fn test_resolve_no_refs() {
let mut value = json!({
"esm": "1.0.0",
"metadata": { "name": "test" },
"models": {
"Main": { "variables": {}, "equations": [] }
}
});
let result = resolve_subsystem_refs_raw(&mut value, Path::new("/tmp"));
assert!(result.is_ok());
}
#[test]
fn test_resolve_local_subsystem_ref() {
let dir = TempDir::new().unwrap();
let inner = json!({
"esm": "1.0.0",
"metadata": { "name": "inner" },
"models": {
"Inner": {
"variables": {},
"equations": []
}
}
});
std::fs::write(
dir.path().join("inner.json"),
serde_json::to_string(&inner).unwrap(),
)
.unwrap();
let mut value = json!({
"esm": "1.0.0",
"metadata": { "name": "main" },
"models": {
"Outer": {
"variables": {},
"equations": [],
"subsystems": {
"Inner": { "ref": "inner.json" }
}
}
}
});
resolve_subsystem_refs_raw(&mut value, dir.path()).unwrap();
let inner_resolved = &value["models"]["Outer"]["subsystems"]["Inner"];
assert!(inner_resolved.get("variables").is_some());
assert!(inner_resolved.get("ref").is_none());
}
#[test]
fn test_reject_data_source_only_ref() {
let dir = TempDir::new().unwrap();
let source_only = json!({
"esm": "1.0.0",
"metadata": { "name": "source-only" },
"data_sources": {
"MetData": {
"kind": "grid",
"source": {
"url_template": "https://example.org/data/{date:%Y%m%d}.nc"
}
}
}
});
std::fs::write(
dir.path().join("source.esm"),
serde_json::to_string(&source_only).unwrap(),
)
.unwrap();
let mut value = json!({
"esm": "1.0.0",
"metadata": { "name": "main" },
"models": {
"Outer": {
"variables": {},
"equations": [],
"subsystems": {
"Met": { "ref": "source.esm" }
}
}
}
});
let err = resolve_subsystem_refs_raw(&mut value, dir.path())
.expect_err("a source-only file declares no mountable system");
assert!(err.to_string().contains("multiple top-level systems"));
}
#[test]
fn test_resolve_model_plus_its_data_sources_ref() {
let dir = TempDir::new().unwrap();
let leaf = json!({
"esm": "1.0.0",
"metadata": { "name": "leaf" },
"data_sources": {
"MetData": {
"kind": "grid",
"source": { "url_template": "https://example.org/{var}.nc" }
}
},
"models": {
"Leaf": {
"variables": {
"T": {
"type": "parameter",
"units": "K",
"shape": [],
"update": {
"kind": "data",
"source": "MetData",
"from": { "file_variable": "temperature" }
}
}
},
"equations": []
}
}
});
std::fs::write(
dir.path().join("leaf.esm"),
serde_json::to_string(&leaf).unwrap(),
)
.unwrap();
let mut value = json!({
"esm": "1.0.0",
"metadata": { "name": "main" },
"models": {
"Outer": {
"variables": {},
"equations": [],
"subsystems": {
"Met": { "ref": "leaf.esm" }
}
}
}
});
resolve_subsystem_refs_raw(&mut value, dir.path()).unwrap();
let resolved = &value["models"]["Outer"]["subsystems"]["Met"];
assert!(resolved.get("ref").is_none());
assert_eq!(resolved["variables"]["T"]["update"]["source"], "MetData");
}
#[test]
fn test_reject_remote_ref() {
let mut value = json!({
"esm": "1.0.0",
"metadata": { "name": "main" },
"models": {
"Outer": {
"variables": {},
"equations": [],
"subsystems": {
"Remote": { "ref": "https://example.com/inner.json" }
}
}
}
});
let result = resolve_subsystem_refs_raw(&mut value, Path::new("/tmp"));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Remote subsystem refs")
);
}
#[test]
fn test_circular_ref_detection() {
let dir = TempDir::new().unwrap();
let a = json!({
"esm": "1.0.0",
"metadata": { "name": "a" },
"models": {
"A": {
"variables": {},
"equations": [],
"subsystems": { "Cycle": { "ref": "b.json" } }
}
}
});
let b = json!({
"esm": "1.0.0",
"metadata": { "name": "b" },
"models": {
"B": {
"variables": {},
"equations": [],
"subsystems": { "Cycle": { "ref": "a.json" } }
}
}
});
std::fs::write(
dir.path().join("a.json"),
serde_json::to_string(&a).unwrap(),
)
.unwrap();
std::fs::write(
dir.path().join("b.json"),
serde_json::to_string(&b).unwrap(),
)
.unwrap();
let mut value = json!({
"esm": "1.0.0",
"metadata": { "name": "main" },
"models": {
"Root": {
"variables": {},
"equations": [],
"subsystems": { "Start": { "ref": "a.json" } }
}
}
});
let result = resolve_subsystem_refs_raw(&mut value, dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("circular"));
}
#[test]
fn test_nonexistent_ref() {
let dir = TempDir::new().unwrap();
let mut value = json!({
"esm": "1.0.0",
"metadata": { "name": "main" },
"models": {
"Outer": {
"variables": {},
"equations": [],
"subsystems": {
"Missing": { "ref": "does-not-exist.json" }
}
}
}
});
let result = resolve_subsystem_refs_raw(&mut value, dir.path());
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("failed to resolve ref")
);
}
#[test]
fn test_resolves_inside_reaction_systems() {
let dir = TempDir::new().unwrap();
let sub = json!({
"esm": "1.0.0",
"metadata": { "name": "sub" },
"reaction_systems": {
"Sub": {
"species": {},
"parameters": {},
"reactions": []
}
}
});
std::fs::write(
dir.path().join("sub.json"),
serde_json::to_string(&sub).unwrap(),
)
.unwrap();
let mut value = json!({
"esm": "1.0.0",
"metadata": { "name": "main" },
"reaction_systems": {
"Main": {
"species": {},
"parameters": {},
"reactions": [],
"subsystems": {
"SubKey": { "ref": "sub.json" }
}
}
}
});
resolve_subsystem_refs_raw(&mut value, dir.path()).unwrap();
let resolved = &value["reaction_systems"]["Main"]["subsystems"]["SubKey"];
assert!(resolved.get("species").is_some());
assert!(resolved.get("ref").is_none());
}
#[test]
fn typed_resolve_subsystem_refs_matches_the_raw_pass() {
let dir = TempDir::new().unwrap();
let inner = json!({
"esm": "1.0.0",
"metadata": { "name": "inner" },
"models": { "Inner": { "variables": {}, "equations": [] } }
});
std::fs::write(
dir.path().join("inner.json"),
serde_json::to_string(&inner).unwrap(),
)
.unwrap();
let doc = json!({
"esm": "1.0.0",
"metadata": { "name": "main" },
"models": {
"Outer": {
"variables": {},
"equations": [],
"subsystems": { "Inner": { "ref": "inner.json" } }
}
}
});
let mut raw = doc.clone();
resolve_subsystem_refs_raw(&mut raw, dir.path()).unwrap();
let mut typed: crate::EsmFile = serde_json::from_value(doc).expect("typed document");
resolve_subsystem_refs(&mut typed, dir.path()).unwrap();
let inner_resolved = &typed.models.as_ref().unwrap()["Outer"]
.subsystems
.as_ref()
.unwrap()["Inner"];
assert!(inner_resolved.get("variables").is_some());
assert!(inner_resolved.get("ref").is_none());
assert_eq!(serde_json::to_value(&typed).unwrap(), raw);
}
}