dmn 0.0.1 → 0.0.3
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 +10 -10
- data/lib/{spot_feel → dmn}/configuration.rb +1 -1
- data/lib/dmn/decision.rb +48 -0
- data/lib/dmn/decision_table.rb +51 -0
- data/lib/dmn/definitions.rb +66 -0
- data/lib/{spot_feel/spot_feel.treetop → dmn/dmn.treetop} +1 -1
- data/lib/dmn/information_requirement.rb +27 -0
- data/lib/dmn/input.rb +26 -0
- data/lib/dmn/literal_expression.rb +372 -0
- data/lib/{spot_feel → dmn}/nodes.rb +17 -19
- data/lib/dmn/output.rb +27 -0
- data/lib/{spot_feel → dmn}/parser.rb +7 -7
- data/lib/dmn/rule.rb +61 -0
- data/lib/dmn/unary_tests.rb +25 -0
- data/lib/dmn/variable.rb +25 -0
- data/lib/dmn/version.rb +5 -0
- data/lib/{spot_feel.rb → dmn.rb} +22 -17
- metadata +29 -33
- data/lib/spot_feel/dmn/decision.rb +0 -50
- data/lib/spot_feel/dmn/decision_table.rb +0 -53
- data/lib/spot_feel/dmn/definitions.rb +0 -68
- data/lib/spot_feel/dmn/information_requirement.rb +0 -29
- data/lib/spot_feel/dmn/input.rb +0 -28
- data/lib/spot_feel/dmn/literal_expression.rb +0 -374
- data/lib/spot_feel/dmn/output.rb +0 -29
- data/lib/spot_feel/dmn/rule.rb +0 -63
- data/lib/spot_feel/dmn/unary_tests.rb +0 -27
- data/lib/spot_feel/dmn/variable.rb +0 -27
- data/lib/spot_feel/dmn.rb +0 -17
- data/lib/spot_feel/version.rb +0 -5
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
3
|
+
module DMN
|
4
4
|
class Node < Treetop::Runtime::SyntaxNode
|
5
5
|
#
|
6
6
|
# Takes a context hash and returns an array of qualified names
|
7
7
|
# { "person": { "name": { "first": "Eric", "last": "Carlson" }, "age": 60 } } => ["person", "person.name.first", "person.name.last", "person.age"]
|
8
8
|
#
|
9
|
-
def qualified_names_in_context(hash = {}, prefix =
|
9
|
+
def qualified_names_in_context(hash = {}, prefix = "", qualified_names = Set.new)
|
10
10
|
hash.each do |key, value|
|
11
11
|
new_prefix = prefix.empty? ? "#{key}" : "#{prefix}.#{key}"
|
12
12
|
if value.is_a?(Hash)
|
@@ -114,13 +114,13 @@ module SpotFeel
|
|
114
114
|
second_val = second.eval(context)
|
115
115
|
|
116
116
|
case [start, finish]
|
117
|
-
when [
|
117
|
+
when ["(", ")"]
|
118
118
|
->(input) { first_val < input && input < second_val }
|
119
|
-
when [
|
119
|
+
when ["[", "]"]
|
120
120
|
->(input) { first_val <= input && input <= second_val }
|
121
|
-
when [
|
121
|
+
when ["(", "]"]
|
122
122
|
->(input) { first_val < input && input <= second_val }
|
123
|
-
when [
|
123
|
+
when ["[", ")"]
|
124
124
|
->(input) { first_val <= input && input < second_val }
|
125
125
|
end
|
126
126
|
end
|
@@ -225,11 +225,11 @@ module SpotFeel
|
|
225
225
|
class QualifiedName < Node
|
226
226
|
def eval(context = {})
|
227
227
|
if tail.empty?
|
228
|
-
raise_evaluation_error(head.text_value, context) if
|
228
|
+
raise_evaluation_error(head.text_value, context) if DMN.config.strict && !context.key?(head.text_value.to_sym)
|
229
229
|
context[head.text_value.to_sym]
|
230
230
|
else
|
231
|
-
tail.elements.flat_map { |element| element.name.text_value.split(
|
232
|
-
raise_evaluation_error("#{head.text_value}#{tail.text_value}", context) if
|
231
|
+
tail.elements.flat_map { |element| element.name.text_value.split(".") }.inject(context[head.text_value.to_sym]) do |hash, key|
|
232
|
+
raise_evaluation_error("#{head.text_value}#{tail.text_value}", context) if DMN.config.strict && (hash.blank? || !hash.key?(key.to_sym))
|
233
233
|
return nil unless hash
|
234
234
|
hash[key.to_sym]
|
235
235
|
end
|
@@ -385,7 +385,7 @@ module SpotFeel
|
|
385
385
|
fn = context[fn_name.text_value.to_sym]
|
386
386
|
|
387
387
|
unless fn
|
388
|
-
raise_evaluation_error(fn_name.text_value, context) if
|
388
|
+
raise_evaluation_error(fn_name.text_value, context) if DMN.config.strict
|
389
389
|
return nil
|
390
390
|
end
|
391
391
|
|
@@ -492,12 +492,12 @@ module SpotFeel
|
|
492
492
|
class Comparison < Node
|
493
493
|
def eval(context = {})
|
494
494
|
case operator.text_value
|
495
|
-
when
|
496
|
-
when
|
497
|
-
when
|
498
|
-
when
|
499
|
-
when
|
500
|
-
when
|
495
|
+
when "<" then left.eval(context) < right.eval(context)
|
496
|
+
when "<=" then left.eval(context) <= right.eval(context)
|
497
|
+
when ">=" then left.eval(context) >= right.eval(context)
|
498
|
+
when ">" then left.eval(context) > right.eval(context)
|
499
|
+
when "!=" then left.eval(context) != right.eval(context)
|
500
|
+
when "=" then left.eval(context) == right.eval(context)
|
501
501
|
end
|
502
502
|
end
|
503
503
|
end
|
@@ -522,7 +522,7 @@ module SpotFeel
|
|
522
522
|
# 53. instance of = expression , "instance" , "of" , type ;
|
523
523
|
#
|
524
524
|
class InstanceOf < Node
|
525
|
-
def eval(
|
525
|
+
def eval(_context = {})
|
526
526
|
case type.text_value
|
527
527
|
when "string"
|
528
528
|
->(input) { input.is_a?(String) }
|
@@ -556,8 +556,6 @@ module SpotFeel
|
|
556
556
|
->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:seconds] }
|
557
557
|
when "time duration"
|
558
558
|
->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:hours, :minutes, :seconds] }
|
559
|
-
when "years and months duration"
|
560
|
-
->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:months, :years] }
|
561
559
|
when "list"
|
562
560
|
->(input) { input.is_a?(Array) }
|
563
561
|
when "interval"
|
data/lib/dmn/output.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DMN
|
4
|
+
class Output
|
5
|
+
attr_reader :id, :label, :name, :type_ref
|
6
|
+
|
7
|
+
def self.from_json(json)
|
8
|
+
Output.new(id: json[:id], label: json[:label], name: json[:name], type_ref: json[:type_ref])
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(id:, label:, name:, type_ref:)
|
12
|
+
@id = id
|
13
|
+
@label = label
|
14
|
+
@name = name
|
15
|
+
@type_ref = type_ref
|
16
|
+
end
|
17
|
+
|
18
|
+
def as_json
|
19
|
+
{
|
20
|
+
id: id,
|
21
|
+
label: label,
|
22
|
+
name: name,
|
23
|
+
type_ref: type_ref,
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
3
|
+
module DMN
|
4
4
|
class Parser
|
5
|
-
# Load the Treetop grammar from the '
|
5
|
+
# Load the Treetop grammar from the 'feel' file, and create a new
|
6
6
|
# instance of that parser as a class variable so we don't have to re-create
|
7
7
|
# it every time we need to parse a string
|
8
|
-
Treetop.load(File.expand_path(File.join(File.dirname(__FILE__),
|
9
|
-
@@parser =
|
8
|
+
Treetop.load(File.expand_path(File.join(File.dirname(__FILE__), "dmn.treetop")))
|
9
|
+
@@parser = DMNParser.new
|
10
10
|
|
11
11
|
def self.parse(expression, root: nil)
|
12
12
|
@@parser.parse(expression, root:).tap do |ast|
|
@@ -15,15 +15,15 @@ module SpotFeel
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def self.parse_test(expression)
|
18
|
-
@@parser.parse(expression ||
|
18
|
+
@@parser.parse(expression || "-", root: :simple_unary_tests).tap do |ast|
|
19
19
|
raise SyntaxError, "Invalid unary test: #{expression.inspect}" unless ast
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
23
|
def self.clean_tree(root_node)
|
24
24
|
return if(root_node.elements.nil?)
|
25
|
-
root_node.elements.delete_if{|node| node.class.name == "Treetop::Runtime::SyntaxNode" }
|
26
|
-
root_node.elements.each {|node| self.clean_tree(node) }
|
25
|
+
root_node.elements.delete_if{ |node| node.class.name == "Treetop::Runtime::SyntaxNode" }
|
26
|
+
root_node.elements.each { |node| self.clean_tree(node) }
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
data/lib/dmn/rule.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DMN
|
4
|
+
class Rule
|
5
|
+
attr_accessor :id, :input_entries, :output_entries, :description
|
6
|
+
|
7
|
+
def self.from_json(json)
|
8
|
+
input_entries = Array.wrap(json[:input_entry]).map { |input_entry| UnaryTests.from_json(input_entry) }
|
9
|
+
output_entries = Array.wrap(json[:output_entry]).map { |output_entry| LiteralExpression.from_json(output_entry) }
|
10
|
+
Rule.new(id: json[:id], input_entries:, output_entries:, description: json[:description])
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(id:, input_entries:, output_entries:, description: nil)
|
14
|
+
@id = id
|
15
|
+
@input_entries = input_entries
|
16
|
+
@output_entries = output_entries
|
17
|
+
@description = description
|
18
|
+
end
|
19
|
+
|
20
|
+
def evaluate(input_values = [], variables = {})
|
21
|
+
[].tap do |test_results|
|
22
|
+
input_entries.each_with_index do |input_entry, index|
|
23
|
+
test_results.push input_entry.test(input_values[index], variables)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def as_json
|
29
|
+
{
|
30
|
+
id: id,
|
31
|
+
input_entries: input_entries.map(&:as_json),
|
32
|
+
output_entries: output_entries.map(&:as_json),
|
33
|
+
description: description,
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def output_value(outputs, variables)
|
38
|
+
HashWithIndifferentAccess.new.tap do |ov|
|
39
|
+
output_entries.each_with_index do |output_entry, index|
|
40
|
+
if output_entry.valid?
|
41
|
+
val = output_entry.evaluate(variables)
|
42
|
+
nested_hash_value(ov, outputs[index].name, val)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def nested_hash_value(hash, key_string, value)
|
51
|
+
keys = key_string.split(".")
|
52
|
+
current = hash
|
53
|
+
keys[0...-1].each do |key|
|
54
|
+
current[key] ||= {}
|
55
|
+
current = current[key]
|
56
|
+
end
|
57
|
+
current[keys.last] = value
|
58
|
+
hash
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DMN
|
4
|
+
class UnaryTests < LiteralExpression
|
5
|
+
attr_reader :id, :text
|
6
|
+
|
7
|
+
def self.from_json(json)
|
8
|
+
UnaryTests.new(id: json[:id], text: json[:text])
|
9
|
+
end
|
10
|
+
|
11
|
+
def tree
|
12
|
+
@tree ||= Parser.parse_test(text)
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid?
|
16
|
+
return true if text.nil? || text == "-"
|
17
|
+
tree.present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def test(input, variables = {})
|
21
|
+
return true if text.nil? || text == "-"
|
22
|
+
tree.eval(functions.merge(variables)).call(input)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/dmn/variable.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DMN
|
4
|
+
class Variable
|
5
|
+
attr_reader :id, :name, :type_ref
|
6
|
+
|
7
|
+
def self.from_json(json)
|
8
|
+
Variable.new(id: json[:id], name: json[:name], type_ref: json[:type_ref])
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(id:, name:, type_ref:)
|
12
|
+
@id = id
|
13
|
+
@name = name
|
14
|
+
@type_ref = type_ref
|
15
|
+
end
|
16
|
+
|
17
|
+
def as_json
|
18
|
+
{
|
19
|
+
id: id,
|
20
|
+
name: name,
|
21
|
+
type_ref: type_ref,
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/dmn/version.rb
ADDED
data/lib/{spot_feel.rb → dmn.rb}
RENAMED
@@ -1,56 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
4
|
-
|
5
|
-
require "awesome_print"
|
3
|
+
require_relative "dmn/version"
|
6
4
|
|
7
5
|
require "active_support"
|
8
|
-
require "active_support/duration"
|
9
6
|
require "active_support/time"
|
10
7
|
require "active_support/core_ext/hash"
|
11
|
-
require "active_support/core_ext/object/json"
|
12
|
-
require "active_support/configurable"
|
13
8
|
|
14
9
|
require "treetop"
|
15
10
|
require "xmlhasher"
|
16
11
|
|
17
|
-
require "
|
18
|
-
require "
|
19
|
-
require "
|
12
|
+
require "dmn/configuration"
|
13
|
+
require "dmn/nodes"
|
14
|
+
require "dmn/parser"
|
15
|
+
|
16
|
+
require "dmn/variable"
|
17
|
+
require "dmn/literal_expression"
|
18
|
+
require "dmn/unary_tests"
|
19
|
+
require "dmn/input"
|
20
|
+
require "dmn/output"
|
21
|
+
require "dmn/rule"
|
22
|
+
require "dmn/decision_table"
|
23
|
+
require "dmn/information_requirement"
|
24
|
+
require "dmn/decision"
|
25
|
+
require "dmn/definitions"
|
20
26
|
|
21
|
-
require "spot_feel/dmn"
|
22
27
|
|
23
|
-
module
|
28
|
+
module DMN
|
24
29
|
class SyntaxError < StandardError; end
|
25
30
|
class EvaluationError < StandardError; end
|
26
31
|
|
27
32
|
def self.evaluate(expression_text, variables: {})
|
28
|
-
literal_expression =
|
33
|
+
literal_expression = DMN::LiteralExpression.new(text: expression_text)
|
29
34
|
raise SyntaxError, "Expression is not valid" unless literal_expression.valid?
|
30
35
|
literal_expression.evaluate(variables)
|
31
36
|
end
|
32
37
|
|
33
38
|
def self.test(input, unary_tests_text, variables: {})
|
34
|
-
unary_tests =
|
39
|
+
unary_tests = DMN::UnaryTests.new(text: unary_tests_text)
|
35
40
|
raise SyntaxError, "Unary tests are not valid" unless unary_tests.valid?
|
36
41
|
unary_tests.test(input, variables)
|
37
42
|
end
|
38
43
|
|
39
44
|
def self.decide(decision_id, definitions: nil, definitions_json: nil, definitions_xml: nil, variables: {})
|
40
45
|
if definitions_xml.present?
|
41
|
-
definitions =
|
46
|
+
definitions = DMN::Definitions.from_xml(definitions_xml)
|
42
47
|
elsif definitions_json.present?
|
43
|
-
definitions =
|
48
|
+
definitions = DMN::Definitions.from_json(definitions_json)
|
44
49
|
end
|
45
50
|
definitions.evaluate(decision_id, variables: variables)
|
46
51
|
end
|
47
52
|
|
48
53
|
def self.definitions_from_xml(xml)
|
49
|
-
|
54
|
+
DMN::Definitions.from_xml(xml)
|
50
55
|
end
|
51
56
|
|
52
57
|
def self.definitions_from_json(json)
|
53
|
-
|
58
|
+
DMN::Definitions.from_json(json)
|
54
59
|
end
|
55
60
|
|
56
61
|
def self.config
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dmn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Connected Bits
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-03-03 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: activemodel
|
@@ -39,33 +38,33 @@ dependencies:
|
|
39
38
|
- !ruby/object:Gem::Version
|
40
39
|
version: 7.0.2.3
|
41
40
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
41
|
+
name: ostruct
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
44
43
|
requirements:
|
45
|
-
- - "
|
44
|
+
- - ">="
|
46
45
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
46
|
+
version: '0'
|
48
47
|
type: :runtime
|
49
48
|
prerelease: false
|
50
49
|
version_requirements: !ruby/object:Gem::Requirement
|
51
50
|
requirements:
|
52
|
-
- - "
|
51
|
+
- - ">="
|
53
52
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
53
|
+
version: '0'
|
55
54
|
- !ruby/object:Gem::Dependency
|
56
55
|
name: treetop
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
58
57
|
requirements:
|
59
|
-
- -
|
58
|
+
- - '='
|
60
59
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
60
|
+
version: 1.6.12
|
62
61
|
type: :runtime
|
63
62
|
prerelease: false
|
64
63
|
version_requirements: !ruby/object:Gem::Requirement
|
65
64
|
requirements:
|
66
|
-
- -
|
65
|
+
- - '='
|
67
66
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
67
|
+
version: 1.6.12
|
69
68
|
- !ruby/object:Gem::Dependency
|
70
69
|
name: xmlhasher
|
71
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -271,30 +270,28 @@ extra_rdoc_files: []
|
|
271
270
|
files:
|
272
271
|
- README.md
|
273
272
|
- Rakefile
|
274
|
-
- lib/
|
275
|
-
- lib/
|
276
|
-
- lib/
|
277
|
-
- lib/
|
278
|
-
- lib/
|
279
|
-
- lib/
|
280
|
-
- lib/
|
281
|
-
- lib/
|
282
|
-
- lib/
|
283
|
-
- lib/
|
284
|
-
- lib/
|
285
|
-
- lib/
|
286
|
-
- lib/
|
287
|
-
- lib/
|
288
|
-
- lib/
|
289
|
-
- lib/
|
290
|
-
- lib/spot_feel/version.rb
|
273
|
+
- lib/dmn.rb
|
274
|
+
- lib/dmn/configuration.rb
|
275
|
+
- lib/dmn/decision.rb
|
276
|
+
- lib/dmn/decision_table.rb
|
277
|
+
- lib/dmn/definitions.rb
|
278
|
+
- lib/dmn/dmn.treetop
|
279
|
+
- lib/dmn/information_requirement.rb
|
280
|
+
- lib/dmn/input.rb
|
281
|
+
- lib/dmn/literal_expression.rb
|
282
|
+
- lib/dmn/nodes.rb
|
283
|
+
- lib/dmn/output.rb
|
284
|
+
- lib/dmn/parser.rb
|
285
|
+
- lib/dmn/rule.rb
|
286
|
+
- lib/dmn/unary_tests.rb
|
287
|
+
- lib/dmn/variable.rb
|
288
|
+
- lib/dmn/version.rb
|
291
289
|
homepage: https://www.connectedbits.com
|
292
290
|
licenses:
|
293
291
|
- MIT
|
294
292
|
metadata:
|
295
293
|
homepage_uri: https://www.connectedbits.com
|
296
|
-
source_code_uri: https://github.com/connectedbits/feel
|
297
|
-
post_install_message:
|
294
|
+
source_code_uri: https://github.com/connectedbits/bpmn/feel
|
298
295
|
rdoc_options: []
|
299
296
|
require_paths:
|
300
297
|
- lib
|
@@ -309,8 +306,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
309
306
|
- !ruby/object:Gem::Version
|
310
307
|
version: '0'
|
311
308
|
requirements: []
|
312
|
-
rubygems_version: 3.
|
313
|
-
signing_key:
|
309
|
+
rubygems_version: 3.6.5
|
314
310
|
specification_version: 4
|
315
311
|
summary: A light-weight DMN FEEL expression evaluator and business rule engine in
|
316
312
|
Ruby.
|
@@ -1,50 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SpotFeel
|
4
|
-
module Dmn
|
5
|
-
class Decision
|
6
|
-
attr_reader :id, :name, :decision_table, :variable, :literal_expression, :information_requirements
|
7
|
-
|
8
|
-
def self.from_json(json)
|
9
|
-
information_requirements = Array.wrap(json[:information_requirement]).map { |ir| InformationRequirement.from_json(ir) } if json[:information_requirement]
|
10
|
-
decision_table = DecisionTable.from_json(json[:decision_table]) if json[:decision_table]
|
11
|
-
literal_expression = LiteralExpression.from_json(json[:literal_expression]) if json[:literal_expression]
|
12
|
-
variable = Variable.from_json(json[:variable]) if json[:variable]
|
13
|
-
Decision.new(id: json[:id], name: json[:name], decision_table:, variable:, literal_expression:, information_requirements:)
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize(id:, name:, decision_table:, variable:, literal_expression:, information_requirements:)
|
17
|
-
@id = id
|
18
|
-
@name = name
|
19
|
-
@decision_table = decision_table
|
20
|
-
@variable = variable
|
21
|
-
@literal_expression = literal_expression
|
22
|
-
@information_requirements = information_requirements
|
23
|
-
end
|
24
|
-
|
25
|
-
def evaluate(variables = {})
|
26
|
-
if literal_expression.present?
|
27
|
-
result = literal_expression.evaluate(variables)
|
28
|
-
variable.present? ? { variable.name => result } : result
|
29
|
-
elsif decision_table.present?
|
30
|
-
decision_table.evaluate(variables)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def required_decision_ids
|
35
|
-
information_requirements&.map(&:required_decision_id)
|
36
|
-
end
|
37
|
-
|
38
|
-
def as_json
|
39
|
-
{
|
40
|
-
id: id,
|
41
|
-
name: name,
|
42
|
-
decision_table: decision_table.as_json,
|
43
|
-
variable: variable.as_json,
|
44
|
-
literal_expression: literal_expression.as_json,
|
45
|
-
information_requirements: information_requirements&.map(&:as_json),
|
46
|
-
}
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
@@ -1,53 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SpotFeel
|
4
|
-
module Dmn
|
5
|
-
class DecisionTable
|
6
|
-
attr_reader :id, :hit_policy, :inputs, :outputs, :rules
|
7
|
-
|
8
|
-
def self.from_json(json)
|
9
|
-
inputs = Array.wrap(json[:input]).map { |input| Input.from_json(input) }
|
10
|
-
outputs = Array.wrap(json[:output]).map { |output| Output.from_json(output) }
|
11
|
-
rules = Array.wrap(json[:rule]).map { |rule| Rule.from_json(rule) }
|
12
|
-
DecisionTable.new(id: json[:id], hit_policy: json[:hit_policy], inputs: inputs, outputs: outputs, rules: rules)
|
13
|
-
end
|
14
|
-
|
15
|
-
def initialize(id:, hit_policy:, inputs:, outputs:, rules:)
|
16
|
-
@id = id
|
17
|
-
@hit_policy = hit_policy&.downcase&.to_sym || :unique
|
18
|
-
@inputs = inputs
|
19
|
-
@outputs = outputs
|
20
|
-
@rules = rules
|
21
|
-
end
|
22
|
-
|
23
|
-
def evaluate(variables = {})
|
24
|
-
output_values = []
|
25
|
-
|
26
|
-
input_values = inputs.map do |input|
|
27
|
-
input.input_expression.evaluate(variables)
|
28
|
-
end
|
29
|
-
|
30
|
-
rules.each do |rule|
|
31
|
-
results = rule.evaluate(input_values, variables)
|
32
|
-
if results.all?
|
33
|
-
output_value = rule.output_value(outputs, variables)
|
34
|
-
return output_value if hit_policy == :first || hit_policy == :unique
|
35
|
-
output_values << output_value
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
output_values.empty? ? nil : output_values
|
40
|
-
end
|
41
|
-
|
42
|
-
def as_json
|
43
|
-
{
|
44
|
-
id: id,
|
45
|
-
hit_policy: hit_policy,
|
46
|
-
inputs: inputs.map(&:as_json),
|
47
|
-
outputs: outputs.map(&:as_json),
|
48
|
-
rules: rules.map(&:as_json),
|
49
|
-
}
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
@@ -1,68 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SpotFeel
|
4
|
-
module Dmn
|
5
|
-
class Definitions
|
6
|
-
attr_reader :id, :name, :namespace, :exporter, :exporter_version, :execution_platform, :execution_platform_version
|
7
|
-
attr_reader :decisions
|
8
|
-
|
9
|
-
def self.from_xml(xml)
|
10
|
-
XmlHasher.configure do |config|
|
11
|
-
config.snakecase = true
|
12
|
-
config.ignore_namespaces = true
|
13
|
-
config.string_keys = false
|
14
|
-
end
|
15
|
-
json = XmlHasher.parse(xml)
|
16
|
-
Definitions.from_json(json[:definitions])
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.from_json(json)
|
20
|
-
decisions = Array.wrap(json[:decision]).map { |decision| Decision.from_json(decision) }
|
21
|
-
Definitions.new(id: json[:id], name: json[:name], namespace: json[:namespace], exporter: json[:exporter], exporter_version: json[:exporter_version], execution_platform: json[:execution_platform], execution_platform_version: json[:execution_platform_version], decisions: decisions)
|
22
|
-
end
|
23
|
-
|
24
|
-
def initialize(id:, name:, namespace:, exporter:, exporter_version:, execution_platform:, execution_platform_version:, decisions:)
|
25
|
-
@id = id
|
26
|
-
@name = name
|
27
|
-
@namespace = namespace
|
28
|
-
@exporter = exporter
|
29
|
-
@exporter_version = exporter_version
|
30
|
-
@execution_platform = execution_platform
|
31
|
-
@execution_platform_version = execution_platform_version
|
32
|
-
@decisions = decisions
|
33
|
-
end
|
34
|
-
|
35
|
-
def evaluate(decision_id, variables: {}, already_evaluated_decisions: {})
|
36
|
-
decision = decisions.find { |d| d.id == decision_id }
|
37
|
-
raise EvaluationError, "Decision #{decision_id} not found" unless decision
|
38
|
-
|
39
|
-
# Evaluate required decisions recursively
|
40
|
-
decision.required_decision_ids&.each do |required_decision_id|
|
41
|
-
next if already_evaluated_decisions[required_decision_id]
|
42
|
-
next if decisions.find { |d| d.id == required_decision_id }.nil?
|
43
|
-
|
44
|
-
result = evaluate(required_decision_id, variables:, already_evaluated_decisions:)
|
45
|
-
|
46
|
-
variables.merge!(result) if result.is_a?(Hash)
|
47
|
-
|
48
|
-
already_evaluated_decisions[required_decision_id] = true
|
49
|
-
end
|
50
|
-
|
51
|
-
decision.evaluate(variables)
|
52
|
-
end
|
53
|
-
|
54
|
-
def as_json
|
55
|
-
{
|
56
|
-
id: id,
|
57
|
-
name: name,
|
58
|
-
namespace: namespace,
|
59
|
-
exporter: exporter,
|
60
|
-
exporter_version: exporter_version,
|
61
|
-
execution_platform: execution_platform,
|
62
|
-
execution_platform_version: execution_platform_version,
|
63
|
-
decisions: decisions.map(&:as_json),
|
64
|
-
}
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SpotFeel
|
4
|
-
module Dmn
|
5
|
-
class InformationRequirement
|
6
|
-
attr_reader :id, :required_input_id, :required_decision_id
|
7
|
-
|
8
|
-
def self.from_json(json)
|
9
|
-
required_input_id = json[:required_input][:href].delete_prefix("#") if json[:required_input]
|
10
|
-
required_decision_id = json[:required_decision][:href].delete_prefix("#") if json[:required_decision]
|
11
|
-
InformationRequirement.new(id: json[:id], required_input_id: required_input_id, required_decision_id: required_decision_id)
|
12
|
-
end
|
13
|
-
|
14
|
-
def initialize(id:, required_input_id:, required_decision_id:)
|
15
|
-
@id = id
|
16
|
-
@required_input_id = required_input_id
|
17
|
-
@required_decision_id = required_decision_id
|
18
|
-
end
|
19
|
-
|
20
|
-
def as_json
|
21
|
-
{
|
22
|
-
id: id,
|
23
|
-
required_decision_id: required_decision_id,
|
24
|
-
required_input_id: required_input_id,
|
25
|
-
}
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|