From 105b48852eacfc41a77b6ad00c00db6657ead432 Mon Sep 17 00:00:00 2001 From: Dariusz Depta <141360751+DariuszDepta@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:43:16 +0100 Subject: [PATCH 1/3] Updates. --- Cargo.lock | 5 + common/src/href.rs | 2 +- dsntk/src/actions.rs | 8 +- examples/src/compatibility/level_2/2_0001.dmn | 85 +++--- .../src/compatibility/level_2/2_0001.idml | 80 +++++ examples/src/compatibility/level_2/mod.rs | 1 + gendoc/src/tests/ascii_model.rs | 6 +- gendoc/src/tests/mod.rs | 2 +- model-evaluator/benches/compatibility/mod.rs | 8 +- model-evaluator/src/input_data.rs | 2 +- model-evaluator/src/input_data_context.rs | 2 +- model-evaluator/src/item_definition.rs | 2 +- .../src/item_definition_context.rs | 2 +- model-evaluator/src/item_definition_type.rs | 2 +- .../compatibility/non_compliant/dmn_n_0088.rs | 2 +- model-evaluator/src/tests/mod.rs | 11 +- model/Cargo.toml | 2 + model/src/errors.rs | 11 + model/src/idml_parser.rs | 283 ++++++++++++++++++ model/src/idml_utils.rs | 100 +++++++ model/src/lib.rs | 5 +- model/src/model.rs | 60 ++-- model/src/parser.rs | 2 +- model/src/tests/parser/full_model.rs | 6 +- model/src/tests/parser/invalid_models.rs | 34 +-- .../model_validator/item_definition_cycles.rs | 6 +- model/src/xml_utils.rs | 2 +- .../compatibility/level_2/idml_2_0001.rs | 9 + model/tests/compatibility/level_2/mod.rs | 1 + model/tests/compatibility/mod.rs | 1 + model/tests/mod.rs | 1 + workspace/src/builder.rs | 2 +- 32 files changed, 619 insertions(+), 126 deletions(-) create mode 100644 examples/src/compatibility/level_2/2_0001.idml create mode 100644 model/src/idml_parser.rs create mode 100644 model/src/idml_utils.rs create mode 100644 model/tests/compatibility/level_2/idml_2_0001.rs create mode 100644 model/tests/compatibility/level_2/mod.rs create mode 100644 model/tests/compatibility/mod.rs create mode 100644 model/tests/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 89e131e6..760d3590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -764,6 +764,7 @@ dependencies = [ "dsntk-feel-parser", "dsntk-macros", "dsntk-recognizer", + "idml", "petgraph", "roxmltree", ] @@ -1249,6 +1250,10 @@ dependencies = [ "syn", ] +[[package]] +name = "idml" +version = "0.0.1" + [[package]] name = "idna" version = "1.0.3" diff --git a/common/src/href.rs b/common/src/href.rs index 05ae06fa..a1028d85 100644 --- a/common/src/href.rs +++ b/common/src/href.rs @@ -10,7 +10,7 @@ use crate::DsntkError; use uriparse::URIReference; /// URI reference used for utilizing `href` attribute. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct HRef { /// Namespace built from URI's path components. namespace: Option, diff --git a/dsntk/src/actions.rs b/dsntk/src/actions.rs index 2e0772e7..a31a5ab4 100644 --- a/dsntk/src/actions.rs +++ b/dsntk/src/actions.rs @@ -997,7 +997,7 @@ fn export_decision_table(dectab_file_name: &str, html_file_name: &str, dectab_fi /// Parses DMN model loaded from XML file and prints ASCII report. fn parse_dmn_model(dmn_file_name: &str, cm: ColorMode) { match fs::read_to_string(dmn_file_name) { - Ok(dmn_file_content) => match dsntk_model::parse(&dmn_file_content) { + Ok(dmn_file_content) => match dsntk_model::from_xml(&dmn_file_content) { Ok(definitions) => { dsntk_gendoc::print_model(definitions, cm); } @@ -1012,7 +1012,7 @@ fn evaluate_dmn_model(input_file_name: &str, dmn_file_name: &str, invocable_name match fs::read_to_string(dmn_file_name) { Ok(dmn_file_content) => match fs::read_to_string(input_file_name) { Ok(input_file_content) => match dsntk_evaluator::evaluate_context(&FeelScope::default(), &input_file_content) { - Ok(input_data) => match dsntk_model::parse(&dmn_file_content) { + Ok(input_data) => match dsntk_model::from_xml(&dmn_file_content) { Ok(definitions) => { let model_namespace = definitions.namespace().to_string(); let model_name = definitions.name().to_string(); @@ -1043,7 +1043,7 @@ fn test_dmn_model(test_file_name: &str, dmn_file_name: &str, invocable_name: &st return; } }; - let definitions = match dsntk_model::parse(&dmn_file_content) { + let definitions = match dsntk_model::from_xml(&dmn_file_content) { Ok(definitions) => definitions, Err(reason) => { eprintln!("parsing model file failed with reason: {reason}"); @@ -1085,7 +1085,7 @@ fn test_dmn_model(test_file_name: &str, dmn_file_name: &str, invocable_name: &st /// Exports DMN model loaded from `XML` file to `HTML` output file. fn export_dmn_model(dmn_file_name: &str, html_file_name: &str) { match fs::read_to_string(dmn_file_name) { - Ok(dmn_file_content) => match dsntk_model::parse(&dmn_file_content) { + Ok(dmn_file_content) => match dsntk_model::from_xml(&dmn_file_content) { Ok(definitions) => { let html_output = dsntk_gendoc::dmn_model_to_html(&definitions); if let Err(reason) = fs::write(html_file_name, html_output) { diff --git a/examples/src/compatibility/level_2/2_0001.dmn b/examples/src/compatibility/level_2/2_0001.dmn index 991085bb..e1bb210c 100644 --- a/examples/src/compatibility/level_2/2_0001.dmn +++ b/examples/src/compatibility/level_2/2_0001.dmn @@ -1,6 +1,6 @@ - Compliance level 2: Test 0001 - - The decision named **Greeting Message** has a label defined in diagram definition. - - In the diagram this decision is depicted as **GREETING MESSAGE**. - - The output variable name remains **Greeting Message**. + + The decision named **Greeting Message** has a label defined in diagram definition. + In the diagram this decision is depicted as **GREETING MESSAGE**. + The output variable name remains **Greeting Message**. - + This decision prepares a greeting message. - 'Hello' is prepended to the value of the input variable named 'Full Name'. + 'Hello' is prepended to the value of the input variable named 'Full Name'. What is the greeting suitable for our customer? - The proper greeting is in the format: - Hello {customer's full name} + The proper greeting is in the format: Hello {customer's full name} - - + + - + "Hello " + Full Name - + Full name of the customer provided by calling service. - + Full name of the person that will be sent greetings from this decision model. - - - - - - - - GREETING MESSAGE - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/src/compatibility/level_2/2_0001.idml b/examples/src/compatibility/level_2/2_0001.idml new file mode 100644 index 00000000..ea6c24ba --- /dev/null +++ b/examples/src/compatibility/level_2/2_0001.idml @@ -0,0 +1,80 @@ +.NAMESPACE https://decision-toolkit.org/2_0001/ +.NAME 2_0001 +.ID _2_0001 +.VERSION https://www.omg.org/spec/DMN/20191111/MODEL/ +.DESCRIPTION + Compliance level 2: Test 0001 + + The decision named **Greeting Message** has a label defined in diagram definition. + In the diagram this decision is depicted as **GREETING MESSAGE**. + The output variable name remains **Greeting Message**. + +.DECISION + .NAME Greeting Message + .ID _75b3add2-4d36-4a19-a76c-268b49b2f436 + .DESCRIPTION + This decision prepares a greeting message. + 'Hello' is prepended to the value of the input variable named 'Full Name'. + .QUESTION + What is the greeting suitable for our customer? + .ALLOWED-ANSWERS + The proper greeting is in the format: Hello {customer's full name} + .VARIABLE + .NAME Greeting Message + .ID _3215b422-b937-4360-9d1c-4c677cae5dfd + .TYPE-REF string + .LABEL GREETING MESSAGE + .INFORMATION-REQUIREMENT + .ID _70c3f69a-63f3-4197-96ce-b206c8bd2a6b + .REQUIRED-INPUT + .HREF #_cba86e4d-e91c-46a2-9176-e9adf88e15db + .LITERAL-EXPRESSION + .ID _5baa6245-f6fc-4685-8973-fa873817e2c1 + .TEXT "Hello " + Full Name + +.INPUT-DATA + .NAME Full Name + .ID _cba86e4d-e91c-46a2-9176-e9adf88e15db + .DESCRIPTION + Full name of the customer provided by calling service. + .VARIABLE + .NAME Full Name + .ID _4bc2161f-2f3b-4260-b454-0a01aed0e46b + .LABEL Customer's name + .TYPE-REF string + .DESCRIPTION + Full name of the person that will be sent greetings from this decision model. + +.DIAGRAM + .NAME Decision Requirement Diagram + .ID _d3a3312e-5924-4f7b-ac0e-232ef9203ff6 + .RESOLUTION 300 + .SIZE 190.0 240.0 + .SHAPE + .ID _ebf33cfc-0ee3-4708-af8b-91c52237b7d6 + .DMN_ELEMENT_REF _75b3add2-4d36-4a19-a76c-268b49b2f436 + .BOUNDS 20.0 20.0 150.0 60.0 + .LABEL + .TEXT `GREETING MESSAGE` + .SHARED_STYLE style1 + .SHARED_STYLE style1 + .SHAPE + .ID _48ea7a1d-2575-4cb7-8b63-8baa4cb3b371 + .DMN_ELEMENT_REF _cba86e4d-e91c-46a2-9176-e9adf88e15db + .BOUNDS 20.0 160.0 150.0 60.0 + .SHARED_STYLE style2 + .EDGE + .ID _e9a73517-0ba2-4b31-b308-82279ae21591 + .DMN_ELEMENT_REF _70c3f69a-63f3-4197-96ce-b206c8bd2a6b + .WAYPOINTS 95.0 160.0, 95.0 80.0 + .STYLE + .ID style1 + .FONT bold underline italic strikethrough 18 Arial, Helvetica, sans-serif + .LABEL_VERTICAL_ALIGN start + .FILL_COLOR 10 255 255 + .STROKE_COLOR 255 0 0 + .FONT_COLOR 0 200 0 + .STYLE + .ID style2 + .FONT bold underline 12 Arial, "Fira Sans", sans-serif + .STROKE_COLOR 255 0 0 diff --git a/examples/src/compatibility/level_2/mod.rs b/examples/src/compatibility/level_2/mod.rs index 198ad116..ba640999 100644 --- a/examples/src/compatibility/level_2/mod.rs +++ b/examples/src/compatibility/level_2/mod.rs @@ -1,6 +1,7 @@ //! # Decision models for compatibility tests level 2 pub const DMN_2_0001: &str = include_str!("2_0001.dmn"); +pub const IDML_2_0001: &str = include_str!("2_0001.idml"); pub const DMN_2_0002: &str = include_str!("2_0002.dmn"); pub const DMN_2_0003: &str = include_str!("2_0003.dmn"); pub const DMN_2_0004: &str = include_str!("2_0004.dmn"); diff --git a/gendoc/src/tests/ascii_model.rs b/gendoc/src/tests/ascii_model.rs index 0143ce4d..34058fdb 100644 --- a/gendoc/src/tests/ascii_model.rs +++ b/gendoc/src/tests/ascii_model.rs @@ -8,7 +8,7 @@ macro_rules! test_print_model { #[test] #[allow(clippy::redundant_clone)] fn $test_name() { - let definitions = dsntk_model::parse(dsntk_examples::$model_name).expect("parsing model failed"); + let definitions = dsntk_model::from_xml(dsntk_examples::$model_name).expect("parsing model failed"); print_model(definitions.clone(), ColorMode::On); let expected = format!("{:?}", definitions); let actual = format!("{:?}", definitions); @@ -151,12 +151,12 @@ test_print_model!(_3_1130, DMN_3_1130); #[test] fn test_single_model() { - let definitions = dsntk_model::parse(dsntk_examples::DMN_3_1108).expect("parsing model failed"); + let definitions = dsntk_model::from_xml(dsntk_examples::DMN_3_1108).expect("parsing model failed"); print_model(definitions, ColorMode::On); } #[test] fn test_full_model() { - let definitions = dsntk_model::parse(dsntk_examples::DMN_FULL).expect("parsing model failed"); + let definitions = dsntk_model::from_xml(dsntk_examples::DMN_FULL).expect("parsing model failed"); print_model(definitions, ColorMode::On); } diff --git a/gendoc/src/tests/mod.rs b/gendoc/src/tests/mod.rs index d012ef5d..d44a33ad 100644 --- a/gendoc/src/tests/mod.rs +++ b/gendoc/src/tests/mod.rs @@ -15,7 +15,7 @@ const TARGET_DIR: &str = "../target/gendoc"; /// Utility function for generating HTML file for decision table defined as text. fn gen_html_from_model(model: &str, output_file_name: &str) { - let definitions = dsntk_model::parse(model).expect("parsing model failed"); + let definitions = dsntk_model::from_xml(model).expect("parsing model failed"); let html = crate::dmn_model_to_html(&definitions); assert_eq!("", &html[0..15]); fs::create_dir_all(TARGET_DIR).expect("creating target directories failed"); diff --git a/model-evaluator/benches/compatibility/mod.rs b/model-evaluator/benches/compatibility/mod.rs index edcfe9d1..c45e622a 100644 --- a/model-evaluator/benches/compatibility/mod.rs +++ b/model-evaluator/benches/compatibility/mod.rs @@ -69,7 +69,7 @@ use {from_examples, iter, model_evaluator_from_examples, model_name_from_example /// Utility function that builds a model evaluator from a single DMN model. fn build_model_evaluator(model_content: &str) -> Arc { - let definitions = dsntk_model::parse(model_content).unwrap(); + let definitions = dsntk_model::from_xml(model_content).unwrap(); ModelEvaluator::new(&[definitions]).unwrap() } @@ -77,20 +77,20 @@ fn build_model_evaluator(model_content: &str) -> Arc { fn build_model_evaluators(model_content: &[&str]) -> Arc { let mut definitions = vec![]; for content in model_content { - definitions.push(dsntk_model::parse(content).unwrap()); + definitions.push(dsntk_model::from_xml(content).unwrap()); } ModelEvaluator::new(&definitions).unwrap() } /// Utility function that returns a model namespace from a single DMN model. fn build_model_namespace(model_content: &str) -> String { - let definitions = dsntk_model::parse(model_content).unwrap(); + let definitions = dsntk_model::from_xml(model_content).unwrap(); definitions.namespace().to_string() } /// Utility function that returns a model namespace from a single DMN model. fn build_model_name(model_content: &str) -> String { - let definitions = dsntk_model::parse(model_content).unwrap(); + let definitions = dsntk_model::from_xml(model_content).unwrap(); definitions.name().to_string() } diff --git a/model-evaluator/src/input_data.rs b/model-evaluator/src/input_data.rs index c8eb2ce3..f20d7731 100644 --- a/model-evaluator/src/input_data.rs +++ b/model-evaluator/src/input_data.rs @@ -56,7 +56,7 @@ mod tests { /// Utility function for building input data evaluator from definitions, /// and item definition evaluator from definitions. fn build_evaluators(xml: &str) -> (InputDataEvaluator, ItemDefinitionEvaluator) { - let definitions = dsntk_model::parse(xml).unwrap(); + let definitions = dsntk_model::from_xml(xml).unwrap(); let mut def_definitions = DefDefinitions::default(); def_definitions.add_model(&definitions); (InputDataEvaluator::new(&def_definitions), ItemDefinitionEvaluator::new(&def_definitions).unwrap()) diff --git a/model-evaluator/src/input_data_context.rs b/model-evaluator/src/input_data_context.rs index e8bf2d72..b3cf6bff 100644 --- a/model-evaluator/src/input_data_context.rs +++ b/model-evaluator/src/input_data_context.rs @@ -69,7 +69,7 @@ mod tests { /// Utility function for building input data context evaluator from definitions, /// and item definition context evaluator from definitions. fn build_evaluators(xml: &str) -> (InputDataContextEvaluator, ItemDefinitionContextEvaluator) { - let definitions = dsntk_model::parse(xml).unwrap(); + let definitions = dsntk_model::from_xml(xml).unwrap(); let mut def_definitions = DefDefinitions::default(); def_definitions.add_model(&definitions); let input_data_context_evaluator = InputDataContextEvaluator::new(&def_definitions); diff --git a/model-evaluator/src/item_definition.rs b/model-evaluator/src/item_definition.rs index b2538cb4..4edfe28f 100644 --- a/model-evaluator/src/item_definition.rs +++ b/model-evaluator/src/item_definition.rs @@ -458,7 +458,7 @@ mod tests { /// Utility function for building item definition evaluator from definitions. fn build_evaluator(xml: &str) -> ItemDefinitionEvaluator { - let definitions = dsntk_model::parse(xml).unwrap(); + let definitions = dsntk_model::from_xml(xml).unwrap(); let mut def_definitions = DefDefinitions::default(); def_definitions.add_model(&definitions); ItemDefinitionEvaluator::new(&def_definitions).unwrap() diff --git a/model-evaluator/src/item_definition_context.rs b/model-evaluator/src/item_definition_context.rs index f0219f96..fd56f3e2 100644 --- a/model-evaluator/src/item_definition_context.rs +++ b/model-evaluator/src/item_definition_context.rs @@ -173,7 +173,7 @@ mod tests { /// Utility function for building item definition evaluator from definitions. fn build_evaluator(xml: &str) -> ItemDefinitionContextEvaluator { - let definitions = dsntk_model::parse(xml).unwrap(); + let definitions = dsntk_model::from_xml(xml).unwrap(); let mut def_definitions = DefDefinitions::default(); def_definitions.add_model(&definitions); ItemDefinitionContextEvaluator::new(&def_definitions).unwrap() diff --git a/model-evaluator/src/item_definition_type.rs b/model-evaluator/src/item_definition_type.rs index 4437e090..2b1943e7 100644 --- a/model-evaluator/src/item_definition_type.rs +++ b/model-evaluator/src/item_definition_type.rs @@ -190,7 +190,7 @@ mod tests { /// Utility function for building item definition type evaluator from definitions. fn build_evaluator(xml: &str) -> ItemDefinitionTypeEvaluator { - let definitions = dsntk_model::parse(xml).unwrap(); + let definitions = dsntk_model::from_xml(xml).unwrap(); let mut def_definitions = DefDefinitions::default(); def_definitions.add_model(&definitions); ItemDefinitionTypeEvaluator::new(&def_definitions).unwrap() diff --git a/model-evaluator/src/tests/compatibility/non_compliant/dmn_n_0088.rs b/model-evaluator/src/tests/compatibility/non_compliant/dmn_n_0088.rs index 0c712000..bf38558f 100644 --- a/model-evaluator/src/tests/compatibility/non_compliant/dmn_n_0088.rs +++ b/model-evaluator/src/tests/compatibility/non_compliant/dmn_n_0088.rs @@ -4,6 +4,6 @@ use dsntk_examples::DMN_N_0088; fn _0001() { assert_eq!( " cyclic dependency between item definitions", - dsntk_model::parse(DMN_N_0088).err().unwrap().to_string() + dsntk_model::from_xml(DMN_N_0088).err().unwrap().to_string() ); } diff --git a/model-evaluator/src/tests/mod.rs b/model-evaluator/src/tests/mod.rs index 5539a875..d789ddc2 100644 --- a/model-evaluator/src/tests/mod.rs +++ b/model-evaluator/src/tests/mod.rs @@ -5,8 +5,7 @@ use dsntk_feel::FeelScope; use dsntk_model::DmnElement; use std::collections::{BTreeMap, BTreeSet}; use std::fs; -use std::sync::Arc; -use std::sync::LazyLock; +use std::sync::{Arc, LazyLock}; use walkdir::WalkDir; mod compatibility; @@ -73,7 +72,7 @@ pub fn context(input: &str) -> FeelContext { /// Utility function that builds a model evaluator from single XML model definitions. fn build_model_evaluator(model_content: &str) -> Arc { - let definitions = dsntk_model::parse(model_content).unwrap(); + let definitions = dsntk_model::from_xml(model_content).unwrap(); ModelEvaluator::new(&[definitions]).unwrap() } @@ -81,20 +80,20 @@ fn build_model_evaluator(model_content: &str) -> Arc { fn build_model_evaluators(model_content: &[&str]) -> Arc { let mut definitions = vec![]; for content in model_content { - definitions.push(dsntk_model::parse(content).unwrap()); + definitions.push(dsntk_model::from_xml(content).unwrap()); } ModelEvaluator::new(&definitions).unwrap() } /// Utility function that returns a model namespace from a single DMN model. fn build_model_namespace(model_content: &str) -> String { - let definitions = dsntk_model::parse(model_content).unwrap(); + let definitions = dsntk_model::from_xml(model_content).unwrap(); definitions.namespace().to_string() } /// Utility function that returns a model names from a single DMN model. fn build_model_name(model_content: &str) -> String { - let definitions = dsntk_model::parse(model_content).unwrap(); + let definitions = dsntk_model::from_xml(model_content).unwrap(); definitions.name().to_string() } diff --git a/model/Cargo.toml b/model/Cargo.toml index 00b1d5c7..f3e33f43 100644 --- a/model/Cargo.toml +++ b/model/Cargo.toml @@ -19,3 +19,5 @@ dsntk-feel = { workspace = true } dsntk-feel-parser = { workspace = true } dsntk-macros = { workspace = true } dsntk-recognizer = { workspace = true } +# this is just experimental +idml = { path = "../../../EngosSoftware/idml" } diff --git a/model/src/errors.rs b/model/src/errors.rs index ce72d608..7e31f3c0 100644 --- a/model/src/errors.rs +++ b/model/src/errors.rs @@ -69,6 +69,7 @@ pub fn err_number_of_elements_in_row_differs_from_number_of_columns() -> DsntkEr ModelParserError("number of elements in a row differs from the number of columns defined in a relation".to_string()).into() } +/// Raised when parsing XML model fails. pub fn err_xml_parsing_model_failed(s: &str) -> DsntkError { ModelParserError(format!("parsing model from XML failed with reason: {s}")).into() } @@ -94,6 +95,16 @@ pub fn err_xml_expected_mandatory_text_content(s: &str) -> DsntkError { ModelParserError(format!("expected mandatory text content in node '{s}'")).into() } +/// Raised when parsing IDML model fails. +pub fn err_idml_parsing_model_failed(reason: &str) -> DsntkError { + ModelParserError(format!("parsing model from IDML failed with reason: {reason}")).into() +} + +/// Raised when a node has no mandatory child value. +pub fn err_idml_expected_mandatory_value(node_name: &str, child_name: &str) -> DsntkError { + ModelParserError(format!("expected value for mandatory value '{child_name}' in node {node_name}")).into() +} + /// Errors related with validating the decision model. #[derive(ToErrorMessage)] struct ModelValidatorError(String); diff --git a/model/src/idml_parser.rs b/model/src/idml_parser.rs new file mode 100644 index 00000000..6f117ab5 --- /dev/null +++ b/model/src/idml_parser.rs @@ -0,0 +1,283 @@ +use crate::errors::*; +use crate::idml_utils::*; +use crate::{ + Decision, Definitions, DmnId, DrgElement, ExpressionInstance, ExtensionAttribute, ExtensionElement, InformationItem, InformationRequirement, InputData, LiteralExpression, +}; +use dsntk_common::{gen_id, Result}; +use dsntk_feel::{Name, FEEL_TYPE_NAME_ANY}; +use idml::Node; + +/// Parses the IDML input document containing DMN model into [Definitions]. +pub fn from_idml(input: &str) -> Result { + // Parse the IDML document. + match idml::parse(input) { + Ok(document) => { + // Initialize model parser. + let mut model_parser = ModelParser::new(); + // Parse model and convert into definitions. + model_parser.parse_definitions(&document) + } + Err(reason) => Err(err_idml_parsing_model_failed(&reason.to_string())), + } +} + +/// IDML parser for DMN model. +struct ModelParser { + /// Model namespace used in parsed definitions. + namespace: String, + /// Model name used in parsed definitions. + model_name: String, +} + +impl ModelParser { + /// Creates new model parser. + fn new() -> Self { + Self { + namespace: "".to_string(), + model_name: "".to_string(), + } + } + + /// Parses [Definitions]. + fn parse_definitions(&mut self, node: &Node) -> Result { + self.namespace = required_uri(node, IDML_NAMESPACE)?; + self.model_name = required_name(node)?; + let definitions = Definitions { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + name: self.model_name.clone(), + feel_name: required_feel_name(node)?, + id: optional_id(node), + description: optional_description(node), + label: optional_label(node), + extension_elements: self.parse_extension_elements(node), + extension_attributes: self.parse_extension_attributes(node), + expression_language: optional_value(node, IDML_EXPRESSION_LANGUAGE), + type_language: optional_value(node, IDML_TYPE_LANGUAGE), + exporter: optional_value(node, IDML_EXPORTER), + exporter_version: optional_value(node, IDML_EXPORTER_VERSION), + item_definitions: vec![], + drg_elements: self.parse_drg_elements(node)?, + business_context_elements: vec![], + imports: vec![], + dmndi: None, + }; + Ok(definitions) + } + + /// Parses [DrgElement] nodes. + fn parse_drg_elements(&mut self, node: &Node) -> Result> { + let mut drg_elements = vec![]; + drg_elements.append(&mut self.parse_input_data_nodes(node)?); + drg_elements.append(&mut self.parse_decision_nodes(node)?); + // drg_elements.append(&mut self.parse_business_knowledge_model_nodes(node)?); + // drg_elements.append(&mut self.parse_decision_services(node)?); + // drg_elements.append(&mut self.parse_knowledge_sources(node)?); + Ok(drg_elements) + } + + /// Parses [InputData] nodes. + fn parse_input_data_nodes(&self, node: &Node) -> Result> { + let mut input_data_items = vec![]; + for child_node in node.children().filter(filter_name(IDML_INPUT_DATA)) { + let name = required_name(child_node)?; + let feel_name = required_feel_name(child_node)?; + let variable = self + .parse_opt_information_item_child(child_node, IDML_VARIABLE)? + .unwrap_or(self.create_information_item(name.clone(), feel_name.clone())?); + let input_data = InputData { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + id: optional_id(child_node), + description: optional_description(child_node), + label: optional_label(child_node), + extension_elements: self.parse_extension_elements(child_node), + extension_attributes: self.parse_extension_attributes(child_node), + name, + feel_name, + variable, + }; + input_data_items.push(DrgElement::InputData(input_data)); + } + Ok(input_data_items) + } + + /// Parses [Decision] nodes. + fn parse_decision_nodes(&mut self, node: &Node) -> Result> { + let mut decision_items = vec![]; + for child_node in node.children().filter(filter_name(IDML_DECISION)) { + let name = required_name(child_node)?; + let feel_name = required_feel_name(child_node)?; + let variable = self + .parse_opt_information_item_child(child_node, IDML_VARIABLE)? + .unwrap_or(self.create_information_item(name.clone(), feel_name.clone())?); + let decision = Decision { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + name, + feel_name, + id: optional_id(child_node), + description: optional_description(child_node), + label: optional_label(child_node), + extension_elements: self.parse_extension_elements(child_node), + extension_attributes: self.parse_extension_attributes(child_node), + question: optional_value(child_node, IDML_QUESTION), + allowed_answers: optional_value(child_node, IDML_ALLOWED_ANSWERS), + variable, + decision_logic: self.parse_optional_child_expression_instance(child_node)?, + information_requirements: self.parse_information_requirements(child_node, IDML_INFORMATION_REQUIREMENT)?, + knowledge_requirements: vec![], + authority_requirements: vec![], + }; + decision_items.push(DrgElement::Decision(decision)); + } + Ok(decision_items) + } + + /// Creates a new [InformationItem] element populated with name. + fn create_information_item(&self, name: String, feel_name: Name) -> Result { + Ok(InformationItem { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + id: DmnId::Generated(gen_id()), + description: None, + label: None, + extension_elements: vec![], + extension_attributes: vec![], + name, + feel_name, + type_ref: FEEL_TYPE_NAME_ANY.to_string(), + feel_type: None, + }) + } + + /// Parses an optional [InformationItem], contained in a child node having the specified name. + fn parse_opt_information_item_child(&self, node: &Node, child_name: &str) -> Result> { + Ok(if let Some(child_node) = node.children().find(filter_name(child_name)) { + Some(self.parse_information_item(child_node)?) + } else { + None + }) + } + + /// Parses the attributes of the [InformationItem] element. + fn parse_information_item(&self, node: &Node) -> Result { + Ok(InformationItem { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + id: optional_id(node), + description: optional_description(node), + label: optional_label(node), + extension_elements: self.parse_extension_elements(node), + extension_attributes: self.parse_extension_attributes(node), + name: required_name(node)?, + feel_name: required_feel_name(node)?, + type_ref: optional_value(node, IDML_TYPE_REF).unwrap_or(FEEL_TYPE_NAME_ANY.to_string()), + feel_type: None, + }) + } + + /// Parses extension elements. + fn parse_extension_elements(&self, _node: &Node) -> Vec { + // Currently ignored. Ready for future development when needed. + vec![] + } + + /// Parses extension attributes. + fn parse_extension_attributes(&self, _node: &Node) -> Vec { + // Currently ignored. Ready for future development when needed. + vec![] + } + + fn parse_optional_child_expression_instance(&self, node: &Node) -> Result> { + // if let Some(context) = self.parse_optional_context(node)? { + // return Ok(Some(ExpressionInstance::Context(Box::new(context)))); + // } + // if let Some(decision_table) = self.parse_optional_decision_table(node)? { + // return Ok(Some(ExpressionInstance::DecisionTable(Box::new(decision_table)))); + // } + // if let Some(function_definition) = self.parse_optional_function_definition(node)? { + // return Ok(Some(ExpressionInstance::FunctionDefinition(Box::new(function_definition)))); + // } + // if let Some(invocation) = self.parse_optional_invocation(node)? { + // return Ok(Some(ExpressionInstance::Invocation(Box::new(invocation)))); + // } + // if let Some(list) = self.parse_optional_list(node)? { + // return Ok(Some(ExpressionInstance::List(Box::new(list)))); + // } + if let Some(literal_expression) = self.parse_optional_literal_expression(node) { + return Ok(Some(ExpressionInstance::LiteralExpression(Box::new(literal_expression)))); + } + // if let Some(relation) = self.parse_optional_relation(node)? { + // return Ok(Some(ExpressionInstance::Relation(Box::new(relation)))); + // } + // if let Some(conditional) = self.parse_optional_conditional(node)? { + // return Ok(Some(ExpressionInstance::Conditional(Box::new(conditional)))); + // } + // if let Some(filter) = self.parse_optional_filter(node)? { + // return Ok(Some(ExpressionInstance::Filter(Box::new(filter)))); + // } + // if let Some(r#for) = self.parse_optional_for(node)? { + // return Ok(Some(ExpressionInstance::For(Box::new(r#for)))); + // } + // if let Some(every) = self.parse_optional_every(node)? { + // return Ok(Some(ExpressionInstance::Every(Box::new(every)))); + // } + // if let Some(some) = self.parse_optional_some(node)? { + // return Ok(Some(ExpressionInstance::Some(Box::new(some)))); + // } + Ok(None) + } + + /// Searches for the first node named 'literalExpression' among children of the specified `node`. + /// When such node is found, then parses literal expression and returns it, otherwise returns [None]. + fn parse_optional_literal_expression(&self, node: &Node) -> Option { + if let Some(child_node) = node.children().find(filter_name(IDML_LITERAL_EXPRESSION)) { + return Some(self.parse_literal_expression(child_node)); + } + None + } + + /// Parses [LiteralExpression] directly from the specified node. + /// The `literal_expression_node` must be a node named `literalExpression`. + fn parse_literal_expression(&self, node: &Node) -> LiteralExpression { + LiteralExpression { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + id: optional_id(node), + description: optional_description(node), + label: optional_label(node), + extension_elements: self.parse_extension_elements(node), + extension_attributes: self.parse_extension_attributes(node), + type_ref: optional_value(node, IDML_TYPE_REF), + text: optional_value(node, IDML_TEXT), + expression_language: optional_value(node, IDML_EXPRESSION_LANGUAGE), + imported_values: None, + } + } + + /// Parses information requirements. + fn parse_information_requirements(&mut self, node: &Node, child_name: &str) -> Result> { + let mut information_requirement_items = vec![]; + for child_node in node.children().filter(filter_name(child_name)) { + information_requirement_items.push(self.parse_information_requirement(child_node)?); + } + Ok(information_requirement_items) + } + + /// Parses single information requirement. + fn parse_information_requirement(&mut self, node: &Node) -> Result { + let req = InformationRequirement { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + id: optional_id(node), + description: optional_description(node), + label: optional_label(node), + extension_elements: self.parse_extension_elements(node), + extension_attributes: self.parse_extension_attributes(node), + required_decision: optional_child_required_href(node, IDML_REQUIRED_DECISION)?, + required_input: optional_child_required_href(node, IDML_REQUIRED_INPUT)?, + }; + Ok(req) + } +} diff --git a/model/src/idml_utils.rs b/model/src/idml_utils.rs new file mode 100644 index 00000000..c584d07b --- /dev/null +++ b/model/src/idml_utils.rs @@ -0,0 +1,100 @@ +use crate::errors::*; +use crate::DmnId; +use dsntk_common::{gen_id, to_uri, HRef, Result, Uri}; +use dsntk_feel::Name; +use idml::Node; + +pub const IDML_DESCRIPTION: &str = "DESCRIPTION"; +pub const IDML_DECISION: &str = "DECISION"; +pub const IDML_EXPORTER: &str = "EXPORTER"; +pub const IDML_EXPORTER_VERSION: &str = "EXPORTER-VERSION"; +pub const IDML_EXPRESSION_LANGUAGE: &str = "EXPRESSION-LANGUAGE"; +pub const IDML_ID: &str = "ID"; +pub const IDML_QUESTION: &str = "QUESTION"; +pub const IDML_ALLOWED_ANSWERS: &str = "ALLOWED-ANSWERS"; +pub const IDML_INFORMATION_REQUIREMENT: &str = "INFORMATION-REQUIREMENT"; +pub const IDML_LITERAL_EXPRESSION: &str = "LITERAL-EXPRESSION"; +pub const IDML_LABEL: &str = "LABEL"; +pub const IDML_INPUT_DATA: &str = "INPUT-DATA"; +pub const IDML_NAME: &str = "NAME"; +pub const IDML_REQUIRED_INPUT: &str = "REQUIRED-INPUT"; +pub const IDML_REQUIRED_DECISION: &str = "REQUIRED-DECISION"; +pub const IDML_HREF: &str = "HREF"; + +pub const IDML_NAMESPACE: &str = "NAMESPACE"; +pub const IDML_VARIABLE: &str = "VARIABLE"; +pub const IDML_TEXT: &str = "TEXT"; +pub const IDML_TYPE_LANGUAGE: &str = "TYPE-LANGUAGE"; +pub const IDML_TYPE_REF: &str = "TYPE-REF"; + +/// Returns the required string value of a child node with specified name. +pub fn required_value(node: &Node, child_name: &str) -> Result { + if let Some(value) = node.children().find_map(|child| if child.name() == child_name { Some(child.text()) } else { None }) { + Ok(value.to_string()) + } else { + Err(err_idml_expected_mandatory_value(node.name(), child_name)) + } +} + +/// Returns the required URI value. +pub fn required_uri(node: &Node, child_name: &str) -> Result { + to_uri(required_value(node, child_name)?.as_str()) +} + +/// Returns the required name value. +pub fn required_name(node: &Node) -> Result { + required_value(node, IDML_NAME) +} + +/// Returns FEEL name for specified node. +pub fn required_feel_name(node: &Node) -> Result { + let input = required_name(node)?; + Ok(dsntk_feel_parser::parse_longest_name(&input).unwrap_or(input.into())) +} + +/// Returns the optional value of the child node. +pub fn optional_value(node: &Node, child_name: &str) -> Option { + if let Some(value) = node.children().find_map(|child| if child.name() == child_name { Some(child.text()) } else { None }) { + if value.is_empty() { + None + } else { + Some(value.to_string()) + } + } else { + None + } +} + +/// Returns optional identifier provided in model or generates a new one. +pub fn optional_id(node: &Node) -> DmnId { + optional_value(node, IDML_ID).map(DmnId::Provided).unwrap_or(DmnId::Generated(gen_id())) +} + +/// Returns optional description. +pub fn optional_description(node: &Node) -> Option { + optional_value(node, IDML_DESCRIPTION) +} + +/// Returns optional label. +pub fn optional_label(node: &Node) -> Option { + optional_value(node, IDML_LABEL) +} + +/// Returns the required `href` attribute of the optional child node. +pub fn optional_child_required_href(node: &Node, child_name: &str) -> Result> { + if let Some(child_node) = optional_child(node, child_name) { + Ok(Some(HRef::try_from(required_value(child_node, IDML_HREF)?.as_str())?)) + } else { + Ok(None) + } +} + +/// Returns child node when there is a child with the given name. +pub fn optional_child<'a>(node: &'a Node, child_name: &str) -> Option<&'a Node> { + node.children().find(|n| n.name() == child_name) +} + +/// Returns a closure that checks if the name of the node is equal to provided one. +pub fn filter_name(name: &str) -> impl Fn(&&Node) -> bool + '_ { + move |n| n.name() == name +} diff --git a/model/src/lib.rs b/model/src/lib.rs index 4e30734e..c5bcc4eb 100644 --- a/model/src/lib.rs +++ b/model/src/lib.rs @@ -2,6 +2,8 @@ extern crate dsntk_macros; mod errors; +mod idml_parser; +mod idml_utils; mod mapper; mod model; mod parser; @@ -9,5 +11,6 @@ mod tests; mod validators; mod xml_utils; +pub use idml_parser::from_idml; pub use model::*; -pub use parser::parse; +pub use parser::from_xml; diff --git a/model/src/model.rs b/model/src/model.rs index 7f2aa27f..28573196 100644 --- a/model/src/model.rs +++ b/model/src/model.rs @@ -122,7 +122,7 @@ pub struct ExtensionElement; pub struct ExtensionAttribute; /// Enumeration of concrete instances of [BusinessContextElement]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum BusinessContextElementInstance { PerformanceIndicator(PerformanceIndicator), OrganizationUnit(OrganizationUnit), @@ -133,7 +133,7 @@ pub enum BusinessContextElementInstance { #[named_element] #[dmn_element] #[business_context_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct PerformanceIndicator { /// Collection of [Decision] that impact this [PerformanceIndicator]. /// This attribute stores references @@ -151,7 +151,7 @@ impl PerformanceIndicator { #[named_element] #[dmn_element] #[business_context_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct OrganizationUnit { /// Collection of [Decision] that are made by this [OrganizationUnit]. pub(crate) decisions_made: Vec, @@ -173,7 +173,7 @@ impl OrganizationUnit { /// All DMN elements are contained within [Definitions] and that have a graphical /// representation in a DRD. This enumeration specifies the list /// of [DRGElements](DrgElement) contained in [Definitions]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] #[allow(clippy::large_enum_variant)] pub enum DrgElement { Decision(Decision), @@ -196,7 +196,7 @@ pub enum Requirement { /// for all contained elements. #[named_element] #[dmn_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Definitions { /// This attribute identifies the expression language used in /// [LiteralExpressions](LiteralExpression) within the scope @@ -494,7 +494,7 @@ impl FeelTypedElement for InformationItem { /// are defined outside the decision model. #[named_element] #[dmn_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct InputData { /// The instance of [InformationItem] that stores the result of this [InputData]. pub(crate) variable: InformationItem, @@ -675,7 +675,7 @@ impl Binding { /// [Decision] #[named_element] #[dmn_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Decision { /// A natural language question that characterizes the [Decision], /// such that the output of the [Decision] is an answer to the question. @@ -736,7 +736,7 @@ impl Decision { /// The class [InformationRequirement] is used to model an information requirement, /// as represented by a plain arrow in a DRD. #[dmn_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct InformationRequirement { /// Reference to [Decision] that this [InformationRequirement] associates /// with its containing [Decision] element. @@ -761,7 +761,7 @@ impl InformationRequirement { /// The class [KnowledgeRequirement] is used to model a knowledge requirement, /// as represented by a dashed arrow in a DRD. #[dmn_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct KnowledgeRequirement { /// Reference to [Invocable] that this [KnowledgeRequirement] associates with /// its containing [Decision] or [BusinessKnowledgeModel] element. @@ -778,7 +778,7 @@ impl KnowledgeRequirement { /// The class [AuthorityRequirement] is used to model an authority requirement, /// as represented by an arrow drawn with a dashed line and a filled circular head in a DRD #[dmn_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct AuthorityRequirement { /// The instance of [KnowledgeSource] that this [AuthorityRequirement] associates /// with its containing [KnowledgeSource], [Decision] or [BusinessKnowledgeModel] element. @@ -810,7 +810,7 @@ impl AuthorityRequirement { /// In a DRD, an instance of [KnowledgeSource] is represented by a `knowledge source` diagram element. #[named_element] #[dmn_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct KnowledgeSource { /// Collection of the instances of [AuthorityRequirement] that compose this [Decision]. pub(crate) authority_requirements: Vec, @@ -830,7 +830,7 @@ impl KnowledgeSource { /// must be a single FEEL boxed function definition. #[named_element] #[dmn_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct BusinessKnowledgeModel { /// Variable that is bound to the function defined by the [FunctionDefinition] for this [BusinessKnowledgeModel]. pub(crate) variable: InformationItem, @@ -868,7 +868,7 @@ impl RequiredVariable for BusinessKnowledgeModel { /// against the decision model contained in an instance of [Definitions]. #[named_element] #[dmn_element] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct DecisionService { /// Variable for this [DecisionService]. pub(crate) variable: InformationItem, @@ -925,7 +925,7 @@ pub enum ItemDefinitionType { #[named_element] #[dmn_element] #[expression] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ItemDefinition { /// This attribute identifies the type language used to specify the base /// type of this [ItemDefinition]. This value overrides the type @@ -986,7 +986,7 @@ impl ItemDefinition { /// [UnaryTests] is used to model a boolean test, where the argument /// to be tested is implicit or denoted with a **?**. /// Test is specified by text in some specified expression language. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct UnaryTests { /// The text of this [UnaryTests]. /// It SHALL be a valid expression in the expressionLanguage. @@ -1011,7 +1011,7 @@ impl UnaryTests { /// [FunctionItem] defines the signature of a function: /// the parameters and the output type of the function. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct FunctionItem { /// Reference to output type of the function. pub(crate) output_type_ref: Option, @@ -1615,7 +1615,7 @@ pub struct AnnotationEntry { /// [Dmndi] is a container for the shared [DmnStyle](DmnStyle)s /// and all [DmnDiagram]s defined in [Definitions]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Dmndi { /// A list of shared [DmnStyle] that can be referenced /// by all [DmnDiagram] and [DmnDiagramElement]. @@ -1625,7 +1625,7 @@ pub struct Dmndi { } /// Defines possible elements of [DmnDiagramElement]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum DmnDiagramElement { DmnShape(DmnShape), DmnEdge(DmnEdge), @@ -1633,7 +1633,7 @@ pub enum DmnDiagramElement { /// [DmnDiagram] is the container of [DmnDiagramElement] ([DmnShape] (s) and [DmnEdge] (s)). /// [DmnDiagram] cannot include other [DmnDiagrams](DmnDiagram). -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct DmnDiagram { /// [DmnDiagram] id. pub id: Option, @@ -1657,7 +1657,7 @@ pub struct DmnDiagram { /// [DmnShape] represents a [Decision], a [BusinessKnowledgeModel], an [InputData] element, /// a [KnowledgeSource], a [DecisionService] or a [TextAnnotation] that is depicted on the diagram. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct DmnShape { /// Unique identifier of this [DmnShape]. pub id: Option, @@ -1688,7 +1688,7 @@ pub struct DmnShape { } /// Struct defines line inside [DecisionService]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct DmnDecisionServiceDividerLine { pub id: Option, /// A list of points relative to the origin of its parent [DmnDiagram] that specifies @@ -1701,7 +1701,7 @@ pub struct DmnDecisionServiceDividerLine { pub local_style: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct DmnEdge { pub id: Option, /// A list of points relative to the origin of its parent [DmnDiagram] that specifies @@ -1735,7 +1735,7 @@ pub struct Association {} pub struct TextAnnotation {} /// [DmnStyle] is used to keep some non-normative visual attributes such as color and font. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct DmnStyle { /// A unique identifier for this style so it can be referenced. /// Only styles defined in the [Dmndi] can be referenced by [DmnDiagramElement] and [DmnDiagram]. @@ -1768,7 +1768,7 @@ pub struct DmnStyle { } /// Struct represents the depiction of some textual information about a DMN element. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct DmnLabel { /// The bounds of the [DmnLabel]. When not specified, the label is positioned /// at its default position as determined in clause 13.5. @@ -1781,7 +1781,7 @@ pub struct DmnLabel { } /// Defines RGB color. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct DcColor { pub red: u8, pub green: u8, @@ -1803,14 +1803,14 @@ impl DcColor { } /// Defines point. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct DcPoint { pub x: f64, pub y: f64, } /// Defines bounds. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct DcBounds { pub x: f64, pub y: f64, @@ -1819,14 +1819,14 @@ pub struct DcBounds { } /// Defines dimensions. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct DcDimension { pub width: f64, pub height: f64, } /// Defines the kind of element alignment. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum DcAlignmentKind { /// Left or top. Start, @@ -1837,7 +1837,7 @@ pub enum DcAlignmentKind { } /// Defines known colors. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum DcKnownColor { Aqua = 0x00FFFF, Black = 0x000000, diff --git a/model/src/parser.rs b/model/src/parser.rs index b134eb2f..973660d1 100644 --- a/model/src/parser.rs +++ b/model/src/parser.rs @@ -9,7 +9,7 @@ use dsntk_feel::{Name, FEEL_TYPE_NAME_ANY}; use roxmltree::{Node, NodeType}; /// Parses the XML input document containing DMN model into [Definitions]. -pub fn parse(input: &str) -> Result { +pub fn from_xml(input: &str) -> Result { // parse document match roxmltree::Document::parse(input) { Ok(document) => { diff --git a/model/src/tests/parser/full_model.rs b/model/src/tests/parser/full_model.rs index 01b22f4b..3608cac3 100644 --- a/model/src/tests/parser/full_model.rs +++ b/model/src/tests/parser/full_model.rs @@ -1,10 +1,10 @@ +use crate::from_xml; use crate::model::DmnElement; -use crate::parse; use dsntk_examples::DMN_FULL; #[test] fn _0001() { - let definitions = parse(DMN_FULL).unwrap(); + let definitions = from_xml(DMN_FULL).unwrap(); assert_eq!("_id_definitions", definitions.id()); //------------------------------------------------------------------------------------------------ // ITEM DEFINITIONS @@ -30,7 +30,7 @@ fn _0001() { #[test] #[allow(clippy::redundant_clone)] fn _0002() { - let definitions = parse(DMN_FULL).unwrap(); + let definitions = from_xml(DMN_FULL).unwrap(); let cloned_definitions = definitions.clone(); assert_eq!("_id_definitions", cloned_definitions.id()); let expected = format!("{definitions:?}"); diff --git a/model/src/tests/parser/invalid_models.rs b/model/src/tests/parser/invalid_models.rs index 5fa70a8c..f1cd3aaf 100644 --- a/model/src/tests/parser/invalid_models.rs +++ b/model/src/tests/parser/invalid_models.rs @@ -1,9 +1,9 @@ -use crate::parse; +use crate::from_xml; use crate::tests::parser::input_files::*; #[test] fn _0001() { - let definitions = parse(T_DMN_0001); + let definitions = from_xml(T_DMN_0001); assert!(definitions.is_err()); assert_eq!( r#" 'Python' is not a valid function kind, accepted values are: 'FEEL', 'Java', 'PMML'"#, @@ -13,7 +13,7 @@ fn _0001() { #[test] fn _0002() { - let definitions = parse(T_DMN_0002); + let definitions = from_xml(T_DMN_0002); assert!(definitions.is_err()); assert_eq!( r#" 'LAST' is not a valid hit policy, allowed values are: 'UNIQUE', 'FIRST', 'PRIORITY', 'ANY', 'COLLECT', 'RULE ORDER', 'OUTPUT ORDER'"#, @@ -23,7 +23,7 @@ fn _0002() { #[test] fn _0003() { - let definitions = parse(T_DMN_0003); + let definitions = from_xml(T_DMN_0003); assert!(definitions.is_err()); assert_eq!( r#" 'AVG' is not a valid aggregation, allowed values are: 'COUNT', 'SUM', 'MIN', 'MAX'"#, @@ -33,7 +33,7 @@ fn _0003() { #[test] fn _0004() { - let definitions = parse(T_DMN_0004); + let definitions = from_xml(T_DMN_0004); assert!(definitions.is_err()); assert_eq!( r#" required input expression in decision table's input clause is missing"#, @@ -43,14 +43,14 @@ fn _0004() { #[test] fn _0005() { - let definitions = parse(T_DMN_0005); + let definitions = from_xml(T_DMN_0005); assert!(definitions.is_err()); assert_eq!(r#" required expression instance is missing"#, format!("{}", definitions.err().unwrap())) } #[test] fn _0006() { - let definitions = parse(T_DMN_0006); + let definitions = from_xml(T_DMN_0006); assert!(definitions.is_err()); assert_eq!( r#" number of elements in a row differs from the number of columns defined in a relation"#, @@ -60,7 +60,7 @@ fn _0006() { #[test] fn _0007() { - let definitions = parse(T_DMN_0007); + let definitions = from_xml(T_DMN_0007); assert!(definitions.is_err()); assert_eq!( r#" parsing model from XML failed with reason: the root node was opened but never closed"#, @@ -70,7 +70,7 @@ fn _0007() { #[test] fn _0008() { - let definitions = parse(T_DMN_0008); + let definitions = from_xml(T_DMN_0008); assert!(definitions.is_err()); assert_eq!( r#" unexpected XML node, expected: definitions, actual: definition"#, @@ -80,7 +80,7 @@ fn _0008() { #[test] fn _0009() { - let definitions = parse(T_DMN_0009); + let definitions = from_xml(T_DMN_0009); assert!(definitions.is_err()); assert_eq!( r#" expected value for mandatory attribute 'namespace' in node 'definitions' at [2:1]"#, @@ -90,7 +90,7 @@ fn _0009() { #[test] fn _0010() { - let definitions = parse(T_DMN_0010); + let definitions = from_xml(T_DMN_0010); assert!(definitions.is_err()); assert_eq!( r#" expected value for mandatory attribute 'name' in node 'decision' at [11:5]"#, @@ -100,7 +100,7 @@ fn _0010() { #[test] fn _0011() { - let definitions = parse(T_DMN_0011); + let definitions = from_xml(T_DMN_0011); assert!(definitions.is_err()); assert_eq!( r#" expected mandatory text content in node 'text'"#, @@ -110,7 +110,7 @@ fn _0011() { #[test] fn _0012() { - let definitions = parse(T_DMN_0012); + let definitions = from_xml(T_DMN_0012); assert!(definitions.is_err()); assert_eq!( r#" conversion to valid color value failed with reason: number too large to fit in target type"#, @@ -120,7 +120,7 @@ fn _0012() { #[test] fn _0013() { - let definitions = parse(T_DMN_0013); + let definitions = from_xml(T_DMN_0013); assert!(definitions.is_err()); assert_eq!( r#" conversion to valid double value failed with reason: invalid float literal"#, @@ -130,7 +130,7 @@ fn _0013() { #[test] fn _0014() { - let definitions = parse(T_DMN_0014); + let definitions = from_xml(T_DMN_0014); assert!(definitions.is_err()); assert_eq!( r#" expected mandatory child node 'text' in parent node 'outputEntry' at [31:17]"#, @@ -140,13 +140,13 @@ fn _0014() { #[test] fn _0015() { - let definitions = parse(T_DMN_0015); + let definitions = from_xml(T_DMN_0015); assert!(definitions.is_ok()); } #[test] fn _0016() { - let definitions = parse(T_DMN_0016); + let definitions = from_xml(T_DMN_0016); assert!(definitions.is_err()); assert_eq!( r#" required child node 'Bounds' in parent node 'DMNShape' is missing"#, diff --git a/model/src/tests/validators/model_validator/item_definition_cycles.rs b/model/src/tests/validators/model_validator/item_definition_cycles.rs index 93bf41eb..cb60eb9d 100644 --- a/model/src/tests/validators/model_validator/item_definition_cycles.rs +++ b/model/src/tests/validators/model_validator/item_definition_cycles.rs @@ -1,17 +1,17 @@ //! # Test cases for cyclic dependencies between item definitions use super::test_files::*; -use crate::parse; +use crate::from_xml; #[test] fn _0001() { - assert!(parse(DMN_0001).is_ok()); + assert!(from_xml(DMN_0001).is_ok()); } #[test] fn _0002() { assert_eq!( " cyclic dependency between item definitions", - parse(DMN_1001).err().unwrap().to_string() + from_xml(DMN_1001).err().unwrap().to_string() ); } diff --git a/model/src/xml_utils.rs b/model/src/xml_utils.rs index 92afc9bd..21e40a7b 100644 --- a/model/src/xml_utils.rs +++ b/model/src/xml_utils.rs @@ -195,7 +195,7 @@ pub fn required_content(node: &Node) -> Result { /// Returns optional text content of the node with specified name. pub fn opt_content(node: &Node) -> Option { - node.text().map(|text| text.to_owned()) + node.text().map(|text| text.trim().to_string()) } /// Returns required child node or raises an error when there is no child with given name. diff --git a/model/tests/compatibility/level_2/idml_2_0001.rs b/model/tests/compatibility/level_2/idml_2_0001.rs new file mode 100644 index 00000000..4d40e7df --- /dev/null +++ b/model/tests/compatibility/level_2/idml_2_0001.rs @@ -0,0 +1,9 @@ +use dsntk_examples::*; +use dsntk_model::*; + +#[test] +fn _0001() { + let xml = from_xml(DMN_2_0001).unwrap(); + let idml = from_idml(IDML_2_0001).unwrap(); + assert_eq!(xml, idml); +} diff --git a/model/tests/compatibility/level_2/mod.rs b/model/tests/compatibility/level_2/mod.rs new file mode 100644 index 00000000..91f0cfcf --- /dev/null +++ b/model/tests/compatibility/level_2/mod.rs @@ -0,0 +1 @@ +mod idml_2_0001; diff --git a/model/tests/compatibility/mod.rs b/model/tests/compatibility/mod.rs new file mode 100644 index 00000000..96e49562 --- /dev/null +++ b/model/tests/compatibility/mod.rs @@ -0,0 +1 @@ +mod level_2; diff --git a/model/tests/mod.rs b/model/tests/mod.rs new file mode 100644 index 00000000..361e1fe1 --- /dev/null +++ b/model/tests/mod.rs @@ -0,0 +1 @@ +mod compatibility; diff --git a/workspace/src/builder.rs b/workspace/src/builder.rs index 22bc4df7..96a7f8ec 100644 --- a/workspace/src/builder.rs +++ b/workspace/src/builder.rs @@ -125,7 +125,7 @@ impl WorkspaceBuilder { /// Loads decision model from specified file. fn load_model(&mut self, workspace_name: &str, file: &Path) { match fs::read_to_string(file) { - Ok(xml) => match dsntk_model::parse(&xml) { + Ok(xml) => match dsntk_model::from_xml(&xml) { Ok(definitions) => { let namespace = definitions.namespace().to_string(); if to_rdnn(&namespace).is_some() { From f6d76ec5bff9fe7971170768c26f175c32f273b8 Mon Sep 17 00:00:00 2001 From: Dariusz Depta <141360751+DariuszDepta@users.noreply.github.com> Date: Thu, 20 Mar 2025 17:58:05 +0100 Subject: [PATCH 2/3] Updates. --- .../src/compatibility/level_2/2_0001.idml | 3 - examples/src/compatibility/level_2/2_0004.dmn | 84 +-- .../src/compatibility/level_2/2_0004.idml | 59 ++ examples/src/compatibility/level_2/a.html | 508 ++++++++++++++++++ examples/src/compatibility/level_2/mod.rs | 1 + model/src/idml_parser.rs | 46 +- model/src/idml_utils.rs | 21 +- .../compatibility/level_2/idml_2_0004.rs | 9 + model/tests/compatibility/level_2/mod.rs | 1 + 9 files changed, 673 insertions(+), 59 deletions(-) create mode 100644 examples/src/compatibility/level_2/2_0004.idml create mode 100644 examples/src/compatibility/level_2/a.html create mode 100644 model/tests/compatibility/level_2/idml_2_0004.rs diff --git a/examples/src/compatibility/level_2/2_0001.idml b/examples/src/compatibility/level_2/2_0001.idml index ea6c24ba..d51283c8 100644 --- a/examples/src/compatibility/level_2/2_0001.idml +++ b/examples/src/compatibility/level_2/2_0001.idml @@ -8,7 +8,6 @@ The decision named **Greeting Message** has a label defined in diagram definition. In the diagram this decision is depicted as **GREETING MESSAGE**. The output variable name remains **Greeting Message**. - .DECISION .NAME Greeting Message .ID _75b3add2-4d36-4a19-a76c-268b49b2f436 @@ -31,7 +30,6 @@ .LITERAL-EXPRESSION .ID _5baa6245-f6fc-4685-8973-fa873817e2c1 .TEXT "Hello " + Full Name - .INPUT-DATA .NAME Full Name .ID _cba86e4d-e91c-46a2-9176-e9adf88e15db @@ -44,7 +42,6 @@ .TYPE-REF string .DESCRIPTION Full name of the person that will be sent greetings from this decision model. - .DIAGRAM .NAME Decision Requirement Diagram .ID _d3a3312e-5924-4f7b-ac0e-232ef9203ff6 diff --git a/examples/src/compatibility/level_2/2_0004.dmn b/examples/src/compatibility/level_2/2_0004.dmn index e5205e46..a19d2aa6 100644 --- a/examples/src/compatibility/level_2/2_0004.dmn +++ b/examples/src/compatibility/level_2/2_0004.dmn @@ -1,23 +1,23 @@ - - - + + - + - + - + Age @@ -42,7 +42,7 @@ false, true - + "Approved", "Declined" @@ -52,7 +52,7 @@ >=18 - "Medium","Low" + "Medium", "Low" true @@ -66,7 +66,7 @@ <18 - "Medium","Low" + "Medium", "Low" true @@ -106,41 +106,41 @@ - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/src/compatibility/level_2/2_0004.idml b/examples/src/compatibility/level_2/2_0004.idml new file mode 100644 index 00000000..6f79144e --- /dev/null +++ b/examples/src/compatibility/level_2/2_0004.idml @@ -0,0 +1,59 @@ +.NAMESPACE https://decision-toolkit.org +.NAME compliance-level-2-test-0004 +.ID _3859fe63-c8a7-4fc7-8a10-f27830e9d2ac +.VERSION https://www.omg.org/spec/DMN/20191111/MODEL/ +.DECISION + .NAME Approval Status + .ID _3b2953a3-745f-4d2e-b55d-75c8c5ae653c + .VARIABLE + .NAME Approval Status + .ID _8ee5dd22-4857-4ccf-97dd-b3f87cabf05f + .TYPE-REF string + .INFORMATION-REQUIREMENT + .ID _c565bfc1-53ae-4e25-ad35-b26767a9b1d9 + .REQUIRED-INPUT + .HREF #_5a4bdb64-f0ef-4978-9e03-6f1ae64a1f17 + .INFORMATION-REQUIREMENT + .ID _ac23f1ad-562f-47e9-a369-d9cc01f8b415 + .REQUIRED-INPUT + .HREF #_41effb45-b3c4-46ac-b1da-122b3e428a98 + .INFORMATION-REQUIREMENT + .ID _ae239e86-d326-4513-a01b-24e18269de29 + .REQUIRED-INPUT + .HREF #_8ff18665-84e9-49f2-a8df-8981b1844549 + .DECISION-TABLE + ┌───┬───────────┬────────────────────────┬──────────────╥────────────────────────┐ + │ U │ Age │ RiskCategory │ isAffordable ║ Approval Status │ + │ ├───────────┼────────────────────────┼──────────────╫────────────────────────┤ + │ │ [10..120] │"High", "Low", "Medium" │ false, true ║ "Approved", "Declined" │ + ╞═══╪═══════════╪════════════════════════╪══════════════╬════════════════════════╡ + │ 1 │ >=18 │ "Medium", "Low" │ true ║ "Approved" │ + ├───┼───────────┼────────────────────────┼──────────────╫────────────────────────┤ + │ 2 │ <18 │ "Medium", "Low" │ true ║ "Declined" │ + ├───┼───────────┼────────────────────────┼──────────────╫────────────────────────┤ + │ 3 │ - │ "High" │ true ║ "Declined" │ + ├───┼───────────┼────────────────────────┼──────────────╫────────────────────────┤ + │ 4 │ - │ - │ false ║ "Declined" │ + └───┴───────────┴────────────────────────┴──────────────╨────────────────────────┘ + .ID _73ec5c56-978c-45ee-a0b7-9585fb48008e +.INPUT-DATA + .NAME Age + .ID _41effb45-b3c4-46ac-b1da-122b3e428a98 + .VARIABLE + .NAME Age + .ID _e073557f-c20f-42e4-ba1a-b98c15daec50 + .TYPE-REF number +.INPUT-DATA + .NAME RiskCategory + .ID _5a4bdb64-f0ef-4978-9e03-6f1ae64a1f17 + .VARIABLE + .NAME RiskCategory + .ID _7c9ecedf-0010-488d-9b93-122520ddaab5 + .TYPE-REF string +.INPUT-DATA + .NAME isAffordable + .ID _8ff18665-84e9-49f2-a8df-8981b1844549 + .VARIABLE + .NAME isAffordable + .ID _2af698d7-017a-40b9-bea5-94ee748e4a30 + .TYPE-REF boolean diff --git a/examples/src/compatibility/level_2/a.html b/examples/src/compatibility/level_2/a.html new file mode 100644 index 00000000..46cb1827 --- /dev/null +++ b/examples/src/compatibility/level_2/a.html @@ -0,0 +1,508 @@ + + + + + compliance-level-2-test-0004 + + + + + + +

compliance-level-2-test-0004

+
+ +
+

Model diagrams

+
+
Decision Requirement Diagram
+ + + + +
+ Approval Status +
+
+
+ + + +
+ Age +
+
+
+ + + +
+ RiskCategory +
+
+
+ + + +
+ isAffordable +
+
+
+
+
+

Model elements

+
+
Approval Status
+
(Decision)
+
+ +
+
+
Output data
+
+
Name
+
Approval Status
+
Type
+
string
+
+
+
+
Decision Logic (Decision Table)
+
+
+
+
+
+
U
+
Age
+
RiskCategory
+
isAffordable
+
Approval Status
+
[10..120]
+
"High", "Low", "Medium"
+
false, true
+
"Approved", "Declined"
+
1
+
>=18
+
"Medium","Low"
+
true
+
"Approved"
+
2
+
<18
+
"Medium","Low"
+
true
+
"Declined"
+
3
+
-
+
"High"
+
true
+
"Declined"
+
4
+
-
+
-
+
false
+
"Declined"
+
+
+
+
+
+
+
Age
+
(Input Data)
+
+
Input data
+
+
Name
+
Age
+
Type
+
number
+
+
+
+
+
RiskCategory
+
(Input Data)
+
+
Input data
+
+
Name
+
RiskCategory
+
Type
+
string
+
+
+
+
+
isAffordable
+
(Input Data)
+
+
Input data
+
+
Name
+
isAffordable
+
Type
+
boolean
+
+
+
+ + diff --git a/examples/src/compatibility/level_2/mod.rs b/examples/src/compatibility/level_2/mod.rs index ba640999..949e2ffa 100644 --- a/examples/src/compatibility/level_2/mod.rs +++ b/examples/src/compatibility/level_2/mod.rs @@ -5,6 +5,7 @@ pub const IDML_2_0001: &str = include_str!("2_0001.idml"); pub const DMN_2_0002: &str = include_str!("2_0002.dmn"); pub const DMN_2_0003: &str = include_str!("2_0003.dmn"); pub const DMN_2_0004: &str = include_str!("2_0004.dmn"); +pub const IDML_2_0004: &str = include_str!("2_0004.idml"); pub const DMN_2_0005: &str = include_str!("2_0005.dmn"); pub const DMN_2_0006: &str = include_str!("2_0006.dmn"); pub const DMN_2_0007: &str = include_str!("2_0007.dmn"); diff --git a/model/src/idml_parser.rs b/model/src/idml_parser.rs index 6f117ab5..8e11ea81 100644 --- a/model/src/idml_parser.rs +++ b/model/src/idml_parser.rs @@ -1,7 +1,8 @@ use crate::errors::*; use crate::idml_utils::*; use crate::{ - Decision, Definitions, DmnId, DrgElement, ExpressionInstance, ExtensionAttribute, ExtensionElement, InformationItem, InformationRequirement, InputData, LiteralExpression, + Decision, DecisionTable, Definitions, DmnId, DrgElement, ExpressionInstance, ExtensionAttribute, ExtensionElement, InformationItem, InformationRequirement, InputData, + LiteralExpression, }; use dsntk_common::{gen_id, Result}; use dsntk_feel::{Name, FEEL_TYPE_NAME_ANY}; @@ -193,9 +194,9 @@ impl ModelParser { // if let Some(context) = self.parse_optional_context(node)? { // return Ok(Some(ExpressionInstance::Context(Box::new(context)))); // } - // if let Some(decision_table) = self.parse_optional_decision_table(node)? { - // return Ok(Some(ExpressionInstance::DecisionTable(Box::new(decision_table)))); - // } + if let Some(decision_table) = self.parse_optional_decision_table(node)? { + return Ok(Some(ExpressionInstance::DecisionTable(Box::new(decision_table)))); + } // if let Some(function_definition) = self.parse_optional_function_definition(node)? { // return Ok(Some(ExpressionInstance::FunctionDefinition(Box::new(function_definition)))); // } @@ -280,4 +281,41 @@ impl ModelParser { }; Ok(req) } + + fn parse_optional_decision_table(&self, node: &Node) -> Result> { + if let Some(child_node) = node.children().find(filter_name(IDML_DECISION_TABLE)) { + return Ok(Some(self.parse_decision_table(child_node)?)); + } + Ok(None) + } + + fn parse_decision_table(&self, node: &Node) -> Result { + let content = node.text(); + let mut decision_table: DecisionTable = dsntk_recognizer::from_unicode(content, false)?.into(); + decision_table.namespace = self.namespace.clone(); + decision_table.model_name = self.model_name.clone(); + decision_table.id = optional_id(node); + //decision_table.output_label = optional_value(node, IDML_OUTPUT_LABEL); + Ok(decision_table) + + // Ok(DecisionTable { + // namespace: self.namespace.clone(), + // model_name: self.model_name.clone(), + // id: optional_id(node), + // description: optional_description(node), + // label: optional_label(node), + // extension_elements: self.parse_extension_elements(node), + // extension_attributes: self.parse_extension_attributes(node), + // type_ref: optional_value(node, IDML_TYPE_REF), + // information_item_name: None, + // input_clauses: self.parse_decision_table_inputs(node)?, + // output_clauses: self.parse_decision_table_outputs(node)?, + // annotations: vec![], // TODO implement parsing annotations + // rules: self.parse_decision_table_rules(node)?, + // hit_policy: self.parse_hit_policy_attribute(node)?, + // aggregation: None, + // preferred_orientation: self.parse_preferred_orientation_attribute(node)?, + // output_label: optional_value(node, IDML_OUTPUT_LABEL), + // }) + } } diff --git a/model/src/idml_utils.rs b/model/src/idml_utils.rs index c584d07b..a02dbc33 100644 --- a/model/src/idml_utils.rs +++ b/model/src/idml_utils.rs @@ -4,28 +4,29 @@ use dsntk_common::{gen_id, to_uri, HRef, Result, Uri}; use dsntk_feel::Name; use idml::Node; -pub const IDML_DESCRIPTION: &str = "DESCRIPTION"; +pub const IDML_ALLOWED_ANSWERS: &str = "ALLOWED-ANSWERS"; pub const IDML_DECISION: &str = "DECISION"; +pub const IDML_DECISION_TABLE: &str = "DECISION-TABLE"; +pub const IDML_DESCRIPTION: &str = "DESCRIPTION"; pub const IDML_EXPORTER: &str = "EXPORTER"; pub const IDML_EXPORTER_VERSION: &str = "EXPORTER-VERSION"; pub const IDML_EXPRESSION_LANGUAGE: &str = "EXPRESSION-LANGUAGE"; +pub const IDML_HREF: &str = "HREF"; pub const IDML_ID: &str = "ID"; -pub const IDML_QUESTION: &str = "QUESTION"; -pub const IDML_ALLOWED_ANSWERS: &str = "ALLOWED-ANSWERS"; pub const IDML_INFORMATION_REQUIREMENT: &str = "INFORMATION-REQUIREMENT"; -pub const IDML_LITERAL_EXPRESSION: &str = "LITERAL-EXPRESSION"; -pub const IDML_LABEL: &str = "LABEL"; pub const IDML_INPUT_DATA: &str = "INPUT-DATA"; +pub const IDML_LABEL: &str = "LABEL"; +pub const IDML_LITERAL_EXPRESSION: &str = "LITERAL-EXPRESSION"; pub const IDML_NAME: &str = "NAME"; -pub const IDML_REQUIRED_INPUT: &str = "REQUIRED-INPUT"; -pub const IDML_REQUIRED_DECISION: &str = "REQUIRED-DECISION"; -pub const IDML_HREF: &str = "HREF"; - pub const IDML_NAMESPACE: &str = "NAMESPACE"; -pub const IDML_VARIABLE: &str = "VARIABLE"; +//pub const IDML_OUTPUT_LABEL: &str = "OUTPUT-LABEL"; +pub const IDML_QUESTION: &str = "QUESTION"; +pub const IDML_REQUIRED_DECISION: &str = "REQUIRED-DECISION"; +pub const IDML_REQUIRED_INPUT: &str = "REQUIRED-INPUT"; pub const IDML_TEXT: &str = "TEXT"; pub const IDML_TYPE_LANGUAGE: &str = "TYPE-LANGUAGE"; pub const IDML_TYPE_REF: &str = "TYPE-REF"; +pub const IDML_VARIABLE: &str = "VARIABLE"; /// Returns the required string value of a child node with specified name. pub fn required_value(node: &Node, child_name: &str) -> Result { diff --git a/model/tests/compatibility/level_2/idml_2_0004.rs b/model/tests/compatibility/level_2/idml_2_0004.rs new file mode 100644 index 00000000..948b5857 --- /dev/null +++ b/model/tests/compatibility/level_2/idml_2_0004.rs @@ -0,0 +1,9 @@ +use dsntk_examples::*; +use dsntk_model::*; + +#[test] +fn _0001() { + let xml = from_xml(DMN_2_0004).unwrap(); + let idml = from_idml(IDML_2_0004).unwrap(); + assert_eq!(xml, idml); +} diff --git a/model/tests/compatibility/level_2/mod.rs b/model/tests/compatibility/level_2/mod.rs index 91f0cfcf..a98c9884 100644 --- a/model/tests/compatibility/level_2/mod.rs +++ b/model/tests/compatibility/level_2/mod.rs @@ -1 +1,2 @@ mod idml_2_0001; +mod idml_2_0004; From 5c4c0f2aa50eead836c324cbec0b438ae586c7bf Mon Sep 17 00:00:00 2001 From: Dariusz Depta <141360751+DariuszDepta@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:48:13 +0100 Subject: [PATCH 3/3] Updates. --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 760d3590..7f1d7c3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "idml" -version = "0.0.1" +version = "0.1.1" [[package]] name = "idna"