From b40009cda8d63689b0340df2604f3e7c5ddbb277 Mon Sep 17 00:00:00 2001 From: GardevoirX Date: Mon, 1 Jun 2026 13:47:20 +0200 Subject: [PATCH 1/2] Implement `mta_format_metadata` --- metatomic-core/src/c_api/model.rs | 16 ++- metatomic-core/src/metadata.rs | 184 +++++++++++++++++++++++++++++- metatomic-core/tests/misc.cpp | 46 ++++++++ 3 files changed, 244 insertions(+), 2 deletions(-) diff --git a/metatomic-core/src/c_api/model.rs b/metatomic-core/src/c_api/model.rs index bf03e2a4..b06cac4e 100644 --- a/metatomic-core/src/c_api/model.rs +++ b/metatomic-core/src/c_api/model.rs @@ -1,6 +1,9 @@ use std::ffi::{c_void, c_char}; use metatensor::c_api::{mts_labels_t, mts_tensormap_t}; +use crate::c_api::catch_unwind; +use crate::Error; + use super::{mta_status_t, mta_string_t, mta_system_t}; /// A model that computes physical properties of atomistic systems. @@ -209,5 +212,16 @@ pub unsafe extern "C" fn mta_format_metadata( metadata: *const c_char, printed: *mut mta_string_t, ) -> mta_status_t { - todo!() + catch_unwind(|| { + check_pointers_non_null!(metadata, printed); + let metadata_cstr = std::ffi::CStr::from_ptr(metadata); + let metadata_str = metadata_cstr.to_str().map_err(|_| { + Error::InvalidParameter("metadata is not valid UTF-8".into()) + })?; + let metadata_json = json::parse(metadata_str).map_err(|e| { + Error::Serialization(format!("invalid JSON for ModelMetadata: {e}")) + })?; + *printed = mta_string_t::new(crate::ModelMetadata::try_from(&metadata_json)?.print()); + Ok(()) + }) } diff --git a/metatomic-core/src/metadata.rs b/metatomic-core/src/metadata.rs index 48bfeaad..dcff8f1c 100644 --- a/metatomic-core/src/metadata.rs +++ b/metatomic-core/src/metadata.rs @@ -188,6 +188,61 @@ impl<'a> TryFrom<&'a JsonValue> for References { } +fn normalize_whitespace(data: &str) -> String { + let mut normalized_string = String::new(); + for c in data.chars() { + if c == '\n' || c == '\r' || c == '\t' { + normalized_string.push(' '); + } else { + normalized_string.push(c); + } + } + normalized_string +} + + +fn wrap_80_chars(output: &mut String, data: &str, indent: &str) { + let string = normalize_whitespace(data); + let mut chars: Vec = string.chars().collect(); + let line_length = 80 - indent.len(); + assert!(line_length > 50); + let mut first_line = true; + loop { + if chars.len() <= line_length { + // last line + if !first_line { + output.push_str(indent); + } + output.extend(chars); + break; + } else { + // backtrack to find the end of a word + let mut word_found = false; + for i in (0..line_length - 1).rev() { + if chars[i] == ' ' { + word_found = true; + // print the current line + if !first_line { + output.push_str(indent); + } + output.extend(chars.drain(0..i)); + output.push('\n'); + // remove the space + chars.remove(0); + first_line = false; + break; + } + } + + if !word_found { + // this is only hit if a single word takes a full line. + panic!("some words are too long to be wrapped, make them shorter"); + } + } + } +} + + /// Metadata about a model #[derive(Debug, Clone)] pub struct ModelMetadata { @@ -280,6 +335,99 @@ impl<'a> TryFrom<&'a JsonValue> for ModelMetadata { } } +impl ModelMetadata{ + fn validate(&self) { + for author in &self.authors { + if author.is_empty() { + panic!("author can not be empty string in ModelMetadata"); + } + } + + let References { + model, + architecture, + implementation, + } = &self.references; + for m in model.iter() { + if m.is_empty() { + panic!("reference can not be empty string (in 'model' section)"); + } + } + for a in architecture.iter() { + if a.is_empty() { + panic!("reference can not be empty string (in 'architecture' section)"); + } + } + for i in implementation.iter() { + if i.is_empty() { + panic!("reference can not be empty string (in 'implementation' section)"); + } + } + + } + pub fn print(&self) -> String { + let mut output = String::new(); + self.validate(); + if self.name.is_empty() { + output.push_str("This is an unnamed model\n"); + output.push_str("========================\n"); + } else { + output.push_str(&format!("This is the {} model\n", &self.name)); + output.push_str(&format!("============{}======\n", "=".repeat(self.name.len()))); + } + if !self.description.is_empty() { + output.push_str("\n"); + wrap_80_chars(&mut output, &(self.description), ""); + output.push_str("\n"); + } + + if !self.authors.is_empty() { + output.push_str("\nModel authors\n-------------\n\n"); + for author in self.authors.iter() { + output.push_str("- "); + wrap_80_chars(&mut output, &author, " "); + output.push_str("\n"); + } + } + + let mut references_output = String::new(); + if !self.references.model.is_empty() { + references_output.push_str("- about this specific model:\n"); + for reference in self.references.model.iter() { + references_output.push_str(" * "); + wrap_80_chars(&mut references_output, &reference, " "); + references_output.push_str("\n"); + } + } + + if !self.references.architecture.is_empty() { + references_output.push_str("- about the architecture of this model:\n"); + for reference in self.references.architecture.iter() { + references_output.push_str(" * "); + wrap_80_chars(&mut references_output, reference, " "); + references_output.push_str("\n"); + } + } + + if !self.references.implementation.is_empty() { + references_output.push_str("- about the implementation of this model:\n"); + for reference in self.references.implementation.iter() { + references_output.push_str(" * "); + wrap_80_chars(&mut references_output, reference, " "); + references_output.push_str("\n"); + } + } + + if !references_output.is_empty() { + output.push_str("\nModel references\n----------------\n\n"); + output.push_str("Please cite the following references when using this model:\n"); + output.push_str(&references_output); + } + + output + } +} + /// The data type of a model, used for all inputs and outputs. The model can /// still internally use a different data type for its calculations, but it will /// get inputs in this type and must produce outputs in this type. @@ -486,6 +634,7 @@ impl<'a> TryFrom<&'a JsonValue> for ModelCapabilities { } } + #[cfg(test)] mod tests { mod pair_list_options { @@ -602,7 +751,8 @@ mod tests { } mod model_metadata { - use super::super::*; + +use super::super::*; fn example() -> ModelMetadata { ModelMetadata { @@ -704,6 +854,38 @@ mod tests { assert_eq!(error.to_string(), expected); } } + + #[test] + fn printing() { + let metadata = example(); + let output = metadata.print(); + let expected = String::from( + "This is the test-model model +============================ + +A test model + +Model authors +------------- + +- Alice +- Bob + +Model references +---------------- + +Please cite the following references when using this model: +- about this specific model: + * doi:10.1234/test +- about the architecture of this model: + * doi:10.1234/arch +- about the implementation of this model: + * https://github.com/test +" +); + + assert_eq!(output, expected); + } } mod model_capabilities { diff --git a/metatomic-core/tests/misc.cpp b/metatomic-core/tests/misc.cpp index 8c66d756..93baecb6 100644 --- a/metatomic-core/tests/misc.cpp +++ b/metatomic-core/tests/misc.cpp @@ -70,3 +70,49 @@ TEST_CASE("mta_unit_conversion_factor") { "invalid parameter: dimension mismatch in unit conversion: " "'m' has dimension [L] but 'kg' has dimension [M]"); } + +TEST_CASE("mta_format_metadata") { + std::string json =R"({ + "type": "metatomic_model_metadata", + "name": "name", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.", + "authors": ["Short author", "Some extremely long author that will take more than one line in the printed output"], + "references": { + "architecture": ["ref-2", "ref-3"], + "model": ["a very long reference that will take more than one line in the printed output"], + "implementation": [] + }, + "extra": {} +})"; + auto* mta_string = mta_string_create(""); + REQUIRE(mta_string != nullptr); + auto status = mta_format_metadata(json.c_str(), &mta_string); + REQUIRE(status == MTA_SUCCESS); + const auto expected = R"(This is the name model +====================== + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation. + +Model authors +------------- + +- Short author +- Some extremely long author that will take more than one line in the printed + output + +Model references +---------------- + +Please cite the following references when using this model: +- about this specific model: + * a very long reference that will take more than one line in the printed + output +- about the architecture of this model: + * ref-2 + * ref-3 +)"; + CHECK(std::string(mta_string_view(mta_string)) == expected); + mta_string_free(mta_string); +} From 001cf65f7df4baeab940e47ca1205237c9c10224 Mon Sep 17 00:00:00 2001 From: GardevoirX Date: Mon, 1 Jun 2026 16:57:59 +0200 Subject: [PATCH 2/2] Do not panic! --- metatomic-core/src/metadata.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/metatomic-core/src/metadata.rs b/metatomic-core/src/metadata.rs index dcff8f1c..7c940d67 100644 --- a/metatomic-core/src/metadata.rs +++ b/metatomic-core/src/metadata.rs @@ -201,7 +201,7 @@ fn normalize_whitespace(data: &str) -> String { } -fn wrap_80_chars(output: &mut String, data: &str, indent: &str) { +fn wrap_80_chars(output: &mut String, data: &str, indent: &str) -> Result<(), Error> { let string = normalize_whitespace(data); let mut chars: Vec = string.chars().collect(); let line_length = 80 - indent.len(); @@ -236,10 +236,12 @@ fn wrap_80_chars(output: &mut String, data: &str, indent: &str) { if !word_found { // this is only hit if a single word takes a full line. - panic!("some words are too long to be wrapped, make them shorter"); + return Err(Error::InvalidParameter("some words are too long to be wrapped, make them shorter".into())); } } } + + Ok(()) } @@ -325,21 +327,23 @@ impl<'a> TryFrom<&'a JsonValue> for ModelMetadata { extra.insert(key.to_string(), value.to_string()); } - Ok(ModelMetadata { + let metadata = ModelMetadata { name: name.to_string(), authors: authors, description: description, references: references, extra: extra, - }) + }; + metadata.validate()?; + Ok(metadata) } } impl ModelMetadata{ - fn validate(&self) { + fn validate(&self) -> Result<(), Error> { for author in &self.authors { if author.is_empty() { - panic!("author can not be empty string in ModelMetadata"); + return Err(Error::InvalidParameter("author can not be empty string in ModelMetadata".into())); } } @@ -350,24 +354,24 @@ impl ModelMetadata{ } = &self.references; for m in model.iter() { if m.is_empty() { - panic!("reference can not be empty string (in 'model' section)"); + return Err(Error::InvalidParameter("reference can not be empty string (in 'model' section)".into())); } } for a in architecture.iter() { if a.is_empty() { - panic!("reference can not be empty string (in 'architecture' section)"); + return Err(Error::InvalidParameter("reference can not be empty string (in 'architecture' section)".into())); } } for i in implementation.iter() { if i.is_empty() { - panic!("reference can not be empty string (in 'implementation' section)"); + return Err(Error::InvalidParameter("reference can not be empty string (in 'implementation' section)".into())); } } + Ok(()) } pub fn print(&self) -> String { let mut output = String::new(); - self.validate(); if self.name.is_empty() { output.push_str("This is an unnamed model\n"); output.push_str("========================\n"); @@ -377,7 +381,7 @@ impl ModelMetadata{ } if !self.description.is_empty() { output.push_str("\n"); - wrap_80_chars(&mut output, &(self.description), ""); + let _ = wrap_80_chars(&mut output, &(self.description), ""); output.push_str("\n"); } @@ -385,7 +389,7 @@ impl ModelMetadata{ output.push_str("\nModel authors\n-------------\n\n"); for author in self.authors.iter() { output.push_str("- "); - wrap_80_chars(&mut output, &author, " "); + let _ = wrap_80_chars(&mut output, &author, " "); output.push_str("\n"); } } @@ -395,7 +399,7 @@ impl ModelMetadata{ references_output.push_str("- about this specific model:\n"); for reference in self.references.model.iter() { references_output.push_str(" * "); - wrap_80_chars(&mut references_output, &reference, " "); + let _ = wrap_80_chars(&mut references_output, &reference, " "); references_output.push_str("\n"); } } @@ -404,7 +408,7 @@ impl ModelMetadata{ references_output.push_str("- about the architecture of this model:\n"); for reference in self.references.architecture.iter() { references_output.push_str(" * "); - wrap_80_chars(&mut references_output, reference, " "); + let _ = wrap_80_chars(&mut references_output, reference, " "); references_output.push_str("\n"); } } @@ -413,7 +417,7 @@ impl ModelMetadata{ references_output.push_str("- about the implementation of this model:\n"); for reference in self.references.implementation.iter() { references_output.push_str(" * "); - wrap_80_chars(&mut references_output, reference, " "); + let _ = wrap_80_chars(&mut references_output, reference, " "); references_output.push_str("\n"); } }