From a1478bc9604b33df4ad9703c90606d4bd9ba1738 Mon Sep 17 00:00:00 2001 From: adamValent Date: Tue, 6 Feb 2024 21:30:29 +0100 Subject: [PATCH 01/57] Add validation of rule 10201 + 10202 --- src/constants/element.rs | 1 - src/core/validation/constraint.rs | 6 ++- src/core/validation/event.rs | 16 ++++++ src/core/validation/function_definition.rs | 6 ++- src/core/validation/initial_assignment.rs | 6 ++- src/core/validation/math.rs | 59 ++++++++++++++++++++++ src/core/validation/mod.rs | 1 + src/core/validation/reaction.rs | 4 ++ src/core/validation/rule.rs | 8 ++- src/lib.rs | 6 +++ 10 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 src/core/validation/math.rs diff --git a/src/constants/element.rs b/src/constants/element.rs index f75628c..9e9163c 100644 --- a/src/constants/element.rs +++ b/src/constants/element.rs @@ -56,7 +56,6 @@ pub const ALLOWED_ATTRIBUTES: phf::Map<&str, &[&str]> = phf_map! { "eventAssignment" => extended_sbase_attributes!("variable"), }; -// TODO: check if all elements are allowed to have SBASE children and pub const ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { "sbml" => extended_sbase_children!("model"), "model" => extended_sbase_children!("listOfFunctionDefinitions", "listOfUnitDefinitions", "listOfCompartments", "listOfSpecies", "listOfParameters", "listOfInitialAssignments", "listOfRules", "listOfConstraints", "listOfReactions", "listOfEvents"), diff --git a/src/core/validation/constraint.rs b/src/core/validation/constraint.rs index 27a8eca..19833dc 100644 --- a/src/core/validation/constraint.rs +++ b/src/core/validation/constraint.rs @@ -1,10 +1,14 @@ use crate::core::validation::apply_rule_10102; use crate::core::Constraint; -use crate::xml::XmlWrapper; +use crate::xml::{OptionalXmlChild, XmlWrapper}; use crate::SbmlIssue; impl Constraint { pub(crate) fn validate(&self, issues: &mut Vec) { apply_rule_10102(self.xml_element(), issues); + + if let Some(math) = self.math().get() { + math.validate(issues); + } } } diff --git a/src/core/validation/event.rs b/src/core/validation/event.rs index c591bf0..c7aea44 100644 --- a/src/core/validation/event.rs +++ b/src/core/validation/event.rs @@ -35,23 +35,39 @@ impl Event { impl Trigger { pub(crate) fn validate(&self, issues: &mut Vec) { apply_rule_10102(self.xml_element(), issues); + + if let Some(math) = self.math().get() { + math.validate(issues); + } } } impl Priority { pub(crate) fn validate(&self, issues: &mut Vec) { apply_rule_10102(self.xml_element(), issues); + + if let Some(math) = self.math().get() { + math.validate(issues); + } } } impl Delay { pub(crate) fn validate(&self, issues: &mut Vec) { apply_rule_10102(self.xml_element(), issues); + + if let Some(math) = self.math().get() { + math.validate(issues); + } } } impl EventAssignment { pub(crate) fn validate(&self, issues: &mut Vec) { apply_rule_10102(self.xml_element(), issues); + + if let Some(math) = self.math().get() { + math.validate(issues); + } } } diff --git a/src/core/validation/function_definition.rs b/src/core/validation/function_definition.rs index 1db22a5..c0f9530 100644 --- a/src/core/validation/function_definition.rs +++ b/src/core/validation/function_definition.rs @@ -1,10 +1,14 @@ use crate::core::validation::apply_rule_10102; use crate::core::FunctionDefinition; -use crate::xml::XmlWrapper; +use crate::xml::{OptionalXmlChild, XmlWrapper}; use crate::SbmlIssue; impl FunctionDefinition { pub(crate) fn validate(&self, issues: &mut Vec) { apply_rule_10102(self.xml_element(), issues); + + if let Some(math) = self.math().get() { + math.validate(issues); + } } } diff --git a/src/core/validation/initial_assignment.rs b/src/core/validation/initial_assignment.rs index 99aa1de..e64ecd6 100644 --- a/src/core/validation/initial_assignment.rs +++ b/src/core/validation/initial_assignment.rs @@ -1,10 +1,14 @@ use crate::core::validation::apply_rule_10102; use crate::core::InitialAssignment; -use crate::xml::XmlWrapper; +use crate::xml::{OptionalXmlChild, XmlWrapper}; use crate::SbmlIssue; impl InitialAssignment { pub(crate) fn validate(&self, issues: &mut Vec) { apply_rule_10102(self.xml_element(), issues); + + if let Some(math) = self.math().get() { + math.validate(issues); + } } } diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs new file mode 100644 index 0000000..185117a --- /dev/null +++ b/src/core/validation/math.rs @@ -0,0 +1,59 @@ +use crate::constants::namespaces::URL_MATHML; +use crate::core::validation::get_allowed_children; +use crate::core::Math; +use crate::xml::XmlWrapper; +use crate::{SbmlIssue, SbmlIssueSeverity}; +use std::ops::Deref; + +impl Math { + /// ### Rule 10201 + /// is *partially* satisfied by the implementation of the rule + /// [10102](crate::core::validation::apply_rule_10102) as we check each + /// element present for its allowed children (except [Math] element that is + /// the subject of this validation procedure) and thus **MathML** content + /// can be present only within a [Math] element. However, additional check for + /// explicit or implicit valid namespace of a [Math] element must be performed. + pub(crate) fn validate(&self, issues: &mut Vec) { + self.apply_rule_10201(issues); + self.apply_rule_10202(issues); + } + + fn apply_rule_10201(&self, issues: &mut Vec) { + if self.namespace_url() != URL_MATHML { + issues.push(SbmlIssue { + element: self.raw_element(), + message: format!( + "Wrong namespace usage in a math element. Found {0}, but {1} should be used.", + self.namespace_url(), + URL_MATHML + ), + rule: "10201".to_string(), + severity: SbmlIssueSeverity::Error, + }); + } + } + + fn apply_rule_10202(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let children = self.raw_element().children_recursive(doc.deref()); + let allowed_children = get_allowed_children(self.xml_element()); + + for child in children { + if let Some(child_element) = child.as_element() { + let child_tag_name = child_element.name(doc.deref()); + + if !allowed_children.contains(&child_tag_name) { + issues.push(SbmlIssue { + element: child_element, + message: format!( + "Unknown child <{0}> of element <{1}>.", + child_tag_name, "math" + ), + rule: "10202".to_string(), + severity: SbmlIssueSeverity::Error, + }); + } + } + } + } +} diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index 336ad34..ac2853e 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -10,6 +10,7 @@ mod constraint; mod event; mod function_definition; mod initial_assignment; +mod math; mod model; mod parameter; mod reaction; diff --git a/src/core/validation/reaction.rs b/src/core/validation/reaction.rs index cb699be..841818c 100644 --- a/src/core/validation/reaction.rs +++ b/src/core/validation/reaction.rs @@ -73,6 +73,10 @@ impl KineticLaw { if self.local_parameters().is_set() { self.validate_list_of_local_parameters(issues); } + + if let Some(math) = self.math().get() { + math.validate(issues); + } } fn validate_list_of_local_parameters(&self, issues: &mut Vec) { diff --git a/src/core/validation/rule.rs b/src/core/validation/rule.rs index 54d5040..7f6adec 100644 --- a/src/core/validation/rule.rs +++ b/src/core/validation/rule.rs @@ -1,10 +1,14 @@ use crate::core::validation::apply_rule_10102; -use crate::core::AbstractRule; -use crate::xml::XmlWrapper; +use crate::core::{AbstractRule, Rule}; +use crate::xml::{OptionalXmlChild, XmlWrapper}; use crate::SbmlIssue; impl AbstractRule { pub(crate) fn validate(&self, issues: &mut Vec) { apply_rule_10102(self.xml_element(), issues); + + if let Some(math) = self.math().get() { + math.validate(issues); + } } } diff --git a/src/lib.rs b/src/lib.rs index c7abf01..13b8a78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,6 +107,12 @@ impl Sbml { /// is done only in UTF-8 and reading produces error if encoding is different from UTF-8, /// UTF-16, ISO 8859-1, GBK or EUC-KR. /// + /// ### Rule 10102 + /// states that an SBML XML document must not contain undefined elements or attributes in the SBML Level 3 + /// Core namespace or in a SBML Level 3 package namespace. Documents containing unknown + /// elements or attributes placed in an SBML namespace do not conform to the SBML specification. + /// (References: SBML L3V1 Section 4.1; SBML L3V2 Section 4.1.) + /// /// ### Rule 10104 /// is already satisfied implicitly by the use of the package *xml-doc* as loading /// a document without an error ensures that the document conforms to the basic From d028db7a350f9e6e355a9939ad9c95467aad7fcf Mon Sep 17 00:00:00 2001 From: adamValent Date: Tue, 6 Feb 2024 21:35:31 +0100 Subject: [PATCH 02/57] Add comment about validation rule 10202 --- src/core/validation/math.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 185117a..cdfdde1 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -12,7 +12,13 @@ impl Math { /// element present for its allowed children (except [Math] element that is /// the subject of this validation procedure) and thus **MathML** content /// can be present only within a [Math] element. However, additional check for - /// explicit or implicit valid namespace of a [Math] element must be performed. + /// explicit or implicit valid namespace of a [Math] element must be performed. + /// + /// ### Rule 10202 + /// Validates that only allowed subset of **MathML** child elements are present + /// within [Math] element. An SBML package may allow new MathML elements to be + /// added to this list, and if so, the package must define required="true" on + /// the SBML container element . pub(crate) fn validate(&self, issues: &mut Vec) { self.apply_rule_10201(issues); self.apply_rule_10202(issues); From 54fc45469d126b556c29d4d9195c67d50aac9767 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 8 Feb 2024 17:57:53 +0100 Subject: [PATCH 03/57] Do not panic when text node appears as child. --- src/core/validation/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index ac2853e..839885a 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -54,7 +54,7 @@ impl Sbml { root_element .children(doc.deref()) .iter() - .map(|node| node.as_element().unwrap().full_name(doc.deref())) + .filter_map(|node| node.as_element().map(|it| it.full_name(doc.deref()))) .collect(), issues, ); @@ -128,7 +128,7 @@ pub fn apply_rule_10102(xml_element: &XmlElement, issues: &mut Vec) { let children_names = element .children(doc.deref()) .iter() - .map(|node| node.as_element().unwrap().full_name(doc.deref())) + .filter_map(|node| node.as_element().map(|it| it.full_name(doc.deref()))) .collect(); validate_allowed_attributes( From a0e186e4f89e13d6e2701236e917d6ff3121af6b Mon Sep 17 00:00:00 2001 From: Oskar Adam Valent Date: Fri, 9 Feb 2024 09:21:33 +0100 Subject: [PATCH 04/57] Implement rule 10203 --- src/constants/element.rs | 8 +++-- src/core/validation/math.rs | 66 ++++++++++++++++++++++++++++++------- src/core/validation/mod.rs | 13 +++++--- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/constants/element.rs b/src/constants/element.rs index 9e9163c..bd90958 100644 --- a/src/constants/element.rs +++ b/src/constants/element.rs @@ -95,9 +95,11 @@ pub const ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { "priority" => extended_sbase_children!("math"), "delay" => extended_sbase_children!("math"), "listOfEventAssignments" => extended_sbase_children!("eventAssignment"), - "eventAssignment" => extended_sbase_children!("math"), - // partially covers rule 10202 - "math" => &["abs", "and", "annotation", "annotation-xml", "apply", "arccosh", "arccos", "arccoth", + "eventAssignment" => extended_sbase_children!("math") +}; + +pub const MATHML_ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { + "math" => &["abs", "and", "annotation", "annotation-xml", "apply", "arccosh", "arccos", "arccoth", "arccot", "arccsch", "arccsc", "arcsech", "arcsec", "arcsinh", "arcsin", "arctanh", "arctan", "bvar", "ceiling", "ci", "cn", "cosh", "cos", "coth", "cot", "csch", "csc", "csymbol", "degree", "divide", "eq", "exponentiale", "exp", "factorial", "false", diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index cdfdde1..5c07d36 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -1,11 +1,23 @@ +use std::ops::Deref; + +use xml_doc::Element; + use crate::constants::namespaces::URL_MATHML; use crate::core::validation::get_allowed_children; use crate::core::Math; use crate::xml::XmlWrapper; use crate::{SbmlIssue, SbmlIssueSeverity}; -use std::ops::Deref; impl Math { + /// Applies rules: + /// - **[10201](self.apply_rule_10201)** - MathML content is permitted only within [Math] element. + /// - **[10202](self.apply_rule_10202)** - Validates list of permitted elements within [Math] element. + pub(crate) fn validate(&self, issues: &mut Vec) { + self.apply_rule_10201(issues); + self.apply_rule_10202(issues); + self.apply_rule_10203(issues); + } + /// ### Rule 10201 /// is *partially* satisfied by the implementation of the rule /// [10102](crate::core::validation::apply_rule_10102) as we check each @@ -13,17 +25,6 @@ impl Math { /// the subject of this validation procedure) and thus **MathML** content /// can be present only within a [Math] element. However, additional check for /// explicit or implicit valid namespace of a [Math] element must be performed. - /// - /// ### Rule 10202 - /// Validates that only allowed subset of **MathML** child elements are present - /// within [Math] element. An SBML package may allow new MathML elements to be - /// added to this list, and if so, the package must define required="true" on - /// the SBML container element . - pub(crate) fn validate(&self, issues: &mut Vec) { - self.apply_rule_10201(issues); - self.apply_rule_10202(issues); - } - fn apply_rule_10201(&self, issues: &mut Vec) { if self.namespace_url() != URL_MATHML { issues.push(SbmlIssue { @@ -39,6 +40,12 @@ impl Math { } } + // TODO: Complete implementation when adding extensions/packages is solved + /// ### Rule 10202 + /// Validates that only allowed subset of **MathML** child elements are present + /// within [Math] element. An SBML package may allow new MathML elements to be + /// added to this list, and if so, the package must define required="true" on + /// the SBML container element . fn apply_rule_10202(&self, issues: &mut Vec) { let doc = self.read_doc(); let children = self.raw_element().children_recursive(doc.deref()); @@ -62,4 +69,39 @@ impl Math { } } } + + // TODO: Complete implementation when adding extensions/packages is solved + /// ### Rule 10203 + /// In the SBML subset of MathML 2.0, the MathML attribute encoding is only permitted on + /// **csymbol**, **annotation** and **annotation-xml**. No other MathML elements may have + /// an encoding attribute. An SBML package may allow the encoding attribute on other + /// elements, and if so, the package must define required=“true” on the SBML container element . + fn apply_rule_10203(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let allowed = ["csymbol", "annotation", "annotation-xml"]; + let children: Vec = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.attribute(doc.deref(), "encoding").is_some()) + .copied() + .collect(); + + for child in children { + let name = child.name(doc.deref()); + + if !allowed.contains(&name) { + issues.push(SbmlIssue { + element: child, + message: format!( + "Attribute [encoding] found on element <{0}>, which is forbidden. \ + Attribute [encoding] is only permitted on , and .", + name + ), + rule: "10203".to_string(), + severity: SbmlIssueSeverity::Error, + }); + } + } + } } diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index 839885a..1dd312e 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -1,10 +1,12 @@ -use crate::constants::element::{ALLOWED_ATTRIBUTES, ALLOWED_CHILDREN}; -use crate::xml::{XmlElement, XmlWrapper}; -use crate::{Sbml, SbmlIssue, SbmlIssueSeverity}; use std::collections::HashMap; use std::ops::Deref; + use xml_doc::Element; +use crate::constants::element::{ALLOWED_ATTRIBUTES, ALLOWED_CHILDREN, MATHML_ALLOWED_CHILDREN}; +use crate::xml::{XmlElement, XmlWrapper}; +use crate::{Sbml, SbmlIssue, SbmlIssueSeverity}; + mod compartment; mod constraint; mod event; @@ -149,7 +151,10 @@ pub fn apply_rule_10102(xml_element: &XmlElement, issues: &mut Vec) { pub(crate) fn get_allowed_children(xml_element: &XmlElement) -> &'static [&'static str] { let Some(allowed) = ALLOWED_CHILDREN.get(xml_element.tag_name().as_str()) else { - return &[]; + let Some(allowed) = MATHML_ALLOWED_CHILDREN.get(xml_element.tag_name().as_str()) else { + return &[]; + }; + return allowed; }; allowed } From d47706a88a8d8462fa4b91e8c46e85d665a27ad5 Mon Sep 17 00:00:00 2001 From: Oskar Adam Valent Date: Fri, 9 Feb 2024 10:06:21 +0100 Subject: [PATCH 05/57] Implement rule 10204 --- src/constants/element.rs | 7 ++++++- src/core/validation/math.rs | 39 ++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/constants/element.rs b/src/constants/element.rs index bd90958..1037262 100644 --- a/src/constants/element.rs +++ b/src/constants/element.rs @@ -99,7 +99,7 @@ pub const ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { }; pub const MATHML_ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { - "math" => &["abs", "and", "annotation", "annotation-xml", "apply", "arccosh", "arccos", "arccoth", + "math" => &["abs", "and", "annotation", "annotation-xml", "apply", "arccosh", "arccos", "arccoth", "arccot", "arccsch", "arccsc", "arcsech", "arcsec", "arcsinh", "arcsin", "arctanh", "arctan", "bvar", "ceiling", "ci", "cn", "cosh", "cos", "coth", "cot", "csch", "csc", "csymbol", "degree", "divide", "eq", "exponentiale", "exp", "factorial", "false", @@ -108,3 +108,8 @@ pub const MATHML_ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { "piece", "pi", "plus", "power", "quotient", "rem", "root", "sech", "sec", "semantics", "sep", "sinh", "sin", "tanh", "tan", "times", "true", "xor"] }; + +pub const MATHML_ALLOWED_CHILDREN_BY_ATTR: phf::Map<&str, &[&str]> = phf_map! { + "encoding" => &["csymbol", "annotation", "annotation-xml"], + "definitionURL" => &["ci", "csymbol", "semantics"], +}; diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 5c07d36..cfc18ab 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -2,6 +2,7 @@ use std::ops::Deref; use xml_doc::Element; +use crate::constants::element::MATHML_ALLOWED_CHILDREN_BY_ATTR; use crate::constants::namespaces::URL_MATHML; use crate::core::validation::get_allowed_children; use crate::core::Math; @@ -16,6 +17,7 @@ impl Math { self.apply_rule_10201(issues); self.apply_rule_10202(issues); self.apply_rule_10203(issues); + self.apply_rule_10204(issues); } /// ### Rule 10201 @@ -78,7 +80,7 @@ impl Math { /// elements, and if so, the package must define required=“true” on the SBML container element . fn apply_rule_10203(&self, issues: &mut Vec) { let doc = self.read_doc(); - let allowed = ["csymbol", "annotation", "annotation-xml"]; + let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["encoding"]; let children: Vec = self .raw_element() .child_elements_recursive(doc.deref()) @@ -104,4 +106,39 @@ impl Math { } } } + + // TODO: Complete implementation when adding extensions/packages is solved + /// ### Rule 10204 + /// In the SBML subset of MathML 2.0, the MathML attribute definitionURL is only permitted on + /// **ci**, **csymbol** and **semantics**. No other MathML elements may have a definitionURL attribute. An + /// SBML package may allow the definitionURL attribute on other elements, and if so, the package + /// must define required=“true” on the SBML container element . + fn apply_rule_10204(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["definitionURL"]; + let children: Vec = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.attribute(doc.deref(), "definitionURL").is_some()) + .copied() + .collect(); + + for child in children { + let name = child.name(doc.deref()); + + if !allowed.contains(&name) { + issues.push(SbmlIssue { + element: child, + message: format!( + "Attribute [definitionURL] found on element <{0}>, which is forbidden. \ + Attribute [definitionURL] is only permitted on , and .", + name + ), + rule: "10204".to_string(), + severity: SbmlIssueSeverity::Error, + }); + } + } + } } From b4878ab776295cdd4ff1e32088e7e485dfcb8009 Mon Sep 17 00:00:00 2001 From: Oskar Adam Valent Date: Fri, 9 Feb 2024 10:29:20 +0100 Subject: [PATCH 06/57] Implement rule 10205 --- src/constants/element.rs | 7 +++++++ src/core/validation/math.rs | 40 ++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/constants/element.rs b/src/constants/element.rs index 1037262..2279160 100644 --- a/src/constants/element.rs +++ b/src/constants/element.rs @@ -113,3 +113,10 @@ pub const MATHML_ALLOWED_CHILDREN_BY_ATTR: phf::Map<&str, &[&str]> = phf_map! { "encoding" => &["csymbol", "annotation", "annotation-xml"], "definitionURL" => &["ci", "csymbol", "semantics"], }; + +pub const MATHML_ALLOWED_DEFINITION_URLS: &[&str] = &[ + "http://www.sbml.org/sbml/symbols/time", + "http://www.sbml.org/sbml/symbols/delay", + "http://www.sbml.org/sbml/symbols/avogadro", + "http://www.sbml.org/sbml/symbols/rateOf", +]; diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index cfc18ab..0431e04 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -2,12 +2,12 @@ use std::ops::Deref; use xml_doc::Element; -use crate::constants::element::MATHML_ALLOWED_CHILDREN_BY_ATTR; +use crate::{SbmlIssue, SbmlIssueSeverity}; +use crate::constants::element::{MATHML_ALLOWED_CHILDREN_BY_ATTR, MATHML_ALLOWED_DEFINITION_URLS}; use crate::constants::namespaces::URL_MATHML; -use crate::core::validation::get_allowed_children; use crate::core::Math; +use crate::core::validation::get_allowed_children; use crate::xml::XmlWrapper; -use crate::{SbmlIssue, SbmlIssueSeverity}; impl Math { /// Applies rules: @@ -18,6 +18,7 @@ impl Math { self.apply_rule_10202(issues); self.apply_rule_10203(issues); self.apply_rule_10204(issues); + self.apply_rule_10205(issues); } /// ### Rule 10201 @@ -141,4 +142,37 @@ impl Math { } } } + + // TODO: Complete implementation when adding extensions/packages is solved + /// ### Rule 10205 + /// In SBML Level 3, the only values permitted for the attribute definitionURL on a csymbol are + /// “http://www.sbml.org/sbml/symbols/time”, “http://www.sbml.org/sbml/symbols/delay”, + /// “http://www.sbml.org/sbml/symbols/avogadro”, and “http://www.sbml.org/sbml/symbols/rateOf”. + /// An SBML package may allow new values for the definitionURL attribute of a csymbol, and if so, + /// the package must define required=“true” on the SBML container element . + fn apply_rule_10205(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let children: Vec = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| { + child.attribute(doc.deref(), "definitionURL").is_some() + && child.name(doc.deref()) == "csymbol" + }) + .copied() + .collect(); + + for child in children { + let value = child.attribute(doc.deref(), "definitionURL").unwrap(); + if !MATHML_ALLOWED_DEFINITION_URLS.contains(&value) { + issues.push(SbmlIssue { + element: child, + message: format!("Invalid definitionURL value found '{}'.", value), + rule: "10205".to_string(), + severity: SbmlIssueSeverity::Error, + }); + } + } + } } From 1fd04a8305b01a366806409184edf20cd66cbec7 Mon Sep 17 00:00:00 2001 From: Oskar Adam Valent Date: Fri, 9 Feb 2024 10:37:13 +0100 Subject: [PATCH 07/57] Implement rule 10206 --- src/constants/element.rs | 1 + src/core/validation/math.rs | 41 ++++++++++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/constants/element.rs b/src/constants/element.rs index 2279160..0386b87 100644 --- a/src/constants/element.rs +++ b/src/constants/element.rs @@ -112,6 +112,7 @@ pub const MATHML_ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { pub const MATHML_ALLOWED_CHILDREN_BY_ATTR: phf::Map<&str, &[&str]> = phf_map! { "encoding" => &["csymbol", "annotation", "annotation-xml"], "definitionURL" => &["ci", "csymbol", "semantics"], + "type" => &["cn"] }; pub const MATHML_ALLOWED_DEFINITION_URLS: &[&str] = &[ diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 0431e04..8d3f838 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -2,12 +2,12 @@ use std::ops::Deref; use xml_doc::Element; -use crate::{SbmlIssue, SbmlIssueSeverity}; use crate::constants::element::{MATHML_ALLOWED_CHILDREN_BY_ATTR, MATHML_ALLOWED_DEFINITION_URLS}; use crate::constants::namespaces::URL_MATHML; -use crate::core::Math; use crate::core::validation::get_allowed_children; +use crate::core::Math; use crate::xml::XmlWrapper; +use crate::{SbmlIssue, SbmlIssueSeverity}; impl Math { /// Applies rules: @@ -19,6 +19,7 @@ impl Math { self.apply_rule_10203(issues); self.apply_rule_10204(issues); self.apply_rule_10205(issues); + self.apply_rule_10206(issues); } /// ### Rule 10201 @@ -168,11 +169,45 @@ impl Math { if !MATHML_ALLOWED_DEFINITION_URLS.contains(&value) { issues.push(SbmlIssue { element: child, - message: format!("Invalid definitionURL value found '{}'.", value), + message: format!("Invalid definitionURL value found '{0}'.", value), rule: "10205".to_string(), severity: SbmlIssueSeverity::Error, }); } } } + + // TODO: Complete implementation when adding extensions/packages is solved + /// ### Rule 10206 + /// In the SBML subset of MathML 2.0, the MathML attribute type is only permitted on the cn + /// construct. No other MathML elements may have a type attribute. An SBML package may allow the + /// type attribute on other elements, and if so, the package must define required=“true” on the SBML + /// container element . + fn apply_rule_10206(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let children: Vec = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.attribute(doc.deref(), "type").is_some()) + .copied() + .collect(); + + for child in children { + let name = child.name(doc.deref()); + + if !MATHML_ALLOWED_CHILDREN_BY_ATTR["type"].contains(&name) { + issues.push(SbmlIssue { + element: child, + message: format!( + "Attribute [type] found on element <{0}>, which is forbidden. \ + Attribute [type] is only permitted on .", + name + ), + rule: "10204".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } + } + } } From 87fc594b4043efa3177c5f831af436bdd3097a03 Mon Sep 17 00:00:00 2001 From: Oskar Adam Valent Date: Fri, 9 Feb 2024 12:19:37 +0100 Subject: [PATCH 08/57] Implement rule 10206 - not complete --- src/constants/element.rs | 2 ++ src/core/validation/math.rs | 49 ++++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/constants/element.rs b/src/constants/element.rs index 0386b87..e53e095 100644 --- a/src/constants/element.rs +++ b/src/constants/element.rs @@ -121,3 +121,5 @@ pub const MATHML_ALLOWED_DEFINITION_URLS: &[&str] = &[ "http://www.sbml.org/sbml/symbols/avogadro", "http://www.sbml.org/sbml/symbols/rateOf", ]; + +pub const MATHML_ALLOWED_TYPES: &[&str] = &["e-notation", "real", "integer", "rational"]; diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 8d3f838..003f674 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -2,12 +2,14 @@ use std::ops::Deref; use xml_doc::Element; -use crate::constants::element::{MATHML_ALLOWED_CHILDREN_BY_ATTR, MATHML_ALLOWED_DEFINITION_URLS}; +use crate::{SbmlIssue, SbmlIssueSeverity}; +use crate::constants::element::{ + MATHML_ALLOWED_CHILDREN_BY_ATTR, MATHML_ALLOWED_DEFINITION_URLS, MATHML_ALLOWED_TYPES, +}; use crate::constants::namespaces::URL_MATHML; -use crate::core::validation::get_allowed_children; use crate::core::Math; +use crate::core::validation::get_allowed_children; use crate::xml::XmlWrapper; -use crate::{SbmlIssue, SbmlIssueSeverity}; impl Math { /// Applies rules: @@ -20,6 +22,7 @@ impl Math { self.apply_rule_10204(issues); self.apply_rule_10205(issues); self.apply_rule_10206(issues); + self.apply_rule_10207(issues); } /// ### Rule 10201 @@ -79,7 +82,7 @@ impl Math { /// In the SBML subset of MathML 2.0, the MathML attribute encoding is only permitted on /// **csymbol**, **annotation** and **annotation-xml**. No other MathML elements may have /// an encoding attribute. An SBML package may allow the encoding attribute on other - /// elements, and if so, the package must define required=“true” on the SBML container element . + /// elements, and if so, the package must define required="true" on the SBML container element . fn apply_rule_10203(&self, issues: &mut Vec) { let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["encoding"]; @@ -114,7 +117,7 @@ impl Math { /// In the SBML subset of MathML 2.0, the MathML attribute definitionURL is only permitted on /// **ci**, **csymbol** and **semantics**. No other MathML elements may have a definitionURL attribute. An /// SBML package may allow the definitionURL attribute on other elements, and if so, the package - /// must define required=“true” on the SBML container element . + /// must define required="true" on the SBML container element . fn apply_rule_10204(&self, issues: &mut Vec) { let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["definitionURL"]; @@ -147,10 +150,10 @@ impl Math { // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10205 /// In SBML Level 3, the only values permitted for the attribute definitionURL on a csymbol are - /// “http://www.sbml.org/sbml/symbols/time”, “http://www.sbml.org/sbml/symbols/delay”, - /// “http://www.sbml.org/sbml/symbols/avogadro”, and “http://www.sbml.org/sbml/symbols/rateOf”. + /// "http://www.sbml.org/sbml/symbols/time", "http://www.sbml.org/sbml/symbols/delay", + /// "http://www.sbml.org/sbml/symbols/avogadro", and "http://www.sbml.org/sbml/symbols/rateOf". /// An SBML package may allow new values for the definitionURL attribute of a csymbol, and if so, - /// the package must define required=“true” on the SBML container element . + /// the package must define required="true" on the SBML container element . fn apply_rule_10205(&self, issues: &mut Vec) { let doc = self.read_doc(); let children: Vec = self @@ -181,7 +184,7 @@ impl Math { /// ### Rule 10206 /// In the SBML subset of MathML 2.0, the MathML attribute type is only permitted on the cn /// construct. No other MathML elements may have a type attribute. An SBML package may allow the - /// type attribute on other elements, and if so, the package must define required=“true” on the SBML + /// type attribute on other elements, and if so, the package must define required="true" on the SBML /// container element . fn apply_rule_10206(&self, issues: &mut Vec) { let doc = self.read_doc(); @@ -210,4 +213,32 @@ impl Math { } } } + + /// ### Rule 10207 + /// The only permitted values for the attribute type on MathML cn elements are "**e-notation**", "**real**", + /// "**integer**", and "**rational**". An SBML package may allow new values for the type attribute, and if + /// so, the package must define required="true" on the SBML container element . + fn apply_rule_10207(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let children: Vec = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.attribute(doc.deref(), "type").is_some()) + .copied() + .collect(); + + for child in children { + let value = child.attribute(doc.deref(), "type").unwrap(); + + if !MATHML_ALLOWED_TYPES.contains(&value) { + issues.push(SbmlIssue { + element: child, + message: format!("Invalid type value found '{0}'.", value), + rule: "10206".to_string(), + severity: SbmlIssueSeverity::Error, + }); + } + } + } } From f5aff6d8150ec430b92488d7f53ce12d930b8ddf Mon Sep 17 00:00:00 2001 From: adamValent Date: Sun, 11 Feb 2024 13:37:12 +0100 Subject: [PATCH 09/57] Implement rule 10207 - complete --- src/core/validation/math.rs | 41 ++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 003f674..338e696 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -2,14 +2,14 @@ use std::ops::Deref; use xml_doc::Element; -use crate::{SbmlIssue, SbmlIssueSeverity}; use crate::constants::element::{ MATHML_ALLOWED_CHILDREN_BY_ATTR, MATHML_ALLOWED_DEFINITION_URLS, MATHML_ALLOWED_TYPES, }; use crate::constants::namespaces::URL_MATHML; -use crate::core::Math; use crate::core::validation::get_allowed_children; +use crate::core::Math; use crate::xml::XmlWrapper; +use crate::{SbmlIssue, SbmlIssueSeverity}; impl Math { /// Applies rules: @@ -79,9 +79,9 @@ impl Math { // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10203 - /// In the SBML subset of MathML 2.0, the MathML attribute encoding is only permitted on + /// In the SBML subset of MathML 2.0, the MathML attribute **encoding** is only permitted on /// **csymbol**, **annotation** and **annotation-xml**. No other MathML elements may have - /// an encoding attribute. An SBML package may allow the encoding attribute on other + /// an **encoding** attribute. An SBML package may allow the encoding attribute on other /// elements, and if so, the package must define required="true" on the SBML container element . fn apply_rule_10203(&self, issues: &mut Vec) { let doc = self.read_doc(); @@ -114,8 +114,8 @@ impl Math { // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10204 - /// In the SBML subset of MathML 2.0, the MathML attribute definitionURL is only permitted on - /// **ci**, **csymbol** and **semantics**. No other MathML elements may have a definitionURL attribute. An + /// In the SBML subset of MathML 2.0, the MathML attribute **definitionURL** is only permitted on + /// **ci**, **csymbol** and **semantics**. No other MathML elements may have a **definitionURL** attribute. An /// SBML package may allow the definitionURL attribute on other elements, and if so, the package /// must define required="true" on the SBML container element . fn apply_rule_10204(&self, issues: &mut Vec) { @@ -149,9 +149,9 @@ impl Math { // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10205 - /// In SBML Level 3, the only values permitted for the attribute definitionURL on a csymbol are - /// "http://www.sbml.org/sbml/symbols/time", "http://www.sbml.org/sbml/symbols/delay", - /// "http://www.sbml.org/sbml/symbols/avogadro", and "http://www.sbml.org/sbml/symbols/rateOf". + /// In SBML Level 3, the only values permitted for the attribute **definitionURL** on a **csymbol** are + /// "**http://www.sbml.org/sbml/symbols/time**", "**http://www.sbml.org/sbml/symbols/delay**", + /// "**http://www.sbml.org/sbml/symbols/avogadro**", and "**http://www.sbml.org/sbml/symbols/rateOf**". /// An SBML package may allow new values for the definitionURL attribute of a csymbol, and if so, /// the package must define required="true" on the SBML container element . fn apply_rule_10205(&self, issues: &mut Vec) { @@ -172,7 +172,10 @@ impl Math { if !MATHML_ALLOWED_DEFINITION_URLS.contains(&value) { issues.push(SbmlIssue { element: child, - message: format!("Invalid definitionURL value found '{0}'.", value), + message: format!("Invalid definitionURL value found '{0}'. \ + Permitted values are: 'http://www.sbml.org/sbml/symbols/time', \ + 'http://www.sbml.org/sbml/symbols/delay', 'http://www.sbml.org/sbml/symbols/avogadro' \ + and 'http://www.sbml.org/sbml/symbols/rateOf'", value), rule: "10205".to_string(), severity: SbmlIssueSeverity::Error, }); @@ -182,8 +185,8 @@ impl Math { // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10206 - /// In the SBML subset of MathML 2.0, the MathML attribute type is only permitted on the cn - /// construct. No other MathML elements may have a type attribute. An SBML package may allow the + /// In the SBML subset of MathML 2.0, the MathML attribute **type** is only permitted on the **cn** + /// construct. **No** other MathML elements may have a type attribute. An SBML package may allow the /// type attribute on other elements, and if so, the package must define required="true" on the SBML /// container element . fn apply_rule_10206(&self, issues: &mut Vec) { @@ -214,10 +217,12 @@ impl Math { } } + // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10207 - /// The only permitted values for the attribute type on MathML cn elements are "**e-notation**", "**real**", - /// "**integer**", and "**rational**". An SBML package may allow new values for the type attribute, and if - /// so, the package must define required="true" on the SBML container element . + /// The only permitted values for the attribute **type** on MathML cn elements are + /// "**e-notation**", "**real**", "**integer**", and "**rational**". An SBML package may + /// allow new values for the type attribute, and if so, the package must define required="true" + /// on the SBML container element . fn apply_rule_10207(&self, issues: &mut Vec) { let doc = self.read_doc(); let children: Vec = self @@ -234,7 +239,11 @@ impl Math { if !MATHML_ALLOWED_TYPES.contains(&value) { issues.push(SbmlIssue { element: child, - message: format!("Invalid type value found '{0}'.", value), + message: format!( + "Invalid type value found '{0}'. Permitted values are: \ + 'e-notation', 'real', 'integer' and 'rational'", + value + ), rule: "10206".to_string(), severity: SbmlIssueSeverity::Error, }); From eb4cc7eb027bcd584b8b16e4526a7843554fa766 Mon Sep 17 00:00:00 2001 From: adamValent Date: Sun, 11 Feb 2024 16:03:32 +0100 Subject: [PATCH 10/57] Implement rule 10208 --- src/core/validation/math.rs | 134 +++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 25 deletions(-) diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 338e696..7b69c64 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use xml_doc::Element; +use xml_doc::{Document, Element}; use crate::constants::element::{ MATHML_ALLOWED_CHILDREN_BY_ATTR, MATHML_ALLOWED_DEFINITION_URLS, MATHML_ALLOWED_TYPES, @@ -23,6 +23,7 @@ impl Math { self.apply_rule_10205(issues); self.apply_rule_10206(issues); self.apply_rule_10207(issues); + self.apply_rule_10208(issues); } /// ### Rule 10201 @@ -49,10 +50,10 @@ impl Math { // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10202 - /// Validates that only allowed subset of **MathML** child elements are present - /// within [Math] element. An SBML package may allow new MathML elements to be - /// added to this list, and if so, the package must define required="true" on - /// the SBML container element . + /// Validates that only allowed subset of **MathML** child elements are present within [Math] + /// element. An SBML package may allow new MathML elements to be added to this list, and if so, + /// the package must define **required="true"** on the SBML container element + /// [**sbml**](crate::Sbml). fn apply_rule_10202(&self, issues: &mut Vec) { let doc = self.read_doc(); let children = self.raw_element().children_recursive(doc.deref()); @@ -82,7 +83,8 @@ impl Math { /// In the SBML subset of MathML 2.0, the MathML attribute **encoding** is only permitted on /// **csymbol**, **annotation** and **annotation-xml**. No other MathML elements may have /// an **encoding** attribute. An SBML package may allow the encoding attribute on other - /// elements, and if so, the package must define required="true" on the SBML container element . + /// elements, and if so, the package must define **required="true"** on the SBML container + /// element [**sbml**](crate::Sbml). fn apply_rule_10203(&self, issues: &mut Vec) { let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["encoding"]; @@ -114,10 +116,11 @@ impl Math { // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10204 - /// In the SBML subset of MathML 2.0, the MathML attribute **definitionURL** is only permitted on - /// **ci**, **csymbol** and **semantics**. No other MathML elements may have a **definitionURL** attribute. An - /// SBML package may allow the definitionURL attribute on other elements, and if so, the package - /// must define required="true" on the SBML container element . + /// In the SBML subset of MathML 2.0, the MathML attribute **definitionURL** is only permitted + /// on **ci**, **csymbol** and **semantics**. No other MathML elements may have a + /// **definitionURL** attribute. An SBML package may allow the definitionURL attribute on other + /// elements, and if so, the package must define **required="true"** on the SBML container + /// element [**sbml**](crate::Sbml). fn apply_rule_10204(&self, issues: &mut Vec) { let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["definitionURL"]; @@ -149,11 +152,13 @@ impl Math { // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10205 - /// In SBML Level 3, the only values permitted for the attribute **definitionURL** on a **csymbol** are - /// "**http://www.sbml.org/sbml/symbols/time**", "**http://www.sbml.org/sbml/symbols/delay**", - /// "**http://www.sbml.org/sbml/symbols/avogadro**", and "**http://www.sbml.org/sbml/symbols/rateOf**". - /// An SBML package may allow new values for the definitionURL attribute of a csymbol, and if so, - /// the package must define required="true" on the SBML container element . + /// In SBML Level 3, the only values permitted for the attribute **definitionURL** on a + /// **csymbol** are "**http://www.sbml.org/sbml/symbols/time**", + /// "**http://www.sbml.org/sbml/symbols/delay**", + /// "**http://www.sbml.org/sbml/symbols/avogadro**", and + /// "**http://www.sbml.org/sbml/symbols/rateOf**". An SBML package may allow new values for the + /// definitionURL attribute of a csymbol, and if so, the package must define **required="true"** + /// on the SBML container element [**sbml**](crate::Sbml). fn apply_rule_10205(&self, issues: &mut Vec) { let doc = self.read_doc(); let children: Vec = self @@ -172,10 +177,15 @@ impl Math { if !MATHML_ALLOWED_DEFINITION_URLS.contains(&value) { issues.push(SbmlIssue { element: child, - message: format!("Invalid definitionURL value found '{0}'. \ - Permitted values are: 'http://www.sbml.org/sbml/symbols/time', \ - 'http://www.sbml.org/sbml/symbols/delay', 'http://www.sbml.org/sbml/symbols/avogadro' \ - and 'http://www.sbml.org/sbml/symbols/rateOf'", value), + message: format!( + "Invalid definitionURL value found '{0}'. Permitted values are: {1}", + value, + MATHML_ALLOWED_DEFINITION_URLS + .iter() + .map(|url| url.to_string()) + .collect::>() + .join(", ") + ), rule: "10205".to_string(), severity: SbmlIssueSeverity::Error, }); @@ -185,10 +195,11 @@ impl Math { // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10206 - /// In the SBML subset of MathML 2.0, the MathML attribute **type** is only permitted on the **cn** - /// construct. **No** other MathML elements may have a type attribute. An SBML package may allow the - /// type attribute on other elements, and if so, the package must define required="true" on the SBML - /// container element . + /// In the SBML subset of MathML 2.0, the MathML attribute **type** is only permitted on the + /// **cn** + /// construct. **No** other MathML elements may have a type attribute. An SBML package may allow + /// the type attribute on other elements, and if so, the package must define **required="true"** + /// on the SBML container element [**sbml**](crate::Sbml). fn apply_rule_10206(&self, issues: &mut Vec) { let doc = self.read_doc(); let children: Vec = self @@ -221,8 +232,8 @@ impl Math { /// ### Rule 10207 /// The only permitted values for the attribute **type** on MathML cn elements are /// "**e-notation**", "**real**", "**integer**", and "**rational**". An SBML package may - /// allow new values for the type attribute, and if so, the package must define required="true" - /// on the SBML container element . + /// allow new values for the type attribute, and if so, the package must define + /// **required="true"** on the SBML container element [**sbml**](crate::Sbml). fn apply_rule_10207(&self, issues: &mut Vec) { let doc = self.read_doc(); let children: Vec = self @@ -250,4 +261,77 @@ impl Math { } } } + + /// ### Rule 10208 + /// MathML **lambda** elements are only permitted as either the first element inside the + /// [**Math**] element of a [**FunctionDefinition**](crate::core::FunctionDefinition) object, + /// or as the first element of a **semantics** element immediately inside the [**Math**] element + /// of a [**FunctionDefinition**](crate::core::FunctionDefinition) object. MathML **lambda** + /// elements may not be used elsewhere in an SBML model. An SBML package may allow **lambda** + /// elements on other elements, and if so, the package must define **required="true"** on the + /// SBML container element [**sbml**](crate::Sbml). + fn apply_rule_10208(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let children: Vec = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.name(doc.deref()) == "lambda") + .copied() + .collect(); + + for child in children { + let parent = child.parent(doc.deref()).unwrap(); + let parent_name = parent.name(doc.deref()); + + if parent_name == "math" { + let grandparent = parent.parent(doc.deref()).unwrap(); + Self::validate_lambda_placement(doc.deref(), child, parent, grandparent, issues); + } else if parent_name == "semantics" { + let grandparent = parent.parent(doc.deref()).unwrap(); + let top_parent = grandparent.parent(doc.deref()).unwrap(); + Self::validate_lambda_placement(doc.deref(), child, parent, top_parent, issues); + } else { + issues.push(SbmlIssue { + element: child, + message: format!( + "Invalid immediate parent of . Only and are \ + valid immediate parents. Actual parent: <{0}>", + parent_name + ), + rule: "10208".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } + } + } + + /// Checks if: + /// 1. top-level parent of **lambda** is a [**FunctionDefinition**](crate::core::FunctionDefinition). + /// 2. **lambda** is the first child of its immediate parent + fn validate_lambda_placement( + doc: &Document, + child: Element, + parent: Element, + toplevel_parent: Element, + issues: &mut Vec, + ) { + if toplevel_parent.name(doc) != "functionDefinition" { + // the (great)grandparent of must be + issues.push(SbmlIssue { + element: child, + message: format!("The can be present only within (in ). Actual: <{0}>", toplevel_parent.name(doc)), + rule: "10208".to_string(), + severity: SbmlIssueSeverity::Error + }); + } else if *parent.child_elements(doc).first().unwrap() != child { + // the must be the first child inside (or ) + issues.push(SbmlIssue { + element: child, + message: "The must be the first element within .".to_string(), + rule: "10208".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } + } } From d40aab4b4af4b4af74c9d65b633090e739639fef Mon Sep 17 00:00:00 2001 From: adamValent Date: Mon, 12 Feb 2024 08:07:36 +0100 Subject: [PATCH 11/57] Implement rule 10220 - add skeleton for rule 10214 --- src/constants/element.rs | 3 +- src/core/validation/math.rs | 83 ++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/constants/element.rs b/src/constants/element.rs index e53e095..a3aad70 100644 --- a/src/constants/element.rs +++ b/src/constants/element.rs @@ -112,7 +112,8 @@ pub const MATHML_ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { pub const MATHML_ALLOWED_CHILDREN_BY_ATTR: phf::Map<&str, &[&str]> = phf_map! { "encoding" => &["csymbol", "annotation", "annotation-xml"], "definitionURL" => &["ci", "csymbol", "semantics"], - "type" => &["cn"] + "type" => &["cn"], + "units" => &["cn"] }; pub const MATHML_ALLOWED_DEFINITION_URLS: &[&str] = &[ diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 7b69c64..d5ff7fb 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -1,5 +1,4 @@ use std::ops::Deref; - use xml_doc::{Document, Element}; use crate::constants::element::{ @@ -24,6 +23,7 @@ impl Math { self.apply_rule_10206(issues); self.apply_rule_10207(issues); self.apply_rule_10208(issues); + self.apply_rule_10220(issues); } /// ### Rule 10201 @@ -262,6 +262,7 @@ impl Math { } } + // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10208 /// MathML **lambda** elements are only permitted as either the first element inside the /// [**Math**] element of a [**FunctionDefinition**](crate::core::FunctionDefinition) object, @@ -334,4 +335,84 @@ impl Math { }) } } + + // // TODO: load function definition identifiers + // /// ### Rule 10214 + // /// Outside of a [**FunctionDefinition**](crate::core::FunctionDefinition) object, if a MathML + // /// **ci** element is the first element within a MathML apply element, then the **ci** element's + // /// value can only be chosen from the set of identifiers of + // /// [**FunctionDefinition**](crate::core::FunctionDefinition) objects defined in the enclosing + // /// SBML [Model](crate::core::model) object. + // fn apply_rule_10214(&self, issues: &mut Vec) { + // let doc = self.read_doc(); + // let parent_name = self + // .raw_element() + // .parent(doc.deref()) + // .unwrap() + // .name(doc.deref()); + // + // if parent_name != "functionDefinition" { + // let children = self + // .raw_element() + // .child_elements(doc.deref()) + // .iter() + // .filter(|child| { + // child.name(doc.deref()) == "apply" + // && child + // .child_elements(doc.deref()) + // .first() + // .unwrap() + // .name(doc.deref()) + // == "ci" + // }) + // .copied() + // .collect::>(); + // + // // let identifiers = + // // for child in children { + // // let value = child.text_content(doc.deref()); + // // if !identifiers.contains(value) { + // // issues.push(SbmlIssue { + // // element: child, + // // message: format!("Function '{0}' not defined. Function referred by must be defined in object.", value), + // // rule: "10214".to_string(), + // // severity: SbmlIssueSeverity::Error + // // }) + // // } + // // } + // } + // } + + // TODO: Complete implementation when adding extensions/packages is solved + /// The SBML attribute **units** may only be added to MathML **cn** elements; no other MathML elements + /// are permitted to have the **units** attribute. An SBML package may allow the **units** attribute + /// on other elements, and if so, the package must define **required="true"** on the SBML container + /// element [**sbml**](crate::Sbml). + fn apply_rule_10220(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let children: Vec = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.attribute(doc.deref(), "units").is_some()) + .copied() + .collect(); + + for child in children { + let name = child.name(doc.deref()); + + if !MATHML_ALLOWED_CHILDREN_BY_ATTR["units"].contains(&name) { + issues.push(SbmlIssue { + element: child, + message: format!( + "Attribute [units] found on element <{0}>, which is forbidden. \ + Attribute [units] is only permitted on .", + name + ), + rule: "10220".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } + } + } } From 363b2c149f1340d32c913d2df63a6e65b789c2b1 Mon Sep 17 00:00:00 2001 From: adamValent Date: Mon, 12 Feb 2024 09:11:34 +0100 Subject: [PATCH 12/57] Implement rule 10223 --- src/core/validation/math.rs | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index d5ff7fb..6f59224 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -24,6 +24,7 @@ impl Math { self.apply_rule_10207(issues); self.apply_rule_10208(issues); self.apply_rule_10220(issues); + self.apply_rule_10223(issues); } /// ### Rule 10201 @@ -384,6 +385,7 @@ impl Math { // } // TODO: Complete implementation when adding extensions/packages is solved + /// ### Rule 10220 /// The SBML attribute **units** may only be added to MathML **cn** elements; no other MathML elements /// are permitted to have the **units** attribute. An SBML package may allow the **units** attribute /// on other elements, and if so, the package must define **required="true"** on the SBML container @@ -415,4 +417,50 @@ impl Math { } } } + + /// ### Rule 10223 + /// The single argument for the *rateOf* **csymbol** function must be a **ci** element. + fn apply_rule_10223(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let children = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| { + child.name(doc.deref()) == "csymbol" + && child + .attribute(doc.deref(), "definitionURL") + .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf") + }) + .copied() + .collect::>(); + + for child in children { + let child_count = child.child_elements(doc.deref()).len(); + + if child_count != 1 { + issues.push(SbmlIssue { + element: child, + message: format!("Invalid number ({0}) of children in . The must have precisely one child (argument).", child_count), + rule: "10223".to_string(), + severity: SbmlIssueSeverity::Error + }); + continue; + } + + let single_child_name = child + .child_elements(doc.deref()) + .first() + .unwrap() + .name(doc.deref()); + if single_child_name != "ci" { + issues.push(SbmlIssue { + element: child, + message: format!("Invalid child <{0}> of . The must have as its only child (argument).", single_child_name), + rule: "10223".to_string(), + severity: SbmlIssueSeverity::Error + }) + } + } + } } From 5b516a942074d1b4dbac4b42514f29785fd04b38 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Mon, 12 Feb 2024 10:22:27 +0100 Subject: [PATCH 13/57] Add `Model::for_child_element`. --- src/core/model.rs | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/core/model.rs b/src/core/model.rs index 53e7042..1fe334c 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -1,10 +1,13 @@ +use crate::constants::namespaces::URL_SBML_CORE; use crate::core::sbase::SbmlUtils; use crate::core::{ AbstractRule, Compartment, Constraint, Event, FunctionDefinition, InitialAssignment, Parameter, Reaction, Species, UnitDefinition, }; -use crate::xml::{OptionalChild, XmlDefault, XmlDocument, XmlElement, XmlList}; +use crate::xml::{OptionalChild, XmlDefault, XmlDocument, XmlElement, XmlList, XmlWrapper}; use macros::{SBase, XmlWrapper}; +use std::ops::Deref; +use xml_doc::{Document, Element}; /// A type-safe representation of an SBML element. #[derive(Clone, Debug, XmlWrapper, SBase)] @@ -19,6 +22,38 @@ impl XmlDefault for Model { /// Public functions to manipulate with the contents of SBML [Model] /// i.e., optional lists inside SBML model impl Model { + /// Try to find an instance of a `Model` element for the given child element. + /// + /// The child can be any SBML tag, as long as it appears in an SBML model (i.e. one of + /// its transitive parents is a `Model` element). If this is not satisfied, the method + /// returns `None`. + pub fn for_child_element(doc: XmlDocument, child: XmlElement) -> Option { + let parent = { + let read_doc = doc.read().unwrap(); + fn is_model(doc: &Document, e: Element) -> bool { + let name = e.name(doc); + let Some(namespace) = e.namespace(doc) else { + return false; + }; + + name == "model" && namespace == URL_SBML_CORE + } + + let mut parent = child.raw_element(); + while !is_model(read_doc.deref(), parent) { + let Some(e) = parent.parent(read_doc.deref()) else { + return None; + }; + parent = e; + } + + parent + }; + let model = XmlElement::new_raw(doc, parent); + // Safe because we checked that the element has the correct tag name and namespace. + Some(unsafe { Model::unchecked_cast(model) }) + } + pub fn function_definitions(&self) -> OptionalChild> { self.optional_sbml_child("listOfFunctionDefinitions") } From 3c5d09f32dd0faa04f88cf569d1be782db0272d3 Mon Sep 17 00:00:00 2001 From: adamValent Date: Mon, 12 Feb 2024 12:51:50 +0100 Subject: [PATCH 14/57] Implement rule 10214 - add utility function for extraction of identifiers of function definitions - add utility function to convert `XmlList` into `Vec` --- src/core/model.rs | 31 ++++++-- src/core/validation/math.rs | 150 +++++++++++++++++++----------------- src/xml/xml_list.rs | 10 +++ 3 files changed, 117 insertions(+), 74 deletions(-) diff --git a/src/core/model.rs b/src/core/model.rs index 1fe334c..7321ea9 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -2,9 +2,12 @@ use crate::constants::namespaces::URL_SBML_CORE; use crate::core::sbase::SbmlUtils; use crate::core::{ AbstractRule, Compartment, Constraint, Event, FunctionDefinition, InitialAssignment, Parameter, - Reaction, Species, UnitDefinition, + Reaction, SBase, Species, UnitDefinition, +}; +use crate::xml::{ + OptionalChild, OptionalXmlChild, OptionalXmlProperty, XmlDefault, XmlDocument, XmlElement, + XmlList, XmlWrapper, }; -use crate::xml::{OptionalChild, XmlDefault, XmlDocument, XmlElement, XmlList, XmlWrapper}; use macros::{SBase, XmlWrapper}; use std::ops::Deref; use xml_doc::{Document, Element}; @@ -27,7 +30,7 @@ impl Model { /// The child can be any SBML tag, as long as it appears in an SBML model (i.e. one of /// its transitive parents is a `Model` element). If this is not satisfied, the method /// returns `None`. - pub fn for_child_element(doc: XmlDocument, child: XmlElement) -> Option { + pub fn for_child_element(doc: XmlDocument, child: &XmlElement) -> Option { let parent = { let read_doc = doc.read().unwrap(); fn is_model(doc: &Document, e: Element) -> bool { @@ -41,10 +44,10 @@ impl Model { let mut parent = child.raw_element(); while !is_model(read_doc.deref(), parent) { - let Some(e) = parent.parent(read_doc.deref()) else { + let Some(node) = parent.parent(read_doc.deref()) else { return None; }; - parent = e; + parent = node; } parent @@ -58,6 +61,24 @@ impl Model { self.optional_sbml_child("listOfFunctionDefinitions") } + /// Returns a vector of [FunctionDefinition]s' identifiers (attribute **id**). If the identifier is not set, + /// it is not included in the output. + pub(crate) fn function_definition_identifiers(&self) -> Vec { + let function_definitions = self.function_definitions(); + + if function_definitions.is_set() { + function_definitions + .get() + .unwrap() + .as_vec() + .iter() + .filter_map(|def| def.id().get()) + .collect() + } else { + vec![] + } + } + pub fn unit_definitions(&self) -> OptionalChild> { self.optional_sbml_child("listOfUnitDefinitions") } diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 6f59224..936ac2d 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -6,7 +6,7 @@ use crate::constants::element::{ }; use crate::constants::namespaces::URL_MATHML; use crate::core::validation::get_allowed_children; -use crate::core::Math; +use crate::core::{Math, Model}; use crate::xml::XmlWrapper; use crate::{SbmlIssue, SbmlIssueSeverity}; @@ -23,6 +23,7 @@ impl Math { self.apply_rule_10206(issues); self.apply_rule_10207(issues); self.apply_rule_10208(issues); + self.apply_rule_10214(issues); self.apply_rule_10220(issues); self.apply_rule_10223(issues); } @@ -89,15 +90,15 @@ impl Math { fn apply_rule_10203(&self, issues: &mut Vec) { let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["encoding"]; - let children: Vec = self + let children_of_interest = self .raw_element() .child_elements_recursive(doc.deref()) .iter() .filter(|child| child.attribute(doc.deref(), "encoding").is_some()) .copied() - .collect(); + .collect::>(); - for child in children { + for child in children_of_interest { let name = child.name(doc.deref()); if !allowed.contains(&name) { @@ -125,15 +126,15 @@ impl Math { fn apply_rule_10204(&self, issues: &mut Vec) { let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["definitionURL"]; - let children: Vec = self + let children_of_interest = self .raw_element() .child_elements_recursive(doc.deref()) .iter() .filter(|child| child.attribute(doc.deref(), "definitionURL").is_some()) .copied() - .collect(); + .collect::>(); - for child in children { + for child in children_of_interest { let name = child.name(doc.deref()); if !allowed.contains(&name) { @@ -162,7 +163,7 @@ impl Math { /// on the SBML container element [**sbml**](crate::Sbml). fn apply_rule_10205(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children: Vec = self + let children_of_interest = self .raw_element() .child_elements_recursive(doc.deref()) .iter() @@ -171,9 +172,9 @@ impl Math { && child.name(doc.deref()) == "csymbol" }) .copied() - .collect(); + .collect::>(); - for child in children { + for child in children_of_interest { let value = child.attribute(doc.deref(), "definitionURL").unwrap(); if !MATHML_ALLOWED_DEFINITION_URLS.contains(&value) { issues.push(SbmlIssue { @@ -203,15 +204,15 @@ impl Math { /// on the SBML container element [**sbml**](crate::Sbml). fn apply_rule_10206(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children: Vec = self + let children_of_interest = self .raw_element() .child_elements_recursive(doc.deref()) .iter() .filter(|child| child.attribute(doc.deref(), "type").is_some()) .copied() - .collect(); + .collect::>(); - for child in children { + for child in children_of_interest { let name = child.name(doc.deref()); if !MATHML_ALLOWED_CHILDREN_BY_ATTR["type"].contains(&name) { @@ -237,15 +238,15 @@ impl Math { /// **required="true"** on the SBML container element [**sbml**](crate::Sbml). fn apply_rule_10207(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children: Vec = self + let children_of_interest = self .raw_element() .child_elements_recursive(doc.deref()) .iter() .filter(|child| child.attribute(doc.deref(), "type").is_some()) .copied() - .collect(); + .collect::>(); - for child in children { + for child in children_of_interest { let value = child.attribute(doc.deref(), "type").unwrap(); if !MATHML_ALLOWED_TYPES.contains(&value) { @@ -274,15 +275,15 @@ impl Math { /// SBML container element [**sbml**](crate::Sbml). fn apply_rule_10208(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children: Vec = self + let children_of_interest = self .raw_element() .child_elements_recursive(doc.deref()) .iter() .filter(|child| child.name(doc.deref()) == "lambda") .copied() - .collect(); + .collect::>(); - for child in children { + for child in children_of_interest { let parent = child.parent(doc.deref()).unwrap(); let parent_name = parent.name(doc.deref()); @@ -337,52 +338,63 @@ impl Math { } } - // // TODO: load function definition identifiers - // /// ### Rule 10214 - // /// Outside of a [**FunctionDefinition**](crate::core::FunctionDefinition) object, if a MathML - // /// **ci** element is the first element within a MathML apply element, then the **ci** element's - // /// value can only be chosen from the set of identifiers of - // /// [**FunctionDefinition**](crate::core::FunctionDefinition) objects defined in the enclosing - // /// SBML [Model](crate::core::model) object. - // fn apply_rule_10214(&self, issues: &mut Vec) { - // let doc = self.read_doc(); - // let parent_name = self - // .raw_element() - // .parent(doc.deref()) - // .unwrap() - // .name(doc.deref()); - // - // if parent_name != "functionDefinition" { - // let children = self - // .raw_element() - // .child_elements(doc.deref()) - // .iter() - // .filter(|child| { - // child.name(doc.deref()) == "apply" - // && child - // .child_elements(doc.deref()) - // .first() - // .unwrap() - // .name(doc.deref()) - // == "ci" - // }) - // .copied() - // .collect::>(); - // - // // let identifiers = - // // for child in children { - // // let value = child.text_content(doc.deref()); - // // if !identifiers.contains(value) { - // // issues.push(SbmlIssue { - // // element: child, - // // message: format!("Function '{0}' not defined. Function referred by must be defined in object.", value), - // // rule: "10214".to_string(), - // // severity: SbmlIssueSeverity::Error - // // }) - // // } - // // } - // } - // } + /// ### Rule 10214 + /// Outside of a [**FunctionDefinition**](crate::core::FunctionDefinition) object, if a MathML + /// **ci** element is the first element within a MathML apply element, then the **ci** element's + /// value can only be chosen from the set of identifiers of + /// [**FunctionDefinition**](crate::core::FunctionDefinition) objects defined in the enclosing + /// SBML [Model](crate::core::model) object. + fn apply_rule_10214(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let parent_name = self + .raw_element() + .parent(doc.deref()) + .unwrap() + .name(doc.deref()); + + if parent_name != "functionDefinition" { + let children_of_interest = self + .raw_element() + .child_elements(doc.deref()) + .iter() + .filter(|child| { + child.name(doc.deref()) == "apply" + && child + .child_elements(doc.deref()) + .first() + .unwrap() + .name(doc.deref()) + == "ci" + }) + .copied() + .collect::>(); + + let identifiers = Model::for_child_element(self.document(), self.xml_element()) + .unwrap() + .function_definition_identifiers(); + + for child in children_of_interest { + let value = match child.child_elements(doc.deref()).first() { + Some(element) => element.text_content(doc.deref()), + None => "".to_string(), + }; + + if !identifiers.contains(&value) { + issues.push(SbmlIssue { + element: child, + message: format!( + "Function '{0}' not defined. \ + Function referred by must be defined in object \ + with relevant identifier (id).", + value + ), + rule: "10214".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } + } + } + } // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10220 @@ -392,7 +404,7 @@ impl Math { /// element [**sbml**](crate::Sbml). fn apply_rule_10220(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children: Vec = self + let children_of_interest: Vec = self .raw_element() .child_elements_recursive(doc.deref()) .iter() @@ -400,7 +412,7 @@ impl Math { .copied() .collect(); - for child in children { + for child in children_of_interest { let name = child.name(doc.deref()); if !MATHML_ALLOWED_CHILDREN_BY_ATTR["units"].contains(&name) { @@ -422,7 +434,7 @@ impl Math { /// The single argument for the *rateOf* **csymbol** function must be a **ci** element. fn apply_rule_10223(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children = self + let children_of_interest = self .raw_element() .child_elements_recursive(doc.deref()) .iter() @@ -435,7 +447,7 @@ impl Math { .copied() .collect::>(); - for child in children { + for child in children_of_interest { let child_count = child.child_elements(doc.deref()).len(); if child_count != 1 { diff --git a/src/xml/xml_list.rs b/src/xml/xml_list.rs index ad9a022..8801123 100644 --- a/src/xml/xml_list.rs +++ b/src/xml/xml_list.rs @@ -155,6 +155,16 @@ impl XmlList { pub fn is_empty(&self) -> bool { self.len() == 0 } + + pub fn as_vec(&self) -> Vec { + let mut vec: Vec = vec![]; + + for i in 0..self.len() { + vec.push(self.get(i)); + } + + vec + } } // TODO: From 183f681865cd61e3a7d7353b98f79f09c18331b9 Mon Sep 17 00:00:00 2001 From: adamValent Date: Tue, 13 Feb 2024 16:48:24 +0100 Subject: [PATCH 15/57] Implement rule 10216 - prototype --- examples/basic_example.rs | 2 + src/core/function_definition.rs | 36 ++++++++++++++++- src/core/model.rs | 68 ++++++++++++++++++++++----------- src/core/reaction.rs | 55 +++++++++++++++++++++++++- src/core/validation/math.rs | 44 ++++++++++++++++++++- 5 files changed, 179 insertions(+), 26 deletions(-) diff --git a/examples/basic_example.rs b/examples/basic_example.rs index 901643b..5f08aee 100644 --- a/examples/basic_example.rs +++ b/examples/basic_example.rs @@ -7,6 +7,8 @@ use biodivine_lib_sbml::{Sbml, SbmlIssue}; // for the example binary, not for `cargo` itself. fn main() { let doc = Sbml::read_path("test-inputs/COVID19_immunotherapy_Mathematical_Model.xml").unwrap(); + let doc = + Sbml::read_path("test-inputs/cholesterol_metabolism_and_atherosclerosis.xml").unwrap(); // let model = doc.model().get().unwrap(); // Print the whole document: // println!("{}", model.read_doc().write_str().unwrap()); diff --git a/src/core/function_definition.rs b/src/core/function_definition.rs index ad9da17..cade36b 100644 --- a/src/core/function_definition.rs +++ b/src/core/function_definition.rs @@ -1,13 +1,47 @@ +use crate::constants::namespaces::URL_SBML_CORE; use crate::core::sbase::SbmlUtils; use crate::core::Math; -use crate::xml::{OptionalChild, XmlDefault, XmlDocument, XmlElement}; +use crate::xml::{OptionalChild, XmlDefault, XmlDocument, XmlElement, XmlWrapper}; use macros::{SBase, XmlWrapper}; +use std::ops::Deref; +use xml_doc::{Document, Element}; /// Individual function definition #[derive(Clone, Debug, XmlWrapper, SBase)] pub struct FunctionDefinition(XmlElement); impl FunctionDefinition { + /// Try to find an instance of a [FunctionDefinition] element for the given child element. + /// + /// The child can be any SBML tag, as long as it appears in an SBML model (i.e. one of + /// its transitive parents is a [Model] element). If this is not satisfied, the method + /// returns `None`. + pub fn for_child_element(doc: XmlDocument, child: &XmlElement) -> Option { + let parent = { + let read_doc = doc.read().unwrap(); + fn is_function_definition(doc: &Document, e: Element) -> bool { + let name = e.name(doc); + let Some(namespace) = e.namespace(doc) else { + return false; + }; + + name == "functionDefinition" && namespace == URL_SBML_CORE + } + + let mut parent = child.raw_element(); + while !is_function_definition(read_doc.deref(), parent) { + let Some(node) = parent.parent(read_doc.deref()) else { + return None; + }; + parent = node; + } + parent + }; + let model = XmlElement::new_raw(doc, parent); + // Safe because we checked that the element has the correct tag name and namespace. + Some(unsafe { FunctionDefinition::unchecked_cast(model) }) + } + pub fn math(&self) -> OptionalChild { self.optional_math_child("math") } diff --git a/src/core/model.rs b/src/core/model.rs index 7321ea9..cdbeadb 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -5,8 +5,8 @@ use crate::core::{ Reaction, SBase, Species, UnitDefinition, }; use crate::xml::{ - OptionalChild, OptionalXmlChild, OptionalXmlProperty, XmlDefault, XmlDocument, XmlElement, - XmlList, XmlWrapper, + OptionalChild, OptionalXmlChild, OptionalXmlProperty, RequiredXmlProperty, XmlDefault, + XmlDocument, XmlElement, XmlList, XmlWrapper, }; use macros::{SBase, XmlWrapper}; use std::ops::Deref; @@ -25,10 +25,10 @@ impl XmlDefault for Model { /// Public functions to manipulate with the contents of SBML [Model] /// i.e., optional lists inside SBML model impl Model { - /// Try to find an instance of a `Model` element for the given child element. + /// Try to find an instance of a [Model] element for the given child element. /// /// The child can be any SBML tag, as long as it appears in an SBML model (i.e. one of - /// its transitive parents is a `Model` element). If this is not satisfied, the method + /// its transitive parents is a [Model] element). If this is not satisfied, the method /// returns `None`. pub fn for_child_element(doc: XmlDocument, child: &XmlElement) -> Option { let parent = { @@ -61,24 +61,6 @@ impl Model { self.optional_sbml_child("listOfFunctionDefinitions") } - /// Returns a vector of [FunctionDefinition]s' identifiers (attribute **id**). If the identifier is not set, - /// it is not included in the output. - pub(crate) fn function_definition_identifiers(&self) -> Vec { - let function_definitions = self.function_definitions(); - - if function_definitions.is_set() { - function_definitions - .get() - .unwrap() - .as_vec() - .iter() - .filter_map(|def| def.id().get()) - .collect() - } else { - vec![] - } - } - pub fn unit_definitions(&self) -> OptionalChild> { self.optional_sbml_child("listOfUnitDefinitions") } @@ -114,4 +96,46 @@ impl Model { pub fn events(&self) -> OptionalChild> { self.optional_sbml_child("listOfEvents") } + + /// Returns a vector of [FunctionDefinition]s' identifiers (attribute **id**). If the identifier is not set, + /// it is not included in the output. + pub(crate) fn function_definition_identifiers(&self) -> Vec { + let function_definitions = self.function_definitions(); + + if function_definitions.is_set() { + function_definitions + .get() + .unwrap() + .as_vec() + .iter() + .filter_map(|def| def.id().get()) + .collect() + } else { + vec![] + } + } + + /// Returns a vector of all [LocalParameter]s' identifiers (attribute **id**). + pub(crate) fn local_parameter_identifiers(&self) -> Vec { + let reactions = self.reactions(); + let mut vec: Vec = vec![]; + + if reactions.is_set() { + for reaction in reactions.get().unwrap().as_vec() { + let kinetic_law = reaction.kinetic_law(); + + if kinetic_law.is_set() { + let kinetic_law = kinetic_law.get().unwrap(); + let local_params = kinetic_law.local_parameters(); + + if local_params.is_set() { + for param in local_params.get().unwrap().as_vec() { + vec.push(param.id().get()); + } + } + } + } + } + vec + } } diff --git a/src/core/reaction.rs b/src/core/reaction.rs index 8dd6da0..a5fc6ff 100644 --- a/src/core/reaction.rs +++ b/src/core/reaction.rs @@ -1,10 +1,13 @@ +use crate::constants::namespaces::URL_SBML_CORE; use crate::core::sbase::SbmlUtils; use crate::core::{Math, SBase}; use crate::xml::{ - OptionalChild, OptionalProperty, RequiredProperty, RequiredXmlProperty, XmlDefault, - XmlDocument, XmlElement, XmlList, + OptionalChild, OptionalProperty, OptionalXmlChild, RequiredProperty, RequiredXmlProperty, + XmlDefault, XmlDocument, XmlElement, XmlList, XmlWrapper, }; use macros::{SBase, XmlWrapper}; +use std::ops::Deref; +use xml_doc::{Document, Element}; #[derive(Clone, Debug, XmlWrapper, SBase)] pub struct Reaction(XmlElement); @@ -97,6 +100,38 @@ impl XmlDefault for KineticLaw { } impl KineticLaw { + /// Try to find an instance of a [KineticLaw] element for the given child element. + /// + /// The child can be any SBML tag, as long as it appears in an SBML model (i.e. one of + /// its transitive parents is a [KineticLaw] element). If this is not satisfied, the method + /// returns `None`. + pub fn for_child_element(doc: XmlDocument, child: &XmlElement) -> Option { + let parent = { + let read_doc = doc.read().unwrap(); + fn is_kinetic_law(doc: &Document, e: Element) -> bool { + let name = e.name(doc); + let Some(namespace) = e.namespace(doc) else { + return false; + }; + + name == "kineticLaw" && namespace == URL_SBML_CORE + } + + let mut parent = child.raw_element(); + while !is_kinetic_law(read_doc.deref(), parent) { + let Some(node) = parent.parent(read_doc.deref()) else { + return None; + }; + parent = node; + } + + parent + }; + let xml_element = XmlElement::new_raw(doc, parent); + // Safe because we checked that the element has the correct tag name and namespace. + Some(unsafe { KineticLaw::unchecked_cast(xml_element) }) + } + pub fn math(&self) -> OptionalChild { self.optional_math_child("math") } @@ -104,6 +139,22 @@ impl KineticLaw { pub fn local_parameters(&self) -> OptionalChild> { self.optional_sbml_child("listOfLocalParameters") } + + pub(crate) fn local_parameter_identifiers(&self) -> Vec { + let local_parameters = self.local_parameters(); + + if local_parameters.is_set() { + local_parameters + .get() + .unwrap() + .as_vec() + .iter() + .map(|param| param.id().get()) + .collect() + } else { + vec![] + } + } } #[derive(Clone, Debug, XmlWrapper, SBase)] diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 936ac2d..c08b583 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -6,7 +6,7 @@ use crate::constants::element::{ }; use crate::constants::namespaces::URL_MATHML; use crate::core::validation::get_allowed_children; -use crate::core::{Math, Model}; +use crate::core::{FunctionDefinition, KineticLaw, Math, Model}; use crate::xml::XmlWrapper; use crate::{SbmlIssue, SbmlIssueSeverity}; @@ -24,6 +24,8 @@ impl Math { self.apply_rule_10207(issues); self.apply_rule_10208(issues); self.apply_rule_10214(issues); + // self.apply_rule_10215(issues); + self.apply_rule_10216(issues); self.apply_rule_10220(issues); self.apply_rule_10223(issues); } @@ -396,6 +398,46 @@ impl Math { } } + // TODO: needs review + /// ### Rule 10216 + /// The id attribute value of a [LocalParameter] object defined within a [KineticLaw] object may only be + /// used, in core, in MathML ci elements within the math element of that same [KineticLaw]; in other + /// words, the identifier of the [LocalParameter] object is not visible to other parts of the model outside + /// of that [Reaction] instance. In package constructs, the **id** attribute value of a [LocalParameter] object + /// may only be used in MathML ci elements or as the target of an SIdRef attribute if that package + /// construct is a child of the parent [Reaction]. + fn apply_rule_10216(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + let all_local_param_ids = model.local_parameter_identifiers(); + let scoped_local_param_ids = + match KineticLaw::for_child_element(self.document(), self.xml_element()) { + Some(k) => k.local_parameter_identifiers(), + None => vec![], + }; + + let children_of_interest = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.name(doc.deref()) == "ci") + .copied() + .collect::>(); + + for child in children_of_interest { + let value = child.text_content(doc.deref()); + if all_local_param_ids.contains(&value) && !scoped_local_param_ids.contains(&value) { + issues.push(SbmlIssue { + element: child, + message: format!("A identifier '{0}' found out of scope of its ", value), + rule: "10216".to_string(), + severity: SbmlIssueSeverity::Error + }); + } else { + } + } + } + // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10220 /// The SBML attribute **units** may only be added to MathML **cn** elements; no other MathML elements From a036b54b13a863895a38169d491d060bd3475d81 Mon Sep 17 00:00:00 2001 From: adamValent Date: Wed, 14 Feb 2024 11:54:08 +0100 Subject: [PATCH 16/57] Implement rule 10215 --- examples/basic_example.rs | 6 +- src/core/model.rs | 114 ++++++++- src/core/validation/math.rs | 104 +++++++- ...esterol_metabolism_and_atherosclerosis.xml | 222 +++++++++--------- 4 files changed, 318 insertions(+), 128 deletions(-) diff --git a/examples/basic_example.rs b/examples/basic_example.rs index 5f08aee..b089755 100644 --- a/examples/basic_example.rs +++ b/examples/basic_example.rs @@ -6,7 +6,7 @@ use biodivine_lib_sbml::{Sbml, SbmlIssue}; // Note the use of `--` to indicate that ARG_x values are meant as arguments // for the example binary, not for `cargo` itself. fn main() { - let doc = Sbml::read_path("test-inputs/COVID19_immunotherapy_Mathematical_Model.xml").unwrap(); + // let doc = Sbml::read_path("test-inputs/COVID19_immunotherapy_Mathematical_Model.xml").unwrap(); let doc = Sbml::read_path("test-inputs/cholesterol_metabolism_and_atherosclerosis.xml").unwrap(); // let model = doc.model().get().unwrap(); @@ -16,6 +16,8 @@ fn main() { doc.validate(&mut issues); println!("No. of issues: {}", issues.len()); - println!("{:?}", issues); + for issue in issues { + println!("{:?}", issue); + } // assert_eq!(issues.len(), 0); } diff --git a/src/core/model.rs b/src/core/model.rs index cdbeadb..4c81c59 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -129,13 +129,121 @@ impl Model { let local_params = kinetic_law.local_parameters(); if local_params.is_set() { - for param in local_params.get().unwrap().as_vec() { - vec.push(param.id().get()); - } + let mut param_ids = local_params + .get() + .unwrap() + .as_vec() + .iter() + .map(|param| param.id().get()) + .collect::>(); + vec.append(&mut param_ids); } } } } vec } + + /// Returns a vector of all [Species]' identifiers (attribute **id**). + pub(crate) fn species_identifiers(&self) -> Vec { + let species = self.species(); + + if species.is_set() { + species + .get() + .unwrap() + .as_vec() + .iter() + .map(|species| species.id().get()) + .collect() + } else { + vec![] + } + } + + /// Returns a vector of all [Compartment]s' identifiers (attribute **id**). + pub(crate) fn compartment_identifiers(&self) -> Vec { + let compartment = self.compartments(); + + if compartment.is_set() { + compartment + .get() + .unwrap() + .as_vec() + .iter() + .map(|compartment| compartment.id().get()) + .collect() + } else { + vec![] + } + } + + /// Returns a vector of all [Parameter]s' identifiers (attribute **id**). + pub(crate) fn parameter_identifiers(&self) -> Vec { + let parameters = self.parameters(); + + if parameters.is_set() { + parameters + .get() + .unwrap() + .as_vec() + .iter() + .map(|param| param.id().get()) + .collect() + } else { + vec![] + } + } + + /// Returns a vector of all [SpeciesReference](crate::core::SpeciesReference)' identifiers (attribute **id**). + /// If the identifier is not set, it is not included in the output. + pub(crate) fn species_reference_identifiers(&self) -> Vec { + let reactions = self.reactions(); + let mut identifiers: Vec = vec![]; + + if reactions.is_set() { + for reaction in reactions.get().unwrap().as_vec() { + let mut reactants = match reaction.reactants().get() { + Some(reactants) => reactants + .as_vec() + .iter() + .filter_map(|reactant| reactant.id().get()) + .collect::>(), + None => vec![], + }; + + let mut products = match reaction.products().get() { + Some(products) => products + .as_vec() + .iter() + .filter_map(|product| product.id().get()) + .collect::>(), + None => vec![], + }; + + identifiers.append(&mut reactants); + identifiers.append(&mut products); + } + identifiers + } else { + vec![] + } + } + + /// Returns a vector of all [Reaction]s' identifiers (attribute **id**). + pub(crate) fn reaction_identifiers(&self) -> Vec { + let reactions = self.reactions(); + + if reactions.is_set() { + reactions + .get() + .unwrap() + .as_vec() + .iter() + .map(|reaction| reaction.id().get()) + .collect::>() + } else { + vec![] + } + } } diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index c08b583..6cbbda2 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -24,7 +24,7 @@ impl Math { self.apply_rule_10207(issues); self.apply_rule_10208(issues); self.apply_rule_10214(issues); - // self.apply_rule_10215(issues); + self.apply_rule_10215(issues); self.apply_rule_10216(issues); self.apply_rule_10220(issues); self.apply_rule_10223(issues); @@ -325,9 +325,13 @@ impl Math { // the (great)grandparent of must be issues.push(SbmlIssue { element: child, - message: format!("The can be present only within (in ). Actual: <{0}>", toplevel_parent.name(doc)), + message: format!( + "A found in invalid scope of <{0}>. \ + The can be located only within (in ).", + toplevel_parent.name(doc) + ), rule: "10208".to_string(), - severity: SbmlIssueSeverity::Error + severity: SbmlIssueSeverity::Error, }); } else if *parent.child_elements(doc).first().unwrap() != child { // the must be the first child inside (or ) @@ -357,7 +361,7 @@ impl Math { if parent_name != "functionDefinition" { let children_of_interest = self .raw_element() - .child_elements(doc.deref()) + .child_elements_recursive(doc.deref()) .iter() .filter(|child| { child.name(doc.deref()) == "apply" @@ -398,6 +402,68 @@ impl Math { } } + // TODO: needs review + /// ### Rule 10215 + /// Outside of a [FunctionDefinition] object, if a MathML **ci** element is not the first element within + /// a MathML **apply**, then the **ci** element's value may only be chosen from the following set of + /// identifiers: the identifiers of [Species](crate::core::species::Species), + /// [Compartment](crate::core::compartment::Compartment), [Parameter](crate::core::parameter::Parameter), + /// [SpeciesReference](crate::core::reaction::SpeciesReference) and [Reaction] + /// objects defined in the enclosing [Model] object; the identifiers of + /// [LocalParameter](crate::core::reaction::LocalParameter) objects that are children of the + /// [Reaction](crate::core::reaction::Reaction) in which the [FunctionDefinition] appears (if it appears inside + /// the [Math] object of a [KineticLaw]); and any identifiers (in the SId namespace of the model) belonging to an + /// object class defined by an SBML Level 3 package as having mathematical meaning. + fn apply_rule_10215(&self, issues: &mut Vec) { + let is_out_of_function_definition = + FunctionDefinition::for_child_element(self.document(), self.xml_element()).is_none(); + + if is_out_of_function_definition { + let doc = self.read_doc(); + let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + let identifiers = [ + model.species_reference_identifiers(), + model.compartment_identifiers(), + model.parameter_identifiers(), + model.species_identifiers(), + model.species_reference_identifiers(), + model.reaction_identifiers(), + model.local_parameter_identifiers(), + ] + .concat(); + let apply_elements = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.name(doc.deref()) == "apply") + .copied() + .collect::>(); + + for apply in apply_elements { + let ci_elements = apply + .child_elements(doc.deref()) + .iter() + .filter(|child| child.name(doc.deref()) == "ci") + .skip(1) + .copied() + .collect::>(); + + for ci in ci_elements { + let value = ci.text_content(doc.deref()); + + if !identifiers.contains(&value) { + issues.push(SbmlIssue { + element: ci, + message: format!("Invalid identifier value '{0}' in .", value), + rule: "10215".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } + } + } + } + } + // TODO: needs review /// ### Rule 10216 /// The id attribute value of a [LocalParameter] object defined within a [KineticLaw] object may only be @@ -415,8 +481,19 @@ impl Math { Some(k) => k.local_parameter_identifiers(), None => vec![], }; - - let children_of_interest = self + let b_variables = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.name(doc.deref()) == "bvar") + .map(|bvar| { + bvar.child_elements(doc.deref()) + .first() + .unwrap() + .text_content(doc.deref()) + }) + .collect::>(); + let ci_elements = self .raw_element() .child_elements_recursive(doc.deref()) .iter() @@ -424,16 +501,19 @@ impl Math { .copied() .collect::>(); - for child in children_of_interest { - let value = child.text_content(doc.deref()); - if all_local_param_ids.contains(&value) && !scoped_local_param_ids.contains(&value) { + for ci in ci_elements { + let value = ci.text_content(doc.deref()); + if !b_variables.contains(&value) + && all_local_param_ids.contains(&value) + && !scoped_local_param_ids.contains(&value) + { issues.push(SbmlIssue { - element: child, - message: format!("A identifier '{0}' found out of scope of its ", value), + element: ci, + message: format!("A identifier '{0}' found out of scope of its ", + value), rule: "10216".to_string(), severity: SbmlIssueSeverity::Error }); - } else { } } } diff --git a/test-inputs/cholesterol_metabolism_and_atherosclerosis.xml b/test-inputs/cholesterol_metabolism_and_atherosclerosis.xml index 03406ad..695bbdd 100644 --- a/test-inputs/cholesterol_metabolism_and_atherosclerosis.xml +++ b/test-inputs/cholesterol_metabolism_and_atherosclerosis.xml @@ -4191,7 +4191,7 @@ - + @@ -4249,11 +4249,11 @@ - - - - - + + + + + @@ -4303,9 +4303,9 @@ species_4 - - - + + + @@ -4355,9 +4355,9 @@ species_5 - - - + + + @@ -4407,9 +4407,9 @@ species_5 - - - + + + @@ -4466,9 +4466,9 @@ - - - + + + @@ -4520,9 +4520,9 @@ species_5 - - - + + + @@ -4574,9 +4574,9 @@ species_5 - - - + + + @@ -4626,9 +4626,9 @@ species_11 - - - + + + @@ -4665,11 +4665,11 @@ species_7 - - - - - + + + + + @@ -4711,11 +4711,11 @@ - - - - - + + + + + @@ -4757,9 +4757,9 @@ - - - + + + @@ -4801,9 +4801,9 @@ - - - + + + @@ -4853,9 +4853,9 @@ species_11 - - - + + + @@ -4890,9 +4890,9 @@ species_7 - - - + + + @@ -4934,9 +4934,9 @@ - - - + + + @@ -4976,9 +4976,9 @@ - - - + + + @@ -5013,9 +5013,9 @@ species_17 - - - + + + @@ -5057,9 +5057,9 @@ - - - + + + @@ -5094,9 +5094,9 @@ species_21 - - - + + + @@ -5138,9 +5138,9 @@ - - - + + + @@ -5177,9 +5177,9 @@ species_23 - - - + + + @@ -5214,9 +5214,9 @@ species_23 - - - + + + @@ -5253,9 +5253,9 @@ species_25 - - - + + + species_23 - - - + + + @@ -5335,9 +5335,9 @@ - - - + + + @@ -5377,9 +5377,9 @@ - - - + + + @@ -5416,9 +5416,9 @@ species_14 - - - + + + species_28 - - - + + + @@ -5498,9 +5498,9 @@ - - - + + + @@ -5540,9 +5540,9 @@ species_31 - - - + + + @@ -5584,11 +5584,11 @@ - - - - - + + + + + @@ -5645,9 +5645,9 @@ - - - + + + @@ -5704,9 +5704,9 @@ - - - + + + @@ -5758,9 +5758,9 @@ species_34 - - - + + + From 822094eff7f771bf34ae5c5237ac1fb0ba690056 Mon Sep 17 00:00:00 2001 From: adamValent Date: Thu, 15 Feb 2024 13:16:36 +0100 Subject: [PATCH 17/57] Implement rule 10218 --- src/constants/element.rs | 98 +++++++++++++++++++++++++++++++++++++ src/core/validation/math.rs | 84 +++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) diff --git a/src/constants/element.rs b/src/constants/element.rs index a3aad70..5589c70 100644 --- a/src/constants/element.rs +++ b/src/constants/element.rs @@ -124,3 +124,101 @@ pub const MATHML_ALLOWED_DEFINITION_URLS: &[&str] = &[ ]; pub const MATHML_ALLOWED_TYPES: &[&str] = &["e-notation", "real", "integer", "rational"]; + +// source: https://www.w3.org/TR/MathML2/chapter4.html#contm.funopqual +pub const MATHML_UNARY_OPERATORS: &[&str] = &[ + "factorial", + "minus", + "abs", + "conjugate", + "arg", + "real", + "imaginary", + "floor", + "ceiling", + "not", + "inverse", + "ident", + "domain", + "codomain", + "image", + "sin", + "cos", + "tan", + "sec", + "csc", + "cot", + "sinh", + "cosh", + "tanh", + "sech", + "csch", + "coth", + "arcsin", + "arccos", + "arctan", + "arccosh", + "arccot", + "arccoth", + "arccsc", + "arccsch", + "arcsec", + "arcsech", + "arcsinh", + "arctanh", + "exp", + "ln", + "log", + "determinant", + "transpose", + "divergence", + "grad", + "curl", + "laplacian", + "card", +]; + +// source: https://www.w3.org/TR/MathML2/chapter4.html#contm.funopqual +pub const MATHML_BINARY_OPERATORS: &[&str] = &[ + "quotient", + "divide", + "minus", + "power", + "rem", + "root", // special operator of which one argument (degree) is by default 2 and therefore one argument is sufficient + "implies", + "equivalent", + "approx", + "setdiff", + "vectorproduct", + "scalarproduct", + "outerproduct", +]; + +// source: https://www.w3.org/TR/MathML2/chapter4.html#contm.funopqual +pub const MATHML_NARY_OPERATORS: &[&str] = &[ + "plus", + "times", + "max", + "min", + "gcd", + "lcm", + "mean", + "sdev", + "variance", + "median", + "mode", + "union", + "intersect", + "cartesianproduct", + "selector", + "and", + "or", + "xor", + "eq", + "neq", + "leq", + "lt", + "geq", + "gt", +]; diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 6cbbda2..a188acd 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -3,6 +3,7 @@ use xml_doc::{Document, Element}; use crate::constants::element::{ MATHML_ALLOWED_CHILDREN_BY_ATTR, MATHML_ALLOWED_DEFINITION_URLS, MATHML_ALLOWED_TYPES, + MATHML_BINARY_OPERATORS, MATHML_NARY_OPERATORS, MATHML_UNARY_OPERATORS, }; use crate::constants::namespaces::URL_MATHML; use crate::core::validation::get_allowed_children; @@ -26,6 +27,7 @@ impl Math { self.apply_rule_10214(issues); self.apply_rule_10215(issues); self.apply_rule_10216(issues); + self.apply_rule_10218(issues); self.apply_rule_10220(issues); self.apply_rule_10223(issues); } @@ -518,6 +520,88 @@ impl Math { } } + /// ### Rule 10218 + /// A MathML operator must be supplied the number of arguments appropriate for that operator. + fn apply_rule_10218(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let apply_elements = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.name(doc.deref()) == "apply") + .copied() + .collect::>(); + + for apply in apply_elements { + let children = apply.child_elements(doc.deref()); + let child_count = children.len(); + + // iterate through children of an element + for child in children { + let name = child.name(doc.deref()); + + if MATHML_UNARY_OPERATORS.contains(&name) { + // is allowed to have 1 OR 2 arguments + if name == "minus" && child_count - 1 != 1 && child_count - 1 != 2 { + issues.push(SbmlIssue { + element: child, + message: format!( + "Invalid number ({0}) of arguments for operator . \ + The operator can take either 1 or 2 arguments.", + child_count - 1 + ), + rule: "10218".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } else if child_count - 1 != 1 && name != "minus" { + issues.push(SbmlIssue { + element: child, + message: format!( + "Invalid number ({0}) of arguments for unary operator <{1}>", + child_count - 1, + name + ), + rule: "10218".to_string(), + severity: SbmlIssueSeverity::Error, + }); + } + } else if MATHML_BINARY_OPERATORS.contains(&name) { + // root is allowed to have 1 OR 2 arguments + if name == "root" && child_count - 1 != 1 && child_count - 1 != 2 { + issues.push(SbmlIssue { + element: child, + message: format!( + "Invalid number ({0}) of arguments for operator . \ + The operator can take either 1 or 2 arguments.", + child_count - 1 + ), + rule: "10218".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } else if child_count - 1 != 2 && name != "root" { + issues.push(SbmlIssue { + element: child, + message: format!( + "Invalid number ({0}) of arguments for binary operator <{1}>.", + child_count - 1, + name + ), + rule: "10218".to_string(), + severity: SbmlIssueSeverity::Error, + }); + } + } else if MATHML_NARY_OPERATORS.contains(&name) && child_count - 1 == 0 { + issues.push(SbmlIssue { + element: child, + message: format!("An N-ary operator <{0}> with 0 arguments found. Use of N-ary operators without any arguments is discouraged.", name), + rule: "10218".to_string(), + severity: SbmlIssueSeverity::Warning + }); + } + } + } + } + // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10220 /// The SBML attribute **units** may only be added to MathML **cn** elements; no other MathML elements From 09ad46236b616a96256d9f343885a455e622732c Mon Sep 17 00:00:00 2001 From: adamValent Date: Thu, 15 Feb 2024 14:19:56 +0100 Subject: [PATCH 18/57] Implement rule 10219 --- src/core/model.rs | 31 +++++++++++++++++++++++ src/core/validation/math.rs | 49 ++++++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/core/model.rs b/src/core/model.rs index 4c81c59..7ddb7f3 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -115,6 +115,37 @@ impl Model { } } + /// Find a [FunctionDefinition] by its *id* and return a number of arguments this function expects. + /// More precisely, find a number of **bvar** elements inside **lambda** inside **math** element of + /// [FunctionDefinition]. If [FunctionDefinition] cannot be found, returns 0. + pub(crate) fn function_definition_arguments(&self, id: &str) -> i32 { + let function_definitions = self.function_definitions(); + + if function_definitions.is_set() { + let function_definitions = function_definitions.get().unwrap().as_vec(); + let function = function_definitions + .iter() + .find(|function| function.id().get() == Some(id.to_string())); + + if function.is_some() && function.unwrap().math().is_set() { + let doc = self.read_doc(); + let math = function.unwrap().math().get().unwrap(); + let lambda = math.raw_element().find(doc.deref(), "lambda"); + + if lambda.is_some() { + return lambda + .unwrap() + .child_elements(doc.deref()) + .iter() + .filter(|child| child.name(doc.deref()) == "bvar") + .collect::>() + .len() as i32; + } + } + } + 0 + } + /// Returns a vector of all [LocalParameter]s' identifiers (attribute **id**). pub(crate) fn local_parameter_identifiers(&self) -> Vec { let reactions = self.reactions(); diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index a188acd..e351c7f 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -28,6 +28,7 @@ impl Math { self.apply_rule_10215(issues); self.apply_rule_10216(issues); self.apply_rule_10218(issues); + self.apply_rule_10219(issues); self.apply_rule_10220(issues); self.apply_rule_10223(issues); } @@ -456,7 +457,10 @@ impl Math { if !identifiers.contains(&value) { issues.push(SbmlIssue { element: ci, - message: format!("Invalid identifier value '{0}' in .", value), + message: format!( + "Invalid identifier value '{0}' in . Identifier not found.", + value + ), rule: "10215".to_string(), severity: SbmlIssueSeverity::Error, }) @@ -602,6 +606,49 @@ impl Math { } } + /// ### Rule 10219 + fn apply_rule_10219(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + + let apply_elements = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.name(doc.deref()) == "apply") + .copied() + .collect::>(); + + for apply in apply_elements { + let children = apply.child_elements(doc.deref()); + let child_count = children.len() as i32; + let function_call = children.first(); + + if function_call.is_some() && function_call.unwrap().name(doc.deref()) == "ci" { + let function_call = function_call.unwrap(); + let func_identifiers = model.function_definition_identifiers(); + let id = function_call.text_content(doc.deref()); + + if func_identifiers.contains(&id) { + let expected_args = model.function_definition_arguments(id.as_str()); + + if child_count - 1 != expected_args { + issues.push(SbmlIssue { + element: *function_call, + message: format!( + "Invalid number of arguments ({0}) provided for function '{1}'", + child_count - 1, + id + ), + rule: "10219".to_string(), + severity: SbmlIssueSeverity::Error, + }); + } + } + } + } + } + // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10220 /// The SBML attribute **units** may only be added to MathML **cn** elements; no other MathML elements From 15cf4e7d1907520ec0a622e1c39e34230318b035 Mon Sep 17 00:00:00 2001 From: adamValent Date: Thu, 15 Feb 2024 14:44:08 +0100 Subject: [PATCH 19/57] Implement rule 10221 --- src/core/model.rs | 18 +++++++++++++ src/core/validation/math.rs | 51 ++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/core/model.rs b/src/core/model.rs index 7ddb7f3..ec95023 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -146,6 +146,24 @@ impl Model { 0 } + /// Returns a vector of [UnitDefinition]s' identifiers (attribute **id**). If the identifier is not set, + /// it is not included in the output. + pub(crate) fn unit_definition_identifiers(&self) -> Vec { + let unit_definitions = self.unit_definitions(); + + if unit_definitions.is_set() { + unit_definitions + .get() + .unwrap() + .as_vec() + .iter() + .filter_map(|unit| unit.id().get()) + .collect() + } else { + vec![] + } + } + /// Returns a vector of all [LocalParameter]s' identifiers (attribute **id**). pub(crate) fn local_parameter_identifiers(&self) -> Vec { let reactions = self.reactions(); diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index e351c7f..d2e41e8 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -1,4 +1,5 @@ use std::ops::Deref; +use std::str::FromStr; use xml_doc::{Document, Element}; use crate::constants::element::{ @@ -7,7 +8,7 @@ use crate::constants::element::{ }; use crate::constants::namespaces::URL_MATHML; use crate::core::validation::get_allowed_children; -use crate::core::{FunctionDefinition, KineticLaw, Math, Model}; +use crate::core::{BaseUnit, FunctionDefinition, KineticLaw, Math, Model}; use crate::xml::XmlWrapper; use crate::{SbmlIssue, SbmlIssueSeverity}; @@ -30,6 +31,7 @@ impl Math { self.apply_rule_10218(issues); self.apply_rule_10219(issues); self.apply_rule_10220(issues); + self.apply_rule_10221(issues); self.apply_rule_10223(issues); } @@ -272,9 +274,9 @@ impl Math { // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10208 /// MathML **lambda** elements are only permitted as either the first element inside the - /// [**Math**] element of a [**FunctionDefinition**](crate::core::FunctionDefinition) object, + /// [**Math**] element of a [**FunctionDefinition**](FunctionDefinition) object, /// or as the first element of a **semantics** element immediately inside the [**Math**] element - /// of a [**FunctionDefinition**](crate::core::FunctionDefinition) object. MathML **lambda** + /// of a [**FunctionDefinition**](FunctionDefinition) object. MathML **lambda** /// elements may not be used elsewhere in an SBML model. An SBML package may allow **lambda** /// elements on other elements, and if so, the package must define **required="true"** on the /// SBML container element [**sbml**](crate::Sbml). @@ -315,7 +317,7 @@ impl Math { } /// Checks if: - /// 1. top-level parent of **lambda** is a [**FunctionDefinition**](crate::core::FunctionDefinition). + /// 1. top-level parent of **lambda** is a [**FunctionDefinition**](FunctionDefinition). /// 2. **lambda** is the first child of its immediate parent fn validate_lambda_placement( doc: &Document, @@ -348,10 +350,10 @@ impl Math { } /// ### Rule 10214 - /// Outside of a [**FunctionDefinition**](crate::core::FunctionDefinition) object, if a MathML + /// Outside of a [**FunctionDefinition**](FunctionDefinition) object, if a MathML /// **ci** element is the first element within a MathML apply element, then the **ci** element's /// value can only be chosen from the set of identifiers of - /// [**FunctionDefinition**](crate::core::FunctionDefinition) objects defined in the enclosing + /// [**FunctionDefinition**](FunctionDefinition) objects defined in the enclosing /// SBML [Model](crate::core::model) object. fn apply_rule_10214(&self, issues: &mut Vec) { let doc = self.read_doc(); @@ -683,6 +685,43 @@ impl Math { } } + /// ### Rule 10221 + /// The value of the SBML attribute units on a MathML cn element must be chosen from either the + /// set of identifiers of UnitDefinition objects in the model, or the set of base units defined by SBML. + fn apply_rule_10221(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let unit_identifiers = Model::for_child_element(self.document(), self.xml_element()) + .unwrap() + .unit_definition_identifiers(); + let cn_elements = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| { + child.name(doc.deref()) == "cn" && child.attribute(doc.deref(), "units").is_some() + }) + .copied() + .collect::>(); + + for cn in cn_elements { + let value = cn.attribute(doc.deref(), "units").unwrap(); + + if !unit_identifiers.contains(&value.to_string()) && BaseUnit::from_str(value).is_err() + { + issues.push(SbmlIssue { + element: cn, + message: format!( + "Invalid unit identifier '{0}' found. \ + Only identifiers of objects and base units can be used in .", + value + ), + rule: "10221".to_string(), + severity: SbmlIssueSeverity::Error + }) + } + } + } + /// ### Rule 10223 /// The single argument for the *rateOf* **csymbol** function must be a **ci** element. fn apply_rule_10223(&self, issues: &mut Vec) { From d63d598ec510519d2f6491f737d6e83af8600b66 Mon Sep 17 00:00:00 2001 From: adamValent Date: Thu, 15 Feb 2024 19:58:35 +0100 Subject: [PATCH 20/57] Fix rule 10223 implementation --- src/core/model.rs | 4 +-- src/core/validation/math.rs | 53 ++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/core/model.rs b/src/core/model.rs index ec95023..0306f5b 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -130,11 +130,9 @@ impl Model { if function.is_some() && function.unwrap().math().is_set() { let doc = self.read_doc(); let math = function.unwrap().math().get().unwrap(); - let lambda = math.raw_element().find(doc.deref(), "lambda"); - if lambda.is_some() { + if let Some(lambda) = math.raw_element().find(doc.deref(), "lambda") { return lambda - .unwrap() .child_elements(doc.deref()) .iter() .filter(|child| child.name(doc.deref()) == "bvar") diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index d2e41e8..a6727cd 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -731,39 +731,48 @@ impl Math { .child_elements_recursive(doc.deref()) .iter() .filter(|child| { - child.name(doc.deref()) == "csymbol" - && child - .attribute(doc.deref(), "definitionURL") - .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf") + child.name(doc.deref()) == "apply" && !child.child_elements(doc.deref()).is_empty() + }) + .filter(|apply| { + apply + .child_elements(doc.deref()) + .first() + .unwrap() + .attribute(doc.deref(), "definitionURL") + .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf") }) .copied() .collect::>(); for child in children_of_interest { - let child_count = child.child_elements(doc.deref()).len(); + let apply_children = child.child_elements(doc.deref()); - if child_count != 1 { + if apply_children.len() != 2 { issues.push(SbmlIssue { element: child, - message: format!("Invalid number ({0}) of children in . The must have precisely one child (argument).", child_count), + message: format!( + "Invalid number ({0}) of arguments provided for rateOf . \ + The call of rateOf must have precisely one argument.", + apply_children.len() - 1 + ), rule: "10223".to_string(), - severity: SbmlIssueSeverity::Error + severity: SbmlIssueSeverity::Error, }); - continue; - } + } else { + let argument_name = apply_children.last().unwrap().name(doc.deref()); - let single_child_name = child - .child_elements(doc.deref()) - .first() - .unwrap() - .name(doc.deref()); - if single_child_name != "ci" { - issues.push(SbmlIssue { - element: child, - message: format!("Invalid child <{0}> of . The must have as its only child (argument).", single_child_name), - rule: "10223".to_string(), - severity: SbmlIssueSeverity::Error - }) + if argument_name != "ci" { + issues.push(SbmlIssue { + element: child, + message: format!( + "Invalid argument <{0}> provided for .\ + The rateOf must have as its only argument.", + argument_name + ), + rule: "10223".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } } } } From 3d8674a2d6c0589231c973a4fb78156c87103959 Mon Sep 17 00:00:00 2001 From: adamValent Date: Fri, 16 Feb 2024 11:01:30 +0100 Subject: [PATCH 21/57] Implement rule 10224 --- src/core/model.rs | 155 +++++++++++++++++++----------------- src/core/validation/math.rs | 58 ++++++++++++++ 2 files changed, 138 insertions(+), 75 deletions(-) diff --git a/src/core/model.rs b/src/core/model.rs index 0306f5b..c345cb3 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -1,14 +1,16 @@ use crate::constants::namespaces::URL_SBML_CORE; use crate::core::sbase::SbmlUtils; use crate::core::{ - AbstractRule, Compartment, Constraint, Event, FunctionDefinition, InitialAssignment, Parameter, - Reaction, SBase, Species, UnitDefinition, + AbstractRule, AlgebraicRule, AssignmentRule, Compartment, Constraint, Event, + FunctionDefinition, InitialAssignment, Parameter, Reaction, Rule, SBase, Species, + UnitDefinition, }; use crate::xml::{ OptionalChild, OptionalXmlChild, OptionalXmlProperty, RequiredXmlProperty, XmlDefault, - XmlDocument, XmlElement, XmlList, XmlWrapper, + XmlDocument, XmlElement, XmlList, XmlSupertype, XmlWrapper, }; use macros::{SBase, XmlWrapper}; + use std::ops::Deref; use xml_doc::{Document, Element}; @@ -100,12 +102,8 @@ impl Model { /// Returns a vector of [FunctionDefinition]s' identifiers (attribute **id**). If the identifier is not set, /// it is not included in the output. pub(crate) fn function_definition_identifiers(&self) -> Vec { - let function_definitions = self.function_definitions(); - - if function_definitions.is_set() { + if let Some(function_definitions) = self.function_definitions().get() { function_definitions - .get() - .unwrap() .as_vec() .iter() .filter_map(|def| def.id().get()) @@ -119,25 +117,27 @@ impl Model { /// More precisely, find a number of **bvar** elements inside **lambda** inside **math** element of /// [FunctionDefinition]. If [FunctionDefinition] cannot be found, returns 0. pub(crate) fn function_definition_arguments(&self, id: &str) -> i32 { - let function_definitions = self.function_definitions(); - - if function_definitions.is_set() { - let function_definitions = function_definitions.get().unwrap().as_vec(); - let function = function_definitions + // if list of function definitions is present + if let Some(function_definitions) = self.function_definitions().get() { + let function_definitions = function_definitions.as_vec(); + // and we have found a function with given id + if let Some(function) = function_definitions .iter() - .find(|function| function.id().get() == Some(id.to_string())); - - if function.is_some() && function.unwrap().math().is_set() { - let doc = self.read_doc(); - let math = function.unwrap().math().get().unwrap(); - - if let Some(lambda) = math.raw_element().find(doc.deref(), "lambda") { - return lambda - .child_elements(doc.deref()) - .iter() - .filter(|child| child.name(doc.deref()) == "bvar") - .collect::>() - .len() as i32; + .find(|function| function.id().get() == Some(id.to_string())) + { + // and this function has its math element specified + if let Some(math) = function.math().get() { + let doc = self.read_doc(); + // and a lambda element within math is present + if let Some(lambda) = math.raw_element().find(doc.deref(), "lambda") { + // we return a number of bvar elements + return lambda + .child_elements(doc.deref()) + .iter() + .filter(|child| child.name(doc.deref()) == "bvar") + .collect::>() + .len() as i32; + } } } } @@ -147,12 +147,8 @@ impl Model { /// Returns a vector of [UnitDefinition]s' identifiers (attribute **id**). If the identifier is not set, /// it is not included in the output. pub(crate) fn unit_definition_identifiers(&self) -> Vec { - let unit_definitions = self.unit_definitions(); - - if unit_definitions.is_set() { + if let Some(unit_definitions) = self.unit_definitions().get() { unit_definitions - .get() - .unwrap() .as_vec() .iter() .filter_map(|unit| unit.id().get()) @@ -164,41 +160,29 @@ impl Model { /// Returns a vector of all [LocalParameter]s' identifiers (attribute **id**). pub(crate) fn local_parameter_identifiers(&self) -> Vec { - let reactions = self.reactions(); - let mut vec: Vec = vec![]; - - if reactions.is_set() { - for reaction in reactions.get().unwrap().as_vec() { - let kinetic_law = reaction.kinetic_law(); - - if kinetic_law.is_set() { - let kinetic_law = kinetic_law.get().unwrap(); - let local_params = kinetic_law.local_parameters(); + let mut identifiers: Vec = vec![]; - if local_params.is_set() { + if let Some(reactions) = self.reactions().get() { + for reaction in reactions.as_vec() { + if let Some(kinetic_law) = reaction.kinetic_law().get() { + if let Some(local_params) = kinetic_law.local_parameters().get() { let mut param_ids = local_params - .get() - .unwrap() .as_vec() .iter() .map(|param| param.id().get()) .collect::>(); - vec.append(&mut param_ids); + identifiers.append(&mut param_ids); } } } } - vec + identifiers } /// Returns a vector of all [Species]' identifiers (attribute **id**). pub(crate) fn species_identifiers(&self) -> Vec { - let species = self.species(); - - if species.is_set() { + if let Some(species) = self.species().get() { species - .get() - .unwrap() .as_vec() .iter() .map(|species| species.id().get()) @@ -210,12 +194,8 @@ impl Model { /// Returns a vector of all [Compartment]s' identifiers (attribute **id**). pub(crate) fn compartment_identifiers(&self) -> Vec { - let compartment = self.compartments(); - - if compartment.is_set() { + if let Some(compartment) = self.compartments().get() { compartment - .get() - .unwrap() .as_vec() .iter() .map(|compartment| compartment.id().get()) @@ -227,12 +207,8 @@ impl Model { /// Returns a vector of all [Parameter]s' identifiers (attribute **id**). pub(crate) fn parameter_identifiers(&self) -> Vec { - let parameters = self.parameters(); - - if parameters.is_set() { + if let Some(parameters) = self.parameters().get() { parameters - .get() - .unwrap() .as_vec() .iter() .map(|param| param.id().get()) @@ -245,11 +221,11 @@ impl Model { /// Returns a vector of all [SpeciesReference](crate::core::SpeciesReference)' identifiers (attribute **id**). /// If the identifier is not set, it is not included in the output. pub(crate) fn species_reference_identifiers(&self) -> Vec { - let reactions = self.reactions(); let mut identifiers: Vec = vec![]; - - if reactions.is_set() { - for reaction in reactions.get().unwrap().as_vec() { + // if list of reactions is present + if let Some(reactions) = self.reactions().get() { + for reaction in reactions.as_vec() { + // we extract identifiers of reactants let mut reactants = match reaction.reactants().get() { Some(reactants) => reactants .as_vec() @@ -258,7 +234,7 @@ impl Model { .collect::>(), None => vec![], }; - + // and product identifiers as well let mut products = match reaction.products().get() { Some(products) => products .as_vec() @@ -267,24 +243,18 @@ impl Model { .collect::>(), None => vec![], }; - + // and then we include results in the output identifiers.append(&mut reactants); identifiers.append(&mut products); } - identifiers - } else { - vec![] } + identifiers } /// Returns a vector of all [Reaction]s' identifiers (attribute **id**). pub(crate) fn reaction_identifiers(&self) -> Vec { - let reactions = self.reactions(); - - if reactions.is_set() { + if let Some(reactions) = self.reactions().get() { reactions - .get() - .unwrap() .as_vec() .iter() .map(|reaction| reaction.id().get()) @@ -293,4 +263,39 @@ impl Model { vec![] } } + + /// Returns a vector of *variables* of all [AssignmentRule]s. + pub(crate) fn assignment_rule_variables(&self) -> Vec { + if let Some(rules) = self.rules().get() { + return rules + .as_vec() + .iter() + .filter_map(|rule| rule.try_downcast::()) + .map(|assignment_rule| assignment_rule.variable().get()) + .collect::>(); + } + vec![] + } + + /// Returns a vector of values from within **ci** element. + pub(crate) fn algebraic_rule_ci_values(&self) -> Vec { + if let Some(rules) = self.rules().get() { + let doc = self.read_doc(); + return rules + .as_vec() + .iter() + .filter_map(|rule| rule.try_downcast::()) + .filter_map(|algebraic_rule| algebraic_rule.math().get()) + .flat_map(|math| { + math.raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| child.name(doc.deref()) == "ci") + .map(|ci| ci.text_content(doc.deref())) + .collect::>() + }) + .collect::>(); + } + vec![] + } } diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index a6727cd..ec57f01 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -33,6 +33,7 @@ impl Math { self.apply_rule_10220(issues); self.apply_rule_10221(issues); self.apply_rule_10223(issues); + self.apply_rule_10224(issues); } /// ### Rule 10201 @@ -776,4 +777,61 @@ impl Math { } } } + + /// ### Rule 10224 + /// The target of a *rateOf* **csymbol** function must not appear as the *variable* of an + /// [AssignmentRule](crate::core::rule::AssignmentRule), nor may its value be determined by an + /// [AlgebraicRule](crate::core::rule::AlgebraicRule). + fn apply_rule_10224(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + let ci_elements = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| { + child.name(doc.deref()) == "apply" + && child.child_elements(doc.deref()).len() > 1 + && child + .child_elements(doc.deref()) + .first() + .unwrap() + .attribute(doc.deref(), "definitionURL") + .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf") + && child + .child_elements(doc.deref()) + .get(1) + .unwrap() + .name(doc.deref()) + == "ci" + }) + .copied() + .collect::>(); + let assignment_rule_variables = model.assignment_rule_variables(); + let algebraic_rule_determinants = model.algebraic_rule_ci_values(); + + for ci in ci_elements { + let value = ci.text_content(doc.deref()); + + if assignment_rule_variables.contains(&value) { + issues.push(SbmlIssue { + element: ci, + message: + format!("The value of target ('{0}') of rateOf found as a variable of .", + value), + rule: "10224".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } else if algebraic_rule_determinants.contains(&value) { + issues.push(SbmlIssue { + element: ci, + message: + format!("The value of target ('{0}') of rateOf determined by an .", + value), + rule: "10224".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } + } + } } From f405d24fa8d4671c2b828ba19367a52844700ae7 Mon Sep 17 00:00:00 2001 From: adamValent Date: Fri, 16 Feb 2024 13:39:16 +0100 Subject: [PATCH 22/57] Implement rule 10225 --- examples/basic_example.rs | 6 ++- src/core/model.rs | 32 +++++++++++++++ src/core/validation/math.rs | 82 +++++++++++++++++++++++++++++++++++-- 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/examples/basic_example.rs b/examples/basic_example.rs index b089755..541f47f 100644 --- a/examples/basic_example.rs +++ b/examples/basic_example.rs @@ -7,8 +7,10 @@ use biodivine_lib_sbml::{Sbml, SbmlIssue}; // for the example binary, not for `cargo` itself. fn main() { // let doc = Sbml::read_path("test-inputs/COVID19_immunotherapy_Mathematical_Model.xml").unwrap(); - let doc = - Sbml::read_path("test-inputs/cholesterol_metabolism_and_atherosclerosis.xml").unwrap(); + // let doc = + // Sbml::read_path("test-inputs/cholesterol_metabolism_and_atherosclerosis.xml").unwrap(); + let doc = Sbml::read_path("test-inputs/Mukandavire2020.xml").unwrap(); + // let model = doc.model().get().unwrap(); // Print the whole document: // println!("{}", model.read_doc().write_str().unwrap()); diff --git a/src/core/model.rs b/src/core/model.rs index c345cb3..6958a87 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -298,4 +298,36 @@ impl Model { } vec![] } + + /// Finds a species with given *id*. If not found, returns None. + pub(crate) fn find_species(&self, id: &str) -> Option { + if let Some(species) = self.species().get() { + match species + .as_vec() + .iter() + .find(|species| species.id().get() == id) + { + Some(species) => Some(species.clone()), + None => None, + } + } else { + None + } + } + + /// Finds a compartment with given *id*. If not found, returns None. + pub(crate) fn find_compartment(&self, id: &str) -> Option { + if let Some(compartments) = self.compartments().get() { + match compartments + .as_vec() + .iter() + .find(|compartment| compartment.id().get() == id) + { + Some(compartment) => Some(compartment.clone()), + None => None, + } + } else { + None + } + } } diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index ec57f01..926ef49 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -9,7 +9,7 @@ use crate::constants::element::{ use crate::constants::namespaces::URL_MATHML; use crate::core::validation::get_allowed_children; use crate::core::{BaseUnit, FunctionDefinition, KineticLaw, Math, Model}; -use crate::xml::XmlWrapper; +use crate::xml::{RequiredXmlProperty, XmlWrapper}; use crate::{SbmlIssue, SbmlIssueSeverity}; impl Math { @@ -34,6 +34,7 @@ impl Math { self.apply_rule_10221(issues); self.apply_rule_10223(issues); self.apply_rule_10224(issues); + self.apply_rule_10225(issues); } /// ### Rule 10201 @@ -639,9 +640,12 @@ impl Math { issues.push(SbmlIssue { element: *function_call, message: format!( - "Invalid number of arguments ({0}) provided for function '{1}'", + "Invalid number of arguments ({0}) provided for function '{1}'. \ + The function '{2}' takes {3} arguments.", child_count - 1, - id + id, + id, + expected_args ), rule: "10219".to_string(), severity: SbmlIssueSeverity::Error, @@ -822,6 +826,7 @@ impl Math { rule: "10224".to_string(), severity: SbmlIssueSeverity::Error, }) + // TODO: what does "determined by algebraicRule" mean and how to check it? } else if algebraic_rule_determinants.contains(&value) { issues.push(SbmlIssue { element: ci, @@ -834,4 +839,75 @@ impl Math { } } } + + /// ### Rule 10225 + /// If the target of a *rateOf* **csymbol** function is a [Species](crate::core::species::Species) with a + /// *hasOnlySubstanceUnits* value of *"false"*, the **compartment** of that [Species](crate::core::species::Species) + /// must not appear as the *variable* of an [AssignmentRule](crate::core::rule::AssignmentRule), + /// nor may its *size* be determined by an [AlgebraicRule](crate::core::rule::AlgebraicRule). + fn apply_rule_10225(&self, issues: &mut Vec) { + let doc = self.read_doc(); + let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + let assignment_rule_variables = model.assignment_rule_variables(); + let algebraic_ci_values = model.algebraic_rule_ci_values(); + let ci_elements = self + .raw_element() + .child_elements_recursive(doc.deref()) + .iter() + .filter(|child| { + child.name(doc.deref()) == "apply" + && child.child_elements(doc.deref()).len() > 1 + && child + .child_elements(doc.deref()) + .first() + .unwrap() + .attribute(doc.deref(), "definitionURL") + .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf") + && child + .child_elements(doc.deref()) + .get(1) + .unwrap() + .name(doc.deref()) + == "ci" + }) + .copied() + .collect::>(); + + for ci in ci_elements { + let value = ci.text_content(doc.deref()); + + if let Some(species) = model.find_species(value.as_str()) { + if !species.has_only_substance_units().get() { + let compartment = model + .find_compartment(species.compartment().get().as_str()) + .unwrap(); + let compartment_id = compartment.id().get(); + + if assignment_rule_variables.contains(&compartment_id) { + issues.push(SbmlIssue { + element: ci, + message: format!( + "The with id '{0}' found as the [variable] of an .", + compartment_id + ), + rule: "10225".to_string(), + severity: SbmlIssueSeverity::Error + }) + } else if !compartment.constant().get() + && algebraic_ci_values.contains(&compartment_id) + { + issues.push(SbmlIssue { + element: ci, + message: format!( + "The 's size with id '{0}' is possible to determine by an .", + compartment_id + ), + rule: "10225".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } + } + } + } + } } From c3ade9665aff473857cf39f8a534e69e6ccec0da Mon Sep 17 00:00:00 2001 From: adamValent Date: Sat, 17 Feb 2024 10:15:57 +0100 Subject: [PATCH 23/57] Fix clippy warnings --- src/core/model.rs | 14 ++++---------- src/core/validation/model.rs | 22 ++++++++++++---------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/core/model.rs b/src/core/model.rs index 6958a87..a1b3472 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -302,14 +302,11 @@ impl Model { /// Finds a species with given *id*. If not found, returns None. pub(crate) fn find_species(&self, id: &str) -> Option { if let Some(species) = self.species().get() { - match species + species .as_vec() .iter() .find(|species| species.id().get() == id) - { - Some(species) => Some(species.clone()), - None => None, - } + .cloned() } else { None } @@ -318,14 +315,11 @@ impl Model { /// Finds a compartment with given *id*. If not found, returns None. pub(crate) fn find_compartment(&self, id: &str) -> Option { if let Some(compartments) = self.compartments().get() { - match compartments + compartments .as_vec() .iter() .find(|compartment| compartment.id().get() == id) - { - Some(compartment) => Some(compartment.clone()), - None => None, - } + .cloned() } else { None } diff --git a/src/core/validation/model.rs b/src/core/validation/model.rs index 371f0bb..75a5408 100644 --- a/src/core/validation/model.rs +++ b/src/core/validation/model.rs @@ -41,9 +41,9 @@ impl Model { fn validate_list_of_function_definitions(&self, issues: &mut Vec) { let list = self.function_definitions().get().unwrap(); + let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); - let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let function_def = list.get(i); if allowed.contains(&function_def.tag_name().as_str()) { @@ -54,9 +54,9 @@ impl Model { fn validate_list_of_unit_definitions(&self, issues: &mut Vec) { let list = self.unit_definitions().get().unwrap(); + let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); - let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let unit_def = list.get(i); if allowed.contains(&unit_def.tag_name().as_str()) { @@ -67,9 +67,9 @@ impl Model { fn validate_list_of_compartments(&self, issues: &mut Vec) { let list = self.compartments().get().unwrap(); + let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); - let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let compartment = list.get(i); if allowed.contains(&compartment.tag_name().as_str()) { @@ -80,9 +80,9 @@ impl Model { fn validate_list_of_species(&self, issues: &mut Vec) { let list = self.species().get().unwrap(); + let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); - let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let species = list.get(i); if allowed.contains(&species.tag_name().as_str()) { @@ -93,9 +93,9 @@ impl Model { fn validate_list_of_parameters(&self, issues: &mut Vec) { let list = self.parameters().get().unwrap(); + let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); - let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let parameter = list.get(i); if allowed.contains(¶meter.tag_name().as_str()) { @@ -106,9 +106,9 @@ impl Model { fn validate_list_of_initial_assignments(&self, issues: &mut Vec) { let list = self.initial_assignments().get().unwrap(); + let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); - let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let initial_assignment = list.get(i); if allowed.contains(&initial_assignment.tag_name().as_str()) { @@ -119,9 +119,9 @@ impl Model { fn validate_list_of_rules(&self, issues: &mut Vec) { let list = self.rules().get().unwrap(); + let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); - let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let rule = list.get(i); if allowed.contains(&rule.tag_name().as_str()) { @@ -132,9 +132,9 @@ impl Model { fn validate_list_of_constraints(&self, issues: &mut Vec) { let list = self.constraints().get().unwrap(); + let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); - let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let constraint = list.get(i); if allowed.contains(&constraint.tag_name().as_str()) { @@ -145,9 +145,9 @@ impl Model { fn validate_list_of_reactions(&self, issues: &mut Vec) { let list = self.reactions().get().unwrap(); + let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); - let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let reaction = list.get(i); if allowed.contains(&reaction.tag_name().as_str()) { @@ -158,9 +158,9 @@ impl Model { fn validate_list_of_events(&self, issues: &mut Vec) { let list = self.events().get().unwrap(); + let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); - let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let event = list.get(i); if allowed.contains(&event.tag_name().as_str()) { @@ -168,4 +168,6 @@ impl Model { } } } + + // fn apply_rule_10301(&self, issues: &mut Vec) {} } From ff6c232b52cdd7c1e769ea02d20c23f5572c46b5 Mon Sep 17 00:00:00 2001 From: adamValent Date: Sat, 17 Feb 2024 14:51:19 +0100 Subject: [PATCH 24/57] Fix rule 10225 - handle the case when a compartment cannot be found --- src/core/validation/math.rs | 53 +++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 926ef49..74b5bb0 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -878,33 +878,34 @@ impl Math { if let Some(species) = model.find_species(value.as_str()) { if !species.has_only_substance_units().get() { - let compartment = model - .find_compartment(species.compartment().get().as_str()) - .unwrap(); - let compartment_id = compartment.id().get(); - - if assignment_rule_variables.contains(&compartment_id) { - issues.push(SbmlIssue { - element: ci, - message: format!( - "The with id '{0}' found as the [variable] of an .", - compartment_id - ), - rule: "10225".to_string(), - severity: SbmlIssueSeverity::Error - }) - } else if !compartment.constant().get() - && algebraic_ci_values.contains(&compartment_id) + if let Some(compartment) = + model.find_compartment(species.compartment().get().as_str()) { - issues.push(SbmlIssue { - element: ci, - message: format!( - "The 's size with id '{0}' is possible to determine by an .", - compartment_id - ), - rule: "10225".to_string(), - severity: SbmlIssueSeverity::Error, - }) + let compartment_id = compartment.id().get(); + + if assignment_rule_variables.contains(&compartment_id) { + issues.push(SbmlIssue { + element: ci, + message: format!( + "The with id '{0}' found as the [variable] of an .", + compartment_id + ), + rule: "10225".to_string(), + severity: SbmlIssueSeverity::Error + }) + } else if !compartment.constant().get() + && algebraic_ci_values.contains(&compartment_id) + { + issues.push(SbmlIssue { + element: ci, + message: format!( + "The 's size with id '{0}' is possible to determine by an .", + compartment_id + ), + rule: "10225".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } } } } From 1e9ac58b424158b592ea04eb104c9f6228d612ee Mon Sep 17 00:00:00 2001 From: adamValent Date: Sat, 17 Feb 2024 17:45:51 +0100 Subject: [PATCH 25/57] Implement rule 10301 - improve validation mechanism by generalization of frequently used functions - remove code duplication to some extent --- examples/basic_example.rs | 8 +- src/core/validation/compartment.rs | 15 +- src/core/validation/constraint.rs | 12 +- src/core/validation/event.rs | 54 +++--- src/core/validation/function_definition.rs | 12 +- src/core/validation/initial_assignment.rs | 12 +- src/core/validation/mod.rs | 153 +++++++++++------ src/core/validation/model.rs | 182 +++------------------ src/core/validation/parameter.rs | 15 +- src/core/validation/reaction.rs | 100 ++++------- src/core/validation/rule.rs | 12 +- src/core/validation/species.rs | 15 +- src/core/validation/unit.rs | 12 +- src/core/validation/unit_definition.rs | 31 ++-- src/lib.rs | 24 +-- 15 files changed, 288 insertions(+), 369 deletions(-) diff --git a/examples/basic_example.rs b/examples/basic_example.rs index 541f47f..90c92ad 100644 --- a/examples/basic_example.rs +++ b/examples/basic_example.rs @@ -1,4 +1,4 @@ -use biodivine_lib_sbml::{Sbml, SbmlIssue}; +use biodivine_lib_sbml::Sbml; // To run this example, execute `cargo run --example basic_example`. // If you want to add command line arguments, you can use @@ -7,15 +7,13 @@ use biodivine_lib_sbml::{Sbml, SbmlIssue}; // for the example binary, not for `cargo` itself. fn main() { // let doc = Sbml::read_path("test-inputs/COVID19_immunotherapy_Mathematical_Model.xml").unwrap(); - // let doc = - // Sbml::read_path("test-inputs/cholesterol_metabolism_and_atherosclerosis.xml").unwrap(); + // let doc = Sbml::read_path("test-inputs/cholesterol_metabolism_and_atherosclerosis.xml").unwrap(); let doc = Sbml::read_path("test-inputs/Mukandavire2020.xml").unwrap(); // let model = doc.model().get().unwrap(); // Print the whole document: // println!("{}", model.read_doc().write_str().unwrap()); - let mut issues: Vec = Vec::new(); - doc.validate(&mut issues); + let issues = doc.validate(); println!("No. of issues: {}", issues.len()); for issue in issues { diff --git a/src/core/validation/compartment.rs b/src/core/validation/compartment.rs index d7e3fc4..bcba302 100644 --- a/src/core/validation/compartment.rs +++ b/src/core/validation/compartment.rs @@ -1,10 +1,17 @@ -use crate::core::validation::apply_rule_10102; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; use crate::core::Compartment; -use crate::xml::XmlWrapper; +use crate::xml::{RequiredXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl Compartment { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for Compartment { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301( + Some(self.id().get()), + self.xml_element(), + issues, + identifiers, + ); } } diff --git a/src/core/validation/constraint.rs b/src/core/validation/constraint.rs index 19833dc..50386c1 100644 --- a/src/core/validation/constraint.rs +++ b/src/core/validation/constraint.rs @@ -1,11 +1,13 @@ -use crate::core::validation::apply_rule_10102; -use crate::core::Constraint; -use crate::xml::{OptionalXmlChild, XmlWrapper}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::{Constraint, SBase}; +use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl Constraint { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for Constraint { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); if let Some(math) = self.math().get() { math.validate(issues); diff --git a/src/core/validation/event.rs b/src/core/validation/event.rs index c7aea44..699285e 100644 --- a/src/core/validation/event.rs +++ b/src/core/validation/event.rs @@ -1,40 +1,35 @@ -use crate::core::validation::apply_rule_10102; -use crate::core::{Delay, Event, EventAssignment, Priority, Trigger}; -use crate::xml::{OptionalXmlChild, XmlWrapper}; +use crate::core::validation::{ + apply_rule_10102, apply_rule_10301, validate_list_of_objects, SbmlValidable, +}; +use crate::core::{Delay, Event, EventAssignment, Priority, SBase, Trigger}; +use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl Event { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for Event { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); if let Some(trigger) = self.trigger().get() { - trigger.validate(issues); + trigger.validate(issues, identifiers); } if let Some(priority) = self.priority().get() { - priority.validate(issues); + priority.validate(issues, identifiers); } if let Some(delay) = self.delay().get() { - delay.validate(issues); + delay.validate(issues, identifiers); } - if self.event_assignments().is_set() { - self.validate_list_of_event_assignments(issues); - } - } - - fn validate_list_of_event_assignments(&self, issues: &mut Vec) { - let list = self.event_assignments().get().unwrap(); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let evt_assignment = list.get(i); - evt_assignment.validate(issues); + if let Some(list_of_event_assignments) = self.event_assignments().get() { + validate_list_of_objects(&list_of_event_assignments, issues, identifiers); } } } -impl Trigger { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for Trigger { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); if let Some(math) = self.math().get() { math.validate(issues); @@ -42,9 +37,10 @@ impl Trigger { } } -impl Priority { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for Priority { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); if let Some(math) = self.math().get() { math.validate(issues); @@ -52,9 +48,10 @@ impl Priority { } } -impl Delay { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for Delay { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); if let Some(math) = self.math().get() { math.validate(issues); @@ -62,9 +59,10 @@ impl Delay { } } -impl EventAssignment { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for EventAssignment { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); if let Some(math) = self.math().get() { math.validate(issues); diff --git a/src/core/validation/function_definition.rs b/src/core/validation/function_definition.rs index c0f9530..aec7c52 100644 --- a/src/core/validation/function_definition.rs +++ b/src/core/validation/function_definition.rs @@ -1,11 +1,13 @@ -use crate::core::validation::apply_rule_10102; -use crate::core::FunctionDefinition; -use crate::xml::{OptionalXmlChild, XmlWrapper}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::{FunctionDefinition, SBase}; +use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl FunctionDefinition { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for FunctionDefinition { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); if let Some(math) = self.math().get() { math.validate(issues); diff --git a/src/core/validation/initial_assignment.rs b/src/core/validation/initial_assignment.rs index e64ecd6..413f0e6 100644 --- a/src/core/validation/initial_assignment.rs +++ b/src/core/validation/initial_assignment.rs @@ -1,11 +1,13 @@ -use crate::core::validation::apply_rule_10102; -use crate::core::InitialAssignment; -use crate::xml::{OptionalXmlChild, XmlWrapper}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::{InitialAssignment, SBase}; +use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl InitialAssignment { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for InitialAssignment { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); if let Some(math) = self.math().get() { math.validate(issues); diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index 1dd312e..f3c9183 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -1,11 +1,10 @@ -use std::collections::HashMap; -use std::ops::Deref; - -use xml_doc::Element; - use crate::constants::element::{ALLOWED_ATTRIBUTES, ALLOWED_CHILDREN, MATHML_ALLOWED_CHILDREN}; -use crate::xml::{XmlElement, XmlWrapper}; +use crate::core::SBase; +use crate::xml::{OptionalXmlProperty, XmlElement, XmlList, XmlWrapper}; use crate::{Sbml, SbmlIssue, SbmlIssueSeverity}; +use std::collections::{HashMap, HashSet}; +use std::ops::Deref; +use xml_doc::Element; mod compartment; mod constraint; @@ -21,28 +20,33 @@ mod species; mod unit; mod unit_definition; +/// Denotes an element that can be (and should be) validated against the SBML +/// validation rules. +pub(crate) trait SbmlValidable: XmlWrapper { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet); +} + impl Sbml { + /// ### Rule 10102 /// An SBML XML document must not contain undefined elements or attributes in the SBML Level 3 /// Core namespace or in a SBML Level 3 package namespace. Documents containing unknown /// elements or attributes placed in an SBML namespace do not conform to the SBML /// [specification](https://sbml.org/specifications/sbml-level-3/version-2/core/release-2/sbml-level-3-version-2-release-2-core.pdf). pub(crate) fn apply_rule_10102(&self, issues: &mut Vec) { let doc = self.xml.read().unwrap(); - let rule_number = "10102"; if doc.container().child_elements(doc.deref()).len() != 1 { issues.push(SbmlIssue { element: doc.container(), + message: "The document contains multiple root nodes. Only one root object is allowed.".to_string(), + rule: "10102".to_string(), severity: SbmlIssueSeverity::Error, - rule: rule_number.to_string(), - message: "The document contains multiple root nodes.".to_string(), }) } if let Some(root_element) = doc.root_element() { if root_element.name(doc.deref()) == "sbml" { validate_allowed_attributes( - rule_number, root_element, root_element.name(doc.deref()), root_element.attributes(doc.deref()), @@ -50,7 +54,6 @@ impl Sbml { ); validate_allowed_children( - rule_number, root_element, root_element.name(doc.deref()), root_element @@ -63,17 +66,19 @@ impl Sbml { } else { issues.push(SbmlIssue { element: root_element, + message: format!( + "Invalid root element <{}> found.", + root_element.name(doc.deref()) + ), + rule: "10102".to_string(), severity: SbmlIssueSeverity::Error, - rule: rule_number.to_string(), - message: format!("Unknown root element <{}>", root_element.name(doc.deref())), }) } } } } -pub fn validate_allowed_attributes( - rule: &str, +pub(crate) fn validate_allowed_attributes( element: Element, element_name: &str, attrs: &HashMap, @@ -86,19 +91,18 @@ pub fn validate_allowed_attributes( if !allowed_attributes.contains(&attr_name) { issues.push(SbmlIssue { element, - severity: SbmlIssueSeverity::Error, - rule: rule.to_string(), message: format!( - "Unknown attribute [{}] at element <{}>", + "An unknown attribute [{}] of the element <{}> found.", attr_name, element_name ), + rule: "10102".to_string(), + severity: SbmlIssueSeverity::Error, }) } } } -pub fn validate_allowed_children( - rule: &str, +pub(crate) fn validate_allowed_children( element: Element, element_name: &str, children_names: Vec<&str>, @@ -111,42 +115,31 @@ pub fn validate_allowed_children( if !allowed_children.contains(&child_name) { issues.push(SbmlIssue { element, - severity: SbmlIssueSeverity::Error, - rule: rule.to_string(), message: format!( - "Unknown child <{}> of element <{}>", + "An unknown child <{}> of the element <{}> found.", child_name, element_name ), + rule: "10102".to_string(), + severity: SbmlIssueSeverity::Error, }) } } } -pub fn apply_rule_10102(xml_element: &XmlElement, issues: &mut Vec) { - let rule_number = "10102"; - let doc = xml_element.read_doc(); - let element = xml_element.raw_element(); - let attributes = element.attributes(doc.deref()); - let children_names = element - .children(doc.deref()) - .iter() - .filter_map(|node| node.as_element().map(|it| it.full_name(doc.deref()))) - .collect(); +pub(crate) fn validate_list_of_objects( + list: &XmlList, + issues: &mut Vec, + identifiers: &mut HashSet, +) { + let allowed = get_allowed_children(list.xml_element()); + apply_rule_10102(list.xml_element(), issues); + apply_rule_10301(list.id().get(), list.xml_element(), issues, identifiers); - validate_allowed_attributes( - rule_number, - element, - xml_element.tag_name().as_str(), - attributes, - issues, - ); - validate_allowed_children( - rule_number, - element, - xml_element.tag_name().as_str(), - children_names, - issues, - ); + for object in list.as_vec() { + if allowed.contains(&object.tag_name().as_str()) { + object.validate(issues, identifiers); + } + } } pub(crate) fn get_allowed_children(xml_element: &XmlElement) -> &'static [&'static str] { @@ -158,3 +151,69 @@ pub(crate) fn get_allowed_children(xml_element: &XmlElement) -> &'static [&'stat }; allowed } + +/// ### Rule 10102 +/// An SBML XML document must not contain undefined elements or attributes in the SBML Level 3 +/// Core namespace or in a SBML Level 3 package namespace. Documents containing unknown +/// elements or attributes placed in an SBML namespace do not conform to the SBML +/// [specification](https://sbml.org/specifications/sbml-level-3/version-2/core/release-2/sbml-level-3-version-2-release-2-core.pdf). +pub(crate) fn apply_rule_10102(xml_element: &XmlElement, issues: &mut Vec) { + let doc = xml_element.read_doc(); + let element = xml_element.raw_element(); + let element_name = xml_element.tag_name(); + let attributes = element.attributes(doc.deref()); + let children_names = element + .children(doc.deref()) + .iter() + .filter_map(|node| node.as_element().map(|it| it.full_name(doc.deref()))) + .collect(); + + validate_allowed_attributes(element, element_name.as_str(), attributes, issues); + validate_allowed_children(element, element_name.as_str(), children_names, issues); +} + +/// ### Rule 10301 +/// The value of the attribute id on every instance of the following classes of objects must be unique +/// across the set of all id attribute values of all such objects in a model: +/// [AlgebraicRule](crate::core::rule::AlgebraicRule), [AssignmentRule](crate::core::rule::AssignmentRule), +/// [Compartment](compartment::Compartment), [Constraint](constraint::Constraint), [Delay](event::Delay), +/// [Event](event::Event), [EventAssignment](event::EventAssignment), +/// [FunctionDefinition](function_definition::FunctionDefinition), +/// [InitialAssignment](initial_assignment::InitialAssignment), [KineticLaw](reaction::KineticLaw), +/// [ListOfCompartments](model::Model::compartments), [ListOfConstraints](model::Model::constraints), +/// [ListOfEventAssignments](event::Event::event_assignments), [ListOfEvents](model::Model::events), +/// [ListOfFunctionDefinitions](model::Model::function_definitions), +/// [ListOfInitialAssignments](model::Model::initial_assignments), +/// [ListOfLocalParameters](reaction::KineticLaw::local_parameters), +/// [ListOfModifierSpeciesReferences](reaction::Reaction::modifiers), [ListOfParameters](model::Model::parameters), +/// [ListOfReactions](model::Model::reactions), [ListOfRules](model::Model::rules), +/// [ListOfSpecies](model::Model::species), [ListOfSpeciesReferences](reaction::Reaction::reactants), +/// [ListOfUnitDefinitions](model::Model::unit_definitions), [ListOfUnits](unit_definition::UnitDefinition::units), +/// [Model](model::Model), [ModifierSpeciesReference](reaction::ModifierSpeciesReference), +/// [Parameter](parameter::Parameter), [Priority](event::Priority), [RateRule](rule::RateRule), +/// [Reaction](reaction::Reaction), [Species](species::Species), [SpeciesReference](reaction::SpeciesReference), +/// [Trigger](event::Trigger), and [Unit](unit::Unit), plus the *id* attribute values of any SBML Level 3 package +/// element defined to be in the *SId* namespace of the [Model](model::Model). +pub(crate) fn apply_rule_10301( + id: Option, + xml_element: &XmlElement, + issues: &mut Vec, + identifiers: &mut HashSet, +) { + if let Some(id) = id { + if identifiers.contains(&id) { + issues.push(SbmlIssue { + element: xml_element.raw_element(), + message: format!( + "The identifier ('{0}') of <{1}> is already present in the .", + id, + xml_element.tag_name() + ), + rule: "10301".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } else { + identifiers.insert(id); + } + } +} diff --git a/src/core/validation/model.rs b/src/core/validation/model.rs index 75a5408..913d56a 100644 --- a/src/core/validation/model.rs +++ b/src/core/validation/model.rs @@ -1,173 +1,43 @@ -use crate::core::validation::{apply_rule_10102, get_allowed_children}; -use crate::core::Model; -use crate::xml::{OptionalXmlChild, XmlWrapper}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, validate_list_of_objects}; +use crate::core::{Model, SBase}; +use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; impl Model { - pub(crate) fn validate(&self, issues: &mut Vec) { + pub(crate) fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); - if self.function_definitions().is_set() { - self.validate_list_of_function_definitions(issues); + if let Some(list_of_function_definition) = self.function_definitions().get() { + validate_list_of_objects(&list_of_function_definition, issues, identifiers); } - if self.unit_definitions().is_set() { - self.validate_list_of_unit_definitions(issues); + if let Some(list_of_unit_definitions) = self.unit_definitions().get() { + validate_list_of_objects(&list_of_unit_definitions, issues, identifiers); } - if self.compartments().is_set() { - self.validate_list_of_compartments(issues); + if let Some(list_of_compartments) = self.compartments().get() { + validate_list_of_objects(&list_of_compartments, issues, identifiers); } - if self.species().is_set() { - self.validate_list_of_species(issues); + if let Some(list_of_species) = self.species().get() { + validate_list_of_objects(&list_of_species, issues, identifiers); } - if self.parameters().is_set() { - self.validate_list_of_parameters(issues); + if let Some(list_of_parameters) = self.parameters().get() { + validate_list_of_objects(&list_of_parameters, issues, identifiers); } - if self.initial_assignments().is_set() { - self.validate_list_of_initial_assignments(issues); + if let Some(list_of_initial_assignment) = self.initial_assignments().get() { + validate_list_of_objects(&list_of_initial_assignment, issues, identifiers); } - if self.rules().is_set() { - self.validate_list_of_rules(issues); + if let Some(list_of_rules) = self.rules().get() { + validate_list_of_objects(&list_of_rules, issues, identifiers); } - if self.constraints().is_set() { - self.validate_list_of_constraints(issues); + if let Some(list_of_constraint) = self.constraints().get() { + validate_list_of_objects(&list_of_constraint, issues, identifiers); } - if self.reactions().is_set() { - self.validate_list_of_reactions(issues); + if let Some(list_of_reactions) = self.reactions().get() { + validate_list_of_objects(&list_of_reactions, issues, identifiers); } - if self.events().is_set() { - self.validate_list_of_events(issues); + if let Some(list_of_events) = self.events().get() { + validate_list_of_objects(&list_of_events, issues, identifiers); } } - - fn validate_list_of_function_definitions(&self, issues: &mut Vec) { - let list = self.function_definitions().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let function_def = list.get(i); - if allowed.contains(&function_def.tag_name().as_str()) { - function_def.validate(issues); - } - } - } - - fn validate_list_of_unit_definitions(&self, issues: &mut Vec) { - let list = self.unit_definitions().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let unit_def = list.get(i); - if allowed.contains(&unit_def.tag_name().as_str()) { - unit_def.validate(issues); - } - } - } - - fn validate_list_of_compartments(&self, issues: &mut Vec) { - let list = self.compartments().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let compartment = list.get(i); - if allowed.contains(&compartment.tag_name().as_str()) { - compartment.validate(issues); - } - } - } - - fn validate_list_of_species(&self, issues: &mut Vec) { - let list = self.species().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let species = list.get(i); - if allowed.contains(&species.tag_name().as_str()) { - species.validate(issues); - } - } - } - - fn validate_list_of_parameters(&self, issues: &mut Vec) { - let list = self.parameters().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let parameter = list.get(i); - if allowed.contains(¶meter.tag_name().as_str()) { - parameter.validate(issues); - } - } - } - - fn validate_list_of_initial_assignments(&self, issues: &mut Vec) { - let list = self.initial_assignments().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let initial_assignment = list.get(i); - if allowed.contains(&initial_assignment.tag_name().as_str()) { - initial_assignment.validate(issues); - } - } - } - - fn validate_list_of_rules(&self, issues: &mut Vec) { - let list = self.rules().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let rule = list.get(i); - if allowed.contains(&rule.tag_name().as_str()) { - rule.validate(issues); - } - } - } - - fn validate_list_of_constraints(&self, issues: &mut Vec) { - let list = self.constraints().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let constraint = list.get(i); - if allowed.contains(&constraint.tag_name().as_str()) { - constraint.validate(issues); - } - } - } - - fn validate_list_of_reactions(&self, issues: &mut Vec) { - let list = self.reactions().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let reaction = list.get(i); - if allowed.contains(&reaction.tag_name().as_str()) { - reaction.validate(issues); - } - } - } - - fn validate_list_of_events(&self, issues: &mut Vec) { - let list = self.events().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let event = list.get(i); - if allowed.contains(&event.tag_name().as_str()) { - event.validate(issues); - } - } - } - - // fn apply_rule_10301(&self, issues: &mut Vec) {} } diff --git a/src/core/validation/parameter.rs b/src/core/validation/parameter.rs index 0e8d11b..3dd2a79 100644 --- a/src/core/validation/parameter.rs +++ b/src/core/validation/parameter.rs @@ -1,10 +1,17 @@ -use crate::core::validation::apply_rule_10102; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; use crate::core::Parameter; -use crate::xml::XmlWrapper; +use crate::xml::{RequiredXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl Parameter { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for Parameter { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301( + Some(self.id().get()), + self.xml_element(), + issues, + identifiers, + ); } } diff --git a/src/core/validation/reaction.rs b/src/core/validation/reaction.rs index 841818c..6256b30 100644 --- a/src/core/validation/reaction.rs +++ b/src/core/validation/reaction.rs @@ -1,97 +1,69 @@ -use crate::core::validation::apply_rule_10102; +use crate::core::validation::{ + apply_rule_10102, apply_rule_10301, validate_list_of_objects, SbmlValidable, +}; use crate::core::{ - KineticLaw, LocalParameter, ModifierSpeciesReference, Reaction, SpeciesReference, + KineticLaw, LocalParameter, ModifierSpeciesReference, Reaction, SBase, SpeciesReference, }; -use crate::xml::{OptionalXmlChild, XmlWrapper}; +use crate::xml::{OptionalXmlChild, OptionalXmlProperty, RequiredXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl Reaction { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for Reaction { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); - - if self.reactants().is_set() { - self.validate_list_of_reactants(issues); + apply_rule_10301( + Some(self.id().get()), + self.xml_element(), + issues, + identifiers, + ); + + if let Some(list_of_reactants) = self.reactants().get() { + validate_list_of_objects(&list_of_reactants, issues, identifiers); } - if self.products().is_set() { - self.validate_list_of_products(issues); + if let Some(list_of_products) = self.products().get() { + validate_list_of_objects(&list_of_products, issues, identifiers); } - if self.modifiers().is_set() { - self.validate_list_of_modifiers(issues); + if let Some(list_of_modifiers) = self.modifiers().get() { + validate_list_of_objects(&list_of_modifiers, issues, identifiers); } if let Some(kinetic_law) = self.kinetic_law().get() { - kinetic_law.validate(issues); - } - } - - fn validate_list_of_modifiers(&self, issues: &mut Vec) { - let list = self.modifiers().get().unwrap(); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let modifier = list.get(i); - modifier.validate(issues); - } - } - - fn validate_list_of_products(&self, issues: &mut Vec) { - let list = self.products().get().unwrap(); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let product = list.get(i); - product.validate(issues); - } - } - - fn validate_list_of_reactants(&self, issues: &mut Vec) { - let list = self.reactants().get().unwrap(); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let reactant = list.get(i); - reactant.validate(issues); + kinetic_law.validate(issues, identifiers); } } } -impl SpeciesReference { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for SpeciesReference { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); } } -impl ModifierSpeciesReference { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for ModifierSpeciesReference { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); } } -impl KineticLaw { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for KineticLaw { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); - if self.local_parameters().is_set() { - self.validate_list_of_local_parameters(issues); + if let Some(list_of_local_parameters) = self.local_parameters().get() { + validate_list_of_objects(&list_of_local_parameters, issues, identifiers); } if let Some(math) = self.math().get() { math.validate(issues); } } - - fn validate_list_of_local_parameters(&self, issues: &mut Vec) { - let list = self.local_parameters().get().unwrap(); - apply_rule_10102(list.xml_element(), issues); - - for i in 0..list.len() { - let local_param = list.get(i); - local_param.validate(issues); - } - } } -impl LocalParameter { - pub(crate) fn validate(&self, issues: &mut Vec) { - apply_rule_10102(self.xml_element(), issues); +impl SbmlValidable for LocalParameter { + fn validate(&self, issues: &mut Vec, _identifiers: &mut HashSet) { + apply_rule_10102(self.xml_element(), issues) } } diff --git a/src/core/validation/rule.rs b/src/core/validation/rule.rs index 7f6adec..2e1e63c 100644 --- a/src/core/validation/rule.rs +++ b/src/core/validation/rule.rs @@ -1,11 +1,13 @@ -use crate::core::validation::apply_rule_10102; -use crate::core::{AbstractRule, Rule}; -use crate::xml::{OptionalXmlChild, XmlWrapper}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::{AbstractRule, Rule, SBase}; +use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl AbstractRule { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for AbstractRule { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); if let Some(math) = self.math().get() { math.validate(issues); diff --git a/src/core/validation/species.rs b/src/core/validation/species.rs index 2cc505c..bba820f 100644 --- a/src/core/validation/species.rs +++ b/src/core/validation/species.rs @@ -1,10 +1,17 @@ -use crate::core::validation::apply_rule_10102; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; use crate::core::Species; -use crate::xml::XmlWrapper; +use crate::xml::{RequiredXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl Species { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for Species { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301( + Some(self.id().get()), + self.xml_element(), + issues, + identifiers, + ); } } diff --git a/src/core/validation/unit.rs b/src/core/validation/unit.rs index bfaf702..ef86631 100644 --- a/src/core/validation/unit.rs +++ b/src/core/validation/unit.rs @@ -1,10 +1,12 @@ -use crate::core::validation::apply_rule_10102; -use crate::core::Unit; -use crate::xml::XmlWrapper; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::{SBase, Unit}; +use crate::xml::{OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl Unit { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for Unit { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); } } diff --git a/src/core/validation/unit_definition.rs b/src/core/validation/unit_definition.rs index b8fc748..16d30e3 100644 --- a/src/core/validation/unit_definition.rs +++ b/src/core/validation/unit_definition.rs @@ -1,27 +1,18 @@ -use crate::core::validation::{apply_rule_10102, get_allowed_children}; -use crate::core::UnitDefinition; -use crate::xml::{OptionalXmlChild, XmlWrapper}; +use crate::core::validation::{ + apply_rule_10102, apply_rule_10301, validate_list_of_objects, SbmlValidable, +}; +use crate::core::{SBase, UnitDefinition}; +use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; +use std::collections::HashSet; -impl UnitDefinition { - pub(crate) fn validate(&self, issues: &mut Vec) { +impl SbmlValidable for UnitDefinition { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); + apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); - if self.units().is_set() { - self.validate_list_of_units(issues); - } - } - - fn validate_list_of_units(&self, issues: &mut Vec) { - let list = self.units().get().unwrap(); - apply_rule_10102(list.xml_element(), issues); - - let allowed = get_allowed_children(list.xml_element()); - for i in 0..list.len() { - let unit = list.get(i); - if allowed.contains(&unit.tag_name().as_str()) { - unit.validate(issues); - } + if let Some(list_of_units) = self.units().get() { + validate_list_of_objects(&list_of_units, issues, identifiers); } } } diff --git a/src/lib.rs b/src/lib.rs index 13b8a78..4a5ce31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::str::FromStr; use std::sync::{Arc, RwLock}; @@ -12,7 +13,7 @@ use crate::xml::{OptionalXmlChild, XmlDocument, XmlElement}; /// A module with useful types that are not directly part of the SBML specification, but help /// us work with XML documents in a sane and safe way. In particular: /// - [XmlDocument] | A thread and memory safe reference to a [Document]. -/// - [XmlElement] | A thread and memory safe reference to an [xml_doc::Element]. +/// - [XmlElement] | A thread and memory safe reference to an [Element]. /// - [xml::XmlWrapper] | A trait with utility functions for working with types /// derived from [XmlElement]. /// - [xml::XmlDefault] | An extension of [xml::XmlWrapper] which allows creation of "default" @@ -100,29 +101,28 @@ impl Sbml { RequiredProperty::new(&self.sbml_root, "version") } - /// Validates the document against validation rules specified in the [specification](https://sbml.org/specifications/sbml-level-3/version-2/core/release-2/sbml-level-3-version-2-release-2-core.pdf) - /// + /// Validates the document against validation rules specified in the + /// [specification](https://sbml.org/specifications/sbml-level-3/version-2/core/release-2/sbml-level-3-version-2-release-2-core.pdf). + /// Eventual issues are returned in the vector. Empty vector represents a valid document. /// ### Rule 10101 /// is already satisfied implicitly by the use of the package *xml-doc* as writing /// is done only in UTF-8 and reading produces error if encoding is different from UTF-8, /// UTF-16, ISO 8859-1, GBK or EUC-KR. /// - /// ### Rule 10102 - /// states that an SBML XML document must not contain undefined elements or attributes in the SBML Level 3 - /// Core namespace or in a SBML Level 3 package namespace. Documents containing unknown - /// elements or attributes placed in an SBML namespace do not conform to the SBML specification. - /// (References: SBML L3V1 Section 4.1; SBML L3V2 Section 4.1.) - /// /// ### Rule 10104 /// is already satisfied implicitly by the use of the package *xml-doc* as loading /// a document without an error ensures that the document conforms to the basic /// structural and syntactic constraints. - pub fn validate(&self, issues: &mut Vec) { - self.apply_rule_10102(issues); + pub fn validate(&self) -> Vec { + let mut issues: Vec = vec![]; + let mut identifiers: HashSet = HashSet::new(); + self.apply_rule_10102(&mut issues); if let Some(model) = self.model().get() { - model.validate(issues); + model.validate(&mut issues, &mut identifiers); } + + issues } } From 54b62a05c9ef60c731332972f684c5f96d4998ae Mon Sep 17 00:00:00 2001 From: adamValent Date: Mon, 19 Feb 2024 18:08:54 +0100 Subject: [PATCH 26/57] Implement rule 10302 --- src/core/validation/math.rs | 86 ++++++++++++++++++-------- src/core/validation/mod.rs | 1 + src/core/validation/model.rs | 3 +- src/core/validation/unit_definition.rs | 33 ++++++++-- 4 files changed, 91 insertions(+), 32 deletions(-) diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 74b5bb0..fbc3618 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -13,9 +13,41 @@ use crate::xml::{RequiredXmlProperty, XmlWrapper}; use crate::{SbmlIssue, SbmlIssueSeverity}; impl Math { - /// Applies rules: - /// - **[10201](self.apply_rule_10201)** - MathML content is permitted only within [Math] element. - /// - **[10202](self.apply_rule_10202)** - Validates list of permitted elements within [Math] element. + /// ### Applies rules: + /// - **[10201](Math::apply_rule_10201)** - MathML content is permitted only within [Math] element. + /// - **[10202](Math::apply_rule_10202)** - Validates list of permitted elements within [Math] element. + /// - **[10203](Math::apply_rule_10203)** - Ensures *encoding* attribute correct placement. + /// - **[10204](Math::apply_rule_10204)** - Ensures *definitionURL* attribute correct placement. + /// - **[10205](Math::apply_rule_10205)** - Ensures *definitionURL* attribute correct value. + /// - **[10206](Math::apply_rule_10206)** - Ensures *type* attribute correct placement. + /// - **[10207](Math::apply_rule_10207)** - Ensures *type* attribute correct value. + /// - **[10208](Math::apply_rule_10208)** - Validates *lambda* element usage. + /// - **[10214](Math::apply_rule_10214)** - Validates first *ci* element usage outside of [FunctionDefinition]. + /// - **[10215](Math::apply_rule_10215)** - Validates non-first *ci* element usage outside of [FunctionDefinition]. + /// - **[10216](Math::apply_rule_10216)** - Validates [LocalParameter](crate::core::LocalParameter) *id* occurrence. + /// - **[10218](Math::apply_rule_10218)** - Validates number of arguments for operators. + /// - **[10219](Math::apply_rule_10219)** - Validates number of arguments for [FunctionDefinition]. + /// - **[10220](Math::apply_rule_10220)** - Ensures *units* attribute correct placement. + /// - **[10221](Math::apply_rule_10220)** - Ensures *units* attribute correct value. + /// - **[10223](Math::apply_rule_10223)** - Validates *rateOf* *csymbol* element has single argument. + /// - **[10224](Math::apply_rule_10224)** - Validates the argument of *rateOf* *csymbol* element. + /// - **[10225](Math::apply_rule_10225)** - Validates the value of argument of *rateOf* *csymbol* element. + /// + /// ### Ignored rules as of SBML Level 3 Version 1 Core: + /// - **10209** - "The arguments of the MathML logical operators and, not, or, and xor must evaluate to Boolean values." + /// - **10210** - "The arguments to the following MathML constructs must evaluate to numeric values (more specifically, they + /// must evaluate to MathML real, integer, rational, or "e-notation" numbers, or the time, delay, avogadro, csymbol elements): abs, + /// arccosh, arccos, arccoth, arccot, arccsch, arccsc, arcsech, arcsec, arcsinh, arcsin, arctanh, arctan, ceiling, cosh, cos, coth, + /// cot, csch, csc, divide, exp, factorial, floor, ln, log, minus, plus, power, root, sech, sec, sinh, sin, tanh, tan, and times." + /// - **10211** - "The values of all arguments to MathML eq and neq operators must evaluate to the same type, either all + /// Boolean or all numeric." + /// - **10212** - "The types of the values within MathML piecewise operators should all be consistent; i.e., the set of expressions + /// that make up the first arguments of the piece and otherwise operators within the same piecewise operator should all return + /// values of the same type." + /// - **10213** - "The second argument of a MathML piece operator must evaluate to a Boolean value." + /// - **10217** - "The MathML formulas in the following elements must yield numeric values (that is, MathML real, integer + /// or "e-notation" numbers, or the time, delay, avogadro, or rateOf csymbol): math in KineticLaw, math in InitialAssignment, math in + /// AssignmentRule, math in RateRule, math in AlgebraicRule, math in Event Delay, and math in EventAssignment." pub(crate) fn validate(&self, issues: &mut Vec) { self.apply_rule_10201(issues); self.apply_rule_10202(issues); @@ -65,7 +97,7 @@ impl Math { /// element. An SBML package may allow new MathML elements to be added to this list, and if so, /// the package must define **required="true"** on the SBML container element /// [**sbml**](crate::Sbml). - fn apply_rule_10202(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10202(&self, issues: &mut Vec) { let doc = self.read_doc(); let children = self.raw_element().children_recursive(doc.deref()); let allowed_children = get_allowed_children(self.xml_element()); @@ -96,7 +128,7 @@ impl Math { /// an **encoding** attribute. An SBML package may allow the encoding attribute on other /// elements, and if so, the package must define **required="true"** on the SBML container /// element [**sbml**](crate::Sbml). - fn apply_rule_10203(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10203(&self, issues: &mut Vec) { let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["encoding"]; let children_of_interest = self @@ -132,7 +164,7 @@ impl Math { /// **definitionURL** attribute. An SBML package may allow the definitionURL attribute on other /// elements, and if so, the package must define **required="true"** on the SBML container /// element [**sbml**](crate::Sbml). - fn apply_rule_10204(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10204(&self, issues: &mut Vec) { let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["definitionURL"]; let children_of_interest = self @@ -170,7 +202,7 @@ impl Math { /// "**http://www.sbml.org/sbml/symbols/rateOf**". An SBML package may allow new values for the /// definitionURL attribute of a csymbol, and if so, the package must define **required="true"** /// on the SBML container element [**sbml**](crate::Sbml). - fn apply_rule_10205(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10205(&self, issues: &mut Vec) { let doc = self.read_doc(); let children_of_interest = self .raw_element() @@ -211,7 +243,7 @@ impl Math { /// construct. **No** other MathML elements may have a type attribute. An SBML package may allow /// the type attribute on other elements, and if so, the package must define **required="true"** /// on the SBML container element [**sbml**](crate::Sbml). - fn apply_rule_10206(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10206(&self, issues: &mut Vec) { let doc = self.read_doc(); let children_of_interest = self .raw_element() @@ -245,7 +277,7 @@ impl Math { /// "**e-notation**", "**real**", "**integer**", and "**rational**". An SBML package may /// allow new values for the type attribute, and if so, the package must define /// **required="true"** on the SBML container element [**sbml**](crate::Sbml). - fn apply_rule_10207(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10207(&self, issues: &mut Vec) { let doc = self.read_doc(); let children_of_interest = self .raw_element() @@ -282,7 +314,7 @@ impl Math { /// elements may not be used elsewhere in an SBML model. An SBML package may allow **lambda** /// elements on other elements, and if so, the package must define **required="true"** on the /// SBML container element [**sbml**](crate::Sbml). - fn apply_rule_10208(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10208(&self, issues: &mut Vec) { let doc = self.read_doc(); let children_of_interest = self .raw_element() @@ -357,7 +389,7 @@ impl Math { /// value can only be chosen from the set of identifiers of /// [**FunctionDefinition**](FunctionDefinition) objects defined in the enclosing /// SBML [Model](crate::core::model) object. - fn apply_rule_10214(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10214(&self, issues: &mut Vec) { let doc = self.read_doc(); let parent_name = self .raw_element() @@ -409,7 +441,8 @@ impl Math { } } - // TODO: needs review + // TODO: create an issue "the identifiers of [LocalParameter](crate::core::reaction::LocalParameter) objects that are [Reaction](crate::core::reaction::Reaction) in which the [FunctionDefinition] appears (if it appears inside the [Math] object of a [KineticLaw])" + // TODO: add comment about "any identifiers (in the SId namespace of the model) belonging to an object class defined by an SBML Level 3 package as having mathematical meaning." to existing issue about adding extensions/packages /// ### Rule 10215 /// Outside of a [FunctionDefinition] object, if a MathML **ci** element is not the first element within /// a MathML **apply**, then the **ci** element's value may only be chosen from the following set of @@ -421,7 +454,7 @@ impl Math { /// [Reaction](crate::core::reaction::Reaction) in which the [FunctionDefinition] appears (if it appears inside /// the [Math] object of a [KineticLaw]); and any identifiers (in the SId namespace of the model) belonging to an /// object class defined by an SBML Level 3 package as having mathematical meaning. - fn apply_rule_10215(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10215(&self, issues: &mut Vec) { let is_out_of_function_definition = FunctionDefinition::for_child_element(self.document(), self.xml_element()).is_none(); @@ -435,7 +468,7 @@ impl Math { model.species_identifiers(), model.species_reference_identifiers(), model.reaction_identifiers(), - model.local_parameter_identifiers(), + model.local_parameter_identifiers(), // TODO: include only localParameters whose kineticLaw::math element contains some functionDefinition id ] .concat(); let apply_elements = self @@ -474,7 +507,6 @@ impl Math { } } - // TODO: needs review /// ### Rule 10216 /// The id attribute value of a [LocalParameter] object defined within a [KineticLaw] object may only be /// used, in core, in MathML ci elements within the math element of that same [KineticLaw]; in other @@ -482,7 +514,7 @@ impl Math { /// of that [Reaction] instance. In package constructs, the **id** attribute value of a [LocalParameter] object /// may only be used in MathML ci elements or as the target of an SIdRef attribute if that package /// construct is a child of the parent [Reaction]. - fn apply_rule_10216(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10216(&self, issues: &mut Vec) { let doc = self.read_doc(); let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); let all_local_param_ids = model.local_parameter_identifiers(); @@ -530,7 +562,7 @@ impl Math { /// ### Rule 10218 /// A MathML operator must be supplied the number of arguments appropriate for that operator. - fn apply_rule_10218(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10218(&self, issues: &mut Vec) { let doc = self.read_doc(); let apply_elements = self .raw_element() @@ -611,7 +643,11 @@ impl Math { } /// ### Rule 10219 - fn apply_rule_10219(&self, issues: &mut Vec) { + /// The number of arguments used in a call to a function defined by a [FunctionDefinition] object must + /// equal the number of arguments accepted by that function, if defined. In other words, it must equal + /// the number of MathML **bvar** elements inside the **lambda** element of the function definition, if + /// present. + pub(crate) fn apply_rule_10219(&self, issues: &mut Vec) { let doc = self.read_doc(); let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); @@ -662,7 +698,7 @@ impl Math { /// are permitted to have the **units** attribute. An SBML package may allow the **units** attribute /// on other elements, and if so, the package must define **required="true"** on the SBML container /// element [**sbml**](crate::Sbml). - fn apply_rule_10220(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10220(&self, issues: &mut Vec) { let doc = self.read_doc(); let children_of_interest: Vec = self .raw_element() @@ -693,7 +729,7 @@ impl Math { /// ### Rule 10221 /// The value of the SBML attribute units on a MathML cn element must be chosen from either the /// set of identifiers of UnitDefinition objects in the model, or the set of base units defined by SBML. - fn apply_rule_10221(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10221(&self, issues: &mut Vec) { let doc = self.read_doc(); let unit_identifiers = Model::for_child_element(self.document(), self.xml_element()) .unwrap() @@ -729,7 +765,7 @@ impl Math { /// ### Rule 10223 /// The single argument for the *rateOf* **csymbol** function must be a **ci** element. - fn apply_rule_10223(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10223(&self, issues: &mut Vec) { let doc = self.read_doc(); let children_of_interest = self .raw_element() @@ -786,7 +822,7 @@ impl Math { /// The target of a *rateOf* **csymbol** function must not appear as the *variable* of an /// [AssignmentRule](crate::core::rule::AssignmentRule), nor may its value be determined by an /// [AlgebraicRule](crate::core::rule::AlgebraicRule). - fn apply_rule_10224(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10224(&self, issues: &mut Vec) { let doc = self.read_doc(); let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); let ci_elements = self @@ -827,6 +863,7 @@ impl Math { severity: SbmlIssueSeverity::Error, }) // TODO: what does "determined by algebraicRule" mean and how to check it? + // TODO: same as 10225 } else if algebraic_rule_determinants.contains(&value) { issues.push(SbmlIssue { element: ci, @@ -845,7 +882,7 @@ impl Math { /// *hasOnlySubstanceUnits* value of *"false"*, the **compartment** of that [Species](crate::core::species::Species) /// must not appear as the *variable* of an [AssignmentRule](crate::core::rule::AssignmentRule), /// nor may its *size* be determined by an [AlgebraicRule](crate::core::rule::AlgebraicRule). - fn apply_rule_10225(&self, issues: &mut Vec) { + pub(crate) fn apply_rule_10225(&self, issues: &mut Vec) { let doc = self.read_doc(); let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); let assignment_rule_variables = model.assignment_rule_variables(); @@ -853,7 +890,7 @@ impl Math { let ci_elements = self .raw_element() .child_elements_recursive(doc.deref()) - .iter() + .into_iter() .filter(|child| { child.name(doc.deref()) == "apply" && child.child_elements(doc.deref()).len() > 1 @@ -870,7 +907,6 @@ impl Math { .name(doc.deref()) == "ci" }) - .copied() .collect::>(); for ci in ci_elements { diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index f3c9183..1771065 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -172,6 +172,7 @@ pub(crate) fn apply_rule_10102(xml_element: &XmlElement, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); - apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); if let Some(list_of_units) = self.units().get() { validate_list_of_objects(&list_of_units, issues, identifiers); } } } + +impl UnitDefinition { + pub(crate) fn apply_rule_10302( + list_of_unit_definitions: &XmlList, + issues: &mut Vec, + ) { + let mut identifiers: HashSet = HashSet::new(); + + for unit_definition in list_of_unit_definitions.as_vec() { + let id = unit_definition.id().get().unwrap_or_default(); + if identifiers.contains(&id) { + issues.push(SbmlIssue { + element: unit_definition.raw_element(), + message: format!("The identifier ('{0}') of is already present in the .", + id), + rule: "10302".to_string(), + severity: SbmlIssueSeverity::Error + }) + } else { + identifiers.insert(id); + } + } + } +} From 2175e2a65fd1a59f31a5906a69201303db50b763 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Mon, 19 Feb 2024 20:56:54 +0100 Subject: [PATCH 27/57] Rollback formatting changes. --- src/core/validation/model.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/core/validation/model.rs b/src/core/validation/model.rs index 75a5408..371f0bb 100644 --- a/src/core/validation/model.rs +++ b/src/core/validation/model.rs @@ -41,9 +41,9 @@ impl Model { fn validate_list_of_function_definitions(&self, issues: &mut Vec) { let list = self.function_definitions().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); + let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let function_def = list.get(i); if allowed.contains(&function_def.tag_name().as_str()) { @@ -54,9 +54,9 @@ impl Model { fn validate_list_of_unit_definitions(&self, issues: &mut Vec) { let list = self.unit_definitions().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); + let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let unit_def = list.get(i); if allowed.contains(&unit_def.tag_name().as_str()) { @@ -67,9 +67,9 @@ impl Model { fn validate_list_of_compartments(&self, issues: &mut Vec) { let list = self.compartments().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); + let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let compartment = list.get(i); if allowed.contains(&compartment.tag_name().as_str()) { @@ -80,9 +80,9 @@ impl Model { fn validate_list_of_species(&self, issues: &mut Vec) { let list = self.species().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); + let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let species = list.get(i); if allowed.contains(&species.tag_name().as_str()) { @@ -93,9 +93,9 @@ impl Model { fn validate_list_of_parameters(&self, issues: &mut Vec) { let list = self.parameters().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); + let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let parameter = list.get(i); if allowed.contains(¶meter.tag_name().as_str()) { @@ -106,9 +106,9 @@ impl Model { fn validate_list_of_initial_assignments(&self, issues: &mut Vec) { let list = self.initial_assignments().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); + let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let initial_assignment = list.get(i); if allowed.contains(&initial_assignment.tag_name().as_str()) { @@ -119,9 +119,9 @@ impl Model { fn validate_list_of_rules(&self, issues: &mut Vec) { let list = self.rules().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); + let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let rule = list.get(i); if allowed.contains(&rule.tag_name().as_str()) { @@ -132,9 +132,9 @@ impl Model { fn validate_list_of_constraints(&self, issues: &mut Vec) { let list = self.constraints().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); + let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let constraint = list.get(i); if allowed.contains(&constraint.tag_name().as_str()) { @@ -145,9 +145,9 @@ impl Model { fn validate_list_of_reactions(&self, issues: &mut Vec) { let list = self.reactions().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); + let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let reaction = list.get(i); if allowed.contains(&reaction.tag_name().as_str()) { @@ -158,9 +158,9 @@ impl Model { fn validate_list_of_events(&self, issues: &mut Vec) { let list = self.events().get().unwrap(); - let allowed = get_allowed_children(list.xml_element()); apply_rule_10102(list.xml_element(), issues); + let allowed = get_allowed_children(list.xml_element()); for i in 0..list.len() { let event = list.get(i); if allowed.contains(&event.tag_name().as_str()) { @@ -168,6 +168,4 @@ impl Model { } } } - - // fn apply_rule_10301(&self, issues: &mut Vec) {} } From 05995b64bd3ce1c2da1d5e70d96de7c679de6845 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Mon, 19 Feb 2024 21:02:49 +0100 Subject: [PATCH 28/57] Minor styling. --- src/core/validation/mod.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index 1dd312e..ae22d7f 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -150,11 +150,12 @@ pub fn apply_rule_10102(xml_element: &XmlElement, issues: &mut Vec) { } pub(crate) fn get_allowed_children(xml_element: &XmlElement) -> &'static [&'static str] { - let Some(allowed) = ALLOWED_CHILDREN.get(xml_element.tag_name().as_str()) else { - let Some(allowed) = MATHML_ALLOWED_CHILDREN.get(xml_element.tag_name().as_str()) else { - return &[]; - }; - return allowed; - }; - allowed + let tag_name = xml_element.tag_name(); + if let Some(allowed) = ALLOWED_CHILDREN.get(&tag_name) { + allowed + } else if let Some(allowed) = MATHML_ALLOWED_CHILDREN.get(&tag_name) { + allowed + } else { + &[] + } } From 7e1ed7fe7b873995f4bb108fa10524ec0ca60f78 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Mon, 19 Feb 2024 22:24:30 +0100 Subject: [PATCH 29/57] Refactor duplicate `for_child_element`. --- src/core/function_definition.rs | 34 ++++------------------------- src/core/model.rs | 28 ++---------------------- src/core/reaction.rs | 38 ++++++--------------------------- src/core/sbase.rs | 33 ++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 88 deletions(-) diff --git a/src/core/function_definition.rs b/src/core/function_definition.rs index cade36b..8097527 100644 --- a/src/core/function_definition.rs +++ b/src/core/function_definition.rs @@ -1,10 +1,7 @@ -use crate::constants::namespaces::URL_SBML_CORE; use crate::core::sbase::SbmlUtils; use crate::core::Math; -use crate::xml::{OptionalChild, XmlDefault, XmlDocument, XmlElement, XmlWrapper}; +use crate::xml::{OptionalChild, XmlDefault, XmlDocument, XmlElement}; use macros::{SBase, XmlWrapper}; -use std::ops::Deref; -use xml_doc::{Document, Element}; /// Individual function definition #[derive(Clone, Debug, XmlWrapper, SBase)] @@ -13,33 +10,10 @@ pub struct FunctionDefinition(XmlElement); impl FunctionDefinition { /// Try to find an instance of a [FunctionDefinition] element for the given child element. /// - /// The child can be any SBML tag, as long as it appears in an SBML model (i.e. one of - /// its transitive parents is a [Model] element). If this is not satisfied, the method - /// returns `None`. + /// The child can be any SBML tag, as long as one of its transitive parents is a + /// [FunctionDefinition] element. If this is not satisfied, the method returns `None`. pub fn for_child_element(doc: XmlDocument, child: &XmlElement) -> Option { - let parent = { - let read_doc = doc.read().unwrap(); - fn is_function_definition(doc: &Document, e: Element) -> bool { - let name = e.name(doc); - let Some(namespace) = e.namespace(doc) else { - return false; - }; - - name == "functionDefinition" && namespace == URL_SBML_CORE - } - - let mut parent = child.raw_element(); - while !is_function_definition(read_doc.deref(), parent) { - let Some(node) = parent.parent(read_doc.deref()) else { - return None; - }; - parent = node; - } - parent - }; - let model = XmlElement::new_raw(doc, parent); - // Safe because we checked that the element has the correct tag name and namespace. - Some(unsafe { FunctionDefinition::unchecked_cast(model) }) + Self::search_in_parents(doc, child, "functionDefinition") } pub fn math(&self) -> OptionalChild { diff --git a/src/core/model.rs b/src/core/model.rs index a1b3472..34ada56 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -1,4 +1,3 @@ -use crate::constants::namespaces::URL_SBML_CORE; use crate::core::sbase::SbmlUtils; use crate::core::{ AbstractRule, AlgebraicRule, AssignmentRule, Compartment, Constraint, Event, @@ -12,7 +11,7 @@ use crate::xml::{ use macros::{SBase, XmlWrapper}; use std::ops::Deref; -use xml_doc::{Document, Element}; +use xml_doc::Element; /// A type-safe representation of an SBML element. #[derive(Clone, Debug, XmlWrapper, SBase)] @@ -33,30 +32,7 @@ impl Model { /// its transitive parents is a [Model] element). If this is not satisfied, the method /// returns `None`. pub fn for_child_element(doc: XmlDocument, child: &XmlElement) -> Option { - let parent = { - let read_doc = doc.read().unwrap(); - fn is_model(doc: &Document, e: Element) -> bool { - let name = e.name(doc); - let Some(namespace) = e.namespace(doc) else { - return false; - }; - - name == "model" && namespace == URL_SBML_CORE - } - - let mut parent = child.raw_element(); - while !is_model(read_doc.deref(), parent) { - let Some(node) = parent.parent(read_doc.deref()) else { - return None; - }; - parent = node; - } - - parent - }; - let model = XmlElement::new_raw(doc, parent); - // Safe because we checked that the element has the correct tag name and namespace. - Some(unsafe { Model::unchecked_cast(model) }) + Self::search_in_parents(doc, child, "model") } pub fn function_definitions(&self) -> OptionalChild> { diff --git a/src/core/reaction.rs b/src/core/reaction.rs index a5fc6ff..8b87d4f 100644 --- a/src/core/reaction.rs +++ b/src/core/reaction.rs @@ -1,13 +1,10 @@ -use crate::constants::namespaces::URL_SBML_CORE; use crate::core::sbase::SbmlUtils; use crate::core::{Math, SBase}; use crate::xml::{ OptionalChild, OptionalProperty, OptionalXmlChild, RequiredProperty, RequiredXmlProperty, - XmlDefault, XmlDocument, XmlElement, XmlList, XmlWrapper, + XmlDefault, XmlDocument, XmlElement, XmlList, }; use macros::{SBase, XmlWrapper}; -use std::ops::Deref; -use xml_doc::{Document, Element}; #[derive(Clone, Debug, XmlWrapper, SBase)] pub struct Reaction(XmlElement); @@ -100,36 +97,13 @@ impl XmlDefault for KineticLaw { } impl KineticLaw { - /// Try to find an instance of a [KineticLaw] element for the given child element. + /// Try to find an instance of a [KineticLaw] element that is a parent of the given + /// child element. /// - /// The child can be any SBML tag, as long as it appears in an SBML model (i.e. one of - /// its transitive parents is a [KineticLaw] element). If this is not satisfied, the method - /// returns `None`. + /// The child can be any SBML tag, as long as one of its transitive parents is a + /// [KineticLaw] element. If this is not satisfied, the method returns `None`. pub fn for_child_element(doc: XmlDocument, child: &XmlElement) -> Option { - let parent = { - let read_doc = doc.read().unwrap(); - fn is_kinetic_law(doc: &Document, e: Element) -> bool { - let name = e.name(doc); - let Some(namespace) = e.namespace(doc) else { - return false; - }; - - name == "kineticLaw" && namespace == URL_SBML_CORE - } - - let mut parent = child.raw_element(); - while !is_kinetic_law(read_doc.deref(), parent) { - let Some(node) = parent.parent(read_doc.deref()) else { - return None; - }; - parent = node; - } - - parent - }; - let xml_element = XmlElement::new_raw(doc, parent); - // Safe because we checked that the element has the correct tag name and namespace. - Some(unsafe { KineticLaw::unchecked_cast(xml_element) }) + Self::search_in_parents(doc, child, "kineticLaw") } pub fn math(&self) -> OptionalChild { diff --git a/src/core/sbase.rs b/src/core/sbase.rs index d706e0d..df8a356 100644 --- a/src/core/sbase.rs +++ b/src/core/sbase.rs @@ -8,6 +8,8 @@ use crate::xml::{ OptionalChild, OptionalProperty, RequiredProperty, XmlDocument, XmlElement, XmlPropertyType, XmlWrapper, }; +use std::ops::Deref; +use xml_doc::{Document, Element}; /// Abstract class SBase that is the parent of most of the elements in SBML. /// Thus there is no need to implement concrete structure. @@ -41,6 +43,37 @@ pub trait SBase: XmlWrapper { /// In the end, this trait probably should not be accessible from the outside, but we can /// discuss this later. pub(crate) trait SbmlUtils: SBase { + /// Create a new instance of `Self` by traversing the parents of the given + /// XML element until the appropriate tag is discovered. If no such tag is found, + /// returns `None`. + /// + /// TODO: Currently, this requires SBML core namespace. + fn search_in_parents(doc: XmlDocument, child: &XmlElement, tag_name: &str) -> Option { + let parent = { + let read_doc = doc.read().unwrap(); + fn check_name(doc: &Document, e: Element, tag_name: &str) -> bool { + let name = e.name(doc); + let Some(namespace) = e.namespace(doc) else { + return false; + }; + + name == tag_name && namespace == URL_SBML_CORE + } + + let mut parent = child.raw_element(); + while !check_name(read_doc.deref(), parent, tag_name) { + let Some(node) = parent.parent(read_doc.deref()) else { + return None; + }; + parent = node; + } + parent + }; + let model = XmlElement::new_raw(doc, parent); + // Safe because we checked that the element has the correct tag name and namespace. + Some(unsafe { Self::unchecked_cast(model) }) + } + /// Create a new instance of `Self` which is just an empty tag with the given `tag_name` /// and using SBML namespace. #[inline(always)] From ff051ff8344c0e087c3d4f502406aeeba47b48ba Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Wed, 21 Feb 2024 15:06:40 +0100 Subject: [PATCH 30/57] Add automated SBML test suite. --- .github/workflows/build.yml | 22 ++++ .gitignore | 5 +- examples/test-suite-syntactic.rs | 197 +++++++++++++++++++++++++++++++ src/lib.rs | 2 +- validated-rules.txt | 28 +++++ 5 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 examples/test-suite-syntactic.rs create mode 100644 validated-rules.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4e8cce..a8fe8c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,6 +74,28 @@ jobs: components: clippy - run: cargo clippy --all-features + # Run SBML test suite + sbml-test-suite: + needs: check + name: SBML Test Suite + runs-on: ubuntu-latest + env: + RUSTFLAGS: "-D warnings" + steps: + - uses: actions/checkout@v3 + - run: wget https://github.com/sbmlteam/sbml-test-suite/releases/download/3.4.0/semantic_tests.v3.4.0.zip + - run: unzip semantic_tests.v3.4.0.zip + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.RUST_VERSION }} + components: rustfmt + - run: cargo run --release --example test-suite-syntactic + - run: zip test-results.zip ./test_suite_errors.txt ./test_suite_info.txt ./test_suite_warning.txt + - uses: actions/upload-artifact@v4 + with: + name: results + path: ./test-results.zip + # Compute code coverage codecov: needs: test diff --git a/.gitignore b/.gitignore index 49db00a..4273f7d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,7 @@ Cargo.lock .idea # VScode project folder -.vscode \ No newline at end of file +.vscode + +# The SBML test suite files. +syntactic \ No newline at end of file diff --git a/examples/test-suite-syntactic.rs b/examples/test-suite-syntactic.rs new file mode 100644 index 0000000..89a488a --- /dev/null +++ b/examples/test-suite-syntactic.rs @@ -0,0 +1,197 @@ +use biodivine_lib_sbml::{Sbml, SbmlIssue, SbmlIssueSeverity}; +use std::collections::{HashMap, HashSet}; +use std::path::Path; +use std::process::exit; + +/// This is an integration test that uses the examples from the SBML test suite +/// to validate the functionality of the library. +/// +/// The test data can be downloaded here: https://github.com/sbmlteam/sbml-test-suite/releases +/// +/// Specifically, the syntactic tests should be extracted into a `syntactic` directory +/// in the main folder of the repository. +/// +/// Since it is not super easy to break down each case into separate test, we instead compile +/// a report of all violations that is printed at the end of the test. +/// +/// If you only want to test a specific subset of rules, you can provide these as command line +/// arguments. +fn main() { + let dir_path = "./syntactic"; + + if !Path::new(dir_path).is_dir() { + panic!("Test data is missing.") + } + + let args = std::env::args().collect::>(); + let filter: Option> = if args.len() > 1 { + Some(HashSet::from_iter(args.into_iter().skip(1))) + } else { + None + }; + + if let Some(filter) = filter.as_ref() { + println!( + "Test suite restricted to {} rules: {:?}", + filter.len(), + filter + ); + } + + let test_issue = |id: &str| { + if let Some(filter) = filter.as_ref() { + filter.contains(id) + } else { + true + } + }; + + let mut tested = HashSet::new(); + + let mut error_problems = Vec::new(); + let mut warning_problems = Vec::new(); + let mut info_problems = Vec::new(); + + for rule_dir in std::fs::read_dir(dir_path).unwrap() { + let rule_dir = rule_dir.unwrap(); + let name = rule_dir.file_name(); + let name = name.to_str().unwrap(); + if !rule_dir.path().is_dir() { + println!("Skipping file {} (not a directory).", name); + continue; + } + tested.insert(name.to_string()); + + let mut test_cases = Vec::new(); + for test_file in std::fs::read_dir(rule_dir.path()).unwrap() { + let test_file = test_file.unwrap(); + let test_name = test_file.file_name(); + let test_name = test_name.to_str().unwrap(); + if !test_name.ends_with(".xml") { + continue; + } + if !test_name.contains("l3v2") { + // Skip any tests that are not for SBML level 3 version 2. + continue; + } + + test_cases.push(test_name.to_string()); + } + + println!("Found {} test cases for rule {}.", test_cases.len(), name); + + for test_case in test_cases { + let mut test_file = rule_dir.path(); + test_file.push(test_case.clone()); + let mut result_file = rule_dir.path(); + result_file.push(test_case.replace(".xml", ".txt")); + + println!(" > Testing {:?}", test_file); + let mut expected = read_expected_issues(result_file.to_str().unwrap()); + + let doc = Sbml::read_path(test_file.to_str().unwrap()).unwrap(); + let mut issues: Vec = Vec::new(); + doc.validate(&mut issues); + + for issue in issues { + if test_issue(issue.rule.as_str()) { + if expected.contains_key(&issue.rule) { + expected.remove(&issue.rule); + } else { + println!( + " >> Found issue {} that is not in the expected list.", + issue.rule + ); + let report = format!( + "Test {}/{}: Found unexpected issue {} (severity {:?}).", + name, test_case, issue.rule, issue.severity + ); + match issue.severity { + SbmlIssueSeverity::Error => error_problems.push(report), + SbmlIssueSeverity::Warning => warning_problems.push(report), + SbmlIssueSeverity::Info => info_problems.push(report), + }; + } + } + } + + for (id, sev) in expected { + if test_issue(id.as_str()) { + println!(" >> Missed expected issue {}.", id); + let report = format!( + "Test {}/{}: Missed issue {} (severity {:?}).", + name, test_case, id, sev, + ); + match sev { + SbmlIssueSeverity::Error => error_problems.push(report), + SbmlIssueSeverity::Warning => warning_problems.push(report), + SbmlIssueSeverity::Info => info_problems.push(report), + }; + } + } + } + } + + if let Some(filter) = filter { + let missing = Vec::from_iter(filter.difference(&tested)); + println!( + "WARNING: {} rules were requested but not found in the test suite: {:?}", + missing.len(), + missing + ); + } + + println!("Found:"); + println!(" > {} error issues.", error_problems.len()); + println!(" > {} warning issues.", warning_problems.len()); + println!(" > {} info issues.", info_problems.len()); + + let errors = error_problems.join("\n"); + std::fs::write("test_suite_error.txt", errors).unwrap(); + + let warning = warning_problems.join("\n"); + std::fs::write("test_suite_warning.txt", warning).unwrap(); + + let infos = info_problems.join("\n"); + std::fs::write("test_suite_info.txt", infos).unwrap(); + + println!("Report written."); + + assert!(error_problems.is_empty()); + assert!(warning_problems.is_empty()); + assert!(info_problems.is_empty()); +} + +fn read_expected_issues(result_file: &str) -> HashMap { + // TODO: + // This doesn't really work if the issue appears in the file multiple times. + // But it seems that this is not a problem for the cases that we are testing at the moment? + let content = std::fs::read_to_string(result_file).unwrap(); + let mut last_rule = None; + let mut result = HashMap::new(); + for line in content.lines() { + let split = Vec::from_iter(line.split(':')); + if split.len() != 2 { + continue; + } + if split[0].trim() == "Validation id" { + assert!(last_rule.is_none()); + last_rule = Some(split[1].trim().to_string()); + } + if split[0].trim() == "Severity" { + assert!(last_rule.is_some()); + let s = match split[1].trim() { + "Error" => SbmlIssueSeverity::Error, + "Warning" => SbmlIssueSeverity::Warning, + "Info" => SbmlIssueSeverity::Info, + _ => { + panic!("Unknown severity {}", split[1].trim()); + } + }; + result.insert(last_rule.as_ref().unwrap().clone(), s); + last_rule = None; + } + } + + result +} diff --git a/src/lib.rs b/src/lib.rs index 13b8a78..96db3cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -149,7 +149,7 @@ pub struct SbmlIssue { pub message: String, } -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] pub enum SbmlIssueSeverity { /// An issue that makes the document impossible to read correctly (e.g. a function is /// used but not declared). diff --git a/validated-rules.txt b/validated-rules.txt new file mode 100644 index 0000000..61af41a --- /dev/null +++ b/validated-rules.txt @@ -0,0 +1,28 @@ +10101 +10102 +10104 +10201 +10202 +10203 +10204 +10205 +10206 +10207 +10208 +10209 +10210 +10211 +10212 +10213 +10214 +10215 +10216 +10217 +10218 +10219 +10220 +10221 +10222 +10223 +10224 +10225 \ No newline at end of file From d7a1a60453ee89a0c42fe8eecfeeb35885ac4154 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Wed, 21 Feb 2024 14:08:43 +0000 Subject: [PATCH 31/57] Update github actions. --- .github/workflows/build.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a8fe8c6..d0b70a1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: env: RUSTFLAGS: "-D warnings" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} @@ -38,7 +38,7 @@ jobs: env: RUSTFLAGS: "-D warnings" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} @@ -52,7 +52,7 @@ jobs: env: RUSTFLAGS: "-D warnings" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} @@ -67,7 +67,7 @@ jobs: env: RUSTFLAGS: "-D warnings" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} @@ -82,7 +82,7 @@ jobs: env: RUSTFLAGS: "-D warnings" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: wget https://github.com/sbmlteam/sbml-test-suite/releases/download/3.4.0/semantic_tests.v3.4.0.zip - run: unzip semantic_tests.v3.4.0.zip - uses: dtolnay/rust-toolchain@stable @@ -102,7 +102,7 @@ jobs: name: Code coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} @@ -112,7 +112,7 @@ jobs: tool: cargo-tarpaulin - run: cargo tarpaulin --verbose --lib --examples --all-features --out xml - name: Upload to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Archive code coverage results From b495b3530cd50183b11a38ed342a744659c2e6f9 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Wed, 21 Feb 2024 14:09:30 +0000 Subject: [PATCH 32/57] Remove unused import. --- examples/test-suite-syntactic.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/test-suite-syntactic.rs b/examples/test-suite-syntactic.rs index 89a488a..bf1728a 100644 --- a/examples/test-suite-syntactic.rs +++ b/examples/test-suite-syntactic.rs @@ -1,7 +1,6 @@ use biodivine_lib_sbml::{Sbml, SbmlIssue, SbmlIssueSeverity}; use std::collections::{HashMap, HashSet}; use std::path::Path; -use std::process::exit; /// This is an integration test that uses the examples from the SBML test suite /// to validate the functionality of the library. From 37af98cbb9aa2f083e7a5e84cce8b98c4d10d55d Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Wed, 21 Feb 2024 14:12:37 +0000 Subject: [PATCH 33/57] Use correct data URL. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d0b70a1..258b9fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,8 +83,8 @@ jobs: RUSTFLAGS: "-D warnings" steps: - uses: actions/checkout@v4 - - run: wget https://github.com/sbmlteam/sbml-test-suite/releases/download/3.4.0/semantic_tests.v3.4.0.zip - - run: unzip semantic_tests.v3.4.0.zip + - run: wget https://github.com/sbmlteam/sbml-test-suite/releases/download/3.4.0/syntactic_tests.v3.4.0.zip + - run: unzip syntactic_tests.v3.4.0.zip - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} From cfc4dd0ce3f0d01aa1cf15d01169f664f69824a6 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Wed, 21 Feb 2024 14:15:11 +0000 Subject: [PATCH 34/57] Test only selected rules. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 258b9fe..b2336f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -89,7 +89,7 @@ jobs: with: toolchain: ${{ env.RUST_VERSION }} components: rustfmt - - run: cargo run --release --example test-suite-syntactic + - run: cargo run --release --example test-suite-syntactic -- `cat validated-rules.txt` - run: zip test-results.zip ./test_suite_errors.txt ./test_suite_info.txt ./test_suite_warning.txt - uses: actions/upload-artifact@v4 with: From cca7e16faae3d01daaea0ea82aced1742286279b Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Wed, 21 Feb 2024 14:17:50 +0000 Subject: [PATCH 35/57] Bundle test artefact even if tests failed. --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b2336f2..45fd166 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,7 +91,9 @@ jobs: components: rustfmt - run: cargo run --release --example test-suite-syntactic -- `cat validated-rules.txt` - run: zip test-results.zip ./test_suite_errors.txt ./test_suite_info.txt ./test_suite_warning.txt + if: always() - uses: actions/upload-artifact@v4 + if: always() with: name: results path: ./test-results.zip From df9c3f8dfeb462fa5b5a3541584454f20cf72d71 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Wed, 21 Feb 2024 14:23:09 +0000 Subject: [PATCH 36/57] Typo. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45fd166..395f57e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,7 +90,7 @@ jobs: toolchain: ${{ env.RUST_VERSION }} components: rustfmt - run: cargo run --release --example test-suite-syntactic -- `cat validated-rules.txt` - - run: zip test-results.zip ./test_suite_errors.txt ./test_suite_info.txt ./test_suite_warning.txt + - run: zip test-results.zip ./test_suite_error.txt ./test_suite_info.txt ./test_suite_warning.txt if: always() - uses: actions/upload-artifact@v4 if: always() From bcd3105742cada7501b7f0a96a39d75f4d3cd04d Mon Sep 17 00:00:00 2001 From: adamValent Date: Thu, 22 Feb 2024 18:56:33 +0100 Subject: [PATCH 37/57] Implement rule 10303 --- src/core/validation/reaction.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/core/validation/reaction.rs b/src/core/validation/reaction.rs index 6256b30..3ca658d 100644 --- a/src/core/validation/reaction.rs +++ b/src/core/validation/reaction.rs @@ -4,8 +4,8 @@ use crate::core::validation::{ use crate::core::{ KineticLaw, LocalParameter, ModifierSpeciesReference, Reaction, SBase, SpeciesReference, }; -use crate::xml::{OptionalXmlChild, OptionalXmlProperty, RequiredXmlProperty, XmlWrapper}; -use crate::SbmlIssue; +use crate::xml::{OptionalXmlChild, OptionalXmlProperty, RequiredXmlProperty, XmlList, XmlWrapper}; +use crate::{SbmlIssue, SbmlIssueSeverity}; use std::collections::HashSet; impl SbmlValidable for Reaction { @@ -54,6 +54,7 @@ impl SbmlValidable for KineticLaw { if let Some(list_of_local_parameters) = self.local_parameters().get() { validate_list_of_objects(&list_of_local_parameters, issues, identifiers); + KineticLaw::apply_rule_10303(&list_of_local_parameters, issues); } if let Some(math) = self.math().get() { @@ -62,6 +63,34 @@ impl SbmlValidable for KineticLaw { } } +impl KineticLaw { + /// ### Rule 10303 + /// The value of the attribute id of every [LocalParameter] object defined within a [KineticLaw] + /// object must be unique across the set of all such parameter definitions within that + /// particular [KineticLaw] instance. + pub(crate) fn apply_rule_10303( + list_of_local_parameters: &XmlList, + issues: &mut Vec, + ) { + let mut identifiers: HashSet = HashSet::new(); + + for local_parameter in list_of_local_parameters.as_vec() { + let id = local_parameter.id().get(); + if identifiers.contains(&id) { + issues.push(SbmlIssue { + element: local_parameter.raw_element(), + message: format!("The identifier ('{0}') of is already present in the .", + id), + rule: "10303".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } else { + identifiers.insert(id); + } + } + } +} + impl SbmlValidable for LocalParameter { fn validate(&self, issues: &mut Vec, _identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues) From 316dd400fc349ce05f9fc40c4f202a300f047c1f Mon Sep 17 00:00:00 2001 From: adamValent Date: Thu, 22 Feb 2024 19:02:02 +0100 Subject: [PATCH 38/57] Fix rule 10302 - two missing identifiers would be considered as two empty ("") identifiers and thus duplicates --- src/core/validation/unit_definition.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/validation/unit_definition.rs b/src/core/validation/unit_definition.rs index ac38d94..c433a05 100644 --- a/src/core/validation/unit_definition.rs +++ b/src/core/validation/unit_definition.rs @@ -22,14 +22,17 @@ impl UnitDefinition { let mut identifiers: HashSet = HashSet::new(); for unit_definition in list_of_unit_definitions.as_vec() { - let id = unit_definition.id().get().unwrap_or_default(); + let Some(id) = unit_definition.id().get() else { + continue; + }; + if identifiers.contains(&id) { issues.push(SbmlIssue { element: unit_definition.raw_element(), message: format!("The identifier ('{0}') of is already present in the .", id), rule: "10302".to_string(), - severity: SbmlIssueSeverity::Error + severity: SbmlIssueSeverity::Error, }) } else { identifiers.insert(id); From 8acc58d738a5084cfa19e206f7cf30d6f60be34e Mon Sep 17 00:00:00 2001 From: adamValent Date: Thu, 22 Feb 2024 19:32:30 +0100 Subject: [PATCH 39/57] Implement rule 10304 --- src/core/validation/model.rs | 3 ++- src/core/validation/rule.rs | 45 +++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/core/validation/model.rs b/src/core/validation/model.rs index 321132f..3113267 100644 --- a/src/core/validation/model.rs +++ b/src/core/validation/model.rs @@ -1,5 +1,5 @@ use crate::core::validation::{apply_rule_10102, apply_rule_10301, validate_list_of_objects}; -use crate::core::{Model, SBase, UnitDefinition}; +use crate::core::{AbstractRule, Model, SBase, UnitDefinition}; use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; use std::collections::HashSet; @@ -30,6 +30,7 @@ impl Model { } if let Some(list_of_rules) = self.rules().get() { validate_list_of_objects(&list_of_rules, issues, identifiers); + AbstractRule::apply_rule_10304(&list_of_rules, issues); } if let Some(list_of_constraint) = self.constraints().get() { validate_list_of_objects(&list_of_constraint, issues, identifiers); diff --git a/src/core/validation/rule.rs b/src/core/validation/rule.rs index 2e1e63c..d03c7b2 100644 --- a/src/core/validation/rule.rs +++ b/src/core/validation/rule.rs @@ -1,7 +1,7 @@ use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; -use crate::core::{AbstractRule, Rule, SBase}; -use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; -use crate::SbmlIssue; +use crate::core::{AbstractRule, Rule, RuleTypes, SBase}; +use crate::xml::{OptionalXmlChild, OptionalXmlProperty, RequiredXmlProperty, XmlList, XmlWrapper}; +use crate::{SbmlIssue, SbmlIssueSeverity}; use std::collections::HashSet; impl SbmlValidable for AbstractRule { @@ -14,3 +14,42 @@ impl SbmlValidable for AbstractRule { } } } + +impl AbstractRule { + /// ### Rule 10304 + /// The value of the attribute variable of every [AssignmentRule](crate::core::rule::AssignmentRule) + /// and [RateRule](crate::core::rule::RateRule) objects must be unique across the set of all + /// [AssignmentRule](crate::core::rule::AssignmentRule) and [RateRule](crate::core::rule::RateRule) + /// objects in a model. In other words, a given model component cannot be the subject of both + /// an assignment rule and a rate rule simultaneously. + pub(crate) fn apply_rule_10304( + list_of_rules: &XmlList, + issues: &mut Vec, + ) { + let mut variables: HashSet = HashSet::new(); + + for rule in list_of_rules.as_vec() { + let variable = match rule.clone().cast() { + RuleTypes::Assignment(rule) => rule.variable().get(), + RuleTypes::Rate(rule) => rule.variable().get(), + _ => continue, + }; + + if variables.contains(&variable) { + issues.push(SbmlIssue { + element: rule.raw_element(), + message: format!( + "The variable ('{0}') of <{1}> is already present in the set of \ + and objects.", + variable, + rule.tag_name() + ), + rule: "10304".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } else { + variables.insert(variable); + } + } + } +} From 782e74f7f0b23cc10cc0b28b557d892d7fd6463c Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 22 Feb 2024 17:12:29 -0800 Subject: [PATCH 40/57] Test SBML suite coverage. --- .github/workflows/build.yml | 3 ++ examples/test-suite-syntactic.rs | 84 ++++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 395f57e..38b9347 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -105,6 +105,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + # Should allow tarpaulin to also measure coverage for the SBML test suite. + - run: wget https://github.com/sbmlteam/sbml-test-suite/releases/download/3.4.0/syntactic_tests.v3.4.0.zip + - run: unzip syntactic_tests.v3.4.0.zip - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} diff --git a/examples/test-suite-syntactic.rs b/examples/test-suite-syntactic.rs index bf1728a..3736d2a 100644 --- a/examples/test-suite-syntactic.rs +++ b/examples/test-suite-syntactic.rs @@ -16,12 +16,6 @@ use std::path::Path; /// If you only want to test a specific subset of rules, you can provide these as command line /// arguments. fn main() { - let dir_path = "./syntactic"; - - if !Path::new(dir_path).is_dir() { - panic!("Test data is missing.") - } - let args = std::env::args().collect::>(); let filter: Option> = if args.len() > 1 { Some(HashSet::from_iter(args.into_iter().skip(1))) @@ -29,6 +23,60 @@ fn main() { None }; + let result = test_inner(filter); + + let error_problems = result.error.clone(); + let warning_problems = result.warning.clone(); + let info_problems = result.info.clone(); + + println!("Found:"); + println!(" > {} error issues.", error_problems.len()); + println!(" > {} warning issues.", warning_problems.len()); + println!(" > {} info issues.", info_problems.len()); + + let errors = error_problems.join("\n"); + std::fs::write("test_suite_error.txt", errors).unwrap(); + + let warning = warning_problems.join("\n"); + std::fs::write("test_suite_warning.txt", warning).unwrap(); + + let infos = info_problems.join("\n"); + std::fs::write("test_suite_info.txt", infos).unwrap(); + + println!("Report written."); + + assert!(error_problems.is_empty()); + assert!(warning_problems.is_empty()); + assert!(info_problems.is_empty()); +} + +/// Allows us to run a "simplified" version of the test when using `cargo test --examples`. +/// This is useful when computing code coverage. +#[test] +fn sbml_test_suite_syntactic() { + let result = test_inner(None); + println!( + "Finished test: {} {} {}", + result.error.len(), + result.warning.len(), + result.info.len() + ); +} + +struct TestResults { + error: Vec, + warning: Vec, + info: Vec, +} + +/// A helper functions that actually runs the test. +fn test_inner(filter: Option>) -> TestResults { + let dir_path = "./syntactic"; + + if !Path::new(dir_path).is_dir() { + panic!("Test data is missing.") + } + if let Some(filter) = filter.as_ref() { println!( "Test suite restricted to {} rules: {:?}", @@ -140,25 +188,11 @@ fn main() { ); } - println!("Found:"); - println!(" > {} error issues.", error_problems.len()); - println!(" > {} warning issues.", warning_problems.len()); - println!(" > {} info issues.", info_problems.len()); - - let errors = error_problems.join("\n"); - std::fs::write("test_suite_error.txt", errors).unwrap(); - - let warning = warning_problems.join("\n"); - std::fs::write("test_suite_warning.txt", warning).unwrap(); - - let infos = info_problems.join("\n"); - std::fs::write("test_suite_info.txt", infos).unwrap(); - - println!("Report written."); - - assert!(error_problems.is_empty()); - assert!(warning_problems.is_empty()); - assert!(info_problems.is_empty()); + TestResults { + error: error_problems, + warning: warning_problems, + info: info_problems, + } } fn read_expected_issues(result_file: &str) -> HashMap { From 6ebc5ec9c4df9a8b480ef4cf65ef49fb7d001931 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 22 Feb 2024 17:49:25 -0800 Subject: [PATCH 41/57] Make a copy of the SBML test suite that is actually a test (for code coverage). --- .github/workflows/build.yml | 2 + Cargo.toml | 5 + examples/test-suite-syntactic.rs | 13 --- src/core/validation/mod.rs | 2 + src/core/validation/test_suite.rs | 163 ++++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 src/core/validation/test_suite.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 38b9347..bd02b3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,6 +53,8 @@ jobs: RUSTFLAGS: "-D warnings" steps: - uses: actions/checkout@v4 + - run: wget https://github.com/sbmlteam/sbml-test-suite/releases/download/3.4.0/syntactic_tests.v3.4.0.zip + - run: unzip syntactic_tests.v3.4.0.zip - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.RUST_VERSION }} diff --git a/Cargo.toml b/Cargo.toml index e7a2d02..40729d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +# When enabled, rust the SBML syntactic test suite as part of unit tests. +# This is mainly used for the purpose of code coverage computation. +sbml_test_suite = [] + [dependencies] const_format = "0.2.31" macros = { path = "macros" } diff --git a/examples/test-suite-syntactic.rs b/examples/test-suite-syntactic.rs index 3736d2a..0f7a7ee 100644 --- a/examples/test-suite-syntactic.rs +++ b/examples/test-suite-syntactic.rs @@ -50,19 +50,6 @@ fn main() { assert!(info_problems.is_empty()); } -/// Allows us to run a "simplified" version of the test when using `cargo test --examples`. -/// This is useful when computing code coverage. -#[test] -fn sbml_test_suite_syntactic() { - let result = test_inner(None); - println!( - "Finished test: {} {} {}", - result.error.len(), - result.warning.len(), - result.info.len() - ); -} - struct TestResults { error: Vec, warning: Vec, diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index ae22d7f..1198628 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -18,6 +18,8 @@ mod parameter; mod reaction; mod rule; mod species; +#[cfg(test)] +mod test_suite; mod unit; mod unit_definition; diff --git a/src/core/validation/test_suite.rs b/src/core/validation/test_suite.rs new file mode 100644 index 0000000..92959b1 --- /dev/null +++ b/src/core/validation/test_suite.rs @@ -0,0 +1,163 @@ +use crate::{Sbml, SbmlIssue, SbmlIssueSeverity}; +use std::collections::{HashMap, HashSet}; +use std::path::Path; + +/// Allows us to run a "simplified" version of the test when using `cargo test --examples`. +/// This is useful when computing code coverage, but otherwise will always pass. The test +/// that can actually fail is implemented as one of the examples. +#[test] +#[cfg_attr(not(feature = "sbml_test_suite"), ignore)] +fn sbml_test_suite_syntactic() { + test_inner(None); +} + +/// A helper functions that actually runs the test. +fn test_inner(filter: Option>) { + let dir_path = "./syntactic"; + + if !Path::new(dir_path).is_dir() { + panic!("Test data is missing.") + } + + if let Some(filter) = filter.as_ref() { + println!( + "Test suite restricted to {} rules: {:?}", + filter.len(), + filter + ); + } + + let test_issue = |id: &str| { + if let Some(filter) = filter.as_ref() { + filter.contains(id) + } else { + true + } + }; + + let mut tested = HashSet::new(); + + let mut error_problems = Vec::new(); + let mut warning_problems = Vec::new(); + let mut info_problems = Vec::new(); + + for rule_dir in std::fs::read_dir(dir_path).unwrap() { + let rule_dir = rule_dir.unwrap(); + let name = rule_dir.file_name(); + let name = name.to_str().unwrap(); + if !rule_dir.path().is_dir() { + println!("Skipping file {} (not a directory).", name); + continue; + } + tested.insert(name.to_string()); + + let mut test_cases = Vec::new(); + for test_file in std::fs::read_dir(rule_dir.path()).unwrap() { + let test_file = test_file.unwrap(); + let test_name = test_file.file_name(); + let test_name = test_name.to_str().unwrap(); + if !test_name.ends_with(".xml") { + continue; + } + if !test_name.contains("l3v2") { + // Skip any tests that are not for SBML level 3 version 2. + continue; + } + + test_cases.push(test_name.to_string()); + } + + println!("Found {} test cases for rule {}.", test_cases.len(), name); + + for test_case in test_cases { + let mut test_file = rule_dir.path(); + test_file.push(test_case.clone()); + let mut result_file = rule_dir.path(); + result_file.push(test_case.replace(".xml", ".txt")); + + println!(" > Testing {:?}", test_file); + let mut expected = read_expected_issues(result_file.to_str().unwrap()); + + let doc = Sbml::read_path(test_file.to_str().unwrap()).unwrap(); + let mut issues: Vec = Vec::new(); + doc.validate(&mut issues); + + for issue in issues { + if test_issue(issue.rule.as_str()) { + if expected.contains_key(&issue.rule) { + expected.remove(&issue.rule); + } else { + println!( + " >> Found issue {} that is not in the expected list.", + issue.rule + ); + let report = format!( + "Test {}/{}: Found unexpected issue {} (severity {:?}).", + name, test_case, issue.rule, issue.severity + ); + match issue.severity { + SbmlIssueSeverity::Error => error_problems.push(report), + SbmlIssueSeverity::Warning => warning_problems.push(report), + SbmlIssueSeverity::Info => info_problems.push(report), + }; + } + } + } + + for (id, sev) in expected { + if test_issue(id.as_str()) { + println!(" >> Missed expected issue {}.", id); + let report = format!( + "Test {}/{}: Missed issue {} (severity {:?}).", + name, test_case, id, sev, + ); + match sev { + SbmlIssueSeverity::Error => error_problems.push(report), + SbmlIssueSeverity::Warning => warning_problems.push(report), + SbmlIssueSeverity::Info => info_problems.push(report), + }; + } + } + } + } + + if let Some(filter) = filter { + let missing = Vec::from_iter(filter.difference(&tested)); + println!( + "WARNING: {} rules were requested but not found in the test suite: {:?}", + missing.len(), + missing + ); + } +} + +fn read_expected_issues(result_file: &str) -> HashMap { + let content = std::fs::read_to_string(result_file).unwrap(); + let mut last_rule = None; + let mut result = HashMap::new(); + for line in content.lines() { + let split = Vec::from_iter(line.split(':')); + if split.len() != 2 { + continue; + } + if split[0].trim() == "Validation id" { + assert!(last_rule.is_none()); + last_rule = Some(split[1].trim().to_string()); + } + if split[0].trim() == "Severity" { + assert!(last_rule.is_some()); + let s = match split[1].trim() { + "Error" => SbmlIssueSeverity::Error, + "Warning" => SbmlIssueSeverity::Warning, + "Info" => SbmlIssueSeverity::Info, + _ => { + panic!("Unknown severity {}", split[1].trim()); + } + }; + result.insert(last_rule.as_ref().unwrap().clone(), s); + last_rule = None; + } + } + + result +} From 2dfacd244ce7c3ae9c3719d7c01f6951902a426a Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 22 Feb 2024 18:23:33 -0800 Subject: [PATCH 42/57] Use the correct core version in tests. --- examples/test-suite-syntactic.rs | 4 ++-- src/core/validation/test_suite.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/test-suite-syntactic.rs b/examples/test-suite-syntactic.rs index 0f7a7ee..11026f5 100644 --- a/examples/test-suite-syntactic.rs +++ b/examples/test-suite-syntactic.rs @@ -104,8 +104,8 @@ fn test_inner(filter: Option>) -> TestResults { if !test_name.ends_with(".xml") { continue; } - if !test_name.contains("l3v2") { - // Skip any tests that are not for SBML level 3 version 2. + if !test_name.contains("l3v1") { + // Skip any tests that are not for SBML level 3 version 1. continue; } diff --git a/src/core/validation/test_suite.rs b/src/core/validation/test_suite.rs index 92959b1..ec748a9 100644 --- a/src/core/validation/test_suite.rs +++ b/src/core/validation/test_suite.rs @@ -59,8 +59,8 @@ fn test_inner(filter: Option>) { if !test_name.ends_with(".xml") { continue; } - if !test_name.contains("l3v2") { - // Skip any tests that are not for SBML level 3 version 2. + if !test_name.contains("l3v1") { + // Skip any tests that are not for SBML level 3 version 1. continue; } From 687ced4d2fd97e4cd4e2889448a93f15a58bde0c Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 22 Feb 2024 18:24:38 -0800 Subject: [PATCH 43/57] Add message to extra issues. --- examples/test-suite-syntactic.rs | 5 +++-- src/core/validation/test_suite.rs | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/test-suite-syntactic.rs b/examples/test-suite-syntactic.rs index 11026f5..b0df567 100644 --- a/examples/test-suite-syntactic.rs +++ b/examples/test-suite-syntactic.rs @@ -133,8 +133,9 @@ fn test_inner(filter: Option>) -> TestResults { expected.remove(&issue.rule); } else { println!( - " >> Found issue {} that is not in the expected list.", - issue.rule + " >> Found issue {} that is not in the expected list: {}", + issue.rule, + issue.message, ); let report = format!( "Test {}/{}: Found unexpected issue {} (severity {:?}).", diff --git a/src/core/validation/test_suite.rs b/src/core/validation/test_suite.rs index ec748a9..4481a9a 100644 --- a/src/core/validation/test_suite.rs +++ b/src/core/validation/test_suite.rs @@ -88,8 +88,9 @@ fn test_inner(filter: Option>) { expected.remove(&issue.rule); } else { println!( - " >> Found issue {} that is not in the expected list.", + " >> Found issue {} that is not in the expected list: {}", issue.rule + issue.message, ); let report = format!( "Test {}/{}: Found unexpected issue {} (severity {:?}).", From cd7b4688b6b213b0401a7a4a6e15dbaf967a1298 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 22 Feb 2024 18:54:33 -0800 Subject: [PATCH 44/57] Docs. --- .gitignore | 5 +++- VALIDATION.md | 47 ++++++++++++++++++++++++++++++++ examples/test-suite-syntactic.rs | 3 +- 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 VALIDATION.md diff --git a/.gitignore b/.gitignore index 4273f7d..ef28060 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ Cargo.lock .vscode # The SBML test suite files. -syntactic \ No newline at end of file +syntactic +test_suite_error.txt +test_suite_warning.txt +test_suite_info.txt diff --git a/VALIDATION.md b/VALIDATION.md new file mode 100644 index 0000000..dacf25b --- /dev/null +++ b/VALIDATION.md @@ -0,0 +1,47 @@ +# Biodivine SBML (validation) + +Currently, the correctness of the implementation is ensured using two +mechanisms: + + - Unit tests. + - SBML syntactic test suite. + +### Unit tests + +These are "straightforward" in the sense that they use standard `cargo` +functionality, and should be thus familiar to anyone testing in the +[Rust ecosystem](https://doc.rust-lang.org/book/ch11-01-writing-tests.html). + +### SBML test suite + +For further validation, we compare our results to the +[SBML test suite](https://github.com/sbmlteam/sbml-test-suite/releases). +Specifically, we only use the `syntactic` subset of the test cases, +because the rest is concerned with simulation, which we do not support. + +This test suite is not part of the standard `cargo test` process. In fact, +it is present twice in the codebase. + + > Both cases expect that the latest version of the syntactic test suite + > is downloaded and extracted in the `./syntactic` folder (you can get + > the zip file in the release section of the repository linked above). + +First, it is present as a "silent" test in `core::validation::test_suite` +module that is enabled by the `sbml_test_suite` feature. To run it, execute +`cargo test --all-features`. Here, we execute all the test cases in the suite, +but we don't check if the result is correct. This is mainly to (a) measure code +coverage, and (b) detect if the library outright fails on some of the examples. + +Second, a dedicated "example" binary (executed as +`cargo run --example test-suite-syntactic`) provides additional options to +tune the testing process. First, it prints additional info about the results +and actually checks that the results conform to the expected outputs. However, +you can request to only test a specific subset of rules by specifying their IDs +as the command line arguments (e.g. `cargo run --example test-suite-syntactic -- 10201 10202`). +This still runs all tests cases, but only shows +an error for the cases where an inconsistency is detected for one of the +requested rules. + +For the purposes of automated testing, we apply the list of rules in the +`validated-rules.txt` file. As such, if you extend the list of actually validated +rules, you have to also extend this file. \ No newline at end of file diff --git a/examples/test-suite-syntactic.rs b/examples/test-suite-syntactic.rs index b0df567..063ff5a 100644 --- a/examples/test-suite-syntactic.rs +++ b/examples/test-suite-syntactic.rs @@ -134,8 +134,7 @@ fn test_inner(filter: Option>) -> TestResults { } else { println!( " >> Found issue {} that is not in the expected list: {}", - issue.rule, - issue.message, + issue.rule, issue.message, ); let report = format!( "Test {}/{}: Found unexpected issue {} (severity {:?}).", From f1a55c3ed9e2e52f225ea7e2ac5784b80eef89c7 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 22 Feb 2024 18:57:57 -0800 Subject: [PATCH 45/57] Default traits. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 96db3cd..018b8c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,7 +140,7 @@ impl Default for Sbml { } } -#[derive(Debug)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct SbmlIssue { /// Refers to the "raw" XML element where the issue occurred. pub element: Element, From 74e142cb555637539f5c877bef81c8a98f60d105 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 22 Feb 2024 18:58:40 -0800 Subject: [PATCH 46/57] Typo. --- src/core/validation/test_suite.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/validation/test_suite.rs b/src/core/validation/test_suite.rs index 4481a9a..b3410e2 100644 --- a/src/core/validation/test_suite.rs +++ b/src/core/validation/test_suite.rs @@ -89,8 +89,7 @@ fn test_inner(filter: Option>) { } else { println!( " >> Found issue {} that is not in the expected list: {}", - issue.rule - issue.message, + issue.rule, issue.message, ); let report = format!( "Test {}/{}: Found unexpected issue {} (severity {:?}).", From a4b010486653887b2bceee6e58f507386a51ef3d Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Thu, 22 Feb 2024 19:01:08 -0800 Subject: [PATCH 47/57] Fix severity name. --- examples/test-suite-syntactic.rs | 2 +- src/core/validation/test_suite.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/test-suite-syntactic.rs b/examples/test-suite-syntactic.rs index 063ff5a..878a183 100644 --- a/examples/test-suite-syntactic.rs +++ b/examples/test-suite-syntactic.rs @@ -203,7 +203,7 @@ fn read_expected_issues(result_file: &str) -> HashMap let s = match split[1].trim() { "Error" => SbmlIssueSeverity::Error, "Warning" => SbmlIssueSeverity::Warning, - "Info" => SbmlIssueSeverity::Info, + "Informational" => SbmlIssueSeverity::Info, _ => { panic!("Unknown severity {}", split[1].trim()); } diff --git a/src/core/validation/test_suite.rs b/src/core/validation/test_suite.rs index b3410e2..ac015b7 100644 --- a/src/core/validation/test_suite.rs +++ b/src/core/validation/test_suite.rs @@ -149,7 +149,7 @@ fn read_expected_issues(result_file: &str) -> HashMap let s = match split[1].trim() { "Error" => SbmlIssueSeverity::Error, "Warning" => SbmlIssueSeverity::Warning, - "Info" => SbmlIssueSeverity::Info, + "Informational" => SbmlIssueSeverity::Info, _ => { panic!("Unknown severity {}", split[1].trim()); } From dfa8586f23d9b7a8a290588ef637b92afc80bdaa Mon Sep 17 00:00:00 2001 From: adamValent Date: Fri, 23 Feb 2024 15:35:19 +0100 Subject: [PATCH 48/57] Fix children list loading - only children from core namespace must be loaded --- src/core/validation/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index 1771065..e642167 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -1,4 +1,5 @@ use crate::constants::element::{ALLOWED_ATTRIBUTES, ALLOWED_CHILDREN, MATHML_ALLOWED_CHILDREN}; +use crate::constants::namespaces::URL_SBML_CORE; use crate::core::SBase; use crate::xml::{OptionalXmlProperty, XmlElement, XmlList, XmlWrapper}; use crate::{Sbml, SbmlIssue, SbmlIssueSeverity}; @@ -163,9 +164,10 @@ pub(crate) fn apply_rule_10102(xml_element: &XmlElement, issues: &mut Vec Date: Fri, 23 Feb 2024 22:14:22 +0100 Subject: [PATCH 49/57] Implement sanity checks (1/2) - check that required children and attributes are set --- src/constants/element.rs | 44 +++++++++++++ src/core/validation/compartment.rs | 4 +- src/core/validation/constraint.rs | 4 +- src/core/validation/event.rs | 30 ++++++++- src/core/validation/function_definition.rs | 4 +- src/core/validation/initial_assignment.rs | 4 +- src/core/validation/mod.rs | 74 ++++++++++++++++++---- src/core/validation/model.rs | 46 +++++++++++++- src/core/validation/parameter.rs | 4 +- src/core/validation/reaction.rs | 38 ++++++++++- src/core/validation/rule.rs | 4 +- src/core/validation/species.rs | 4 +- src/core/validation/unit.rs | 4 +- src/core/validation/unit_definition.rs | 15 ++++- src/lib.rs | 45 ++++++++++--- src/xml/xml_wrapper.rs | 13 ++-- 16 files changed, 300 insertions(+), 37 deletions(-) diff --git a/src/constants/element.rs b/src/constants/element.rs index 5589c70..f7c18df 100644 --- a/src/constants/element.rs +++ b/src/constants/element.rs @@ -56,6 +56,48 @@ pub const ALLOWED_ATTRIBUTES: phf::Map<&str, &[&str]> = phf_map! { "eventAssignment" => extended_sbase_attributes!("variable"), }; +pub const REQUIRED_ATTRIBUTES: phf::Map<&str, &[&str]> = phf_map! { + "sbml" => &["level", "version"], + "model" => &[], + "listOfFunctionDefinitions" => &[], + "functionDefinition" => &["id"], + "listOfUnitDefinitions" => &[], + "unitDefinition" => &["id"], + "listOfUnits" => &[], + "unit" => &["kind", "exponent", "scale", "multiplier"], + "listOfCompartments" => &[], + "compartment" => &["id", "constant"], + "listOfSpecies" => &[], + "species" => &["id", "compartment", "hasOnlySubstanceUnits", "boundaryCondition", "constant"], + "listOfParameters" => &[], + "parameter" => &["id", "constant"], + "listOfInitialAssignments" => &[], + "initialAssignment" => &["symbol"], + "listOfRules" => &[], + "algebraicRule" => &[], + "assignmentRule" => &["variable"], + "rateRule" => &["variable"], + "listOfConstraints" => &[], + "constraint" => &[], + "listOfReactions" => &[], + "reaction" => &["id", "reversible"], + "listOfReactants" => &[], + "listOfProducts" => &[], + "speciesReference" => &["constant"], + "listOfModifiers" => &[], + "modifierSpeciesReference" => &[], + "kineticLaw" => &[], + "listOfLocalParameters" => &[], + "localParameter" => &["id"], + "listOfEvents" => &[], + "event" => &["useValuesFromTriggerTime"], + "trigger" => &["initialValue", "persistent"], + "priority" => &[], + "delay" => &[], + "listOfEventAssignments" => &[], + "eventAssignment" => &["variable"] +}; + pub const ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { "sbml" => extended_sbase_children!("model"), "model" => extended_sbase_children!("listOfFunctionDefinitions", "listOfUnitDefinitions", "listOfCompartments", "listOfSpecies", "listOfParameters", "listOfInitialAssignments", "listOfRules", "listOfConstraints", "listOfReactions", "listOfEvents"), @@ -98,6 +140,8 @@ pub const ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { "eventAssignment" => extended_sbase_children!("math") }; +// There are no required children in SBML core level 3 version 1 + pub const MATHML_ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { "math" => &["abs", "and", "annotation", "annotation-xml", "apply", "arccosh", "arccos", "arccoth", "arccot", "arccsch", "arccsc", "arcsech", "arcsec", "arcsinh", "arcsin", "arctanh", diff --git a/src/core/validation/compartment.rs b/src/core/validation/compartment.rs index bcba302..6bdb64f 100644 --- a/src/core/validation/compartment.rs +++ b/src/core/validation/compartment.rs @@ -1,4 +1,4 @@ -use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SanityCheckable, SbmlValidable}; use crate::core::Compartment; use crate::xml::{RequiredXmlProperty, XmlWrapper}; use crate::SbmlIssue; @@ -15,3 +15,5 @@ impl SbmlValidable for Compartment { ); } } + +impl SanityCheckable for Compartment {} diff --git a/src/core/validation/constraint.rs b/src/core/validation/constraint.rs index 50386c1..fa96269 100644 --- a/src/core/validation/constraint.rs +++ b/src/core/validation/constraint.rs @@ -1,4 +1,4 @@ -use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SanityCheckable, SbmlValidable}; use crate::core::{Constraint, SBase}; use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; @@ -14,3 +14,5 @@ impl SbmlValidable for Constraint { } } } + +impl SanityCheckable for Constraint {} diff --git a/src/core/validation/event.rs b/src/core/validation/event.rs index 699285e..11f07aa 100644 --- a/src/core/validation/event.rs +++ b/src/core/validation/event.rs @@ -1,5 +1,6 @@ use crate::core::validation::{ - apply_rule_10102, apply_rule_10301, validate_list_of_objects, SbmlValidable, + apply_rule_10102, apply_rule_10301, sanity_check, sanity_check_of_list, + validate_list_of_objects, SanityCheckable, SbmlValidable, }; use crate::core::{Delay, Event, EventAssignment, Priority, SBase, Trigger}; use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; @@ -26,6 +27,25 @@ impl SbmlValidable for Event { } } +impl SanityCheckable for Event { + fn sanity_check(&self, issues: &mut Vec) { + sanity_check(self.xml_element(), issues); + + if let Some(trigger) = self.trigger().get() { + trigger.sanity_check(issues); + } + if let Some(priority) = self.priority().get() { + priority.sanity_check(issues); + } + if let Some(delay) = self.delay().get() { + delay.sanity_check(issues); + } + if let Some(list_of_event_assignments) = self.event_assignments().get() { + sanity_check_of_list(&list_of_event_assignments, issues); + } + } +} + impl SbmlValidable for Trigger { fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); @@ -37,6 +57,8 @@ impl SbmlValidable for Trigger { } } +impl SanityCheckable for Trigger {} + impl SbmlValidable for Priority { fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); @@ -48,6 +70,8 @@ impl SbmlValidable for Priority { } } +impl SanityCheckable for Priority {} + impl SbmlValidable for Delay { fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); @@ -59,6 +83,8 @@ impl SbmlValidable for Delay { } } +impl SanityCheckable for Delay {} + impl SbmlValidable for EventAssignment { fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); @@ -69,3 +95,5 @@ impl SbmlValidable for EventAssignment { } } } + +impl SanityCheckable for EventAssignment {} diff --git a/src/core/validation/function_definition.rs b/src/core/validation/function_definition.rs index aec7c52..ec918f3 100644 --- a/src/core/validation/function_definition.rs +++ b/src/core/validation/function_definition.rs @@ -1,4 +1,4 @@ -use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SanityCheckable, SbmlValidable}; use crate::core::{FunctionDefinition, SBase}; use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; @@ -14,3 +14,5 @@ impl SbmlValidable for FunctionDefinition { } } } + +impl SanityCheckable for FunctionDefinition {} diff --git a/src/core/validation/initial_assignment.rs b/src/core/validation/initial_assignment.rs index 413f0e6..930394b 100644 --- a/src/core/validation/initial_assignment.rs +++ b/src/core/validation/initial_assignment.rs @@ -1,4 +1,4 @@ -use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SanityCheckable, SbmlValidable}; use crate::core::{InitialAssignment, SBase}; use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; @@ -14,3 +14,5 @@ impl SbmlValidable for InitialAssignment { } } } + +impl SanityCheckable for InitialAssignment {} diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index e642167..750468e 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -1,9 +1,11 @@ -use crate::constants::element::{ALLOWED_ATTRIBUTES, ALLOWED_CHILDREN, MATHML_ALLOWED_CHILDREN}; +use crate::constants::element::{ + ALLOWED_ATTRIBUTES, ALLOWED_CHILDREN, MATHML_ALLOWED_CHILDREN, REQUIRED_ATTRIBUTES, +}; use crate::constants::namespaces::URL_SBML_CORE; use crate::core::SBase; use crate::xml::{OptionalXmlProperty, XmlElement, XmlList, XmlWrapper}; use crate::{Sbml, SbmlIssue, SbmlIssueSeverity}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::ops::Deref; use xml_doc::Element; @@ -27,6 +29,16 @@ pub(crate) trait SbmlValidable: XmlWrapper { fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet); } +/// Denotes an element that possess a way to self-test against +/// the most critical checks (sanity test). This should be executed **before** actual document +/// validation. Failing sanity tests skips the validation. That is, because reading such a (insane) +/// document would cause panic. +pub(crate) trait SanityCheckable: XmlWrapper { + fn sanity_check(&self, issues: &mut Vec) { + sanity_check(self.xml_element(), issues); + } +} + impl Sbml { /// ### Rule 10102 /// An SBML XML document must not contain undefined elements or attributes in the SBML Level 3 @@ -39,7 +51,9 @@ impl Sbml { if doc.container().child_elements(doc.deref()).len() != 1 { issues.push(SbmlIssue { element: doc.container(), - message: "The document contains multiple root nodes. Only one root object is allowed.".to_string(), + message: "The document contains multiple root nodes. \ + Only one root object is allowed." + .to_string(), rule: "10102".to_string(), severity: SbmlIssueSeverity::Error, }) @@ -50,14 +64,18 @@ impl Sbml { validate_allowed_attributes( root_element, root_element.name(doc.deref()), - root_element.attributes(doc.deref()), + &root_element + .attributes(doc.deref()) + .keys() + .map(|key| key.as_str()) + .collect::>(), issues, ); validate_allowed_children( root_element, root_element.name(doc.deref()), - root_element + &root_element .children(doc.deref()) .iter() .filter_map(|node| node.as_element().map(|it| it.full_name(doc.deref()))) @@ -79,15 +97,45 @@ impl Sbml { } } +pub(crate) fn sanity_check(xml_element: &XmlElement, issues: &mut Vec) { + let attributes = xml_element.attributes(); + let element_name = xml_element.tag_name(); + + for req_attr in REQUIRED_ATTRIBUTES[element_name.as_str()] { + if !attributes.contains_key(&req_attr.to_string()) { + issues.push(SbmlIssue { + element: xml_element.raw_element(), + message: format!( + "Sanity check failed: missing required attribute [{0}] on <{1}>.", + req_attr, element_name + ), + rule: "SANITY_CHECK".to_string(), + severity: SbmlIssueSeverity::Error, + }); + } + } +} + +pub(crate) fn sanity_check_of_list( + xml_list: &XmlList, + issues: &mut Vec, +) { + sanity_check(xml_list.xml_element(), issues); + + for object in xml_list.as_vec() { + object.sanity_check(issues); + } +} + pub(crate) fn validate_allowed_attributes( element: Element, element_name: &str, - attrs: &HashMap, + attributes: &Vec<&str>, issues: &mut Vec, ) { let allowed_attributes = ALLOWED_ATTRIBUTES.get(element_name).unwrap(); - for full_name in attrs.keys() { + for full_name in attributes { let (_prefix, attr_name) = Element::separate_prefix_name(full_name); if !allowed_attributes.contains(&attr_name) { issues.push(SbmlIssue { @@ -106,7 +154,7 @@ pub(crate) fn validate_allowed_attributes( pub(crate) fn validate_allowed_children( element: Element, element_name: &str, - children_names: Vec<&str>, + children_names: &Vec<&str>, issues: &mut Vec, ) { let allowed_children = ALLOWED_CHILDREN.get(element_name).unwrap(); @@ -162,7 +210,11 @@ pub(crate) fn apply_rule_10102(xml_element: &XmlElement, issues: &mut Vec>(); let children_names = element .child_elements(doc.deref()) .iter() @@ -170,8 +222,8 @@ pub(crate) fn apply_rule_10102(xml_element: &XmlElement, issues: &mut Vec, identifiers: &mut HashSet) { +impl SbmlValidable for Model { + fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); @@ -43,3 +46,40 @@ impl Model { } } } + +impl SanityCheckable for Model { + fn sanity_check(&self, issues: &mut Vec) { + sanity_check(self.xml_element(), issues); + + if let Some(list_of_function_definition) = self.function_definitions().get() { + sanity_check_of_list(&list_of_function_definition, issues); + } + if let Some(list_of_unit_definitions) = self.unit_definitions().get() { + sanity_check_of_list(&list_of_unit_definitions, issues); + } + if let Some(list_of_compartments) = self.compartments().get() { + sanity_check_of_list(&list_of_compartments, issues); + } + if let Some(list_of_species) = self.species().get() { + sanity_check_of_list(&list_of_species, issues); + } + if let Some(list_of_parameters) = self.parameters().get() { + sanity_check_of_list(&list_of_parameters, issues); + } + if let Some(list_of_initial_assignment) = self.initial_assignments().get() { + sanity_check_of_list(&list_of_initial_assignment, issues); + } + if let Some(list_of_rules) = self.rules().get() { + sanity_check_of_list(&list_of_rules, issues); + } + if let Some(list_of_constraint) = self.constraints().get() { + sanity_check_of_list(&list_of_constraint, issues); + } + if let Some(list_of_reactions) = self.reactions().get() { + sanity_check_of_list(&list_of_reactions, issues); + } + if let Some(list_of_events) = self.events().get() { + sanity_check_of_list(&list_of_events, issues); + } + } +} diff --git a/src/core/validation/parameter.rs b/src/core/validation/parameter.rs index 3dd2a79..0c95390 100644 --- a/src/core/validation/parameter.rs +++ b/src/core/validation/parameter.rs @@ -1,4 +1,4 @@ -use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SanityCheckable, SbmlValidable}; use crate::core::Parameter; use crate::xml::{RequiredXmlProperty, XmlWrapper}; use crate::SbmlIssue; @@ -15,3 +15,5 @@ impl SbmlValidable for Parameter { ); } } + +impl SanityCheckable for Parameter {} diff --git a/src/core/validation/reaction.rs b/src/core/validation/reaction.rs index 3ca658d..6bae94e 100644 --- a/src/core/validation/reaction.rs +++ b/src/core/validation/reaction.rs @@ -1,5 +1,6 @@ use crate::core::validation::{ - apply_rule_10102, apply_rule_10301, validate_list_of_objects, SbmlValidable, + apply_rule_10102, apply_rule_10301, sanity_check, sanity_check_of_list, + validate_list_of_objects, SanityCheckable, SbmlValidable, }; use crate::core::{ KineticLaw, LocalParameter, ModifierSpeciesReference, Reaction, SBase, SpeciesReference, @@ -33,6 +34,25 @@ impl SbmlValidable for Reaction { } } +impl SanityCheckable for Reaction { + fn sanity_check(&self, issues: &mut Vec) { + sanity_check(self.xml_element(), issues); + + if let Some(list_of_reactants) = self.reactants().get() { + sanity_check_of_list(&list_of_reactants, issues); + } + if let Some(list_of_products) = self.products().get() { + sanity_check_of_list(&list_of_products, issues); + } + if let Some(list_of_modifiers) = self.modifiers().get() { + sanity_check_of_list(&list_of_modifiers, issues); + } + if let Some(kinetic_law) = self.kinetic_law().get() { + kinetic_law.sanity_check(issues); + } + } +} + impl SbmlValidable for SpeciesReference { fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); @@ -40,6 +60,8 @@ impl SbmlValidable for SpeciesReference { } } +impl SanityCheckable for SpeciesReference {} + impl SbmlValidable for ModifierSpeciesReference { fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); @@ -47,6 +69,8 @@ impl SbmlValidable for ModifierSpeciesReference { } } +impl SanityCheckable for ModifierSpeciesReference {} + impl SbmlValidable for KineticLaw { fn validate(&self, issues: &mut Vec, identifiers: &mut HashSet) { apply_rule_10102(self.xml_element(), issues); @@ -63,6 +87,16 @@ impl SbmlValidable for KineticLaw { } } +impl SanityCheckable for KineticLaw { + fn sanity_check(&self, issues: &mut Vec) { + sanity_check(self.xml_element(), issues); + + if let Some(list_of_local_parameters) = self.local_parameters().get() { + sanity_check_of_list(&list_of_local_parameters, issues); + } + } +} + impl KineticLaw { /// ### Rule 10303 /// The value of the attribute id of every [LocalParameter] object defined within a [KineticLaw] @@ -96,3 +130,5 @@ impl SbmlValidable for LocalParameter { apply_rule_10102(self.xml_element(), issues) } } + +impl SanityCheckable for LocalParameter {} diff --git a/src/core/validation/rule.rs b/src/core/validation/rule.rs index d03c7b2..31ccf51 100644 --- a/src/core/validation/rule.rs +++ b/src/core/validation/rule.rs @@ -1,4 +1,4 @@ -use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SanityCheckable, SbmlValidable}; use crate::core::{AbstractRule, Rule, RuleTypes, SBase}; use crate::xml::{OptionalXmlChild, OptionalXmlProperty, RequiredXmlProperty, XmlList, XmlWrapper}; use crate::{SbmlIssue, SbmlIssueSeverity}; @@ -15,6 +15,8 @@ impl SbmlValidable for AbstractRule { } } +impl SanityCheckable for AbstractRule {} + impl AbstractRule { /// ### Rule 10304 /// The value of the attribute variable of every [AssignmentRule](crate::core::rule::AssignmentRule) diff --git a/src/core/validation/species.rs b/src/core/validation/species.rs index bba820f..c2369d3 100644 --- a/src/core/validation/species.rs +++ b/src/core/validation/species.rs @@ -1,4 +1,4 @@ -use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SanityCheckable, SbmlValidable}; use crate::core::Species; use crate::xml::{RequiredXmlProperty, XmlWrapper}; use crate::SbmlIssue; @@ -15,3 +15,5 @@ impl SbmlValidable for Species { ); } } + +impl SanityCheckable for Species {} diff --git a/src/core/validation/unit.rs b/src/core/validation/unit.rs index ef86631..1f35e4c 100644 --- a/src/core/validation/unit.rs +++ b/src/core/validation/unit.rs @@ -1,4 +1,4 @@ -use crate::core::validation::{apply_rule_10102, apply_rule_10301, SbmlValidable}; +use crate::core::validation::{apply_rule_10102, apply_rule_10301, SanityCheckable, SbmlValidable}; use crate::core::{SBase, Unit}; use crate::xml::{OptionalXmlProperty, XmlWrapper}; use crate::SbmlIssue; @@ -10,3 +10,5 @@ impl SbmlValidable for Unit { apply_rule_10301(self.id().get(), self.xml_element(), issues, identifiers); } } + +impl SanityCheckable for Unit {} diff --git a/src/core/validation/unit_definition.rs b/src/core/validation/unit_definition.rs index c433a05..ba2e894 100644 --- a/src/core/validation/unit_definition.rs +++ b/src/core/validation/unit_definition.rs @@ -1,4 +1,7 @@ -use crate::core::validation::{apply_rule_10102, validate_list_of_objects, SbmlValidable}; +use crate::core::validation::{ + apply_rule_10102, sanity_check, sanity_check_of_list, validate_list_of_objects, + SanityCheckable, SbmlValidable, +}; use crate::core::{SBase, UnitDefinition}; use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlList, XmlWrapper}; use crate::{SbmlIssue, SbmlIssueSeverity}; @@ -14,6 +17,16 @@ impl SbmlValidable for UnitDefinition { } } +impl SanityCheckable for UnitDefinition { + fn sanity_check(&self, issues: &mut Vec) { + sanity_check(self.xml_element(), issues); + + if let Some(list_of_units) = self.units().get() { + sanity_check_of_list(&list_of_units, issues); + } + } +} + impl UnitDefinition { pub(crate) fn apply_rule_10302( list_of_unit_definitions: &XmlList, diff --git a/src/lib.rs b/src/lib.rs index 4a5ce31..904154b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,13 @@ +use crate::constants::namespaces::URL_SBML_CORE; +use crate::core::validation::{sanity_check, SanityCheckable, SbmlValidable}; +use crate::core::Model; +use crate::xml::{OptionalXmlChild, XmlDocument, XmlElement, XmlWrapper}; use std::collections::HashSet; +use std::ops::Deref; use std::str::FromStr; use std::sync::{Arc, RwLock}; - -use xml_doc::{Document, Element}; - use xml::{OptionalChild, RequiredProperty}; - -use crate::constants::namespaces::URL_SBML_CORE; -use crate::core::Model; -use crate::xml::{OptionalXmlChild, XmlDocument, XmlElement}; +use xml_doc::{Document, Element}; /// A module with useful types that are not directly part of the SBML specification, but help /// us work with XML documents in a sane and safe way. In particular: @@ -101,6 +100,26 @@ impl Sbml { RequiredProperty::new(&self.sbml_root, "version") } + fn sanity_check(&self, issues: &mut Vec) { + sanity_check(&self.sbml_root, issues); + let doc = self.xml.read().unwrap(); + let element = self.sbml_root.raw_element(); + + if !element.namespace_decls(doc.deref()).contains_key("") { + issues.push(SbmlIssue { + element, + message: + "Sanity check failed: missing required namespace declaration [xmlns] on ." + .to_string(), + rule: "SANITY_CHECK".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } + + if let Some(model) = self.model().get() { + model.sanity_check(issues); + } + } /// Validates the document against validation rules specified in the /// [specification](https://sbml.org/specifications/sbml-level-3/version-2/core/release-2/sbml-level-3-version-2-release-2-core.pdf). /// Eventual issues are returned in the vector. Empty vector represents a valid document. @@ -115,10 +134,19 @@ impl Sbml { /// structural and syntactic constraints. pub fn validate(&self) -> Vec { let mut issues: Vec = vec![]; - let mut identifiers: HashSet = HashSet::new(); + self.sanity_check(&mut issues); + + if !issues.is_empty() { + println!("Sanity check failed, skipping validation..."); + return issues; + } else { + println!("Sanity check passed, proceeding with validation..."); + } + self.apply_rule_10102(&mut issues); if let Some(model) = self.model().get() { + let mut identifiers: HashSet = HashSet::new(); model.validate(&mut issues, &mut identifiers); } @@ -811,6 +839,7 @@ mod tests { let assignment = event_assignments.top(); assignment.math().ensure(); } + #[test] pub fn test_sbase() { let doc = Sbml::read_path("test-inputs/model.sbml").unwrap(); diff --git a/src/xml/xml_wrapper.rs b/src/xml/xml_wrapper.rs index 9e45ff7..81abc76 100644 --- a/src/xml/xml_wrapper.rs +++ b/src/xml/xml_wrapper.rs @@ -110,10 +110,10 @@ pub trait XmlWrapper: Into { /// referenced within this [XmlWrapper]. /// /// Note that full_name generally consists of namespace prefix and actual name in following format: **prefix:name**. - fn attributes(&self) -> &HashMap { - unimplemented!(); - // let doc = self.read_doc(); - // self.raw_element().attributes(doc.deref()) // TODO: cannot return value referencing local variable `doc`. How to fix? + fn attributes(&self) -> HashMap { + // unimplemented!(); + let doc = self.read_doc(); + self.raw_element().attributes(doc.deref()).clone() // TODO: cannot return value referencing local variable `doc`. How to fix? } /// Returns the vector of children as a collection of [Node]s referenced within @@ -124,6 +124,11 @@ pub trait XmlWrapper: Into { // self.raw_element().children(doc.deref()) // TODO: cannot return value referencing local variable `doc`. How to fix? } + fn children_elements(&self) -> Vec { + let doc = self.read_doc(); + self.raw_element().child_elements(doc.deref()) + } + /// Returns the vector of names of children referenced within this [XmlWrapper]. fn children_names(&self) -> Vec<&str> { unimplemented!(); From d9aae64fb0f7afee65ad28f9dba4ffa72fa047e6 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Fri, 23 Feb 2024 17:52:15 -0800 Subject: [PATCH 50/57] Bit more refactoring of utility methods. --- src/core/model.rs | 191 +++++++++++++++++------------------------ src/xml/xml_list.rs | 27 ++++++ src/xml/xml_wrapper.rs | 46 ++++++++-- 3 files changed, 143 insertions(+), 121 deletions(-) diff --git a/src/core/model.rs b/src/core/model.rs index 34ada56..fb6aa1f 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -11,7 +11,6 @@ use crate::xml::{ use macros::{SBase, XmlWrapper}; use std::ops::Deref; -use xml_doc::Element; /// A type-safe representation of an SBML element. #[derive(Clone, Debug, XmlWrapper, SBase)] @@ -75,12 +74,11 @@ impl Model { self.optional_sbml_child("listOfEvents") } - /// Returns a vector of [FunctionDefinition]s' identifiers (attribute **id**). If the identifier is not set, - /// it is not included in the output. + /// Returns a vector of [FunctionDefinition] identifiers (attribute **id**). Function definitions + /// without IDs are not included in the output. pub(crate) fn function_definition_identifiers(&self) -> Vec { if let Some(function_definitions) = self.function_definitions().get() { function_definitions - .as_vec() .iter() .filter_map(|def| def.id().get()) .collect() @@ -91,211 +89,176 @@ impl Model { /// Find a [FunctionDefinition] by its *id* and return a number of arguments this function expects. /// More precisely, find a number of **bvar** elements inside **lambda** inside **math** element of - /// [FunctionDefinition]. If [FunctionDefinition] cannot be found, returns 0. - pub(crate) fn function_definition_arguments(&self, id: &str) -> i32 { - // if list of function definitions is present - if let Some(function_definitions) = self.function_definitions().get() { - let function_definitions = function_definitions.as_vec(); - // and we have found a function with given id - if let Some(function) = function_definitions - .iter() - .find(|function| function.id().get() == Some(id.to_string())) - { - // and this function has its math element specified - if let Some(math) = function.math().get() { - let doc = self.read_doc(); - // and a lambda element within math is present - if let Some(lambda) = math.raw_element().find(doc.deref(), "lambda") { - // we return a number of bvar elements - return lambda - .child_elements(doc.deref()) - .iter() - .filter(|child| child.name(doc.deref()) == "bvar") - .collect::>() - .len() as i32; - } - } - } - } - 0 + /// [FunctionDefinition]. If [FunctionDefinition] cannot be found or the is missing the appropriate + /// math element, returns `None`. + pub(crate) fn function_definition_arguments(&self, id: &str) -> Option { + // Check that the list of a function definitions is present. + let definitions = self.function_definitions().get()?; + + // And that we have found a function with the given id. + let expected = Some(id.to_string()); + let function = definitions + .iter() + .find(|function| function.id().get() == expected)?; + + // And this function has its `math` element with a `lambda` child element specified. + let doc = self.read_doc(); + let math = function.math().get()?; + let lambda = math.raw_element().find(doc.deref(), "lambda")?; + let lambda = XmlElement::new_raw(self.document(), lambda); + + // We then return the number of `bvar` child nodes in the lambda element. + let count = lambda + .child_elements_filtered(|it| it.name(doc.deref()) == "bvar") + .len(); + + Some(count) } - /// Returns a vector of [UnitDefinition]s' identifiers (attribute **id**). If the identifier is not set, - /// it is not included in the output. + /// Returns a vector of [UnitDefinition] identifiers (attribute **id**). Unit definitions + /// without IDs are not included in the output. pub(crate) fn unit_definition_identifiers(&self) -> Vec { if let Some(unit_definitions) = self.unit_definitions().get() { unit_definitions - .as_vec() .iter() .filter_map(|unit| unit.id().get()) .collect() } else { - vec![] + Vec::new() } } - /// Returns a vector of all [LocalParameter]s' identifiers (attribute **id**). + /// Returns a vector of [LocalParameter] identifiers (attribute **id**). pub(crate) fn local_parameter_identifiers(&self) -> Vec { - let mut identifiers: Vec = vec![]; + let mut identifiers: Vec = Vec::new(); - if let Some(reactions) = self.reactions().get() { - for reaction in reactions.as_vec() { - if let Some(kinetic_law) = reaction.kinetic_law().get() { - if let Some(local_params) = kinetic_law.local_parameters().get() { - let mut param_ids = local_params - .as_vec() - .iter() - .map(|param| param.id().get()) - .collect::>(); - identifiers.append(&mut param_ids); - } - } + let Some(reactions) = self.reactions().get() else { + return identifiers; + }; + + for reaction in reactions.iter() { + let local_parameters = reaction + .kinetic_law() + .get() + .and_then(|law| law.local_parameters().get()); + if let Some(local_parameters) = local_parameters { + identifiers.extend(local_parameters.iter().map(|param| param.id().get())); } } + identifiers } - /// Returns a vector of all [Species]' identifiers (attribute **id**). + /// Returns a vector of [Species] identifiers (attribute **id**). pub(crate) fn species_identifiers(&self) -> Vec { if let Some(species) = self.species().get() { - species - .as_vec() - .iter() - .map(|species| species.id().get()) - .collect() + species.iter().map(|species| species.id().get()).collect() } else { - vec![] + Vec::new() } } - /// Returns a vector of all [Compartment]s' identifiers (attribute **id**). + /// Returns a vector of [Compartment] identifiers (attribute **id**). pub(crate) fn compartment_identifiers(&self) -> Vec { if let Some(compartment) = self.compartments().get() { compartment - .as_vec() .iter() .map(|compartment| compartment.id().get()) .collect() } else { - vec![] + Vec::new() } } - /// Returns a vector of all [Parameter]s' identifiers (attribute **id**). + /// Returns a vector of [Parameter] identifiers (attribute **id**). pub(crate) fn parameter_identifiers(&self) -> Vec { if let Some(parameters) = self.parameters().get() { - parameters - .as_vec() - .iter() - .map(|param| param.id().get()) - .collect() + parameters.iter().map(|param| param.id().get()).collect() } else { vec![] } } - /// Returns a vector of all [SpeciesReference](crate::core::SpeciesReference)' identifiers (attribute **id**). - /// If the identifier is not set, it is not included in the output. + /// Returns a vector of [SpeciesReference] identifiers (attribute **id**). Unit definitions + /// without IDs are not included in the output. pub(crate) fn species_reference_identifiers(&self) -> Vec { let mut identifiers: Vec = vec![]; - // if list of reactions is present + // If the list of reactions is present... if let Some(reactions) = self.reactions().get() { for reaction in reactions.as_vec() { - // we extract identifiers of reactants - let mut reactants = match reaction.reactants().get() { - Some(reactants) => reactants - .as_vec() - .iter() - .filter_map(|reactant| reactant.id().get()) - .collect::>(), - None => vec![], - }; - // and product identifiers as well - let mut products = match reaction.products().get() { - Some(products) => products - .as_vec() - .iter() - .filter_map(|product| product.id().get()) - .collect::>(), - None => vec![], - }; - // and then we include results in the output - identifiers.append(&mut reactants); - identifiers.append(&mut products); + // ...we extract identifiers of reactants and products. + for list in &[reaction.reactants(), reaction.products()] { + if let Some(list) = list.get() { + identifiers.extend(list.iter().filter_map(|it| it.id().get())); + } + } } } identifiers } - /// Returns a vector of all [Reaction]s' identifiers (attribute **id**). + /// Returns a vector of [FunctionDefinition] identifiers (attribute **id**). pub(crate) fn reaction_identifiers(&self) -> Vec { if let Some(reactions) = self.reactions().get() { reactions - .as_vec() .iter() .map(|reaction| reaction.id().get()) .collect::>() } else { - vec![] + Vec::new() } } - /// Returns a vector of *variables* of all [AssignmentRule]s. + /// Returns a vector of all *variables* appearing in all [AssignmentRule] objects. pub(crate) fn assignment_rule_variables(&self) -> Vec { if let Some(rules) = self.rules().get() { - return rules - .as_vec() + rules .iter() .filter_map(|rule| rule.try_downcast::()) .map(|assignment_rule| assignment_rule.variable().get()) - .collect::>(); + .collect::>() + } else { + Vec::new() } - vec![] } - /// Returns a vector of values from within **ci** element. + /// Returns a vector of values from within the **ci** elements appearing in all [AlgebraicRule] + /// objects in this model. pub(crate) fn algebraic_rule_ci_values(&self) -> Vec { if let Some(rules) = self.rules().get() { let doc = self.read_doc(); - return rules - .as_vec() + rules .iter() .filter_map(|rule| rule.try_downcast::()) .filter_map(|algebraic_rule| algebraic_rule.math().get()) .flat_map(|math| { - math.raw_element() - .child_elements_recursive(doc.deref()) - .iter() + math.recursive_child_elements() + .into_iter() .filter(|child| child.name(doc.deref()) == "ci") .map(|ci| ci.text_content(doc.deref())) .collect::>() }) - .collect::>(); + .collect::>() + } else { + Vec::new() } - vec![] } - /// Finds a species with given *id*. If not found, returns None. + /// Finds a species with the given *id*. If not found, returns `None`. pub(crate) fn find_species(&self, id: &str) -> Option { if let Some(species) = self.species().get() { - species - .as_vec() - .iter() - .find(|species| species.id().get() == id) - .cloned() + species.iter().find(|species| species.id().get() == id) } else { None } } - /// Finds a compartment with given *id*. If not found, returns None. + /// Finds a compartment with the given *id*. If not found, returns `None`. pub(crate) fn find_compartment(&self, id: &str) -> Option { if let Some(compartments) = self.compartments().get() { compartments - .as_vec() .iter() .find(|compartment| compartment.id().get() == id) - .cloned() } else { None } diff --git a/src/xml/xml_list.rs b/src/xml/xml_list.rs index 8801123..0700409 100644 --- a/src/xml/xml_list.rs +++ b/src/xml/xml_list.rs @@ -165,6 +165,13 @@ impl XmlList { vec } + + pub fn iter(&self) -> XmlListIterator { + XmlListIterator { + list: self, + index: 0, + } + } } // TODO: @@ -175,3 +182,23 @@ impl XmlList { // struct that implements it together with `SBase`, and possibly other implementations that // do not use `SBase`. impl SBase for XmlList {} + +/// A helper structure which allows us to iterate over the elements of a [XmlList]. +pub struct XmlListIterator<'a, T: XmlWrapper> { + list: &'a XmlList, + index: usize, +} + +impl<'a, T: XmlWrapper> Iterator for XmlListIterator<'a, T> { + type Item = T; + + fn next(&mut self) -> Option { + if self.index >= self.list.len() { + None + } else { + let item = self.list.get(self.index); + self.index += 1; + Some(item) + } + } +} diff --git a/src/xml/xml_wrapper.rs b/src/xml/xml_wrapper.rs index 9e45ff7..0705072 100644 --- a/src/xml/xml_wrapper.rs +++ b/src/xml/xml_wrapper.rs @@ -5,7 +5,7 @@ use crate::xml::{ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::sync::{RwLockReadGuard, RwLockWriteGuard}; -use xml_doc::{Document, Element, Node}; +use xml_doc::{Document, Element}; /// [XmlWrapper] is a trait implemented by all types that can behave as an [XmlElement] /// (including [XmlElement] itself). In other words, instances of [XmlWrapper] provide @@ -116,12 +116,44 @@ pub trait XmlWrapper: Into { // self.raw_element().attributes(doc.deref()) // TODO: cannot return value referencing local variable `doc`. How to fix? } - /// Returns the vector of children as a collection of [Node]s referenced within - /// this [XmlWrapper]. - fn children(&self) -> &Vec { - unimplemented!(); - // let doc = self.read_doc(); - // self.raw_element().children(doc.deref()) // TODO: cannot return value referencing local variable `doc`. How to fix? + /// Returns the vector of children referenced within this [XmlWrapper] as a collection + /// of [Element] objects. This method skips any child nodes that are not elements (such as + /// text or comments). + fn child_elements(&self) -> Vec { + let doc = self.read_doc(); + self.raw_element().child_elements(doc.deref()) + } + + /// Version of [Self::child_elements] with additional filtering function applied to the + /// output vector. + fn child_elements_filtered bool>(&self, predicate: P) -> Vec { + let doc = self.read_doc(); + self.raw_element() + .child_elements(doc.deref()) + .into_iter() + .filter(predicate) + .collect() + } + + /// Version of [Self::child_elements] that recursively traverses all child nodes, not just + /// the immediate descendants. + fn recursive_child_elements(&self) -> Vec { + let doc = self.read_doc(); + self.raw_element().child_elements_recursive(doc.deref()) + } + + /// Version of [Self::recursive_child_elements] with additional filtering function applied + /// to the output vector. + fn recursive_child_elements_filtered bool>( + &self, + predicate: P, + ) -> Vec { + let doc = self.read_doc(); + self.raw_element() + .child_elements_recursive(doc.deref()) + .into_iter() + .filter(predicate) + .collect() } /// Returns the vector of names of children referenced within this [XmlWrapper]. From d5c4e82a9e7bc21fcd0c92b65a6cf710b7340168 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Fri, 23 Feb 2024 17:52:15 -0800 Subject: [PATCH 51/57] Bit more refactoring of utility methods. --- src/core/model.rs | 193 +++++++++++++++++------------------------ src/xml/xml_list.rs | 27 ++++++ src/xml/xml_wrapper.rs | 46 ++++++++-- 3 files changed, 144 insertions(+), 122 deletions(-) diff --git a/src/core/model.rs b/src/core/model.rs index 34ada56..e760cbb 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -11,7 +11,6 @@ use crate::xml::{ use macros::{SBase, XmlWrapper}; use std::ops::Deref; -use xml_doc::Element; /// A type-safe representation of an SBML element. #[derive(Clone, Debug, XmlWrapper, SBase)] @@ -75,227 +74,191 @@ impl Model { self.optional_sbml_child("listOfEvents") } - /// Returns a vector of [FunctionDefinition]s' identifiers (attribute **id**). If the identifier is not set, - /// it is not included in the output. + /// Returns a vector of [FunctionDefinition] identifiers (attribute **id**). Function definitions + /// without IDs are not included in the output. pub(crate) fn function_definition_identifiers(&self) -> Vec { if let Some(function_definitions) = self.function_definitions().get() { function_definitions - .as_vec() .iter() .filter_map(|def| def.id().get()) .collect() } else { - vec![] + Vec::new() } } /// Find a [FunctionDefinition] by its *id* and return a number of arguments this function expects. /// More precisely, find a number of **bvar** elements inside **lambda** inside **math** element of - /// [FunctionDefinition]. If [FunctionDefinition] cannot be found, returns 0. - pub(crate) fn function_definition_arguments(&self, id: &str) -> i32 { - // if list of function definitions is present - if let Some(function_definitions) = self.function_definitions().get() { - let function_definitions = function_definitions.as_vec(); - // and we have found a function with given id - if let Some(function) = function_definitions - .iter() - .find(|function| function.id().get() == Some(id.to_string())) - { - // and this function has its math element specified - if let Some(math) = function.math().get() { - let doc = self.read_doc(); - // and a lambda element within math is present - if let Some(lambda) = math.raw_element().find(doc.deref(), "lambda") { - // we return a number of bvar elements - return lambda - .child_elements(doc.deref()) - .iter() - .filter(|child| child.name(doc.deref()) == "bvar") - .collect::>() - .len() as i32; - } - } - } - } - 0 + /// [FunctionDefinition]. If [FunctionDefinition] cannot be found or the is missing the appropriate + /// math element, returns `None`. + pub(crate) fn function_definition_arguments(&self, id: &str) -> Option { + // Check that the list of a function definitions is present. + let definitions = self.function_definitions().get()?; + + // And that we have found a function with the given id. + let expected = Some(id.to_string()); + let function = definitions + .iter() + .find(|function| function.id().get() == expected)?; + + // And this function has its `math` element with a `lambda` child element specified. + let doc = self.read_doc(); + let math = function.math().get()?; + let lambda = math.raw_element().find(doc.deref(), "lambda")?; + let lambda = XmlElement::new_raw(self.document(), lambda); + + // We then return the number of `bvar` child nodes in the lambda element. + let count = lambda + .child_elements_filtered(|it| it.name(doc.deref()) == "bvar") + .len(); + + Some(count) } - /// Returns a vector of [UnitDefinition]s' identifiers (attribute **id**). If the identifier is not set, - /// it is not included in the output. + /// Returns a vector of [UnitDefinition] identifiers (attribute **id**). Unit definitions + /// without IDs are not included in the output. pub(crate) fn unit_definition_identifiers(&self) -> Vec { if let Some(unit_definitions) = self.unit_definitions().get() { unit_definitions - .as_vec() .iter() .filter_map(|unit| unit.id().get()) .collect() } else { - vec![] + Vec::new() } } - /// Returns a vector of all [LocalParameter]s' identifiers (attribute **id**). + /// Returns a vector of [LocalParameter] identifiers (attribute **id**). pub(crate) fn local_parameter_identifiers(&self) -> Vec { - let mut identifiers: Vec = vec![]; + let mut identifiers: Vec = Vec::new(); - if let Some(reactions) = self.reactions().get() { - for reaction in reactions.as_vec() { - if let Some(kinetic_law) = reaction.kinetic_law().get() { - if let Some(local_params) = kinetic_law.local_parameters().get() { - let mut param_ids = local_params - .as_vec() - .iter() - .map(|param| param.id().get()) - .collect::>(); - identifiers.append(&mut param_ids); - } - } + let Some(reactions) = self.reactions().get() else { + return identifiers; + }; + + for reaction in reactions.iter() { + let local_parameters = reaction + .kinetic_law() + .get() + .and_then(|law| law.local_parameters().get()); + if let Some(local_parameters) = local_parameters { + identifiers.extend(local_parameters.iter().map(|param| param.id().get())); } } + identifiers } - /// Returns a vector of all [Species]' identifiers (attribute **id**). + /// Returns a vector of [Species] identifiers (attribute **id**). pub(crate) fn species_identifiers(&self) -> Vec { if let Some(species) = self.species().get() { - species - .as_vec() - .iter() - .map(|species| species.id().get()) - .collect() + species.iter().map(|species| species.id().get()).collect() } else { - vec![] + Vec::new() } } - /// Returns a vector of all [Compartment]s' identifiers (attribute **id**). + /// Returns a vector of [Compartment] identifiers (attribute **id**). pub(crate) fn compartment_identifiers(&self) -> Vec { if let Some(compartment) = self.compartments().get() { compartment - .as_vec() .iter() .map(|compartment| compartment.id().get()) .collect() } else { - vec![] + Vec::new() } } - /// Returns a vector of all [Parameter]s' identifiers (attribute **id**). + /// Returns a vector of [Parameter] identifiers (attribute **id**). pub(crate) fn parameter_identifiers(&self) -> Vec { if let Some(parameters) = self.parameters().get() { - parameters - .as_vec() - .iter() - .map(|param| param.id().get()) - .collect() + parameters.iter().map(|param| param.id().get()).collect() } else { vec![] } } - /// Returns a vector of all [SpeciesReference](crate::core::SpeciesReference)' identifiers (attribute **id**). - /// If the identifier is not set, it is not included in the output. + /// Returns a vector of [SpeciesReference] identifiers (attribute **id**). Unit definitions + /// without IDs are not included in the output. pub(crate) fn species_reference_identifiers(&self) -> Vec { let mut identifiers: Vec = vec![]; - // if list of reactions is present + // If the list of reactions is present... if let Some(reactions) = self.reactions().get() { for reaction in reactions.as_vec() { - // we extract identifiers of reactants - let mut reactants = match reaction.reactants().get() { - Some(reactants) => reactants - .as_vec() - .iter() - .filter_map(|reactant| reactant.id().get()) - .collect::>(), - None => vec![], - }; - // and product identifiers as well - let mut products = match reaction.products().get() { - Some(products) => products - .as_vec() - .iter() - .filter_map(|product| product.id().get()) - .collect::>(), - None => vec![], - }; - // and then we include results in the output - identifiers.append(&mut reactants); - identifiers.append(&mut products); + // ...we extract identifiers of reactants and products. + for list in &[reaction.reactants(), reaction.products()] { + if let Some(list) = list.get() { + identifiers.extend(list.iter().filter_map(|it| it.id().get())); + } + } } } identifiers } - /// Returns a vector of all [Reaction]s' identifiers (attribute **id**). + /// Returns a vector of [FunctionDefinition] identifiers (attribute **id**). pub(crate) fn reaction_identifiers(&self) -> Vec { if let Some(reactions) = self.reactions().get() { reactions - .as_vec() .iter() .map(|reaction| reaction.id().get()) .collect::>() } else { - vec![] + Vec::new() } } - /// Returns a vector of *variables* of all [AssignmentRule]s. + /// Returns a vector of all *variables* appearing in all [AssignmentRule] objects. pub(crate) fn assignment_rule_variables(&self) -> Vec { if let Some(rules) = self.rules().get() { - return rules - .as_vec() + rules .iter() .filter_map(|rule| rule.try_downcast::()) .map(|assignment_rule| assignment_rule.variable().get()) - .collect::>(); + .collect::>() + } else { + Vec::new() } - vec![] } - /// Returns a vector of values from within **ci** element. + /// Returns a vector of values from within the **ci** elements appearing in all [AlgebraicRule] + /// objects in this model. pub(crate) fn algebraic_rule_ci_values(&self) -> Vec { if let Some(rules) = self.rules().get() { let doc = self.read_doc(); - return rules - .as_vec() + rules .iter() .filter_map(|rule| rule.try_downcast::()) .filter_map(|algebraic_rule| algebraic_rule.math().get()) .flat_map(|math| { - math.raw_element() - .child_elements_recursive(doc.deref()) - .iter() + math.recursive_child_elements() + .into_iter() .filter(|child| child.name(doc.deref()) == "ci") .map(|ci| ci.text_content(doc.deref())) .collect::>() }) - .collect::>(); + .collect::>() + } else { + Vec::new() } - vec![] } - /// Finds a species with given *id*. If not found, returns None. + /// Finds a species with the given *id*. If not found, returns `None`. pub(crate) fn find_species(&self, id: &str) -> Option { if let Some(species) = self.species().get() { - species - .as_vec() - .iter() - .find(|species| species.id().get() == id) - .cloned() + species.iter().find(|species| species.id().get() == id) } else { None } } - /// Finds a compartment with given *id*. If not found, returns None. + /// Finds a compartment with the given *id*. If not found, returns `None`. pub(crate) fn find_compartment(&self, id: &str) -> Option { if let Some(compartments) = self.compartments().get() { compartments - .as_vec() .iter() .find(|compartment| compartment.id().get() == id) - .cloned() } else { None } diff --git a/src/xml/xml_list.rs b/src/xml/xml_list.rs index 8801123..0700409 100644 --- a/src/xml/xml_list.rs +++ b/src/xml/xml_list.rs @@ -165,6 +165,13 @@ impl XmlList { vec } + + pub fn iter(&self) -> XmlListIterator { + XmlListIterator { + list: self, + index: 0, + } + } } // TODO: @@ -175,3 +182,23 @@ impl XmlList { // struct that implements it together with `SBase`, and possibly other implementations that // do not use `SBase`. impl SBase for XmlList {} + +/// A helper structure which allows us to iterate over the elements of a [XmlList]. +pub struct XmlListIterator<'a, T: XmlWrapper> { + list: &'a XmlList, + index: usize, +} + +impl<'a, T: XmlWrapper> Iterator for XmlListIterator<'a, T> { + type Item = T; + + fn next(&mut self) -> Option { + if self.index >= self.list.len() { + None + } else { + let item = self.list.get(self.index); + self.index += 1; + Some(item) + } + } +} diff --git a/src/xml/xml_wrapper.rs b/src/xml/xml_wrapper.rs index 9e45ff7..0705072 100644 --- a/src/xml/xml_wrapper.rs +++ b/src/xml/xml_wrapper.rs @@ -5,7 +5,7 @@ use crate::xml::{ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::sync::{RwLockReadGuard, RwLockWriteGuard}; -use xml_doc::{Document, Element, Node}; +use xml_doc::{Document, Element}; /// [XmlWrapper] is a trait implemented by all types that can behave as an [XmlElement] /// (including [XmlElement] itself). In other words, instances of [XmlWrapper] provide @@ -116,12 +116,44 @@ pub trait XmlWrapper: Into { // self.raw_element().attributes(doc.deref()) // TODO: cannot return value referencing local variable `doc`. How to fix? } - /// Returns the vector of children as a collection of [Node]s referenced within - /// this [XmlWrapper]. - fn children(&self) -> &Vec { - unimplemented!(); - // let doc = self.read_doc(); - // self.raw_element().children(doc.deref()) // TODO: cannot return value referencing local variable `doc`. How to fix? + /// Returns the vector of children referenced within this [XmlWrapper] as a collection + /// of [Element] objects. This method skips any child nodes that are not elements (such as + /// text or comments). + fn child_elements(&self) -> Vec { + let doc = self.read_doc(); + self.raw_element().child_elements(doc.deref()) + } + + /// Version of [Self::child_elements] with additional filtering function applied to the + /// output vector. + fn child_elements_filtered bool>(&self, predicate: P) -> Vec { + let doc = self.read_doc(); + self.raw_element() + .child_elements(doc.deref()) + .into_iter() + .filter(predicate) + .collect() + } + + /// Version of [Self::child_elements] that recursively traverses all child nodes, not just + /// the immediate descendants. + fn recursive_child_elements(&self) -> Vec { + let doc = self.read_doc(); + self.raw_element().child_elements_recursive(doc.deref()) + } + + /// Version of [Self::recursive_child_elements] with additional filtering function applied + /// to the output vector. + fn recursive_child_elements_filtered bool>( + &self, + predicate: P, + ) -> Vec { + let doc = self.read_doc(); + self.raw_element() + .child_elements_recursive(doc.deref()) + .into_iter() + .filter(predicate) + .collect() } /// Returns the vector of names of children referenced within this [XmlWrapper]. From acdb92b6c0957f7d91dedb8d7b6277af7dcaac86 Mon Sep 17 00:00:00 2001 From: adamValent Date: Sat, 24 Feb 2024 18:41:33 +0100 Subject: [PATCH 52/57] Implement sanity checks (2/2) - check that attribute values are of the correct type --- src/constants/element.rs | 26 +++++++++++---- src/core/validation/mod.rs | 55 +++++++++++++++++++++++++++++-- src/lib.rs | 18 +++++----- src/xml/impl_xml_property_type.rs | 26 +++++++++++++-- 4 files changed, 105 insertions(+), 20 deletions(-) diff --git a/src/constants/element.rs b/src/constants/element.rs index f7c18df..5c4ef96 100644 --- a/src/constants/element.rs +++ b/src/constants/element.rs @@ -1,4 +1,4 @@ -use phf::phf_map; +use phf::{phf_map, Map}; macro_rules! extended_sbase_attributes { ($($y:expr),*) => { @@ -14,7 +14,7 @@ macro_rules! extended_sbase_children { pub const ALLOWED_SBASE_ATTRIBUTES: &[&str] = extended_sbase_attributes!(); pub const ALLOWED_SBASE_CHILDREN: &[&str] = extended_sbase_children!(); -pub const ALLOWED_ATTRIBUTES: phf::Map<&str, &[&str]> = phf_map! { +pub const ALLOWED_ATTRIBUTES: Map<&str, &[&str]> = phf_map! { "sbml" => extended_sbase_attributes!("xmlns", "level", "version"), "model"=> ALLOWED_SBASE_ATTRIBUTES, "listOfFunctionDefinitions" => ALLOWED_SBASE_ATTRIBUTES, @@ -56,7 +56,21 @@ pub const ALLOWED_ATTRIBUTES: phf::Map<&str, &[&str]> = phf_map! { "eventAssignment" => extended_sbase_attributes!("variable"), }; -pub const REQUIRED_ATTRIBUTES: phf::Map<&str, &[&str]> = phf_map! { +// attributes are omitted as their value is always considered valid nevertheless the actual value +pub const ATTRIBUTE_TYPES: Map<&str, Map<&str, &str>> = phf_map! { + "sbml" => phf_map! { "level" => "positive_int", "version" => "positive_int"}, + "unit" => phf_map! { "exponent" => "double", "scale" => "int", "multiplier" => "double"}, + "compartment" => phf_map! { "spatialDimensions" => "double", "size" => "double", "constant" => "boolean"}, + "species" => phf_map! { "initialAmount" => "double", "initialConcentration" => "double", "hasOnlySubstanceUnits" => "boolean", "boundaryCondition" => "boolean", "constant" => "boolean"}, + "parameter" => phf_map! { "value" => "double", "constant" => "boolean"}, + "reaction" => phf_map! { "reversible" => "boolean"}, + "speciesReference" => phf_map! { "stoichiometry" => "double", "constant" => "boolean"}, + "localParameter" => phf_map! { "value" => "double"}, + "event" => phf_map! { "useValuesFromTriggerTime" => "boolean" }, + "trigger" => phf_map! { "initialValue" => "boolean", "persistent" => "boolean" }, +}; + +pub const REQUIRED_ATTRIBUTES: Map<&str, &[&str]> = phf_map! { "sbml" => &["level", "version"], "model" => &[], "listOfFunctionDefinitions" => &[], @@ -98,7 +112,7 @@ pub const REQUIRED_ATTRIBUTES: phf::Map<&str, &[&str]> = phf_map! { "eventAssignment" => &["variable"] }; -pub const ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { +pub const ALLOWED_CHILDREN: Map<&str, &[&str]> = phf_map! { "sbml" => extended_sbase_children!("model"), "model" => extended_sbase_children!("listOfFunctionDefinitions", "listOfUnitDefinitions", "listOfCompartments", "listOfSpecies", "listOfParameters", "listOfInitialAssignments", "listOfRules", "listOfConstraints", "listOfReactions", "listOfEvents"), "listOfFunctionDefinitions" => extended_sbase_children!("functionDefinition"), @@ -142,7 +156,7 @@ pub const ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { // There are no required children in SBML core level 3 version 1 -pub const MATHML_ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { +pub const MATHML_ALLOWED_CHILDREN: Map<&str, &[&str]> = phf_map! { "math" => &["abs", "and", "annotation", "annotation-xml", "apply", "arccosh", "arccos", "arccoth", "arccot", "arccsch", "arccsc", "arcsech", "arcsec", "arcsinh", "arcsin", "arctanh", "arctan", "bvar", "ceiling", "ci", "cn", "cosh", "cos", "coth", "cot", "csch", "csc", @@ -153,7 +167,7 @@ pub const MATHML_ALLOWED_CHILDREN: phf::Map<&str, &[&str]> = phf_map! { "sep", "sinh", "sin", "tanh", "tan", "times", "true", "xor"] }; -pub const MATHML_ALLOWED_CHILDREN_BY_ATTR: phf::Map<&str, &[&str]> = phf_map! { +pub const MATHML_ALLOWED_CHILDREN_BY_ATTR: Map<&str, &[&str]> = phf_map! { "encoding" => &["csymbol", "annotation", "annotation-xml"], "definitionURL" => &["ci", "csymbol", "semantics"], "type" => &["cn"], diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index 750468e..a7df892 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -1,9 +1,13 @@ use crate::constants::element::{ - ALLOWED_ATTRIBUTES, ALLOWED_CHILDREN, MATHML_ALLOWED_CHILDREN, REQUIRED_ATTRIBUTES, + ALLOWED_ATTRIBUTES, ALLOWED_CHILDREN, ATTRIBUTE_TYPES, MATHML_ALLOWED_CHILDREN, + REQUIRED_ATTRIBUTES, }; use crate::constants::namespaces::URL_SBML_CORE; use crate::core::SBase; -use crate::xml::{OptionalXmlProperty, XmlElement, XmlList, XmlWrapper}; +use crate::xml::{ + DynamicProperty, OptionalXmlProperty, XmlElement, XmlList, XmlProperty, XmlPropertyType, + XmlWrapper, +}; use crate::{Sbml, SbmlIssue, SbmlIssueSeverity}; use std::collections::HashSet; use std::ops::Deref; @@ -97,6 +101,10 @@ impl Sbml { } } +/// Performs very basic and the most critical sanity checks. more precisely: +/// - the document contains all required children and attributes. +/// - each attribute value has correct type. +/// Any failing check is logged in *issues*. pub(crate) fn sanity_check(xml_element: &XmlElement, issues: &mut Vec) { let attributes = xml_element.attributes(); let element_name = xml_element.tag_name(); @@ -114,6 +122,49 @@ pub(crate) fn sanity_check(xml_element: &XmlElement, issues: &mut Vec }); } } + + // check that each attribute contains a value of the correct type + for attr in attributes { + let attr_name = attr.0.as_str(); + let Some(types) = ATTRIBUTE_TYPES.get(element_name.as_str()) else { + break; + }; + + // t => (attribute name, attribute value) + for t in types { + if &attr_name == t.0 { + match t.1 { + &"positive_int" => sanity_type_check::(attr_name, &xml_element, issues), + &"int" => sanity_type_check::(attr_name, &xml_element, issues), + &"double" => sanity_type_check::(attr_name, &xml_element, issues), + &"boolean" => sanity_type_check::(attr_name, &xml_element, issues), + _ => (), + } + }; + } + } +} + +/// Performs a type check of a value of a specific attribute. +/// If check fails, error is logged in *issues*. +fn sanity_type_check( + attribute_name: &str, + xml_element: &XmlElement, + issues: &mut Vec, +) { + let property = DynamicProperty::::new(xml_element, attribute_name).get_checked(); + if property.is_err() { + issues.push(SbmlIssue { + element: xml_element.raw_element(), + message: format!( + "Sanity check failed: {0} On the attribute [{1}].", + property.err().unwrap(), + attribute_name + ), + rule: "SANITY_CHECK".to_string(), + severity: SbmlIssueSeverity::Error, + }) + } } pub(crate) fn sanity_check_of_list( diff --git a/src/lib.rs b/src/lib.rs index 904154b..05ce23d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,16 +13,16 @@ use xml_doc::{Document, Element}; /// us work with XML documents in a sane and safe way. In particular: /// - [XmlDocument] | A thread and memory safe reference to a [Document]. /// - [XmlElement] | A thread and memory safe reference to an [Element]. -/// - [xml::XmlWrapper] | A trait with utility functions for working with types +/// - [XmlWrapper] | A trait with utility functions for working with types /// derived from [XmlElement]. -/// - [xml::XmlDefault] | An extension of [xml::XmlWrapper] which allows creation of "default" +/// - [xml::XmlDefault] | An extension of [XmlWrapper] which allows creation of "default" /// value for the derived type. /// - [xml::XmlProperty] and [xml::XmlPropertyType] | Traits providing an abstraction for /// accessing properties stored in XML attributes. Implementation can be generated using a derive /// macro. /// - [xml::XmlChild] and [xml::XmlChildDefault] | Trait abstraction for accessing singleton /// child tags. Implementation can be generated using a derive macro. -/// - [xml::XmlList] | A generic implementation of [xml::XmlWrapper] which represents +/// - [xml::XmlList] | A generic implementation of [XmlWrapper] which represents /// a typed list of elements. /// - [xml::DynamicChild] and [xml::DynamicProperty] | Generic implementations of /// [xml::XmlProperty] and [xml::XmlChild] that can be used when the name of the property/child @@ -92,11 +92,11 @@ impl Sbml { OptionalChild::new(&self.sbml_root, "model", URL_SBML_CORE) } - pub fn level(&self) -> RequiredProperty { + pub fn level(&self) -> RequiredProperty { RequiredProperty::new(&self.sbml_root, "level") } - pub fn version(&self) -> RequiredProperty { + pub fn version(&self) -> RequiredProperty { RequiredProperty::new(&self.sbml_root, "version") } @@ -186,7 +186,7 @@ pub enum SbmlIssueSeverity { /// invalid (e.g. a variable is declared but never used). Warning, /// A suggestion that would improve the document but does not represent a significant - /// issue (e.g. an property is included when it does not have to be, or unknown tags + /// issue (e.g. a property is included when it does not have to be, or unknown tags /// or attributes are present in the document, e.g. due to the use of unofficial extensions). Info, } @@ -211,7 +211,7 @@ mod tests { use crate::Sbml; /// Checks `SbmlDocument`'s properties such as `version` and `level`. - /// Additionally checks if `Model` retrieval returns correct child. + /// Additionally, checks if `Model` retrieval returns correct child. #[test] pub fn test_document() { let doc = Sbml::read_path("test-inputs/model.sbml").unwrap(); @@ -220,12 +220,12 @@ mod tests { let version = doc.version().get(); assert_eq!( - level, "3", + level, 3, "Wrong level of SBML.\nActual: {}\nExpected: {}", level, "3" ); assert_eq!( - version, "1", + version, 1, "Wrong version of SBML.\nActual: {}\nExpected: {}", version, "1" ); diff --git a/src/xml/impl_xml_property_type.rs b/src/xml/impl_xml_property_type.rs index a147a90..6632420 100644 --- a/src/xml/impl_xml_property_type.rs +++ b/src/xml/impl_xml_property_type.rs @@ -32,7 +32,7 @@ impl XmlPropertyType for bool { Some("1") | Some("true") => Ok(Some(true)), Some("0") | Some("false") => Ok(Some(false)), Some(value) => Err(format!( - "Value `{value}` does not represent a valid `bool`." + "Value '{value}' does not represent a valid 'bool'." )), None => Ok(None), } @@ -57,7 +57,27 @@ impl XmlPropertyType for i32 { match value.parse::() { Ok(x) => Ok(Some(x)), Err(e) => Err(format!( - "Value `{value}` does not represent a valid signed integer ({}).", + "Value '{value}' does not represent a valid signed integer ({}).", + e + )), + } + } else { + Ok(None) + } + } + + fn set(&self) -> Option { + Some(format!("{}", self)) + } +} + +impl XmlPropertyType for u32 { + fn try_get(value: Option<&str>) -> Result, String> { + if let Some(value) = value { + match value.parse::() { + Ok(x) => Ok(Some(x)), + Err(e) => Err(format!( + "Value '{value}' does not represent a valid unsigned integer ({}).", e )), } @@ -82,7 +102,7 @@ impl XmlPropertyType for f64 { Some(value) => match value.parse::() { Ok(x) => Ok(Some(x)), Err(e) => Err(format!( - "Value `{value}` does not represent a valid floating point number ({}).", + "Value '{value}' does not represent a valid floating point number ({}).", e )), }, From 5a36e0a3ca4eb6856def9058aba9e9ed7957231f Mon Sep 17 00:00:00 2001 From: adamValent Date: Sat, 24 Feb 2024 18:52:39 +0100 Subject: [PATCH 53/57] Fix clippy warnings and code inconsistencies --- examples/test-suite-syntactic.rs | 3 +-- src/core/validation/math.rs | 41 +++++++++++++++++-------------- src/core/validation/mod.rs | 10 ++++---- src/core/validation/test_suite.rs | 3 +-- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/examples/test-suite-syntactic.rs b/examples/test-suite-syntactic.rs index 878a183..09eed89 100644 --- a/examples/test-suite-syntactic.rs +++ b/examples/test-suite-syntactic.rs @@ -124,8 +124,7 @@ fn test_inner(filter: Option>) -> TestResults { let mut expected = read_expected_issues(result_file.to_str().unwrap()); let doc = Sbml::read_path(test_file.to_str().unwrap()).unwrap(); - let mut issues: Vec = Vec::new(); - doc.validate(&mut issues); + let issues: Vec = doc.validate(); for issue in issues { if test_issue(issue.rule.as_str()) { diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index fbc3618..aaa71cd 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -550,12 +550,12 @@ impl Math { && !scoped_local_param_ids.contains(&value) { issues.push(SbmlIssue { - element: ci, - message: format!("A identifier '{0}' found out of scope of its ", - value), - rule: "10216".to_string(), - severity: SbmlIssueSeverity::Error - }); + element: ci, + message: format!("A identifier '{0}' found out of scope of its ", + value), + rule: "10216".to_string(), + severity: SbmlIssueSeverity::Error, + }); } } } @@ -632,11 +632,11 @@ impl Math { } } else if MATHML_NARY_OPERATORS.contains(&name) && child_count - 1 == 0 { issues.push(SbmlIssue { - element: child, - message: format!("An N-ary operator <{0}> with 0 arguments found. Use of N-ary operators without any arguments is discouraged.", name), - rule: "10218".to_string(), - severity: SbmlIssueSeverity::Warning - }); + element: child, + message: format!("An N-ary operator <{0}> with 0 arguments found. Use of N-ary operators without any arguments is discouraged.", name), + rule: "10218".to_string(), + severity: SbmlIssueSeverity::Warning, + }); } } } @@ -670,9 +670,12 @@ impl Math { let id = function_call.text_content(doc.deref()); if func_identifiers.contains(&id) { - let expected_args = model.function_definition_arguments(id.as_str()); + let Some(expected_args) = model.function_definition_arguments(id.as_str()) + else { + continue; + }; - if child_count - 1 != expected_args { + if child_count - 1 != expected_args as i32 { issues.push(SbmlIssue { element: *function_call, message: format!( @@ -757,7 +760,7 @@ impl Math { value ), rule: "10221".to_string(), - severity: SbmlIssueSeverity::Error + severity: SbmlIssueSeverity::Error, }) } } @@ -857,8 +860,8 @@ impl Math { issues.push(SbmlIssue { element: ci, message: - format!("The value of target ('{0}') of rateOf found as a variable of .", - value), + format!("The value of target ('{0}') of rateOf found as a variable of .", + value), rule: "10224".to_string(), severity: SbmlIssueSeverity::Error, }) @@ -868,8 +871,8 @@ impl Math { issues.push(SbmlIssue { element: ci, message: - format!("The value of target ('{0}') of rateOf determined by an .", - value), + format!("The value of target ('{0}') of rateOf determined by an .", + value), rule: "10224".to_string(), severity: SbmlIssueSeverity::Error, }) @@ -927,7 +930,7 @@ impl Math { compartment_id ), rule: "10225".to_string(), - severity: SbmlIssueSeverity::Error + severity: SbmlIssueSeverity::Error, }) } else if !compartment.constant().get() && algebraic_ci_values.contains(&compartment_id) diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index 95ebe43..cf558d3 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -135,11 +135,11 @@ pub(crate) fn sanity_check(xml_element: &XmlElement, issues: &mut Vec // t => (attribute name, attribute value) for t in types { if &attr_name == t.0 { - match t.1 { - &"positive_int" => sanity_type_check::(attr_name, &xml_element, issues), - &"int" => sanity_type_check::(attr_name, &xml_element, issues), - &"double" => sanity_type_check::(attr_name, &xml_element, issues), - &"boolean" => sanity_type_check::(attr_name, &xml_element, issues), + match *t.1 { + "positive_int" => sanity_type_check::(attr_name, xml_element, issues), + "int" => sanity_type_check::(attr_name, xml_element, issues), + "double" => sanity_type_check::(attr_name, xml_element, issues), + "boolean" => sanity_type_check::(attr_name, xml_element, issues), _ => (), } }; diff --git a/src/core/validation/test_suite.rs b/src/core/validation/test_suite.rs index ac015b7..5867b81 100644 --- a/src/core/validation/test_suite.rs +++ b/src/core/validation/test_suite.rs @@ -79,8 +79,7 @@ fn test_inner(filter: Option>) { let mut expected = read_expected_issues(result_file.to_str().unwrap()); let doc = Sbml::read_path(test_file.to_str().unwrap()).unwrap(); - let mut issues: Vec = Vec::new(); - doc.validate(&mut issues); + let issues: Vec = doc.validate(); for issue in issues { if test_issue(issue.rule.as_str()) { From eee08f211cadf611aff600ddae06dd39abcbeab3 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Sat, 24 Feb 2024 11:49:44 -0800 Subject: [PATCH 54/57] Some more refactoring of the math module. --- src/core/reaction.rs | 9 +- src/core/validation/math.rs | 824 ++++++++++++++---------------------- src/lib.rs | 15 +- 3 files changed, 339 insertions(+), 509 deletions(-) diff --git a/src/core/reaction.rs b/src/core/reaction.rs index 8b87d4f..622da7a 100644 --- a/src/core/reaction.rs +++ b/src/core/reaction.rs @@ -115,18 +115,13 @@ impl KineticLaw { } pub(crate) fn local_parameter_identifiers(&self) -> Vec { - let local_parameters = self.local_parameters(); - - if local_parameters.is_set() { + if let Some(local_parameters) = self.local_parameters().get() { local_parameters - .get() - .unwrap() - .as_vec() .iter() .map(|param| param.id().get()) .collect() } else { - vec![] + Vec::new() } } } diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index 74b5bb0..a83fce6 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -9,7 +9,7 @@ use crate::constants::element::{ use crate::constants::namespaces::URL_MATHML; use crate::core::validation::get_allowed_children; use crate::core::{BaseUnit, FunctionDefinition, KineticLaw, Math, Model}; -use crate::xml::{RequiredXmlProperty, XmlWrapper}; +use crate::xml::{RequiredXmlProperty, XmlElement, XmlWrapper}; use crate::{SbmlIssue, SbmlIssueSeverity}; impl Math { @@ -46,16 +46,12 @@ impl Math { /// explicit or implicit valid namespace of a [Math] element must be performed. fn apply_rule_10201(&self, issues: &mut Vec) { if self.namespace_url() != URL_MATHML { - issues.push(SbmlIssue { - element: self.raw_element(), - message: format!( - "Wrong namespace usage in a math element. Found {0}, but {1} should be used.", - self.namespace_url(), - URL_MATHML - ), - rule: "10201".to_string(), - severity: SbmlIssueSeverity::Error, - }); + let message = format!( + "Wrong namespace usage in a `math` element. Found `{}`, but `{}` should be used.", + self.namespace_url(), + URL_MATHML + ); + issues.push(SbmlIssue::new_error("10201", self, message)); } } @@ -67,24 +63,17 @@ impl Math { /// [**sbml**](crate::Sbml). fn apply_rule_10202(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children = self.raw_element().children_recursive(doc.deref()); let allowed_children = get_allowed_children(self.xml_element()); - for child in children { - if let Some(child_element) = child.as_element() { - let child_tag_name = child_element.name(doc.deref()); - - if !allowed_children.contains(&child_tag_name) { - issues.push(SbmlIssue { - element: child_element, - message: format!( - "Unknown child <{0}> of element <{1}>.", - child_tag_name, "math" - ), - rule: "10202".to_string(), - severity: SbmlIssueSeverity::Error, - }); - } + for child in self.recursive_child_elements() { + let child_tag_name = child.name(doc.deref()); + + if !allowed_children.contains(&child_tag_name) { + let message = format!( + "Unknown child <{0}> of element <{1}>.", + child_tag_name, "math" + ); + issues.push(SbmlIssue::new_error("10202", self, message)); } } } @@ -99,28 +88,20 @@ impl Math { fn apply_rule_10203(&self, issues: &mut Vec) { let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["encoding"]; - let children_of_interest = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| child.attribute(doc.deref(), "encoding").is_some()) - .copied() - .collect::>(); + let relevant_children = self.recursive_child_elements_filtered(|it| { + it.attribute(doc.deref(), "encoding").is_some() + }); - for child in children_of_interest { + for child in relevant_children { let name = child.name(doc.deref()); if !allowed.contains(&name) { - issues.push(SbmlIssue { - element: child, - message: format!( - "Attribute [encoding] found on element <{0}>, which is forbidden. \ + let message = format!( + "Attribute [encoding] found on element <{0}>, which is forbidden. \ Attribute [encoding] is only permitted on , and .", - name - ), - rule: "10203".to_string(), - severity: SbmlIssueSeverity::Error, - }); + name + ); + issues.push(SbmlIssue::new_error("10203", self, message)); } } } @@ -135,28 +116,21 @@ impl Math { fn apply_rule_10204(&self, issues: &mut Vec) { let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["definitionURL"]; - let children_of_interest = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| child.attribute(doc.deref(), "definitionURL").is_some()) - .copied() - .collect::>(); + let relevant_children = self.recursive_child_elements_filtered(|it| { + it.attribute(doc.deref(), "definitionURL").is_some() + }); - for child in children_of_interest { + for child in relevant_children { let name = child.name(doc.deref()); if !allowed.contains(&name) { - issues.push(SbmlIssue { - element: child, - message: format!( - "Attribute [definitionURL] found on element <{0}>, which is forbidden. \ + let message = format!( + "Attribute [definitionURL] found on element <{0}>, which is forbidden. \ Attribute [definitionURL] is only permitted on , and .", - name - ), - rule: "10204".to_string(), - severity: SbmlIssueSeverity::Error, - }); + name + ); + + issues.push(SbmlIssue::new_error("10204", self, message)); } } } @@ -172,34 +146,19 @@ impl Math { /// on the SBML container element [**sbml**](crate::Sbml). fn apply_rule_10205(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children_of_interest = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| { - child.attribute(doc.deref(), "definitionURL").is_some() - && child.name(doc.deref()) == "csymbol" - }) - .copied() - .collect::>(); + let children_of_interest = self.recursive_child_elements_filtered(|child| { + child.attribute(doc.deref(), "definitionURL").is_some() + && child.name(doc.deref()) == "csymbol" + }); for child in children_of_interest { let value = child.attribute(doc.deref(), "definitionURL").unwrap(); if !MATHML_ALLOWED_DEFINITION_URLS.contains(&value) { - issues.push(SbmlIssue { - element: child, - message: format!( - "Invalid definitionURL value found '{0}'. Permitted values are: {1}", - value, - MATHML_ALLOWED_DEFINITION_URLS - .iter() - .map(|url| url.to_string()) - .collect::>() - .join(", ") - ), - rule: "10205".to_string(), - severity: SbmlIssueSeverity::Error, - }); + let message = format!( + "Invalid definitionURL value found '{}'. Permitted values are: {:?}", + value, MATHML_ALLOWED_DEFINITION_URLS + ); + issues.push(SbmlIssue::new_error("10205", self, message)); } } } @@ -213,28 +172,20 @@ impl Math { /// on the SBML container element [**sbml**](crate::Sbml). fn apply_rule_10206(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children_of_interest = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| child.attribute(doc.deref(), "type").is_some()) - .copied() - .collect::>(); + let children_of_interest = self.recursive_child_elements_filtered(|child| { + child.attribute(doc.deref(), "type").is_some() + }); for child in children_of_interest { let name = child.name(doc.deref()); if !MATHML_ALLOWED_CHILDREN_BY_ATTR["type"].contains(&name) { - issues.push(SbmlIssue { - element: child, - message: format!( - "Attribute [type] found on element <{0}>, which is forbidden. \ + let message = format!( + "Attribute [type] found on element <{0}>, which is forbidden. \ Attribute [type] is only permitted on .", - name - ), - rule: "10204".to_string(), - severity: SbmlIssueSeverity::Error, - }) + name + ); + issues.push(SbmlIssue::new_error("10204", self, message)); } } } @@ -247,28 +198,20 @@ impl Math { /// **required="true"** on the SBML container element [**sbml**](crate::Sbml). fn apply_rule_10207(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children_of_interest = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| child.attribute(doc.deref(), "type").is_some()) - .copied() - .collect::>(); + let children_of_interest = self.recursive_child_elements_filtered(|child| { + child.attribute(doc.deref(), "type").is_some() + }); for child in children_of_interest { let value = child.attribute(doc.deref(), "type").unwrap(); if !MATHML_ALLOWED_TYPES.contains(&value) { - issues.push(SbmlIssue { - element: child, - message: format!( - "Invalid type value found '{0}'. Permitted values are: \ + let message = format!( + "Invalid type value found '{0}'. Permitted values are: \ 'e-notation', 'real', 'integer' and 'rational'", - value - ), - rule: "10206".to_string(), - severity: SbmlIssueSeverity::Error, - }); + value + ); + issues.push(SbmlIssue::new_error("10206", self, message)); } } } @@ -284,13 +227,8 @@ impl Math { /// SBML container element [**sbml**](crate::Sbml). fn apply_rule_10208(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children_of_interest = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| child.name(doc.deref()) == "lambda") - .copied() - .collect::>(); + let children_of_interest = + self.recursive_child_elements_filtered(|child| child.name(doc.deref()) == "lambda"); for child in children_of_interest { let parent = child.parent(doc.deref()).unwrap(); @@ -304,16 +242,12 @@ impl Math { let top_parent = grandparent.parent(doc.deref()).unwrap(); Self::validate_lambda_placement(doc.deref(), child, parent, top_parent, issues); } else { - issues.push(SbmlIssue { - element: child, - message: format!( - "Invalid immediate parent of . Only and are \ + let message = format!( + "Invalid immediate parent of . Only and are \ valid immediate parents. Actual parent: <{0}>", - parent_name - ), - rule: "10208".to_string(), - severity: SbmlIssueSeverity::Error, - }) + parent_name + ); + issues.push(SbmlIssue::new_error("10208", self, message)); } } } @@ -352,11 +286,11 @@ impl Math { } /// ### Rule 10214 - /// Outside of a [**FunctionDefinition**](FunctionDefinition) object, if a MathML + /// Outside a [FunctionDefinition] object, if a MathML /// **ci** element is the first element within a MathML apply element, then the **ci** element's /// value can only be chosen from the set of identifiers of - /// [**FunctionDefinition**](FunctionDefinition) objects defined in the enclosing - /// SBML [Model](crate::core::model) object. + /// [FunctionDefinition] objects defined in the enclosing + /// SBML [Model] object. fn apply_rule_10214(&self, issues: &mut Vec) { let doc = self.read_doc(); let parent_name = self @@ -366,44 +300,36 @@ impl Math { .name(doc.deref()); if parent_name != "functionDefinition" { - let children_of_interest = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| { - child.name(doc.deref()) == "apply" - && child - .child_elements(doc.deref()) - .first() - .unwrap() - .name(doc.deref()) - == "ci" - }) - .copied() - .collect::>(); + let children_of_interest = self.recursive_child_elements_filtered(|child| { + let is_apply = child.name(doc.deref()) == "apply"; + let ci_first = child + .child_elements(doc.deref()) + .first() + .map(|it| it.name(doc.deref()) == "ci") + .unwrap_or(false); + is_apply && ci_first + }); let identifiers = Model::for_child_element(self.document(), self.xml_element()) .unwrap() .function_definition_identifiers(); for child in children_of_interest { - let value = match child.child_elements(doc.deref()).first() { - Some(element) => element.text_content(doc.deref()), - None => "".to_string(), - }; + // This unwrap must succeed because we enforced that ci is the first child. + let value = child + .child_elements(doc.deref()) + .first() + .unwrap() + .text_content(doc.deref()); if !identifiers.contains(&value) { - issues.push(SbmlIssue { - element: child, - message: format!( - "Function '{0}' not defined. \ + let message = format!( + "Function '{0}' not defined. \ Function referred by must be defined in object \ with relevant identifier (id).", - value - ), - rule: "10214".to_string(), - severity: SbmlIssueSeverity::Error, - }) + value + ); + issues.push(SbmlIssue::new_error("10214", self, message)); } } } @@ -411,64 +337,55 @@ impl Math { // TODO: needs review /// ### Rule 10215 - /// Outside of a [FunctionDefinition] object, if a MathML **ci** element is not the first element within + /// Outside a [FunctionDefinition] object, if a MathML **ci** element is not the first element within /// a MathML **apply**, then the **ci** element's value may only be chosen from the following set of - /// identifiers: the identifiers of [Species](crate::core::species::Species), - /// [Compartment](crate::core::compartment::Compartment), [Parameter](crate::core::parameter::Parameter), - /// [SpeciesReference](crate::core::reaction::SpeciesReference) and [Reaction] - /// objects defined in the enclosing [Model] object; the identifiers of - /// [LocalParameter](crate::core::reaction::LocalParameter) objects that are children of the - /// [Reaction](crate::core::reaction::Reaction) in which the [FunctionDefinition] appears (if it appears inside - /// the [Math] object of a [KineticLaw]); and any identifiers (in the SId namespace of the model) belonging to an + /// identifiers: the identifiers of [Species], [Compartment], [Parameter], [SpeciesReference] + /// and [Reaction] objects defined in the enclosing [Model] object; the identifiers of + /// [LocalParameter] objects that are children of the [Reaction] in which the + /// [FunctionDefinition] appears (if it appears inside the [Math] object of a [KineticLaw]); + /// and any identifiers (in the SId namespace of the model) belonging to an /// object class defined by an SBML Level 3 package as having mathematical meaning. fn apply_rule_10215(&self, issues: &mut Vec) { let is_out_of_function_definition = FunctionDefinition::for_child_element(self.document(), self.xml_element()).is_none(); - if is_out_of_function_definition { - let doc = self.read_doc(); - let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); - let identifiers = [ - model.species_reference_identifiers(), - model.compartment_identifiers(), - model.parameter_identifiers(), - model.species_identifiers(), - model.species_reference_identifiers(), - model.reaction_identifiers(), - model.local_parameter_identifiers(), - ] - .concat(); - let apply_elements = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| child.name(doc.deref()) == "apply") - .copied() - .collect::>(); - - for apply in apply_elements { - let ci_elements = apply - .child_elements(doc.deref()) - .iter() - .filter(|child| child.name(doc.deref()) == "ci") - .skip(1) - .copied() - .collect::>(); - - for ci in ci_elements { - let value = ci.text_content(doc.deref()); - - if !identifiers.contains(&value) { - issues.push(SbmlIssue { - element: ci, - message: format!( - "Invalid identifier value '{0}' in . Identifier not found.", - value - ), - rule: "10215".to_string(), - severity: SbmlIssueSeverity::Error, - }) - } + if !is_out_of_function_definition { + return; + } + + let doc = self.read_doc(); + let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + let identifiers = [ + model.species_identifiers(), + model.compartment_identifiers(), + model.parameter_identifiers(), + model.species_reference_identifiers(), + model.reaction_identifiers(), + model.local_parameter_identifiers(), + ] + .concat(); + + let apply_elements = + self.recursive_child_elements_filtered(|child| child.name(doc.deref()) == "apply"); + + for apply in apply_elements { + let ci_elements = apply + .child_elements(doc.deref()) + .into_iter() + .skip(1) + .filter(|child| child.name(doc.deref()) == "ci") + .collect::>(); + + for ci in ci_elements { + let value = ci.text_content(doc.deref()); + + if !identifiers.contains(&value) { + let ci = XmlElement::new_raw(self.document(), ci); + let message = format!( + "Invalid identifier value '{0}' in . Identifier not found.", + value + ); + issues.push(SbmlIssue::new_error("10215", &ci, message)); } } } @@ -489,27 +406,21 @@ impl Math { let scoped_local_param_ids = match KineticLaw::for_child_element(self.document(), self.xml_element()) { Some(k) => k.local_parameter_identifiers(), - None => vec![], + None => Vec::new(), }; let b_variables = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() + .recursive_child_elements() + .into_iter() .filter(|child| child.name(doc.deref()) == "bvar") - .map(|bvar| { + .filter_map(|bvar| { bvar.child_elements(doc.deref()) .first() - .unwrap() - .text_content(doc.deref()) + .map(|it| it.text_content(doc.deref())) }) .collect::>(); - let ci_elements = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| child.name(doc.deref()) == "ci") - .copied() - .collect::>(); + + let ci_elements = + self.recursive_child_elements_filtered(|child| child.name(doc.deref()) == "ci"); for ci in ci_elements { let value = ci.text_content(doc.deref()); @@ -517,13 +428,12 @@ impl Math { && all_local_param_ids.contains(&value) && !scoped_local_param_ids.contains(&value) { - issues.push(SbmlIssue { - element: ci, - message: format!("A identifier '{0}' found out of scope of its ", - value), - rule: "10216".to_string(), - severity: SbmlIssueSeverity::Error - }); + let ci = XmlElement::new_raw(self.document(), ci); + let message = format!( + "A identifier '{0}' found out of scope of its ", + value + ); + issues.push(SbmlIssue::new_error("10216", &ci, message)); } } } @@ -532,80 +442,58 @@ impl Math { /// A MathML operator must be supplied the number of arguments appropriate for that operator. fn apply_rule_10218(&self, issues: &mut Vec) { let doc = self.read_doc(); - let apply_elements = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| child.name(doc.deref()) == "apply") - .copied() - .collect::>(); + let apply_elements = + self.recursive_child_elements_filtered(|child| child.name(doc.deref()) == "apply"); for apply in apply_elements { - let children = apply.child_elements(doc.deref()); + let apply = XmlElement::new_raw(self.document(), apply); + let children = apply.child_elements(); let child_count = children.len(); - - // iterate through children of an element - for child in children { - let name = child.name(doc.deref()); - - if MATHML_UNARY_OPERATORS.contains(&name) { - // is allowed to have 1 OR 2 arguments - if name == "minus" && child_count - 1 != 1 && child_count - 1 != 2 { - issues.push(SbmlIssue { - element: child, - message: format!( - "Invalid number ({0}) of arguments for operator . \ - The operator can take either 1 or 2 arguments.", - child_count - 1 - ), - rule: "10218".to_string(), - severity: SbmlIssueSeverity::Error, - }) - } else if child_count - 1 != 1 && name != "minus" { - issues.push(SbmlIssue { - element: child, - message: format!( - "Invalid number ({0}) of arguments for unary operator <{1}>", - child_count - 1, - name - ), - rule: "10218".to_string(), - severity: SbmlIssueSeverity::Error, - }); - } - } else if MATHML_BINARY_OPERATORS.contains(&name) { - // root is allowed to have 1 OR 2 arguments - if name == "root" && child_count - 1 != 1 && child_count - 1 != 2 { - issues.push(SbmlIssue { - element: child, - message: format!( - "Invalid number ({0}) of arguments for operator . \ - The operator can take either 1 or 2 arguments.", - child_count - 1 - ), - rule: "10218".to_string(), - severity: SbmlIssueSeverity::Error, - }) - } else if child_count - 1 != 2 && name != "root" { - issues.push(SbmlIssue { - element: child, - message: format!( - "Invalid number ({0}) of arguments for binary operator <{1}>.", - child_count - 1, - name - ), - rule: "10218".to_string(), - severity: SbmlIssueSeverity::Error, - }); - } - } else if MATHML_NARY_OPERATORS.contains(&name) && child_count - 1 == 0 { - issues.push(SbmlIssue { - element: child, - message: format!("An N-ary operator <{0}> with 0 arguments found. Use of N-ary operators without any arguments is discouraged.", name), - rule: "10218".to_string(), - severity: SbmlIssueSeverity::Warning - }); + if child_count == 0 { + let message = "No operator specified in .".to_string(); + issues.push(SbmlIssue::new_error("10218", &apply, message)); + continue; + } + let arg_count = child_count - 1; + let operator = children[0].name(doc.deref()); + + if MATHML_UNARY_OPERATORS.contains(&operator) { + // is allowed to have 1 OR 2 arguments + if operator == "minus" && arg_count != 1 && arg_count != 2 { + let message = format!( + "Invalid number ({arg_count}) of arguments for operator . \ + The operator can take either 1 or 2 arguments." + ); + issues.push(SbmlIssue::new_error("10218", &apply, message)); + } else if operator != "minus" && arg_count != 1 { + let message = format!( + "Invalid number ({arg_count}) of arguments for unary operator <{operator}>" + ); + issues.push(SbmlIssue::new_error("10218", &apply, message)); } + } else if MATHML_BINARY_OPERATORS.contains(&operator) { + // root is allowed to have 1 OR 2 arguments + if operator == "root" && arg_count != 1 && arg_count != 2 { + let message = format!( + "Invalid number ({arg_count}) of arguments for operator . \ + The operator can take either 1 or 2 arguments." + ); + issues.push(SbmlIssue::new_error("10218", &apply, message)); + } else if operator != "root" && arg_count != 2 { + let message = format!("Invalid number ({arg_count}) of arguments for binary operator <{operator}>."); + issues.push(SbmlIssue::new_error("10218", &apply, message)); + } + } else if MATHML_NARY_OPERATORS.contains(&operator) && arg_count == 0 { + // TODO: + // This is not correct? N-ary operators with zero arguments are only + // discouraged if the meaning of the operator is not well defined. + let message = format!("An N-ary operator <{operator}> with 0 arguments found. Use of N-ary operators without any arguments is discouraged."); + issues.push(SbmlIssue { + element: apply.raw_element(), + message, + rule: "10218".to_string(), + severity: SbmlIssueSeverity::Warning, + }); } } } @@ -615,42 +503,35 @@ impl Math { let doc = self.read_doc(); let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); - let apply_elements = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| child.name(doc.deref()) == "apply") - .copied() - .collect::>(); + let apply_elements = + self.recursive_child_elements_filtered(|child| child.name(doc.deref()) == "apply"); for apply in apply_elements { let children = apply.child_elements(doc.deref()); - let child_count = children.len() as i32; - let function_call = children.first(); - - if function_call.is_some() && function_call.unwrap().name(doc.deref()) == "ci" { - let function_call = function_call.unwrap(); - let func_identifiers = model.function_definition_identifiers(); - let id = function_call.text_content(doc.deref()); - - if func_identifiers.contains(&id) { - let expected_args = model.function_definition_arguments(id.as_str()); - - if child_count - 1 != expected_args { - issues.push(SbmlIssue { - element: *function_call, - message: format!( - "Invalid number of arguments ({0}) provided for function '{1}'. \ - The function '{2}' takes {3} arguments.", - child_count - 1, - id, - id, - expected_args - ), - rule: "10219".to_string(), - severity: SbmlIssueSeverity::Error, - }); - } + let Some(function_call) = children.first() else { + continue; + }; + + if function_call.name(doc.deref()) != "ci" { + continue; + } + + let arg_count = children.len() - 1; + let func_identifiers = model.function_definition_identifiers(); + let id = function_call.text_content(doc.deref()); + + if func_identifiers.contains(&id) { + let expected_args = model + .function_definition_arguments(id.as_str()) + .unwrap_or(0); + + if arg_count != expected_args { + let message = format!( + "Invalid number of arguments ({arg_count}) provided for function '{id}'. \ + The function '{id}' takes {expected_args} arguments." + ); + let function_call = XmlElement::new_raw(self.document(), *function_call); + issues.push(SbmlIssue::new_error("10219", &function_call, message)); } } } @@ -664,28 +545,20 @@ impl Math { /// element [**sbml**](crate::Sbml). fn apply_rule_10220(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children_of_interest: Vec = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| child.attribute(doc.deref(), "units").is_some()) - .copied() - .collect(); + let children_of_interest: Vec = self.recursive_child_elements_filtered(|child| { + child.attribute(doc.deref(), "units").is_some() + }); for child in children_of_interest { let name = child.name(doc.deref()); if !MATHML_ALLOWED_CHILDREN_BY_ATTR["units"].contains(&name) { - issues.push(SbmlIssue { - element: child, - message: format!( - "Attribute [units] found on element <{0}>, which is forbidden. \ - Attribute [units] is only permitted on .", - name - ), - rule: "10220".to_string(), - severity: SbmlIssueSeverity::Error, - }) + let message = format!( + "Attribute [units] found on element <{name}>, which is forbidden. \ + Attribute [units] is only permitted on ." + ); + let child = XmlElement::new_raw(self.document(), child); + issues.push(SbmlIssue::new_error("10220", &child, message)); } } } @@ -698,31 +571,21 @@ impl Math { let unit_identifiers = Model::for_child_element(self.document(), self.xml_element()) .unwrap() .unit_definition_identifiers(); - let cn_elements = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| { - child.name(doc.deref()) == "cn" && child.attribute(doc.deref(), "units").is_some() - }) - .copied() - .collect::>(); + let cn_elements = self.recursive_child_elements_filtered(|child| { + child.name(doc.deref()) == "cn" && child.attribute(doc.deref(), "units").is_some() + }); for cn in cn_elements { let value = cn.attribute(doc.deref(), "units").unwrap(); if !unit_identifiers.contains(&value.to_string()) && BaseUnit::from_str(value).is_err() { - issues.push(SbmlIssue { - element: cn, - message: format!( - "Invalid unit identifier '{0}' found. \ - Only identifiers of objects and base units can be used in .", - value - ), - rule: "10221".to_string(), - severity: SbmlIssueSeverity::Error - }) + let message = format!( + "Invalid unit identifier '{value}' found. \ + Only identifiers of objects and base units can be used in ." + ); + let cn = XmlElement::new_raw(self.document(), cn); + issues.push(SbmlIssue::new_error("10221", &cn, message)); } } } @@ -731,52 +594,41 @@ impl Math { /// The single argument for the *rateOf* **csymbol** function must be a **ci** element. fn apply_rule_10223(&self, issues: &mut Vec) { let doc = self.read_doc(); - let children_of_interest = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| { - child.name(doc.deref()) == "apply" && !child.child_elements(doc.deref()).is_empty() - }) - .filter(|apply| { - apply - .child_elements(doc.deref()) - .first() - .unwrap() - .attribute(doc.deref(), "definitionURL") - .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf") - }) - .copied() - .collect::>(); + let children_of_interest = self.recursive_child_elements_filtered(|child| { + child.name(doc.deref()) == "apply" && { + if let Some(first_child) = child.child_elements(doc.deref()).first() { + first_child + .attribute(doc.deref(), "definitionURL") + .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf") + } else { + false + } + } + }); for child in children_of_interest { let apply_children = child.child_elements(doc.deref()); if apply_children.len() != 2 { - issues.push(SbmlIssue { - element: child, - message: format!( - "Invalid number ({0}) of arguments provided for rateOf . \ + let message = format!( + "Invalid number ({0}) of arguments provided for rateOf . \ The call of rateOf must have precisely one argument.", - apply_children.len() - 1 - ), - rule: "10223".to_string(), - severity: SbmlIssueSeverity::Error, - }); + apply_children.len() - 1 + ); + let child = XmlElement::new_raw(self.document(), child); + issues.push(SbmlIssue::new_error("10223", &child, message)); } else { + // This unwrap is ok because we only selected elements with at least one child. let argument_name = apply_children.last().unwrap().name(doc.deref()); if argument_name != "ci" { - issues.push(SbmlIssue { - element: child, - message: format!( - "Invalid argument <{0}> provided for .\ + let message = format!( + "Invalid argument <{0}> provided for .\ The rateOf must have as its only argument.", - argument_name - ), - rule: "10223".to_string(), - severity: SbmlIssueSeverity::Error, - }) + argument_name + ); + let child = XmlElement::new_raw(self.document(), child); + issues.push(SbmlIssue::new_error("10223", &child, message)); } } } @@ -789,28 +641,22 @@ impl Math { fn apply_rule_10224(&self, issues: &mut Vec) { let doc = self.read_doc(); let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); - let ci_elements = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| { - child.name(doc.deref()) == "apply" - && child.child_elements(doc.deref()).len() > 1 - && child - .child_elements(doc.deref()) - .first() - .unwrap() + let ci_elements = self.recursive_child_elements_filtered(|child| { + child.name(doc.deref()) == "apply" && { + let children = child.child_elements(doc.deref()); + if children.len() < 2 { + false + } else { + let fst = children[0]; + let snd = children[1]; + let is_rate_of = fst .attribute(doc.deref(), "definitionURL") - .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf") - && child - .child_elements(doc.deref()) - .get(1) - .unwrap() - .name(doc.deref()) - == "ci" - }) - .copied() - .collect::>(); + .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf"); + let is_ci = snd.name(doc.deref()) == "ci"; + is_ci && is_rate_of + } + } + }); let assignment_rule_variables = model.assignment_rule_variables(); let algebraic_rule_determinants = model.algebraic_rule_ci_values(); @@ -818,24 +664,14 @@ impl Math { let value = ci.text_content(doc.deref()); if assignment_rule_variables.contains(&value) { - issues.push(SbmlIssue { - element: ci, - message: - format!("The value of target ('{0}') of rateOf found as a variable of .", - value), - rule: "10224".to_string(), - severity: SbmlIssueSeverity::Error, - }) + let message = format!("The value of target ('{value}') of rateOf found as a variable of ."); + let ci = XmlElement::new_raw(self.document(), ci); + issues.push(SbmlIssue::new_error("10224", &ci, message)); // TODO: what does "determined by algebraicRule" mean and how to check it? } else if algebraic_rule_determinants.contains(&value) { - issues.push(SbmlIssue { - element: ci, - message: - format!("The value of target ('{0}') of rateOf determined by an .", - value), - rule: "10224".to_string(), - severity: SbmlIssueSeverity::Error, - }) + let message = format!("The value of target ('{value}') of rateOf determined by an ."); + let ci = XmlElement::new_raw(self.document(), ci); + issues.push(SbmlIssue::new_error("10224", &ci, message)); } } } @@ -850,64 +686,50 @@ impl Math { let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); let assignment_rule_variables = model.assignment_rule_variables(); let algebraic_ci_values = model.algebraic_rule_ci_values(); - let ci_elements = self - .raw_element() - .child_elements_recursive(doc.deref()) - .iter() - .filter(|child| { - child.name(doc.deref()) == "apply" - && child.child_elements(doc.deref()).len() > 1 - && child - .child_elements(doc.deref()) - .first() - .unwrap() + let ci_elements = self.recursive_child_elements_filtered(|child| { + child.name(doc.deref()) == "apply" && { + let children = child.child_elements(doc.deref()); + if children.len() < 2 { + false + } else { + let fst = children[0]; + let snd = children[1]; + let is_rate_of = fst .attribute(doc.deref(), "definitionURL") - .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf") - && child - .child_elements(doc.deref()) - .get(1) - .unwrap() - .name(doc.deref()) - == "ci" - }) - .copied() - .collect::>(); + .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf"); + let is_ci = snd.name(doc.deref()) == "ci"; + is_ci && is_rate_of + } + } + }); for ci in ci_elements { let value = ci.text_content(doc.deref()); - if let Some(species) = model.find_species(value.as_str()) { - if !species.has_only_substance_units().get() { - if let Some(compartment) = - model.find_compartment(species.compartment().get().as_str()) - { - let compartment_id = compartment.id().get(); - - if assignment_rule_variables.contains(&compartment_id) { - issues.push(SbmlIssue { - element: ci, - message: format!( - "The with id '{0}' found as the [variable] of an .", - compartment_id - ), - rule: "10225".to_string(), - severity: SbmlIssueSeverity::Error - }) - } else if !compartment.constant().get() - && algebraic_ci_values.contains(&compartment_id) - { - issues.push(SbmlIssue { - element: ci, - message: format!( - "The 's size with id '{0}' is possible to determine by an .", - compartment_id - ), - rule: "10225".to_string(), - severity: SbmlIssueSeverity::Error, - }) - } - } - } + let Some(species) = model.find_species(value.as_str()) else { + continue; + }; + + if species.has_only_substance_units().get() { + continue; + } + + let species_compartment = species.compartment().get(); + let Some(compartment) = model.find_compartment(species_compartment.as_str()) else { + continue; + }; + + let compartment_id = compartment.id().get(); + + if assignment_rule_variables.contains(&compartment_id) { + let message = format!("The with id '{compartment_id}' found as the [variable] of an ."); + let ci = XmlElement::new_raw(self.document(), ci); + issues.push(SbmlIssue::new_error("10225", &ci, message)); + } else if !compartment.constant().get() && algebraic_ci_values.contains(&compartment_id) + { + let message = format!("The 's size with id '{compartment_id}' is possible to determine by an ."); + let ci = XmlElement::new_raw(self.document(), ci); + issues.push(SbmlIssue::new_error("10225", &ci, message)); } } } diff --git a/src/lib.rs b/src/lib.rs index 018b8c5..b4bfb7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ use xml::{OptionalChild, RequiredProperty}; use crate::constants::namespaces::URL_SBML_CORE; use crate::core::Model; -use crate::xml::{OptionalXmlChild, XmlDocument, XmlElement}; +use crate::xml::{OptionalXmlChild, XmlDocument, XmlElement, XmlWrapper}; /// A module with useful types that are not directly part of the SBML specification, but help /// us work with XML documents in a sane and safe way. In particular: @@ -149,6 +149,19 @@ pub struct SbmlIssue { pub message: String, } +impl SbmlIssue { + /// A helper method to more easily create an [SbmlIssue] with [SbmlIssueSeverity::Error] + /// severity. + pub fn new_error(rule: &str, element: &E, message: S) -> SbmlIssue { + SbmlIssue { + element: element.raw_element(), + severity: SbmlIssueSeverity::Error, + rule: rule.to_string(), + message: message.to_string(), + } + } +} + #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] pub enum SbmlIssueSeverity { /// An issue that makes the document impossible to read correctly (e.g. a function is From c9e6f218921b3bba4ba0a1f814916d07d5463aa3 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Sat, 24 Feb 2024 13:14:22 -0800 Subject: [PATCH 55/57] Additional refactoring of utility methods. --- src/core/function_definition.rs | 4 +- src/core/model.rs | 11 +- src/core/reaction.rs | 4 +- src/core/sbase.rs | 6 +- src/core/validation/math.rs | 395 +++++++++++++------------------- src/xml/xml_wrapper.rs | 68 +++++- 6 files changed, 238 insertions(+), 250 deletions(-) diff --git a/src/core/function_definition.rs b/src/core/function_definition.rs index 8097527..1e88c1e 100644 --- a/src/core/function_definition.rs +++ b/src/core/function_definition.rs @@ -12,8 +12,8 @@ impl FunctionDefinition { /// /// The child can be any SBML tag, as long as one of its transitive parents is a /// [FunctionDefinition] element. If this is not satisfied, the method returns `None`. - pub fn for_child_element(doc: XmlDocument, child: &XmlElement) -> Option { - Self::search_in_parents(doc, child, "functionDefinition") + pub fn for_child_element(child: &XmlElement) -> Option { + Self::search_in_parents(child, "functionDefinition") } pub fn math(&self) -> OptionalChild { diff --git a/src/core/model.rs b/src/core/model.rs index e760cbb..4e736f5 100644 --- a/src/core/model.rs +++ b/src/core/model.rs @@ -30,8 +30,8 @@ impl Model { /// The child can be any SBML tag, as long as it appears in an SBML model (i.e. one of /// its transitive parents is a [Model] element). If this is not satisfied, the method /// returns `None`. - pub fn for_child_element(doc: XmlDocument, child: &XmlElement) -> Option { - Self::search_in_parents(doc, child, "model") + pub fn for_child_element(child: &XmlElement) -> Option { + Self::search_in_parents(child, "model") } pub fn function_definitions(&self) -> OptionalChild> { @@ -109,7 +109,7 @@ impl Model { // We then return the number of `bvar` child nodes in the lambda element. let count = lambda - .child_elements_filtered(|it| it.name(doc.deref()) == "bvar") + .child_elements_filtered(|it| it.tag_name() == "bvar") .len(); Some(count) @@ -226,7 +226,6 @@ impl Model { /// objects in this model. pub(crate) fn algebraic_rule_ci_values(&self) -> Vec { if let Some(rules) = self.rules().get() { - let doc = self.read_doc(); rules .iter() .filter_map(|rule| rule.try_downcast::()) @@ -234,8 +233,8 @@ impl Model { .flat_map(|math| { math.recursive_child_elements() .into_iter() - .filter(|child| child.name(doc.deref()) == "ci") - .map(|ci| ci.text_content(doc.deref())) + .filter(|child| child.tag_name() == "ci") + .map(|ci| ci.text_content()) .collect::>() }) .collect::>() diff --git a/src/core/reaction.rs b/src/core/reaction.rs index 622da7a..771b4b2 100644 --- a/src/core/reaction.rs +++ b/src/core/reaction.rs @@ -102,8 +102,8 @@ impl KineticLaw { /// /// The child can be any SBML tag, as long as one of its transitive parents is a /// [KineticLaw] element. If this is not satisfied, the method returns `None`. - pub fn for_child_element(doc: XmlDocument, child: &XmlElement) -> Option { - Self::search_in_parents(doc, child, "kineticLaw") + pub fn for_child_element(child: &XmlElement) -> Option { + Self::search_in_parents(child, "kineticLaw") } pub fn math(&self) -> OptionalChild { diff --git a/src/core/sbase.rs b/src/core/sbase.rs index df8a356..3a53377 100644 --- a/src/core/sbase.rs +++ b/src/core/sbase.rs @@ -48,9 +48,9 @@ pub(crate) trait SbmlUtils: SBase { /// returns `None`. /// /// TODO: Currently, this requires SBML core namespace. - fn search_in_parents(doc: XmlDocument, child: &XmlElement, tag_name: &str) -> Option { + fn search_in_parents(child: &XmlElement, tag_name: &str) -> Option { let parent = { - let read_doc = doc.read().unwrap(); + let read_doc = child.read_doc(); fn check_name(doc: &Document, e: Element, tag_name: &str) -> bool { let name = e.name(doc); let Some(namespace) = e.namespace(doc) else { @@ -69,7 +69,7 @@ pub(crate) trait SbmlUtils: SBase { } parent }; - let model = XmlElement::new_raw(doc, parent); + let model = XmlElement::new_raw(child.document(), parent); // Safe because we checked that the element has the correct tag name and namespace. Some(unsafe { Self::unchecked_cast(model) }) } diff --git a/src/core/validation/math.rs b/src/core/validation/math.rs index c371bb6..c0fd3e0 100644 --- a/src/core/validation/math.rs +++ b/src/core/validation/math.rs @@ -1,6 +1,4 @@ -use std::ops::Deref; use std::str::FromStr; -use xml_doc::{Document, Element}; use crate::constants::element::{ MATHML_ALLOWED_CHILDREN_BY_ATTR, MATHML_ALLOWED_DEFINITION_URLS, MATHML_ALLOWED_TYPES, @@ -22,8 +20,8 @@ impl Math { /// - **[10206](Math::apply_rule_10206)** - Ensures *type* attribute correct placement. /// - **[10207](Math::apply_rule_10207)** - Ensures *type* attribute correct value. /// - **[10208](Math::apply_rule_10208)** - Validates *lambda* element usage. - /// - **[10214](Math::apply_rule_10214)** - Validates first *ci* element usage outside of [FunctionDefinition]. - /// - **[10215](Math::apply_rule_10215)** - Validates non-first *ci* element usage outside of [FunctionDefinition]. + /// - **[10214](Math::apply_rule_10214)** - Validates first *ci* element usage outside [FunctionDefinition]. + /// - **[10215](Math::apply_rule_10215)** - Validates non-first *ci* element usage outside [FunctionDefinition]. /// - **[10216](Math::apply_rule_10216)** - Validates [LocalParameter](crate::core::LocalParameter) *id* occurrence. /// - **[10218](Math::apply_rule_10218)** - Validates number of arguments for operators. /// - **[10219](Math::apply_rule_10219)** - Validates number of arguments for [FunctionDefinition]. @@ -36,9 +34,10 @@ impl Math { /// ### Ignored rules as of SBML Level 3 Version 1 Core: /// - **10209** - "The arguments of the MathML logical operators and, not, or, and xor must evaluate to Boolean values." /// - **10210** - "The arguments to the following MathML constructs must evaluate to numeric values (more specifically, they - /// must evaluate to MathML real, integer, rational, or "e-notation" numbers, or the time, delay, avogadro, csymbol elements): abs, - /// arccosh, arccos, arccoth, arccot, arccsch, arccsc, arcsech, arcsec, arcsinh, arcsin, arctanh, arctan, ceiling, cosh, cos, coth, - /// cot, csch, csc, divide, exp, factorial, floor, ln, log, minus, plus, power, root, sech, sec, sinh, sin, tanh, tan, and times." + /// must evaluate to MathML real, integer, rational, or "e-notation" numbers, or the time, delay, avogadro, csymbol elements): `abs`, + /// `arccosh`, `arccos`, `arccoth`, `arccot`, `arccsch`, `arccsc`, `arcsech`, `arcsec`, `arcsinh`, `arcsin`, `arctanh`, `arctan`, `ceiling`, + /// `cosh`, `cos`, `coth`, `cot`, `csch`, `csc`, `divide`, `exp`, `factorial`, `floor`, `ln`, `log`, `minus`, `plus`, `power`, `root`, + /// `sech`, `sec`, `sinh`, `sin`, `tanh`, `tan`, and `times`." /// - **10211** - "The values of all arguments to MathML eq and neq operators must evaluate to the same type, either all /// Boolean or all numeric." /// - **10212** - "The types of the values within MathML piecewise operators should all be consistent; i.e., the set of expressions @@ -70,42 +69,34 @@ impl Math { } /// ### Rule 10201 - /// is *partially* satisfied by the implementation of the rule + /// This rule is *partially* satisfied by the implementation of the rule /// [10102](crate::core::validation::apply_rule_10102) as we check each /// element present for its allowed children (except [Math] element that is /// the subject of this validation procedure) and thus **MathML** content /// can be present only within a [Math] element. However, additional check for /// explicit or implicit valid namespace of a [Math] element must be performed. fn apply_rule_10201(&self, issues: &mut Vec) { - if self.namespace_url() != URL_MATHML { - let message = format!( - "Wrong namespace usage in a `math` element. Found `{}`, but `{}` should be used.", - self.namespace_url(), - URL_MATHML - ); + let namespace = self.namespace_url(); + if namespace != URL_MATHML { + let message = format!("Wrong namespace usage in a `math` element. Found `{namespace}`, but `{URL_MATHML}` should be used."); issues.push(SbmlIssue::new_error("10201", self, message)); } } // TODO: Complete implementation when adding extensions/packages is solved /// ### Rule 10202 - /// Validates that only allowed subset of **MathML** child elements are present within [Math] - /// element. An SBML package may allow new MathML elements to be added to this list, and if so, - /// the package must define **required="true"** on the SBML container element + /// Validates that only the allowed subset of **MathML** child elements are present within + /// a [Math] element. An SBML package may allow new MathML elements to be added to this list, + /// and if so, the package must define **required="true"** on the SBML container element /// [**sbml**](crate::Sbml). pub(crate) fn apply_rule_10202(&self, issues: &mut Vec) { - let doc = self.read_doc(); let allowed_children = get_allowed_children(self.xml_element()); for child in self.recursive_child_elements() { - let child_tag_name = child.name(doc.deref()); - - if !allowed_children.contains(&child_tag_name) { - let message = format!( - "Unknown child <{0}> of element <{1}>.", - child_tag_name, "math" - ); - issues.push(SbmlIssue::new_error("10202", self, message)); + let child_tag_name = child.tag_name(); + if !allowed_children.contains(&child_tag_name.as_str()) { + let message = format!("Unknown child <{child_tag_name}> of element ."); + issues.push(SbmlIssue::new_error("10202", &child, message)); } } } @@ -118,22 +109,18 @@ impl Math { /// elements, and if so, the package must define **required="true"** on the SBML container /// element [**sbml**](crate::Sbml). pub(crate) fn apply_rule_10203(&self, issues: &mut Vec) { - let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["encoding"]; - let relevant_children = self.recursive_child_elements_filtered(|it| { - it.attribute(doc.deref(), "encoding").is_some() - }); + let relevant_children = + self.recursive_child_elements_filtered(|it| it.has_attribute("encoding")); for child in relevant_children { - let name = child.name(doc.deref()); - - if !allowed.contains(&name) { + let name = child.tag_name(); + if !allowed.contains(&name.as_str()) { let message = format!( - "Attribute [encoding] found on element <{0}>, which is forbidden. \ - Attribute [encoding] is only permitted on , and .", - name + "Attribute [encoding] found on element <{name}>, which is forbidden. \ + Attribute [encoding] is only permitted on , and ." ); - issues.push(SbmlIssue::new_error("10203", self, message)); + issues.push(SbmlIssue::new_error("10203", &child, message)); } } } @@ -146,23 +133,18 @@ impl Math { /// elements, and if so, the package must define **required="true"** on the SBML container /// element [**sbml**](crate::Sbml). pub(crate) fn apply_rule_10204(&self, issues: &mut Vec) { - let doc = self.read_doc(); let allowed = MATHML_ALLOWED_CHILDREN_BY_ATTR["definitionURL"]; - let relevant_children = self.recursive_child_elements_filtered(|it| { - it.attribute(doc.deref(), "definitionURL").is_some() - }); + let relevant_children = + self.recursive_child_elements_filtered(|it| it.has_attribute("definitionURL")); for child in relevant_children { - let name = child.name(doc.deref()); - - if !allowed.contains(&name) { + let name = child.tag_name(); + if !allowed.contains(&name.as_str()) { let message = format!( - "Attribute [definitionURL] found on element <{0}>, which is forbidden. \ - Attribute [definitionURL] is only permitted on , and .", - name + "Attribute [definitionURL] found on element <{name}>, which is forbidden. \ + Attribute [definitionURL] is only permitted on , and ." ); - - issues.push(SbmlIssue::new_error("10204", self, message)); + issues.push(SbmlIssue::new_error("10204", &child, message)); } } } @@ -177,20 +159,19 @@ impl Math { /// definitionURL attribute of a csymbol, and if so, the package must define **required="true"** /// on the SBML container element [**sbml**](crate::Sbml). pub(crate) fn apply_rule_10205(&self, issues: &mut Vec) { - let doc = self.read_doc(); let children_of_interest = self.recursive_child_elements_filtered(|child| { - child.attribute(doc.deref(), "definitionURL").is_some() - && child.name(doc.deref()) == "csymbol" + child.tag_name() == "csymbol" && child.has_attribute("definitionURL") }); for child in children_of_interest { - let value = child.attribute(doc.deref(), "definitionURL").unwrap(); - if !MATHML_ALLOWED_DEFINITION_URLS.contains(&value) { + // Unwrap is safe, because we only consider children where the attribute is set. + let value = child.get_attribute("definitionURL").unwrap(); + if !MATHML_ALLOWED_DEFINITION_URLS.contains(&value.as_str()) { let message = format!( "Invalid definitionURL value found '{}'. Permitted values are: {:?}", value, MATHML_ALLOWED_DEFINITION_URLS ); - issues.push(SbmlIssue::new_error("10205", self, message)); + issues.push(SbmlIssue::new_error("10205", &child, message)); } } } @@ -203,21 +184,17 @@ impl Math { /// the type attribute on other elements, and if so, the package must define **required="true"** /// on the SBML container element [**sbml**](crate::Sbml). pub(crate) fn apply_rule_10206(&self, issues: &mut Vec) { - let doc = self.read_doc(); - let children_of_interest = self.recursive_child_elements_filtered(|child| { - child.attribute(doc.deref(), "type").is_some() - }); + let children_of_interest = + self.recursive_child_elements_filtered(|child| child.has_attribute("type")); for child in children_of_interest { - let name = child.name(doc.deref()); - - if !MATHML_ALLOWED_CHILDREN_BY_ATTR["type"].contains(&name) { + let name = child.tag_name(); + if !MATHML_ALLOWED_CHILDREN_BY_ATTR["type"].contains(&name.as_str()) { let message = format!( - "Attribute [type] found on element <{0}>, which is forbidden. \ - Attribute [type] is only permitted on .", - name + "Attribute [type] found on element <{name}>, which is forbidden. \ + Attribute [type] is only permitted on ." ); - issues.push(SbmlIssue::new_error("10204", self, message)); + issues.push(SbmlIssue::new_error("10204", &child, message)); } } } @@ -229,21 +206,18 @@ impl Math { /// allow new values for the type attribute, and if so, the package must define /// **required="true"** on the SBML container element [**sbml**](crate::Sbml). pub(crate) fn apply_rule_10207(&self, issues: &mut Vec) { - let doc = self.read_doc(); - let children_of_interest = self.recursive_child_elements_filtered(|child| { - child.attribute(doc.deref(), "type").is_some() - }); + let children_of_interest = + self.recursive_child_elements_filtered(|child| child.has_attribute("type")); for child in children_of_interest { - let value = child.attribute(doc.deref(), "type").unwrap(); + let value = child.get_attribute("type").unwrap(); - if !MATHML_ALLOWED_TYPES.contains(&value) { + if !MATHML_ALLOWED_TYPES.contains(&value.as_str()) { let message = format!( - "Invalid type value found '{0}'. Permitted values are: \ - 'e-notation', 'real', 'integer' and 'rational'", - value + "Invalid type value found '{value}'. Permitted values are: \ + 'e-notation', 'real', 'integer' and 'rational'" ); - issues.push(SbmlIssue::new_error("10206", self, message)); + issues.push(SbmlIssue::new_error("10206", &child, message)); } } } @@ -258,28 +232,31 @@ impl Math { /// elements on other elements, and if so, the package must define **required="true"** on the /// SBML container element [**sbml**](crate::Sbml). pub(crate) fn apply_rule_10208(&self, issues: &mut Vec) { - let doc = self.read_doc(); let children_of_interest = - self.recursive_child_elements_filtered(|child| child.name(doc.deref()) == "lambda"); + self.recursive_child_elements_filtered(|child| child.tag_name() == "lambda"); for child in children_of_interest { - let parent = child.parent(doc.deref()).unwrap(); - let parent_name = parent.name(doc.deref()); + // The parent must exist, because these are children of this math element. + // Furthermore, we also assume that if the parent is `math`, then it must have + // a parent, and if it is `semantics`, then its parent must have a parent as well. + // This should be a reasonable assumption for any SBML document that is valid-enough + // to get to this point. + let parent = child.parent().unwrap(); + let parent_name = parent.tag_name(); if parent_name == "math" { - let grandparent = parent.parent(doc.deref()).unwrap(); - Self::validate_lambda_placement(doc.deref(), child, parent, grandparent, issues); + let grandparent = parent.parent().unwrap(); + Self::validate_lambda_placement(child, parent, grandparent, issues); } else if parent_name == "semantics" { - let grandparent = parent.parent(doc.deref()).unwrap(); - let top_parent = grandparent.parent(doc.deref()).unwrap(); - Self::validate_lambda_placement(doc.deref(), child, parent, top_parent, issues); + let grandparent = parent.parent().unwrap(); + let top_parent = grandparent.parent().unwrap(); + Self::validate_lambda_placement(child, parent, top_parent, issues); } else { let message = format!( "Invalid immediate parent of . Only and are \ - valid immediate parents. Actual parent: <{0}>", - parent_name + valid immediate parents. Actual parent: <{parent_name}>" ); - issues.push(SbmlIssue::new_error("10208", self, message)); + issues.push(SbmlIssue::new_error("10208", &child, message)); } } } @@ -288,80 +265,67 @@ impl Math { /// 1. top-level parent of **lambda** is a [**FunctionDefinition**](FunctionDefinition). /// 2. **lambda** is the first child of its immediate parent fn validate_lambda_placement( - doc: &Document, - child: Element, - parent: Element, - toplevel_parent: Element, + child: XmlElement, + parent: XmlElement, + toplevel_parent: XmlElement, issues: &mut Vec, ) { - if toplevel_parent.name(doc) != "functionDefinition" { + let toplevel_parent = toplevel_parent.tag_name(); + if toplevel_parent != "functionDefinition" { // the (great)grandparent of must be - issues.push(SbmlIssue { - element: child, - message: format!( - "A found in invalid scope of <{0}>. \ - The can be located only within (in ).", - toplevel_parent.name(doc) - ), - rule: "10208".to_string(), - severity: SbmlIssueSeverity::Error, - }); - } else if *parent.child_elements(doc).first().unwrap() != child { + let message = format!( + "A found in invalid scope of <{toplevel_parent}>. \ + The can be located only within (in )." + ); + issues.push(SbmlIssue::new_error("10208", &child, message)); + return; + } + + let is_first_child = parent + .get_child_at(0) + .map(|it| it.raw_element() == child.raw_element()) + .unwrap_or(false); + + if !is_first_child { // the must be the first child inside (or ) - issues.push(SbmlIssue { - element: child, - message: "The must be the first element within .".to_string(), - rule: "10208".to_string(), - severity: SbmlIssueSeverity::Error, - }) + let message = "The must be the first element within .".to_string(); + issues.push(SbmlIssue::new_error("10208", &child, message)); } } /// ### Rule 10214 - /// Outside a [FunctionDefinition] object, if a MathML - /// **ci** element is the first element within a MathML apply element, then the **ci** element's - /// value can only be chosen from the set of identifiers of - /// [FunctionDefinition] objects defined in the enclosing + /// Outside a [FunctionDefinition] object, if a MathML **ci** element is the first element + /// within a MathML apply element, then the **ci** element's value can only be chosen from + /// the set of identifiers of [FunctionDefinition] objects defined in the enclosing /// SBML [Model] object. pub(crate) fn apply_rule_10214(&self, issues: &mut Vec) { - let doc = self.read_doc(); - let parent_name = self - .raw_element() - .parent(doc.deref()) - .unwrap() - .name(doc.deref()); + let parent_name = self.parent().unwrap().tag_name(); if parent_name != "functionDefinition" { let children_of_interest = self.recursive_child_elements_filtered(|child| { - let is_apply = child.name(doc.deref()) == "apply"; - let ci_first = child - .child_elements(doc.deref()) - .first() - .map(|it| it.name(doc.deref()) == "ci") - .unwrap_or(false); - is_apply && ci_first + child.tag_name() == "apply" && { + child + .get_child_at(0) + .map(|it| it.tag_name() == "ci") + .unwrap_or(false) + } }); - let identifiers = Model::for_child_element(self.document(), self.xml_element()) + let identifiers = Model::for_child_element(self.xml_element()) .unwrap() .function_definition_identifiers(); for child in children_of_interest { // This unwrap must succeed because we enforced that ci is the first child. - let value = child - .child_elements(doc.deref()) - .first() - .unwrap() - .text_content(doc.deref()); + let value = child.get_child_at(0).unwrap().text_content(); if !identifiers.contains(&value) { let message = format!( - "Function '{0}' not defined. \ + "Function '{value}' not defined. \ Function referred by must be defined in object \ - with relevant identifier (id).", - value + with relevant identifier (id)." ); - issues.push(SbmlIssue::new_error("10214", self, message)); + issues.push(SbmlIssue::new_error("10214", &child, message)); } } } @@ -380,14 +344,13 @@ impl Math { /// object class defined by an SBML Level 3 package as having mathematical meaning. pub(crate) fn apply_rule_10215(&self, issues: &mut Vec) { let is_out_of_function_definition = - FunctionDefinition::for_child_element(self.document(), self.xml_element()).is_none(); + FunctionDefinition::for_child_element(self.xml_element()).is_none(); if !is_out_of_function_definition { return; } - let doc = self.read_doc(); - let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + let model = Model::for_child_element(self.xml_element()).unwrap(); let identifiers = [ model.species_identifiers(), model.compartment_identifiers(), @@ -399,24 +362,22 @@ impl Math { .concat(); let apply_elements = - self.recursive_child_elements_filtered(|child| child.name(doc.deref()) == "apply"); + self.recursive_child_elements_filtered(|child| child.tag_name() == "apply"); for apply in apply_elements { let ci_elements = apply - .child_elements(doc.deref()) + .child_elements() .into_iter() .skip(1) - .filter(|child| child.name(doc.deref()) == "ci") + .filter(|child| child.tag_name() == "ci") .collect::>(); for ci in ci_elements { - let value = ci.text_content(doc.deref()); + let value = ci.text_content(); if !identifiers.contains(&value) { - let ci = XmlElement::new_raw(self.document(), ci); let message = format!( - "Invalid identifier value '{0}' in . Identifier not found.", - value + "Invalid identifier value '{value}' in . Identifier not found." ); issues.push(SbmlIssue::new_error("10215", &ci, message)); } @@ -432,35 +393,29 @@ impl Math { /// may only be used in MathML ci elements or as the target of an SIdRef attribute if that package /// construct is a child of the parent [Reaction]. pub(crate) fn apply_rule_10216(&self, issues: &mut Vec) { - let doc = self.read_doc(); - let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + let model = Model::for_child_element(self.xml_element()).unwrap(); let all_local_param_ids = model.local_parameter_identifiers(); - let scoped_local_param_ids = - match KineticLaw::for_child_element(self.document(), self.xml_element()) { - Some(k) => k.local_parameter_identifiers(), - None => Vec::new(), - }; + + let scoped_local_param_ids = match KineticLaw::for_child_element(self.xml_element()) { + Some(k) => k.local_parameter_identifiers(), + None => Vec::new(), + }; + let b_variables = self .recursive_child_elements() .into_iter() - .filter(|child| child.name(doc.deref()) == "bvar") - .filter_map(|bvar| { - bvar.child_elements(doc.deref()) - .first() - .map(|it| it.text_content(doc.deref())) - }) + .filter(|child| child.tag_name() == "bvar") + .filter_map(|bvar| bvar.get_child_at(0).map(|it| it.text_content())) .collect::>(); - let ci_elements = - self.recursive_child_elements_filtered(|child| child.name(doc.deref()) == "ci"); + let ci_elements = self.recursive_child_elements_filtered(|child| child.tag_name() == "ci"); for ci in ci_elements { - let value = ci.text_content(doc.deref()); + let value = ci.text_content(); if !b_variables.contains(&value) && all_local_param_ids.contains(&value) && !scoped_local_param_ids.contains(&value) { - let ci = XmlElement::new_raw(self.document(), ci); let message = format!("A identifier '{value}' found out of scope of its "); issues.push(SbmlIssue::new_error("10216", &ci, message)); } @@ -470,23 +425,20 @@ impl Math { /// ### Rule 10218 /// A MathML operator must be supplied the number of arguments appropriate for that operator. pub(crate) fn apply_rule_10218(&self, issues: &mut Vec) { - let doc = self.read_doc(); let apply_elements = - self.recursive_child_elements_filtered(|child| child.name(doc.deref()) == "apply"); + self.recursive_child_elements_filtered(|child| child.tag_name() == "apply"); for apply in apply_elements { - let apply = XmlElement::new_raw(self.document(), apply); let children = apply.child_elements(); - let child_count = children.len(); - if child_count == 0 { + if children.is_empty() { let message = "No operator specified in .".to_string(); issues.push(SbmlIssue::new_error("10218", &apply, message)); continue; } - let arg_count = child_count - 1; - let operator = children[0].name(doc.deref()); + let arg_count = children.len() - 1; + let operator = children[0].tag_name(); - if MATHML_UNARY_OPERATORS.contains(&operator) { + if MATHML_UNARY_OPERATORS.contains(&operator.as_str()) { // is allowed to have 1 OR 2 arguments if operator == "minus" && arg_count != 1 && arg_count != 2 { let message = format!( @@ -500,7 +452,7 @@ impl Math { ); issues.push(SbmlIssue::new_error("10218", &apply, message)); } - } else if MATHML_BINARY_OPERATORS.contains(&operator) { + } else if MATHML_BINARY_OPERATORS.contains(&operator.as_str()) { // root is allowed to have 1 OR 2 arguments if operator == "root" && arg_count != 1 && arg_count != 2 { let message = format!( @@ -512,7 +464,7 @@ impl Math { let message = format!("Invalid number ({arg_count}) of arguments for binary operator <{operator}>."); issues.push(SbmlIssue::new_error("10218", &apply, message)); } - } else if MATHML_NARY_OPERATORS.contains(&operator) && arg_count == 0 { + } else if MATHML_NARY_OPERATORS.contains(&operator.as_str()) && arg_count == 0 { // TODO: // This is not correct? N-ary operators with zero arguments are only // discouraged if the meaning of the operator is not well defined. @@ -533,25 +485,24 @@ impl Math { /// the number of MathML **bvar** elements inside the **lambda** element of the function definition, if /// present. pub(crate) fn apply_rule_10219(&self, issues: &mut Vec) { - let doc = self.read_doc(); - let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + let model = Model::for_child_element(self.xml_element()).unwrap(); let apply_elements = - self.recursive_child_elements_filtered(|child| child.name(doc.deref()) == "apply"); + self.recursive_child_elements_filtered(|child| child.tag_name() == "apply"); for apply in apply_elements { - let children = apply.child_elements(doc.deref()); + let children = apply.child_elements(); let Some(function_call) = children.first() else { continue; }; - if function_call.name(doc.deref()) != "ci" { + if function_call.tag_name() != "ci" { continue; } let arg_count = children.len() - 1; let func_identifiers = model.function_definition_identifiers(); - let id = function_call.text_content(doc.deref()); + let id = function_call.text_content(); if func_identifiers.contains(&id) { let expected_args = model @@ -563,8 +514,7 @@ impl Math { "Invalid number of arguments ({arg_count}) provided for function '{id}'. \ The function '{id}' takes {expected_args} arguments." ); - let function_call = XmlElement::new_raw(self.document(), *function_call); - issues.push(SbmlIssue::new_error("10219", &function_call, message)); + issues.push(SbmlIssue::new_error("10219", function_call, message)); } } } @@ -577,20 +527,17 @@ impl Math { /// on other elements, and if so, the package must define **required="true"** on the SBML container /// element [**sbml**](crate::Sbml). pub(crate) fn apply_rule_10220(&self, issues: &mut Vec) { - let doc = self.read_doc(); - let children_of_interest: Vec = self.recursive_child_elements_filtered(|child| { - child.attribute(doc.deref(), "units").is_some() - }); + let children_of_interest = + self.recursive_child_elements_filtered(|child| child.has_attribute("units")); for child in children_of_interest { - let name = child.name(doc.deref()); + let name = child.tag_name(); - if !MATHML_ALLOWED_CHILDREN_BY_ATTR["units"].contains(&name) { + if !MATHML_ALLOWED_CHILDREN_BY_ATTR["units"].contains(&name.as_str()) { let message = format!( "Attribute [units] found on element <{name}>, which is forbidden. \ Attribute [units] is only permitted on ." ); - let child = XmlElement::new_raw(self.document(), child); issues.push(SbmlIssue::new_error("10220", &child, message)); } } @@ -600,24 +547,22 @@ impl Math { /// The value of the SBML attribute units on a MathML cn element must be chosen from either the /// set of identifiers of UnitDefinition objects in the model, or the set of base units defined by SBML. pub(crate) fn apply_rule_10221(&self, issues: &mut Vec) { - let doc = self.read_doc(); - let unit_identifiers = Model::for_child_element(self.document(), self.xml_element()) + let unit_identifiers = Model::for_child_element(self.xml_element()) .unwrap() .unit_definition_identifiers(); let cn_elements = self.recursive_child_elements_filtered(|child| { - child.name(doc.deref()) == "cn" && child.attribute(doc.deref(), "units").is_some() + child.tag_name() == "cn" && child.has_attribute("units") }); for cn in cn_elements { - let value = cn.attribute(doc.deref(), "units").unwrap(); + // We can unwrap because we selected only elements where `units` attribute is set. + let value = cn.get_attribute("units").unwrap(); - if !unit_identifiers.contains(&value.to_string()) && BaseUnit::from_str(value).is_err() - { + if !unit_identifiers.contains(&value) && BaseUnit::from_str(&value).is_err() { let message = format!( "Invalid unit identifier '{value}' found. \ Only identifiers of objects and base units can be used in ." ); - let cn = XmlElement::new_raw(self.document(), cn); issues.push(SbmlIssue::new_error("10221", &cn, message)); } } @@ -626,12 +571,11 @@ impl Math { /// ### Rule 10223 /// The single argument for the *rateOf* **csymbol** function must be a **ci** element. pub(crate) fn apply_rule_10223(&self, issues: &mut Vec) { - let doc = self.read_doc(); let children_of_interest = self.recursive_child_elements_filtered(|child| { - child.name(doc.deref()) == "apply" && { - if let Some(first_child) = child.child_elements(doc.deref()).first() { + child.tag_name() == "apply" && { + if let Some(first_child) = child.get_child_at(0) { first_child - .attribute(doc.deref(), "definitionURL") + .get_attribute("definitionURL") .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf") } else { false @@ -640,27 +584,24 @@ impl Math { }); for child in children_of_interest { - let apply_children = child.child_elements(doc.deref()); + let apply_children = child.child_elements(); if apply_children.len() != 2 { let message = format!( - "Invalid number ({0}) of arguments provided for rateOf . \ + "Invalid number ({}) of arguments provided for rateOf . \ The call of rateOf must have precisely one argument.", apply_children.len() - 1 ); - let child = XmlElement::new_raw(self.document(), child); issues.push(SbmlIssue::new_error("10223", &child, message)); } else { // This unwrap is ok because we only selected elements with at least one child. - let argument_name = apply_children.last().unwrap().name(doc.deref()); + let argument_name = apply_children.last().unwrap().tag_name(); if argument_name != "ci" { let message = format!( - "Invalid argument <{0}> provided for .\ - The rateOf must have as its only argument.", - argument_name + "Invalid argument <{argument_name}> provided for .\ + The rateOf must have as its only argument." ); - let child = XmlElement::new_raw(self.document(), child); issues.push(SbmlIssue::new_error("10223", &child, message)); } } @@ -672,20 +613,19 @@ impl Math { /// [AssignmentRule](crate::core::rule::AssignmentRule), nor may its value be determined by an /// [AlgebraicRule](crate::core::rule::AlgebraicRule). pub(crate) fn apply_rule_10224(&self, issues: &mut Vec) { - let doc = self.read_doc(); - let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + let model = Model::for_child_element(self.xml_element()).unwrap(); let ci_elements = self.recursive_child_elements_filtered(|child| { - child.name(doc.deref()) == "apply" && { - let children = child.child_elements(doc.deref()); + child.tag_name() == "apply" && { + let children = child.child_elements(); if children.len() < 2 { false } else { - let fst = children[0]; - let snd = children[1]; + let fst = &children[0]; + let snd = &children[1]; let is_rate_of = fst - .attribute(doc.deref(), "definitionURL") + .get_attribute("definitionURL") .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf"); - let is_ci = snd.name(doc.deref()) == "ci"; + let is_ci = snd.tag_name() == "ci"; is_ci && is_rate_of } } @@ -694,17 +634,15 @@ impl Math { let algebraic_rule_determinants = model.algebraic_rule_ci_values(); for ci in ci_elements { - let value = ci.text_content(doc.deref()); + let value = ci.text_content(); if assignment_rule_variables.contains(&value) { let message = format!("The value of target ('{value}') of rateOf found as a variable of ."); - let ci = XmlElement::new_raw(self.document(), ci); issues.push(SbmlIssue::new_error("10224", &ci, message)); // TODO: what does "determined by algebraicRule" mean and how to check it? // TODO: same as 10225 } else if algebraic_rule_determinants.contains(&value) { let message = format!("The value of target ('{value}') of rateOf determined by an ."); - let ci = XmlElement::new_raw(self.document(), ci); issues.push(SbmlIssue::new_error("10224", &ci, message)); } } @@ -716,29 +654,28 @@ impl Math { /// must not appear as the *variable* of an [AssignmentRule](crate::core::rule::AssignmentRule), /// nor may its *size* be determined by an [AlgebraicRule](crate::core::rule::AlgebraicRule). pub(crate) fn apply_rule_10225(&self, issues: &mut Vec) { - let doc = self.read_doc(); - let model = Model::for_child_element(self.document(), self.xml_element()).unwrap(); + let model = Model::for_child_element(self.xml_element()).unwrap(); let assignment_rule_variables = model.assignment_rule_variables(); let algebraic_ci_values = model.algebraic_rule_ci_values(); let ci_elements = self.recursive_child_elements_filtered(|child| { - child.name(doc.deref()) == "apply" && { - let children = child.child_elements(doc.deref()); + child.tag_name() == "apply" && { + let children = child.child_elements(); if children.len() < 2 { false } else { - let fst = children[0]; - let snd = children[1]; + let fst = &children[0]; + let snd = &children[1]; let is_rate_of = fst - .attribute(doc.deref(), "definitionURL") + .get_attribute("definitionURL") .is_some_and(|url| url == "http://www.sbml.org/sbml/symbols/rateOf"); - let is_ci = snd.name(doc.deref()) == "ci"; + let is_ci = snd.tag_name() == "ci"; is_ci && is_rate_of } } }); for ci in ci_elements { - let value = ci.text_content(doc.deref()); + let value = ci.text_content(); let Some(species) = model.find_species(value.as_str()) else { continue; @@ -757,12 +694,10 @@ impl Math { if assignment_rule_variables.contains(&compartment_id) { let message = format!("The with id '{compartment_id}' found as the [variable] of an ."); - let ci = XmlElement::new_raw(self.document(), ci); issues.push(SbmlIssue::new_error("10225", &ci, message)); } else if !compartment.constant().get() && algebraic_ci_values.contains(&compartment_id) { let message = format!("The 's size with id '{compartment_id}' is possible to determine by an ."); - let ci = XmlElement::new_raw(self.document(), ci); issues.push(SbmlIssue::new_error("10225", &ci, message)); } } diff --git a/src/xml/xml_wrapper.rs b/src/xml/xml_wrapper.rs index 589f592..034d427 100644 --- a/src/xml/xml_wrapper.rs +++ b/src/xml/xml_wrapper.rs @@ -115,42 +115,96 @@ pub trait XmlWrapper: Into { self.raw_element().attributes(doc.deref()).clone() } + /// Returns true if this [XmlWrapper] instance has an attribute of the given name. + fn has_attribute(&self, name: &str) -> bool { + let doc = self.read_doc(); + self.raw_element().attribute(doc.deref(), name).is_some() + } + + /// Return the raw value of the specified attribute, if it is defined. + fn get_attribute(&self, name: &str) -> Option { + let doc = self.read_doc(); + self.raw_element() + .attribute(doc.deref(), name) + .map(|it| it.to_string()) + } + + /// Return the text content of this element and all its children. + fn text_content(&self) -> String { + let doc = self.read_doc(); + self.raw_element().text_content(doc.deref()) + } + + /// Return the parent element of this [XmlWrapper] instance, if any. + fn parent(&self) -> Option { + let doc = self.read_doc(); + self.raw_element() + .parent(doc.deref()) + .map(|it| XmlElement::new_raw(self.document(), it)) + } + /// Returns the vector of children referenced within this [XmlWrapper] as a collection /// of [Element] objects. This method skips any child nodes that are not elements (such as /// text or comments). - fn child_elements(&self) -> Vec { + fn child_elements(&self) -> Vec { let doc = self.read_doc(); - self.raw_element().child_elements(doc.deref()) + self.raw_element() + .child_elements(doc.deref()) + .into_iter() + .map(|it| XmlElement::new_raw(self.document(), it)) + .collect() + } + + /// Get the `i-th` child element of this XML element. This operation ignores comments + /// or text content and only considers "true" child elements. + fn get_child_at(&self, index: usize) -> Option { + let doc = self.read_doc(); + self.raw_element() + .children(doc.deref()) + .iter() + .filter_map(|it| it.as_element()) + .skip(index) + .map(|it| XmlElement::new_raw(self.document(), it)) + .next() } /// Version of [Self::child_elements] with additional filtering function applied to the /// output vector. - fn child_elements_filtered bool>(&self, predicate: P) -> Vec { + fn child_elements_filtered bool>( + &self, + predicate: P, + ) -> Vec { let doc = self.read_doc(); self.raw_element() .child_elements(doc.deref()) .into_iter() + .map(|it| XmlElement::new_raw(self.document(), it)) .filter(predicate) .collect() } /// Version of [Self::child_elements] that recursively traverses all child nodes, not just /// the immediate descendants. - fn recursive_child_elements(&self) -> Vec { + fn recursive_child_elements(&self) -> Vec { let doc = self.read_doc(); - self.raw_element().child_elements_recursive(doc.deref()) + self.raw_element() + .child_elements_recursive(doc.deref()) + .into_iter() + .map(|it| XmlElement::new_raw(self.document(), it)) + .collect() } /// Version of [Self::recursive_child_elements] with additional filtering function applied /// to the output vector. - fn recursive_child_elements_filtered bool>( + fn recursive_child_elements_filtered bool>( &self, predicate: P, - ) -> Vec { + ) -> Vec { let doc = self.read_doc(); self.raw_element() .child_elements_recursive(doc.deref()) .into_iter() + .map(|it| XmlElement::new_raw(self.document(), it)) .filter(predicate) .collect() } From 15217a2932f1621deaa62dd3ee998c7b13feb32b Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Sat, 24 Feb 2024 13:32:45 -0800 Subject: [PATCH 56/57] Update `XmlList` to skip comments and text. --- src/core/validation/mod.rs | 34 ++++++++++----------- src/xml/xml_list.rs | 60 +++++++++++++++++++++++++------------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index cf558d3..58ffb24 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -55,14 +55,10 @@ impl Sbml { let doc = self.xml.read().unwrap(); if doc.container().child_elements(doc.deref()).len() != 1 { - issues.push(SbmlIssue { - element: doc.container(), - message: "The document contains multiple root nodes. \ - Only one root object is allowed." - .to_string(), - rule: "10102".to_string(), - severity: SbmlIssueSeverity::Error, - }) + let container = XmlElement::new_raw(self.xml.clone(), doc.container()); + let message = "The document contains multiple root nodes. \ + Only one root object is allowed."; + issues.push(SbmlIssue::new_error("10102", &container, message)); } if let Some(root_element) = doc.root_element() { @@ -111,17 +107,17 @@ pub(crate) fn sanity_check(xml_element: &XmlElement, issues: &mut Vec let attributes = xml_element.attributes(); let element_name = xml_element.tag_name(); - for req_attr in REQUIRED_ATTRIBUTES[element_name.as_str()] { - if !attributes.contains_key(&req_attr.to_string()) { - issues.push(SbmlIssue { - element: xml_element.raw_element(), - message: format!( - "Sanity check failed: missing required attribute [{0}] on <{1}>.", - req_attr, element_name - ), - rule: "SANITY_CHECK".to_string(), - severity: SbmlIssueSeverity::Error, - }); + if let Some(required) = REQUIRED_ATTRIBUTES.get(element_name.as_str()) { + for req_attr in required.iter() { + if !attributes.contains_key(&req_attr.to_string()) { + // TODO: + // These have their own SBML issue IDs assigned to them, and we should + // probably try to use them here as well. + let message = format!( + "Sanity check failed: missing required attribute [{req_attr}] on <{element_name}>." + ); + issues.push(SbmlIssue::new_error("SANITY_CHECK", xml_element, message)); + } } } diff --git a/src/xml/xml_list.rs b/src/xml/xml_list.rs index 0700409..f7ef86c 100644 --- a/src/xml/xml_list.rs +++ b/src/xml/xml_list.rs @@ -50,6 +50,23 @@ impl XmlWrapper for XmlList { } impl XmlList { + /// Map an "outside index" referencing a child element to an inside index, referencing + /// a proper XML node (i.e. accounting for text and comments). + /// + /// Returns `None` if the index does not exist. + fn remap_index(&self, mut outside_index: usize) -> Option { + let doc = self.read_doc(); + for (inside_index, child) in self.raw_element().children(doc.deref()).iter().enumerate() { + if child.as_element().is_some() { + if outside_index == 0 { + return Some(inside_index); + } + outside_index -= 1; + } + } + None + } + /// Get the element of this list at the position specified by `index`. /// /// # Panics @@ -70,18 +87,16 @@ impl XmlList { /// XML tag (e.g. text). pub fn get_checked(&self, index: usize) -> Option { let doc = self.read_doc(); - self.raw_element() - .children(doc.deref()) - .get(index) - .map(|it| { - let element = it.as_element().unwrap_or_else(|| { - panic!("Item at position {index} is not an XML element."); - }); - unsafe { - // TODO: This really is not safe at the moment. - Type::unchecked_cast(XmlElement::new_raw(self.document(), element)) - } - }) + self.remap_index(index).and_then(|index| { + self.raw_element() + .children(doc.deref()) + .get(index) + .map(|it| { + let it = it.as_element().unwrap(); // This is ok due to the remapped index. + // TODO: This really is not safe at the moment. + unsafe { Type::unchecked_cast(XmlElement::new_raw(self.document(), it)) } + }) + }) } /// Insert a new element into the list. The element must not belong to an existing parent @@ -92,6 +107,7 @@ impl XmlList { /// Panics if `index > len`, or when `value` cannot be attached to the list tag /// (it already has a parent, or is itself the root container tag). pub fn insert(&self, index: usize, value: Type) { + let index = self.remap_index(index).unwrap_or(self.len()); value.try_attach_at(self, Some(index)).unwrap(); } @@ -102,14 +118,18 @@ impl XmlList { /// Panics if `index >= len`, or if the XML node at the given position /// is not an element (for example text). pub fn remove(&self, index: usize) -> Type { + let Some(index) = self.remap_index(index) else { + panic!("Item at position {index} does not exist."); + }; + let mut doc = self.write_doc(); - let removed = self.raw_element().remove_child(doc.deref_mut(), index); - // Here, we assume `removed` is a proper Xml element (i.e. not text or - // other special element type). We also assume that it can be safely converted to `Type` - // which may not be always true. - let removed = removed.as_element().unwrap_or_else(|| { - panic!("Item at position {index} is not an XML element."); - }); + + let removed = self + .raw_element() + .remove_child(doc.deref_mut(), index) + .as_element() + .unwrap(); + unsafe { // TODO: This really is not safe at the moment. Type::unchecked_cast(XmlElement::new_raw(self.document(), removed)) @@ -119,7 +139,7 @@ impl XmlList { /// Insert a new element into the list at the last position similarly as in stack. /// /// # Panics - /// Panics if `value` cannot be attached to the list tag (it already has a parent, + /// Fails if `value` cannot be attached to the list tag (it already has a parent, /// or is itself the root container tag). pub fn push(&self, value: Type) { self.insert(self.len(), value) From dd8d65545ce313ca6c812a06c426f150eb80d3c7 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Sat, 24 Feb 2024 13:59:26 -0800 Subject: [PATCH 57/57] A few more cosmetic tweaks. --- src/core/validation/mod.rs | 17 ++++++----------- src/core/validation/reaction.rs | 11 +++-------- src/core/validation/rule.rs | 19 +++++++------------ src/core/validation/unit_definition.rs | 13 ++++--------- 4 files changed, 20 insertions(+), 40 deletions(-) diff --git a/src/core/validation/mod.rs b/src/core/validation/mod.rs index 58ffb24..d83c948 100644 --- a/src/core/validation/mod.rs +++ b/src/core/validation/mod.rs @@ -171,7 +171,7 @@ pub(crate) fn sanity_check_of_list( ) { sanity_check(xml_list.xml_element(), issues); - for object in xml_list.as_vec() { + for object in xml_list.iter() { object.sanity_check(issues); } } @@ -307,16 +307,11 @@ pub(crate) fn apply_rule_10301( ) { if let Some(id) = id { if identifiers.contains(&id) { - issues.push(SbmlIssue { - element: xml_element.raw_element(), - message: format!( - "The identifier ('{0}') of <{1}> is already present in the .", - id, - xml_element.tag_name() - ), - rule: "10301".to_string(), - severity: SbmlIssueSeverity::Error, - }) + let tag_name = xml_element.tag_name(); + let message = format!( + "The identifier ('{id}') of <{tag_name}> is already present in the ." + ); + issues.push(SbmlIssue::new_error("10301", xml_element, message)); } else { identifiers.insert(id); } diff --git a/src/core/validation/reaction.rs b/src/core/validation/reaction.rs index 6bae94e..ae84e32 100644 --- a/src/core/validation/reaction.rs +++ b/src/core/validation/reaction.rs @@ -6,7 +6,7 @@ use crate::core::{ KineticLaw, LocalParameter, ModifierSpeciesReference, Reaction, SBase, SpeciesReference, }; use crate::xml::{OptionalXmlChild, OptionalXmlProperty, RequiredXmlProperty, XmlList, XmlWrapper}; -use crate::{SbmlIssue, SbmlIssueSeverity}; +use crate::SbmlIssue; use std::collections::HashSet; impl SbmlValidable for Reaction { @@ -111,13 +111,8 @@ impl KineticLaw { for local_parameter in list_of_local_parameters.as_vec() { let id = local_parameter.id().get(); if identifiers.contains(&id) { - issues.push(SbmlIssue { - element: local_parameter.raw_element(), - message: format!("The identifier ('{0}') of is already present in the .", - id), - rule: "10303".to_string(), - severity: SbmlIssueSeverity::Error, - }) + let message = format!("The identifier ('{id}') of is already present in the ."); + issues.push(SbmlIssue::new_error("10303", &local_parameter, message)); } else { identifiers.insert(id); } diff --git a/src/core/validation/rule.rs b/src/core/validation/rule.rs index 31ccf51..0688a12 100644 --- a/src/core/validation/rule.rs +++ b/src/core/validation/rule.rs @@ -1,7 +1,7 @@ use crate::core::validation::{apply_rule_10102, apply_rule_10301, SanityCheckable, SbmlValidable}; use crate::core::{AbstractRule, Rule, RuleTypes, SBase}; use crate::xml::{OptionalXmlChild, OptionalXmlProperty, RequiredXmlProperty, XmlList, XmlWrapper}; -use crate::{SbmlIssue, SbmlIssueSeverity}; +use crate::SbmlIssue; use std::collections::HashSet; impl SbmlValidable for AbstractRule { @@ -38,17 +38,12 @@ impl AbstractRule { }; if variables.contains(&variable) { - issues.push(SbmlIssue { - element: rule.raw_element(), - message: format!( - "The variable ('{0}') of <{1}> is already present in the set of \ - and objects.", - variable, - rule.tag_name() - ), - rule: "10304".to_string(), - severity: SbmlIssueSeverity::Error, - }) + let tag_name = rule.tag_name(); + let message = format!( + "The variable ('{variable}') of <{tag_name}> is already present in the set of \ + and objects." + ); + issues.push(SbmlIssue::new_error("10304", &rule, message)); } else { variables.insert(variable); } diff --git a/src/core/validation/unit_definition.rs b/src/core/validation/unit_definition.rs index ba2e894..c963878 100644 --- a/src/core/validation/unit_definition.rs +++ b/src/core/validation/unit_definition.rs @@ -4,7 +4,7 @@ use crate::core::validation::{ }; use crate::core::{SBase, UnitDefinition}; use crate::xml::{OptionalXmlChild, OptionalXmlProperty, XmlList, XmlWrapper}; -use crate::{SbmlIssue, SbmlIssueSeverity}; +use crate::SbmlIssue; use std::collections::HashSet; impl SbmlValidable for UnitDefinition { @@ -34,19 +34,14 @@ impl UnitDefinition { ) { let mut identifiers: HashSet = HashSet::new(); - for unit_definition in list_of_unit_definitions.as_vec() { + for unit_definition in list_of_unit_definitions.iter() { let Some(id) = unit_definition.id().get() else { continue; }; if identifiers.contains(&id) { - issues.push(SbmlIssue { - element: unit_definition.raw_element(), - message: format!("The identifier ('{0}') of is already present in the .", - id), - rule: "10302".to_string(), - severity: SbmlIssueSeverity::Error, - }) + let message = format!("The identifier ('{id}') of is already present in the ."); + issues.push(SbmlIssue::new_error("10302", &unit_definition, message)); } else { identifiers.insert(id); }