decision_agent 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +41 -1
- data/bin/decision_agent +104 -0
- data/lib/decision_agent/dmn/adapter.rb +135 -0
- data/lib/decision_agent/dmn/cache.rb +306 -0
- data/lib/decision_agent/dmn/decision_graph.rb +327 -0
- data/lib/decision_agent/dmn/decision_tree.rb +192 -0
- data/lib/decision_agent/dmn/errors.rb +30 -0
- data/lib/decision_agent/dmn/exporter.rb +217 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +797 -0
- data/lib/decision_agent/dmn/feel/functions.rb +420 -0
- data/lib/decision_agent/dmn/feel/parser.rb +349 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
- data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
- data/lib/decision_agent/dmn/feel/types.rb +276 -0
- data/lib/decision_agent/dmn/importer.rb +77 -0
- data/lib/decision_agent/dmn/model.rb +197 -0
- data/lib/decision_agent/dmn/parser.rb +191 -0
- data/lib/decision_agent/dmn/testing.rb +333 -0
- data/lib/decision_agent/dmn/validator.rb +315 -0
- data/lib/decision_agent/dmn/versioning.rb +229 -0
- data/lib/decision_agent/dmn/visualizer.rb +513 -0
- data/lib/decision_agent/dsl/condition_evaluator.rb +3 -0
- data/lib/decision_agent/dsl/schema_validator.rb +2 -1
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +221 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/web/dmn_editor.rb +426 -0
- data/lib/decision_agent/web/public/dmn-editor.css +596 -0
- data/lib/decision_agent/web/public/dmn-editor.html +250 -0
- data/lib/decision_agent/web/public/dmn-editor.js +553 -0
- data/lib/decision_agent/web/public/index.html +3 -0
- data/lib/decision_agent/web/public/styles.css +21 -0
- data/lib/decision_agent/web/server.rb +465 -0
- data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
- data/spec/auth/rbac_adapter_spec.rb +228 -0
- data/spec/dmn/decision_graph_spec.rb +282 -0
- data/spec/dmn/decision_tree_spec.rb +203 -0
- data/spec/dmn/feel/errors_spec.rb +18 -0
- data/spec/dmn/feel/functions_spec.rb +400 -0
- data/spec/dmn/feel/simple_parser_spec.rb +274 -0
- data/spec/dmn/feel/types_spec.rb +176 -0
- data/spec/dmn/feel_parser_spec.rb +489 -0
- data/spec/dmn/hit_policy_spec.rb +202 -0
- data/spec/dmn/integration_spec.rb +226 -0
- data/spec/examples.txt +1846 -1570
- data/spec/fixtures/dmn/complex_decision.dmn +81 -0
- data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
- data/spec/fixtures/dmn/simple_decision.dmn +40 -0
- data/spec/monitoring/metrics_collector_spec.rb +37 -35
- data/spec/monitoring/monitored_agent_spec.rb +14 -11
- data/spec/performance_optimizations_spec.rb +10 -3
- data/spec/thread_safety_spec.rb +10 -2
- data/spec/web_ui_rack_spec.rb +294 -0
- metadata +65 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require_relative "parser"
|
|
2
|
+
require_relative "validator"
|
|
3
|
+
require_relative "adapter"
|
|
4
|
+
require_relative "../versioning/version_manager"
|
|
5
|
+
|
|
6
|
+
module DecisionAgent
|
|
7
|
+
module Dmn
|
|
8
|
+
# Imports DMN XML files into DecisionAgent
|
|
9
|
+
class Importer
|
|
10
|
+
def initialize(version_manager: nil)
|
|
11
|
+
@version_manager = version_manager || Versioning::VersionManager.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Import DMN file
|
|
15
|
+
# @param file_path [String] Path to DMN XML file
|
|
16
|
+
# @param ruleset_name [String, nil] Optional custom ruleset name
|
|
17
|
+
# @param created_by [String] User who imported
|
|
18
|
+
# @return [Hash] Import result with model and version info
|
|
19
|
+
def import(file_path, ruleset_name: nil, created_by: "system")
|
|
20
|
+
xml_content = File.read(file_path)
|
|
21
|
+
import_from_xml(xml_content, ruleset_name: ruleset_name, created_by: created_by)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Import from XML string
|
|
25
|
+
def import_from_xml(xml_content, ruleset_name: nil, created_by: "system")
|
|
26
|
+
# Parse DMN XML
|
|
27
|
+
parser = Parser.new(xml_content)
|
|
28
|
+
model = parser.parse
|
|
29
|
+
|
|
30
|
+
# Validate model
|
|
31
|
+
validator = Validator.new(model)
|
|
32
|
+
validator.validate!
|
|
33
|
+
|
|
34
|
+
# Convert to JSON rules
|
|
35
|
+
results = convert_model_to_rules(model)
|
|
36
|
+
|
|
37
|
+
# Store in versioning system
|
|
38
|
+
versions = store_in_versioning(results, ruleset_name, created_by)
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
model: model,
|
|
42
|
+
rules: results,
|
|
43
|
+
versions: versions,
|
|
44
|
+
decisions_imported: results.size
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def convert_model_to_rules(model)
|
|
51
|
+
model.decisions.map do |decision|
|
|
52
|
+
next unless decision.decision_table
|
|
53
|
+
|
|
54
|
+
adapter = Adapter.new(decision.decision_table)
|
|
55
|
+
{
|
|
56
|
+
decision_id: decision.id,
|
|
57
|
+
decision_name: decision.name,
|
|
58
|
+
rules: adapter.to_json_rules
|
|
59
|
+
}
|
|
60
|
+
end.compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def store_in_versioning(results, ruleset_name, created_by)
|
|
64
|
+
results.map do |result|
|
|
65
|
+
rule_id = ruleset_name || result[:decision_id]
|
|
66
|
+
|
|
67
|
+
@version_manager.save_version(
|
|
68
|
+
rule_id: rule_id,
|
|
69
|
+
rule_content: result[:rules],
|
|
70
|
+
created_by: created_by,
|
|
71
|
+
changelog: "Imported DMN decision: #{result[:decision_name]}"
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
require_relative "errors"
|
|
2
|
+
|
|
3
|
+
module DecisionAgent
|
|
4
|
+
module Dmn
|
|
5
|
+
# Root DMN model containing all decisions
|
|
6
|
+
class Model
|
|
7
|
+
attr_reader :id, :name, :namespace, :decisions
|
|
8
|
+
|
|
9
|
+
def initialize(id:, name:, namespace: "http://decision_agent.local")
|
|
10
|
+
@id = id.to_s
|
|
11
|
+
@name = name.to_s
|
|
12
|
+
@namespace = namespace.to_s
|
|
13
|
+
@decisions = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_decision(decision)
|
|
17
|
+
raise TypeError, "Expected Decision, got #{decision.class}" unless decision.is_a?(Decision)
|
|
18
|
+
|
|
19
|
+
@decisions << decision
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find_decision(decision_id)
|
|
23
|
+
@decisions.find { |d| d.id == decision_id.to_s }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def freeze
|
|
27
|
+
@id.freeze
|
|
28
|
+
@name.freeze
|
|
29
|
+
@namespace.freeze
|
|
30
|
+
@decisions.each(&:freeze)
|
|
31
|
+
@decisions.freeze
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Represents a single decision element
|
|
37
|
+
class Decision
|
|
38
|
+
attr_accessor :decision_tree
|
|
39
|
+
attr_reader :id, :name, :decision_table, :description, :information_requirements
|
|
40
|
+
|
|
41
|
+
def initialize(id:, name:, description: nil)
|
|
42
|
+
@id = id.to_s
|
|
43
|
+
@name = name.to_s
|
|
44
|
+
@description = description&.to_s
|
|
45
|
+
@decision_table = nil
|
|
46
|
+
@decision_tree = nil
|
|
47
|
+
@information_requirements = []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def decision_table=(table)
|
|
51
|
+
raise TypeError, "Expected DecisionTable, got #{table.class}" unless table.is_a?(DecisionTable)
|
|
52
|
+
|
|
53
|
+
@decision_table = table
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_information_requirement(requirement)
|
|
57
|
+
@information_requirements << requirement
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def freeze
|
|
61
|
+
@id.freeze
|
|
62
|
+
@name.freeze
|
|
63
|
+
@description.freeze
|
|
64
|
+
@decision_table&.freeze
|
|
65
|
+
@decision_tree&.freeze
|
|
66
|
+
@information_requirements.freeze
|
|
67
|
+
super
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Decision table with inputs, outputs, rules, and hit policy
|
|
72
|
+
class DecisionTable
|
|
73
|
+
attr_reader :id, :hit_policy, :inputs, :outputs, :rules
|
|
74
|
+
|
|
75
|
+
VALID_HIT_POLICIES = %w[UNIQUE FIRST PRIORITY ANY COLLECT].freeze
|
|
76
|
+
|
|
77
|
+
def initialize(id:, hit_policy: "UNIQUE")
|
|
78
|
+
@id = id.to_s
|
|
79
|
+
validate_hit_policy!(hit_policy)
|
|
80
|
+
@hit_policy = hit_policy.to_s
|
|
81
|
+
@inputs = []
|
|
82
|
+
@outputs = []
|
|
83
|
+
@rules = []
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def add_input(input)
|
|
87
|
+
raise TypeError, "Expected Input, got #{input.class}" unless input.is_a?(Input)
|
|
88
|
+
|
|
89
|
+
@inputs << input
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def add_output(output)
|
|
93
|
+
raise TypeError, "Expected Output, got #{output.class}" unless output.is_a?(Output)
|
|
94
|
+
|
|
95
|
+
@outputs << output
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def add_rule(rule)
|
|
99
|
+
raise TypeError, "Expected Rule, got #{rule.class}" unless rule.is_a?(Rule)
|
|
100
|
+
|
|
101
|
+
@rules << rule
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def freeze
|
|
105
|
+
@id.freeze
|
|
106
|
+
@hit_policy.freeze
|
|
107
|
+
@inputs.each(&:freeze)
|
|
108
|
+
@inputs.freeze
|
|
109
|
+
@outputs.each(&:freeze)
|
|
110
|
+
@outputs.freeze
|
|
111
|
+
@rules.each(&:freeze)
|
|
112
|
+
@rules.freeze
|
|
113
|
+
super
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def validate_hit_policy!(policy)
|
|
119
|
+
return if VALID_HIT_POLICIES.include?(policy.to_s)
|
|
120
|
+
|
|
121
|
+
raise UnsupportedHitPolicyError,
|
|
122
|
+
"Hit policy '#{policy}' not supported. " \
|
|
123
|
+
"Supported: #{VALID_HIT_POLICIES.join(', ')}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Input clause (column) in decision table
|
|
128
|
+
class Input
|
|
129
|
+
attr_reader :id, :label, :expression, :type_ref
|
|
130
|
+
|
|
131
|
+
def initialize(id:, label:, expression: nil, type_ref: "string")
|
|
132
|
+
@id = id.to_s
|
|
133
|
+
@label = label.to_s
|
|
134
|
+
@expression = expression&.to_s || label.to_s
|
|
135
|
+
@type_ref = type_ref.to_s
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def freeze
|
|
139
|
+
@id.freeze
|
|
140
|
+
@label.freeze
|
|
141
|
+
@expression.freeze
|
|
142
|
+
@type_ref.freeze
|
|
143
|
+
super
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Output clause (result column) in decision table
|
|
148
|
+
class Output
|
|
149
|
+
attr_reader :id, :label, :name, :type_ref
|
|
150
|
+
|
|
151
|
+
def initialize(id:, label:, name: nil, type_ref: "string")
|
|
152
|
+
@id = id.to_s
|
|
153
|
+
@label = label.to_s
|
|
154
|
+
@name = (name || label).to_s
|
|
155
|
+
@type_ref = type_ref.to_s
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def freeze
|
|
159
|
+
@id.freeze
|
|
160
|
+
@label.freeze
|
|
161
|
+
@name.freeze
|
|
162
|
+
@type_ref.freeze
|
|
163
|
+
super
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Decision table rule (row)
|
|
168
|
+
class Rule
|
|
169
|
+
attr_reader :id, :input_entries, :output_entries, :description
|
|
170
|
+
|
|
171
|
+
def initialize(id:, description: nil)
|
|
172
|
+
@id = id.to_s
|
|
173
|
+
@description = description&.to_s
|
|
174
|
+
@input_entries = []
|
|
175
|
+
@output_entries = []
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def add_input_entry(entry)
|
|
179
|
+
@input_entries << entry.to_s
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def add_output_entry(entry)
|
|
183
|
+
@output_entries << entry
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def freeze
|
|
187
|
+
@id.freeze
|
|
188
|
+
@description.freeze
|
|
189
|
+
@input_entries.each(&:freeze)
|
|
190
|
+
@input_entries.freeze
|
|
191
|
+
@output_entries.map { |e| e.freeze if e.respond_to?(:freeze) }
|
|
192
|
+
@output_entries.freeze
|
|
193
|
+
super
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
require_relative "model"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module DecisionAgent
|
|
7
|
+
module Dmn
|
|
8
|
+
# Parses DMN 1.3 XML files into Ruby model objects
|
|
9
|
+
class Parser
|
|
10
|
+
NAMESPACES = {
|
|
11
|
+
"dmn" => "https://www.omg.org/spec/DMN/20191111/MODEL/",
|
|
12
|
+
"dmn11" => "http://www.omg.org/spec/DMN/20151101/dmn.xsd",
|
|
13
|
+
"dmn13" => "https://www.omg.org/spec/DMN/20191111/MODEL/"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(xml_content)
|
|
17
|
+
@xml_content = xml_content
|
|
18
|
+
@doc = nil
|
|
19
|
+
@namespace = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse
|
|
23
|
+
parse_xml
|
|
24
|
+
extract_model
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def parse_xml
|
|
30
|
+
@doc = Nokogiri::XML(@xml_content)
|
|
31
|
+
|
|
32
|
+
if @doc.errors.any?
|
|
33
|
+
raise InvalidDmnXmlError,
|
|
34
|
+
"XML parsing failed: #{@doc.errors.map(&:to_s).join(', ')}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Detect namespace
|
|
38
|
+
@namespace = detect_namespace
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def detect_namespace
|
|
42
|
+
if @doc.root.namespace&.href&.include?("20191111")
|
|
43
|
+
NAMESPACES["dmn13"]
|
|
44
|
+
elsif @doc.root.namespace&.href&.include?("20151101")
|
|
45
|
+
NAMESPACES["dmn11"]
|
|
46
|
+
else
|
|
47
|
+
# Default to DMN 1.3
|
|
48
|
+
NAMESPACES["dmn13"]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_model
|
|
53
|
+
definitions = @doc.at_xpath("//dmn:definitions", NAMESPACES) ||
|
|
54
|
+
@doc.at_xpath("//*[local-name()='definitions']")
|
|
55
|
+
|
|
56
|
+
raise InvalidDmnXmlError, "No definitions element found" unless definitions
|
|
57
|
+
|
|
58
|
+
model = Model.new(
|
|
59
|
+
id: definitions["id"] || "model",
|
|
60
|
+
name: definitions["name"] || "DMN Model",
|
|
61
|
+
namespace: definitions["namespace"] || @namespace
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Parse all decisions
|
|
65
|
+
decisions = @doc.xpath("//dmn:decision", NAMESPACES)
|
|
66
|
+
decisions = @doc.xpath("//*[local-name()='decision']") if decisions.empty?
|
|
67
|
+
|
|
68
|
+
decisions.each do |decision_node|
|
|
69
|
+
decision = parse_decision(decision_node)
|
|
70
|
+
model.add_decision(decision)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
model.freeze
|
|
74
|
+
model
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_decision(node)
|
|
78
|
+
decision = Decision.new(
|
|
79
|
+
id: node["id"] || SecureRandom.uuid,
|
|
80
|
+
name: node["name"] || "Unnamed Decision",
|
|
81
|
+
description: extract_description(node)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Parse decision table if present
|
|
85
|
+
table_node = node.at_xpath(".//dmn:decisionTable", NAMESPACES) ||
|
|
86
|
+
node.at_xpath(".//*[local-name()='decisionTable']")
|
|
87
|
+
|
|
88
|
+
decision.decision_table = parse_decision_table(table_node) if table_node
|
|
89
|
+
|
|
90
|
+
decision.freeze
|
|
91
|
+
decision
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def parse_decision_table(node)
|
|
95
|
+
table = DecisionTable.new(
|
|
96
|
+
id: node["id"] || SecureRandom.uuid,
|
|
97
|
+
hit_policy: node["hitPolicy"] || "UNIQUE"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Parse inputs
|
|
101
|
+
inputs = node.xpath(".//dmn:input", NAMESPACES)
|
|
102
|
+
inputs = node.xpath(".//*[local-name()='input']") if inputs.empty?
|
|
103
|
+
|
|
104
|
+
inputs.each do |input_node|
|
|
105
|
+
table.add_input(parse_input(input_node))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Parse outputs
|
|
109
|
+
outputs = node.xpath(".//dmn:output", NAMESPACES)
|
|
110
|
+
outputs = node.xpath(".//*[local-name()='output']") if outputs.empty?
|
|
111
|
+
|
|
112
|
+
outputs.each do |output_node|
|
|
113
|
+
table.add_output(parse_output(output_node))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Parse rules
|
|
117
|
+
rules = node.xpath(".//dmn:rule", NAMESPACES)
|
|
118
|
+
rules = node.xpath(".//*[local-name()='rule']") if rules.empty?
|
|
119
|
+
|
|
120
|
+
rules.each do |rule_node|
|
|
121
|
+
table.add_rule(parse_rule(rule_node))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
table.freeze
|
|
125
|
+
table
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parse_input(node)
|
|
129
|
+
input_expr = node.at_xpath(".//dmn:inputExpression", NAMESPACES) ||
|
|
130
|
+
node.at_xpath(".//*[local-name()='inputExpression']")
|
|
131
|
+
|
|
132
|
+
text_node = input_expr&.at_xpath(".//dmn:text", NAMESPACES) ||
|
|
133
|
+
input_expr&.at_xpath(".//*[local-name()='text']")
|
|
134
|
+
|
|
135
|
+
Input.new(
|
|
136
|
+
id: node["id"] || SecureRandom.uuid,
|
|
137
|
+
label: node["label"] || text_node&.text || "Input",
|
|
138
|
+
expression: text_node&.text,
|
|
139
|
+
type_ref: input_expr&.[]("typeRef") || "string"
|
|
140
|
+
).freeze
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def parse_output(node)
|
|
144
|
+
Output.new(
|
|
145
|
+
id: node["id"] || SecureRandom.uuid,
|
|
146
|
+
label: node["label"] || node["name"] || "Output",
|
|
147
|
+
name: node["name"] || node["label"] || "output",
|
|
148
|
+
type_ref: node["typeRef"] || "string"
|
|
149
|
+
).freeze
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def parse_rule(node)
|
|
153
|
+
rule = Rule.new(
|
|
154
|
+
id: node["id"] || SecureRandom.uuid,
|
|
155
|
+
description: extract_description(node)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Parse input entries
|
|
159
|
+
input_entries = node.xpath(".//dmn:inputEntry", NAMESPACES)
|
|
160
|
+
input_entries = node.xpath(".//*[local-name()='inputEntry']") if input_entries.empty?
|
|
161
|
+
|
|
162
|
+
input_entries.each do |entry_node|
|
|
163
|
+
text_node = entry_node.at_xpath(".//dmn:text", NAMESPACES) ||
|
|
164
|
+
entry_node.at_xpath(".//*[local-name()='text']")
|
|
165
|
+
text = text_node&.text || "-"
|
|
166
|
+
rule.add_input_entry(text)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Parse output entries
|
|
170
|
+
output_entries = node.xpath(".//dmn:outputEntry", NAMESPACES)
|
|
171
|
+
output_entries = node.xpath(".//*[local-name()='outputEntry']") if output_entries.empty?
|
|
172
|
+
|
|
173
|
+
output_entries.each do |entry_node|
|
|
174
|
+
text_node = entry_node.at_xpath(".//dmn:text", NAMESPACES) ||
|
|
175
|
+
entry_node.at_xpath(".//*[local-name()='text']")
|
|
176
|
+
text = text_node&.text
|
|
177
|
+
rule.add_output_entry(text)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
rule.freeze
|
|
181
|
+
rule
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def extract_description(node)
|
|
185
|
+
desc_node = node.at_xpath(".//dmn:description", NAMESPACES) ||
|
|
186
|
+
node.at_xpath(".//*[local-name()='description']")
|
|
187
|
+
desc_node&.text
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|