diff --git a/Cargo.lock b/Cargo.lock index 89e131e6..0a9fabbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -766,6 +766,7 @@ dependencies = [ "dsntk-recognizer", "petgraph", "roxmltree", + "yaml-rust", ] [[package]] @@ -1341,6 +1342,12 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "litemap" version = "0.7.5" @@ -2672,6 +2679,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 27125dd2..b1625e88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ uriparse = "0.6.4" url = "2.5.4" urlencoding = "2.1.3" walkdir = "2.5.0" +yaml-rust = "0.4.5" dsntk-common = { path = "common" } dsntk-evaluator = { path = "evaluator" } dsntk-examples = { path = "examples" } diff --git a/Taskfile.yml b/Taskfile.yml index 4ba305e6..30648a1c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -233,6 +233,11 @@ tasks: cmds: - cmd: cargo +stable test -p dsntk-feel-parser + test-model: + desc: Runs tests in debug mode + cmds: + - cmd: cargo +stable test -p dsntk-model + test-recognizer: desc: Runs tests in debug mode cmds: diff --git a/bbt/tests/cli/noargs/0001_OK/expected b/bbt/tests/cli/noargs/0001_OK/expected index 672a28b9..6b4b22b9 100644 --- a/bbt/tests/cli/noargs/0001_OK/expected +++ b/bbt/tests/cli/noargs/0001_OK/expected @@ -1,3 +1,3 @@ -dsntk | DecisionToolkit | 0.2.0 +dsntk | DecisionToolkit | 0.3.0-dev Try 'dsntk --help' to see all available commands. For more information, visit https://decision-toolkit.org diff --git a/bbt/tests/cli/version/long-version/expected b/bbt/tests/cli/version/long-version/expected index 0ea3a944..d5109100 100644 --- a/bbt/tests/cli/version/long-version/expected +++ b/bbt/tests/cli/version/long-version/expected @@ -1 +1 @@ -0.2.0 +0.3.0-dev diff --git a/bbt/tests/cli/version/short-version/expected b/bbt/tests/cli/version/short-version/expected index 0ea3a944..d5109100 100644 --- a/bbt/tests/cli/version/short-version/expected +++ b/bbt/tests/cli/version/short-version/expected @@ -1 +1 @@ -0.2.0 +0.3.0-dev diff --git a/bbt/tests/server/action_none/expected b/bbt/tests/server/action_none/expected index 672a28b9..6b4b22b9 100644 --- a/bbt/tests/server/action_none/expected +++ b/bbt/tests/server/action_none/expected @@ -1,3 +1,3 @@ -dsntk | DecisionToolkit | 0.2.0 +dsntk | DecisionToolkit | 0.3.0-dev Try 'dsntk --help' to see all available commands. For more information, visit https://decision-toolkit.org 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/common/src/lib.rs b/common/src/lib.rs index 7cdb41b4..3055f203 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -8,6 +8,7 @@ mod href; mod idents; mod jsonify; mod namespace; +mod text_utils; mod uri; pub use errors::{DsntkError, Result, ToErrorMessage}; @@ -15,4 +16,5 @@ pub use href::HRef; pub use idents::gen_id; pub use jsonify::Jsonify; pub use namespace::to_rdnn; +pub use text_utils::trim_multiline; pub use uri::{encode_segments, to_uri, Uri}; diff --git a/common/src/text_utils.rs b/common/src/text_utils.rs new file mode 100644 index 00000000..78ef0382 --- /dev/null +++ b/common/src/text_utils.rs @@ -0,0 +1,4 @@ +/// Trims whitespaces before and after each line, preserves empty lines. +pub fn trim_multiline(input: String) -> String { + input.trim().lines().map(|line| line.trim().to_string()).collect::>().join("\n") +} 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.dmm b/examples/src/compatibility/level_2/2_0001.dmm new file mode 100644 index 00000000..a759833f --- /dev/null +++ b/examples/src/compatibility/level_2/2_0001.dmm @@ -0,0 +1,80 @@ +MODEL + 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 + INPUT #_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 + DMN_ELEMENT_REF _75b3add2-4d36-4a19-a76c-268b49b2f436 + ID _ebf33cfc-0ee3-4708-af8b-91c52237b7d6 + BOUNDS 20.0 20.0 150.0 60.0 + LABEL + TEXT `GREETING MESSAGE` + SHARED-STYLE style1 + SHARED_STYLE style1 + SHAPE + DMN_ELEMENT_REF _cba86e4d-e91c-46a2-9176-e9adf88e15db + ID _48ea7a1d-2575-4cb7-8b63-8baa4cb3b371 + 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/2_0001.dmn b/examples/src/compatibility/level_2/2_0001.dmn index 991085bb..f6140a58 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.yml b/examples/src/compatibility/level_2/2_0001.yml new file mode 100644 index 00000000..23743ac5 --- /dev/null +++ b/examples/src/compatibility/level_2/2_0001.yml @@ -0,0 +1,82 @@ +namespace: https://decision-toolkit.org/2_0001/ +name: 2_0001 +id: _2_0001 +xmlns: 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**. + +definitions: + - 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? + allowedAnswers: | + The proper greeting is in the format: Hello {customer's full name} + variable: + name: "Greeting Message" + id: _3215b422-b937-4360-9d1c-4c677cae5dfd + typeRef: string + label: "GREETING MESSAGE" + informationRequirement: + - id: _70c3f69a-63f3-4197-96ce-b206c8bd2a6b + requiredInput: + href: "#_cba86e4d-e91c-46a2-9176-e9adf88e15db" + literalExpression: + id: _5baa6245-f6fc-4685-8973-fa873817e2c1 + text: | + "Hello " + Full Name + - inputData: + 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" + typeRef: string + description: | + Full name of the person that will be sent greetings from this decision model. + +diagrams: + - diagram: + name: Decision Requirement Diagram + id: _d3a3312e-5924-4f7b-ac0e-232ef9203ff6 + resolution: 300 + size: 190.0 240.0 + shapes: + - dmnElementRef: _75b3add2-4d36-4a19-a76c-268b49b2f436 + id: _ebf33cfc-0ee3-4708-af8b-91c52237b7d6 + bounds: 20.0 20.0 150.0 60.0 + label: + text: GREETING MESSAGE + sharedStyle: style1 + sharedStyle: style1 + - id: _48ea7a1d-2575-4cb7-8b63-8baa4cb3b371 + dmnElementRef: _cba86e4d-e91c-46a2-9176-e9adf88e15db + bounds: 20.0 160.0 150.0 60.0 + sharedStyle: style2 + edges: + - dmnElementRef: _70c3f69a-63f3-4197-96ce-b206c8bd2a6b + id: _e9a73517-0ba2-4b31-b308-82279ae21591 + waypoints: + - point: 95.0 160.0 + - point: 95.0 80.0 + styles: + - id: style1 + font: bold underline italic strikethrough 18 Arial, Helvetica, sans-serif + labelVerticalAlignment: start + fillColor: 10 255 255 + strokeColor: 255 0 0 + fontColor: 0 200 0 + - id: style2 + font: bold underline 12 Arial, "Fira Sans", sans-serif + strokeColor: 255 0 0 diff --git a/examples/src/compatibility/level_2/mod.rs b/examples/src/compatibility/level_2/mod.rs index 198ad116..51dba324 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 YAML_2_0001: &str = include_str!("2_0001.yml"); 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..6fc91128 100644 --- a/model-evaluator/src/input_data.rs +++ b/model-evaluator/src/input_data.rs @@ -55,8 +55,8 @@ 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(); + fn build_evaluators(content: &str) -> (InputDataEvaluator, ItemDefinitionEvaluator) { + let definitions = dsntk_model::from_xml(content).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..80f28ab0 100644 --- a/model/Cargo.toml +++ b/model/Cargo.toml @@ -13,9 +13,12 @@ edition = { workspace = true } [dependencies] petgraph = { workspace = true } roxmltree = { workspace = true } +yaml-rust = { workspace = true } dsntk-common = { workspace = true } -dsntk-examples = { workspace = true } dsntk-feel = { workspace = true } dsntk-feel-parser = { workspace = true } dsntk-macros = { workspace = true } dsntk-recognizer = { workspace = true } + +[dev-dependencies] +dsntk-examples = { workspace = true } diff --git a/model/src/errors.rs b/model/src/errors.rs index ce72d608..ec414ae6 100644 --- a/model/src/errors.rs +++ b/model/src/errors.rs @@ -94,6 +94,14 @@ pub fn err_xml_expected_mandatory_text_content(s: &str) -> DsntkError { ModelParserError(format!("expected mandatory text content in node '{s}'")).into() } +pub fn err_yaml_parsing_model_failed(s: &str) -> DsntkError { + ModelParserError(format!("parsing model from YAML failed with reason: {s}")).into() +} + +pub fn err_yaml_expected_mandatory_attribute(attr_name: &str) -> DsntkError { + ModelParserError(format!("expected value for mandatory attribute '{attr_name}'")).into() +} + /// Errors related with validating the decision model. #[derive(ToErrorMessage)] struct ModelValidatorError(String); diff --git a/model/src/lib.rs b/model/src/lib.rs index 4e30734e..5304a789 100644 --- a/model/src/lib.rs +++ b/model/src/lib.rs @@ -4,10 +4,13 @@ extern crate dsntk_macros; mod errors; mod mapper; mod model; -mod parser; mod tests; mod validators; +mod xml_parser; mod xml_utils; +mod yaml_parser; +mod yaml_utils; pub use model::*; -pub use parser::parse; +pub use xml_parser::from_xml; +pub use yaml_parser::from_yaml; 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/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/parser.rs b/model/src/xml_parser.rs similarity index 93% rename from model/src/parser.rs rename to model/src/xml_parser.rs index b134eb2f..a704c570 100644 --- a/model/src/parser.rs +++ b/model/src/xml_parser.rs @@ -9,14 +9,14 @@ 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) => { // firstly validate the document against the XML Schema let node = validate_schema(&document)?; // initialize the model parser - let mut model_parser = ModelParser::new(); + let mut model_parser = Parser::new(); // parse the model into definitions let definitions = model_parser.parse_definitions(&node)?; // validate the final model against several rules defined in specification @@ -26,15 +26,15 @@ pub fn parse(input: &str) -> Result { } } -/// XML parser for DMN model. -pub struct ModelParser { +/// DMN model parser from XML format. +struct Parser { /// Model namespace used in parsed definitions. namespace: String, /// Model name used in parsed definitions. model_name: String, } -impl ModelParser { +impl Parser { /// Creates new model parser. fn new() -> Self { Self { @@ -46,15 +46,15 @@ impl ModelParser { /// Parses model [Definitions]. fn parse_definitions(&mut self, node: &Node) -> Result { self.namespace = required_uri(node, ATTR_NAMESPACE)?; - self.model_name = required_attribute(node, ATTR_NAME)?; + self.model_name = required_name(node)?; let definitions = Definitions { namespace: self.namespace.clone(), model_name: self.model_name.clone(), - name: required_name(node)?, + name: self.model_name.clone(), feel_name: required_feel_name(node)?, id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_uri(node, ATTR_EXPRESSION_LANGUAGE)?, @@ -86,8 +86,8 @@ impl ModelParser { let name = required_name(node)?; let feel_name = required_feel_name(node)?; let id = optional_id(node); - let description = optional_child_optional_content(node, NODE_DESCRIPTION); - let label = optional_attribute(node, ATTR_LABEL); + let description = optional_description(node); + let label = optional_label(node); let extension_elements = self.parse_extension_elements(node); let extension_attributes = self.parse_extension_attributes(node); let type_language = optional_attribute(node, ATTR_TYPE_LANGUAGE); @@ -167,8 +167,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(child_node), - description: optional_child_optional_content(child_node, NODE_DESCRIPTION), - label: optional_attribute(child_node, ATTR_LABEL), + 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, @@ -195,12 +195,12 @@ impl ModelParser { name, feel_name, id: optional_id(child_node), - description: optional_child_optional_content(child_node, NODE_DESCRIPTION), - label: optional_attribute(child_node, ATTR_LABEL), + 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_child_optional_content(child_node, NODE_QUESTION), - allowed_answers: optional_child_optional_content(child_node, NODE_ALLOWED_ANSWERS), + question: optional_child_optional_content(child_node, NODE_QUESTION).map(|value| value.trim().to_string()), + allowed_answers: optional_child_optional_content(child_node, NODE_ALLOWED_ANSWERS).map(|value| value.trim().to_string()), variable, decision_logic: self.parse_optional_child_expression_instance(child_node)?, information_requirements: self.parse_information_requirements(child_node, NODE_INFORMATION_REQUIREMENT)?, @@ -227,8 +227,8 @@ impl ModelParser { name, feel_name, id: optional_id(child_node), - description: optional_child_optional_content(child_node, NODE_DESCRIPTION), - label: optional_attribute(child_node, ATTR_LABEL), + 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), variable, @@ -256,8 +256,8 @@ impl ModelParser { name, feel_name, id: optional_id(child_node), - description: optional_child_optional_content(child_node, NODE_DESCRIPTION), - label: optional_attribute(child_node, ATTR_LABEL), + 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), variable, @@ -279,8 +279,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(child_node), - description: optional_child_optional_content(child_node, NODE_DESCRIPTION), - label: optional_attribute(child_node, ATTR_LABEL), + 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: required_name(child_node)?, @@ -323,8 +323,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -355,8 +355,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(child_node), - description: optional_child_optional_content(child_node, NODE_DESCRIPTION), - label: optional_attribute(child_node, ATTR_LABEL), + 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: required_name(child_node)?, @@ -371,8 +371,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(child_node), - description: optional_child_optional_content(child_node, NODE_DESCRIPTION), - label: optional_attribute(child_node, ATTR_LABEL), + 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: required_name(child_node)?, @@ -394,8 +394,8 @@ impl ModelParser { namespace: required_uri(child_node, ATTR_NAMESPACE)?, model_name: self.model_name.clone(), id: optional_id(child_node), - description: optional_child_optional_content(child_node, NODE_DESCRIPTION), - label: optional_attribute(child_node, ATTR_LABEL), + 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: required_name(child_node)?, @@ -441,8 +441,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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)?, @@ -484,8 +484,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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, NODE_REQUIRED_DECISION)?, @@ -509,8 +509,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + description: optional_description(node), + label: optional_label(node), extension_elements: self.parse_extension_elements(node), extension_attributes: self.parse_extension_attributes(node), required_knowledge: required_child_required_href(node, NODE_REQUIRED_KNOWLEDGE)?, @@ -533,8 +533,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + description: optional_description(node), + label: optional_label(node), extension_elements: self.parse_extension_elements(node), extension_attributes: self.parse_extension_attributes(node), required_authority: optional_child_required_href(node, NODE_REQUIRED_AUTHORITY)?, @@ -638,8 +638,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -772,8 +772,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -804,8 +804,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -850,8 +850,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -875,8 +875,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -914,8 +914,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(row_node), - description: optional_child_optional_content(row_node, NODE_DESCRIPTION), - label: optional_attribute(row_node, ATTR_LABEL), + description: optional_description(row_node), + label: optional_label(row_node), extension_elements: self.parse_extension_elements(row_node), extension_attributes: self.parse_extension_attributes(row_node), type_ref: optional_attribute(row_node, ATTR_TYPE_REF), @@ -926,8 +926,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -951,8 +951,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -976,8 +976,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -1000,8 +1000,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -1025,8 +1025,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), @@ -1050,8 +1050,8 @@ impl ModelParser { namespace: self.namespace.clone(), model_name: self.model_name.clone(), id: optional_id(node), - description: optional_child_optional_content(node, NODE_DESCRIPTION), - label: optional_attribute(node, ATTR_LABEL), + 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_attribute(node, ATTR_TYPE_REF), diff --git a/model/src/xml_utils.rs b/model/src/xml_utils.rs index 92afc9bd..41ee673f 100644 --- a/model/src/xml_utils.rs +++ b/model/src/xml_utils.rs @@ -1,7 +1,7 @@ //! # XML utilities use crate::errors::*; -use dsntk_common::Result; +use dsntk_common::{trim_multiline, Result}; use roxmltree::Node; use std::str::FromStr; @@ -257,3 +257,13 @@ pub fn node_name_pos(node: &Node) -> String { pub fn name_eq(name: &str) -> impl Fn(&Node) -> bool + '_ { move |node: &Node| node.tag_name().name() == name } + +/// Returns the optional description. +pub fn optional_description(node: &Node) -> Option { + optional_child_optional_content(node, NODE_DESCRIPTION).map(trim_multiline) +} + +/// Returns the optional label. +pub fn optional_label(node: &Node) -> Option { + optional_attribute(node, ATTR_LABEL).map(|value| value.trim().to_string()) +} diff --git a/model/src/yaml_parser.rs b/model/src/yaml_parser.rs new file mode 100644 index 00000000..9c6f22f4 --- /dev/null +++ b/model/src/yaml_parser.rs @@ -0,0 +1,288 @@ +//! # YAML parser for DMN model + +use super::errors::*; +use crate::yaml_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 yaml_rust::{Yaml, YamlLoader}; + +/// Parses the YAML input document containing DMN model into [Definitions]. +pub fn from_yaml(input: &str) -> Result { + match YamlLoader::load_from_str(input) { + Ok(docs) => match docs.len() { + 0 => Err(err_yaml_parsing_model_failed("empty YAML model")), + 1 => { + let mut parser = Parser::new(); + parser.parse_definitions(&docs[0]) + } + other => Err(err_yaml_parsing_model_failed(&format!("expected only one document in YAML model, actual is {}", other))), + }, + Err(reason) => Err(err_yaml_parsing_model_failed(&reason.to_string())), + } +} + +/// DMN model parser from YAML format. +struct Parser { + /// Model namespace used in parsed definitions. + namespace: String, + /// Model name used in parsed definitions. + model_name: String, +} + +impl Parser { + /// Creates new model parser. + fn new() -> Self { + Self { + namespace: "".to_string(), + model_name: "".to_string(), + } + } + + /// Parses model [Definitions]. + fn parse_definitions(&mut self, yaml: &Yaml) -> Result { + self.namespace = required_uri(yaml, YAML_NAMESPACE)?; + self.model_name = required_name(yaml)?; + let definitions = Definitions { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + name: self.model_name.clone(), + feel_name: required_feel_name(yaml)?, + id: optional_id(yaml), + description: optional_description(yaml), + label: optional_label(yaml), + extension_elements: self.parse_extension_elements(yaml), + extension_attributes: self.parse_extension_attributes(yaml), + expression_language: optional_uri(yaml, YAML_EXPRESSION_LANGUAGE)?, + type_language: optional_attribute(yaml, YAML_TYPE_LANGUAGE), + exporter: None, + exporter_version: None, + item_definitions: vec![], + drg_elements: self.parse_drg_elements(yaml)?, + business_context_elements: vec![], + imports: vec![], + dmndi: None, + }; + Ok(definitions) + } + + /// Parses DRG elements. + fn parse_drg_elements(&mut self, yaml: &Yaml) -> Result> { + let mut drg_elements = vec![]; + let mut input_data_items = vec![]; + let mut decision_items = vec![]; + if let Some(definitions) = yaml[YAML_DEFINITIONS].as_vec() { + for item in definitions { + if let Some(input_data_yaml) = scalar(item, YAML_INPUT_DATA) { + input_data_items.push(self.parse_input_data(input_data_yaml)?); + } + if let Some(decision_yaml) = scalar(item, YAML_DECISION) { + decision_items.push(self.parse_decision(decision_yaml)?); + } + } + } + drg_elements.append(&mut input_data_items); + drg_elements.append(&mut decision_items); + // 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]. + fn parse_input_data(&self, yaml: &Yaml) -> Result { + let name = required_name(yaml)?; + let feel_name = required_feel_name(yaml)?; + let variable = self + .parse_opt_information_item_child(yaml, YAML_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(yaml), + description: optional_description(yaml), + label: optional_label(yaml), + extension_elements: self.parse_extension_elements(yaml), + extension_attributes: self.parse_extension_attributes(yaml), + name, + feel_name, + variable, + }; + Ok(DrgElement::InputData(input_data)) + } + + /// Parses [Decision]. + fn parse_decision(&mut self, yaml: &Yaml) -> Result { + let name = required_name(yaml)?; + let feel_name = required_feel_name(yaml)?; + let variable = self + .parse_opt_information_item_child(yaml, YAML_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(yaml), + description: optional_description(yaml), + label: optional_label(yaml), + extension_elements: self.parse_extension_elements(yaml), + extension_attributes: self.parse_extension_attributes(yaml), + question: optional_attribute(yaml, YAML_QUESTION).map(|value| value.trim().to_string()), + allowed_answers: optional_attribute(yaml, YAML_ALLOWED_ANSWERS).map(|value| value.trim().to_string()), + variable, + decision_logic: self.parse_optional_child_expression_instance(yaml)?, + information_requirements: self.parse_information_requirements(yaml, YAML_INFORMATION_REQUIREMENT)?, + knowledge_requirements: vec![], // self.parse_knowledge_requirements(child_node, NODE_KNOWLEDGE_REQUIREMENT)?, + authority_requirements: vec![], // self.parse_authority_requirements(child_node, NODE_AUTHORITY_REQUIREMENT)?, + }; + Ok(DrgElement::Decision(decision)) + } + + fn parse_optional_child_expression_instance(&self, yaml: &Yaml) -> 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(yaml) { + 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 literal expression attribute. + /// If found, then parses literal expression and returns it, otherwise returns [None]. + fn parse_optional_literal_expression(&self, yaml: &Yaml) -> Option { + scalar(yaml, YAML_LITERAL_EXPRESSION).map(|literal_expression_yaml| self.parse_literal_expression(literal_expression_yaml)) + } + + /// Parses [LiteralExpression]. + fn parse_literal_expression(&self, yaml: &Yaml) -> LiteralExpression { + LiteralExpression { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + id: optional_id(yaml), + description: optional_description(yaml), + label: optional_label(yaml), + extension_elements: self.parse_extension_elements(yaml), + extension_attributes: self.parse_extension_attributes(yaml), + type_ref: optional_attribute(yaml, YAML_TYPE_REF), + text: optional_attribute(yaml, YAML_TEXT), + expression_language: optional_attribute(yaml, YAML_EXPRESSION_LANGUAGE), + imported_values: None, + } + } + + /// Parses an optional [InformationItem]. + fn parse_opt_information_item_child(&self, yaml: &Yaml, attr_name: &str) -> Result> { + Ok(if let Some(information_item_yaml) = scalar(yaml, attr_name) { + Some(self.parse_information_item(information_item_yaml)?) + } else { + None + }) + } + + /// Parses [InformationItem]. + fn parse_information_item(&self, yaml: &Yaml) -> Result { + Ok(InformationItem { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + id: optional_id(yaml), + description: optional_description(yaml), + label: optional_label(yaml), + extension_elements: self.parse_extension_elements(yaml), + extension_attributes: self.parse_extension_attributes(yaml), + name: required_name(yaml)?, + feel_name: required_feel_name(yaml)?, + type_ref: optional_attribute(yaml, YAML_TYPE_REF).unwrap_or(FEEL_TYPE_NAME_ANY.to_string()), + feel_type: None, + }) + } + + /// Creates a new [InformationItem]. + 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 a list of [InformationRequirement]. + fn parse_information_requirements(&mut self, node: &Yaml, child_name: &str) -> Result> { + let mut information_requirements = vec![]; + if let Some(items) = node[child_name].as_vec() { + for item in items { + information_requirements.push(self.parse_information_requirement(item)?); + } + } + Ok(information_requirements) + } + + /// Parses [InformationRequirement]. + fn parse_information_requirement(&mut self, yaml: &Yaml) -> Result { + let req = InformationRequirement { + namespace: self.namespace.clone(), + model_name: self.model_name.clone(), + id: optional_id(yaml), + description: optional_description(yaml), + label: optional_label(yaml), + extension_elements: self.parse_extension_elements(yaml), + extension_attributes: self.parse_extension_attributes(yaml), + required_decision: optional_attribute_required_href(yaml, YAML_REQUIRED_DECISION)?, + required_input: optional_attribute_required_href(yaml, YAML_REQUIRED_INPUT)?, + }; + Ok(req) + } + + /// Parses extension elements. + fn parse_extension_elements(&self, _yaml: &Yaml) -> Vec { + // Currently ignored. Ready for future development when needed. + vec![] + } + + /// Parses extension attributes. + fn parse_extension_attributes(&self, _yaml: &Yaml) -> Vec { + // Currently ignored. Ready for future development when needed. + vec![] + } +} diff --git a/model/src/yaml_utils.rs b/model/src/yaml_utils.rs new file mode 100644 index 00000000..f7ec3103 --- /dev/null +++ b/model/src/yaml_utils.rs @@ -0,0 +1,102 @@ +use crate::errors::*; +use crate::DmnId; +use dsntk_common::{gen_id, to_uri, trim_multiline, HRef, Result, Uri}; +use dsntk_feel::Name; +use yaml_rust::Yaml; + +pub const YAML_ALLOWED_ANSWERS: &str = "allowedAnswers"; +pub const YAML_DECISION: &str = "decision"; +pub const YAML_DEFINITIONS: &str = "definitions"; +const YAML_DESCRIPTION: &str = "description"; +pub const YAML_EXPRESSION_LANGUAGE: &str = "expressionLanguage"; +pub const YAML_HREF: &str = "href"; +const YAML_LABEL: &str = "label"; +pub const YAML_ID: &str = "id"; +pub const YAML_INFORMATION_REQUIREMENT: &str = "informationRequirement"; +pub const YAML_INPUT_DATA: &str = "inputData"; +pub const YAML_LITERAL_EXPRESSION: &str = "literalExpression"; +pub const YAML_NAME: &str = "name"; +pub const YAML_NAMESPACE: &str = "namespace"; +pub const YAML_QUESTION: &str = "question"; +pub const YAML_REQUIRED_DECISION: &str = "requiredDecision"; +pub const YAML_REQUIRED_INPUT: &str = "requiredInput"; +pub const YAML_TEXT: &str = "text"; +pub const YAML_TYPE_LANGUAGE: &str = "typeLanguage"; +pub const YAML_TYPE_REF: &str = "typeRef"; +pub const YAML_VARIABLE: &str = "variable"; + +/// Returns the value of the required attribute. +pub fn required_attribute(yaml: &Yaml, attr_name: &str) -> Result { + Ok( + scalar(yaml, attr_name) + .ok_or(err_yaml_expected_mandatory_attribute(attr_name))? + .as_str() + .ok_or(err_yaml_expected_mandatory_attribute(attr_name))? + .trim() + .to_string(), + ) +} + +/// Returns the value of the optional attribute. +pub fn optional_attribute(yaml: &Yaml, attr_name: &str) -> Option { + scalar(yaml, attr_name).map(|value| value.as_str().map(|value| value.trim().to_string())).flatten() +} + +/// Returns the required URI attribute. +pub fn required_uri(yaml: &Yaml, attr_name: &str) -> Result { + to_uri(required_attribute(yaml, attr_name)?.as_str()) +} + +/// Returns an optional URI attribute. +pub fn optional_uri(node: &Yaml, attr_name: &str) -> Result> { + Ok(if let Some(value) = optional_attribute(node, attr_name) { + Some(to_uri(value.as_str())?) + } else { + None + }) +} + +/// Returns required name attribute. +pub fn required_name(yaml: &Yaml) -> Result { + required_attribute(yaml, YAML_NAME) +} + +/// Returns optional identifier if provided in the model, otherwise generates a new one. +pub fn optional_id(yaml: &Yaml) -> DmnId { + optional_attribute(yaml, YAML_ID).map(DmnId::Provided).unwrap_or(DmnId::Generated(gen_id())) +} + +/// Returns the required FEEL name. +pub fn required_feel_name(node: &Yaml) -> Result { + let input = required_name(node)?; + Ok(dsntk_feel_parser::parse_longest_name(&input).unwrap_or(input.into())) +} + +/// Returns optional description. +pub fn optional_description(yaml: &Yaml) -> Option { + optional_attribute(yaml, YAML_DESCRIPTION).map(trim_multiline) +} + +/// Returns optional label. +pub fn optional_label(yaml: &Yaml) -> Option { + optional_attribute(yaml, YAML_LABEL).map(|value| value.trim().to_string()) +} + +/// Returns the required `href` attribute of the specified optional attribute. +pub fn optional_attribute_required_href(yaml: &Yaml, attr_name: &str) -> Result> { + Ok(if let Some(child_yaml) = scalar(yaml, attr_name) { + Some(HRef::try_from(required_attribute(child_yaml, YAML_HREF)?.as_str())?) + } else { + None + }) +} + +/// Returns a scalar attribute with specified name. +pub fn scalar<'a>(yaml: &'a Yaml, attr_name: &'a str) -> Option<&'a Yaml> { + let value = &yaml[attr_name]; + if value.is_badvalue() || value.is_null() || value.is_array() { + None + } else { + Some(value) + } +} diff --git a/model/tests/compatibility/level_2/mod.rs b/model/tests/compatibility/level_2/mod.rs new file mode 100644 index 00000000..baf34115 --- /dev/null +++ b/model/tests/compatibility/level_2/mod.rs @@ -0,0 +1 @@ +mod yml_2_0001; diff --git a/model/tests/compatibility/level_2/yml_2_0001.rs b/model/tests/compatibility/level_2/yml_2_0001.rs new file mode 100644 index 00000000..b44d4074 --- /dev/null +++ b/model/tests/compatibility/level_2/yml_2_0001.rs @@ -0,0 +1,9 @@ +use dsntk_examples::*; +use dsntk_model::{from_xml, from_yaml}; + +#[test] +fn _0001() { + let xml = from_xml(DMN_2_0001).unwrap(); + let yaml = from_yaml(YAML_2_0001).unwrap(); + assert_eq!(xml, yaml); +} 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/invalid_models/mod.rs b/model/tests/invalid_models/mod.rs new file mode 100644 index 00000000..a651cea0 --- /dev/null +++ b/model/tests/invalid_models/mod.rs @@ -0,0 +1 @@ +mod yaml; diff --git a/model/tests/invalid_models/yaml/basic.rs b/model/tests/invalid_models/yaml/basic.rs new file mode 100644 index 00000000..489cb5af --- /dev/null +++ b/model/tests/invalid_models/yaml/basic.rs @@ -0,0 +1,37 @@ +use dsntk_model::from_yaml; + +#[test] +fn _0001() { + // Parsing an invalid file should fail. + let yaml = r#"key: one\n key: one"#; + assert_eq!( + " parsing model from YAML failed with reason: mapping values are not allowed in this context at line 1 column 15", + from_yaml(yaml).unwrap_err().to_string() + ); +} + +#[test] +fn _0002() { + // Parsing an empty file should fail. + let yaml = r#""#; + assert_eq!( + " parsing model from YAML failed with reason: empty YAML model", + from_yaml(yaml).unwrap_err().to_string() + ); +} + +#[test] +fn _0003() { + // Parsing multiple documents in one file should fail. + let yaml = r#" +key: "1st document" +--- +key: "2nd document" +--- +key: "3rd document" + "#; + assert_eq!( + " parsing model from YAML failed with reason: expected only one document in YAML model, actual is 3", + from_yaml(yaml).unwrap_err().to_string() + ); +} diff --git a/model/tests/invalid_models/yaml/mod.rs b/model/tests/invalid_models/yaml/mod.rs new file mode 100644 index 00000000..1bca5f8c --- /dev/null +++ b/model/tests/invalid_models/yaml/mod.rs @@ -0,0 +1 @@ +mod basic; diff --git a/model/tests/mod.rs b/model/tests/mod.rs new file mode 100644 index 00000000..91b83dcc --- /dev/null +++ b/model/tests/mod.rs @@ -0,0 +1,2 @@ +mod compatibility; +mod invalid_models; 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() {