dmn 0.0.1 → 0.0.2

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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SpotFeel
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
@@ -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 SpotFeel.config.strict && !context.key?(head.text_value.to_sym)
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
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 SpotFeel.config.strict && (hash.blank? || !hash.key?(key.to_sym))
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 SpotFeel.config.strict
388
+ raise_evaluation_error(fn_name.text_value, context) if DMN.config.strict
389
389
  return nil
390
390
  end
391
391
 
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 SpotFeel
3
+ module DMN
4
4
  class Parser
5
- # Load the Treetop grammar from the 'spot_feel' file, and create a new
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__), 'spot_feel.treetop')))
9
- @@parser = SpotFeelParser.new
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|
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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DMN
4
+ VERSION = '0.0.2'
5
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "spot_feel/version"
3
+ require_relative "dmn/version"
4
4
 
5
5
  require "awesome_print"
6
6
 
@@ -14,43 +14,53 @@ require "active_support/configurable"
14
14
  require "treetop"
15
15
  require "xmlhasher"
16
16
 
17
- require "spot_feel/configuration"
18
- require "spot_feel/nodes"
19
- require "spot_feel/parser"
17
+ require "dmn/configuration"
18
+ require "dmn/nodes"
19
+ require "dmn/parser"
20
20
 
21
- require "spot_feel/dmn"
21
+ require "dmn/variable"
22
+ require "dmn/literal_expression"
23
+ require "dmn/unary_tests"
24
+ require "dmn/input"
25
+ require "dmn/output"
26
+ require "dmn/rule"
27
+ require "dmn/decision_table"
28
+ require "dmn/information_requirement"
29
+ require "dmn/decision"
30
+ require "dmn/definitions"
22
31
 
23
- module SpotFeel
32
+
33
+ module DMN
24
34
  class SyntaxError < StandardError; end
25
35
  class EvaluationError < StandardError; end
26
36
 
27
37
  def self.evaluate(expression_text, variables: {})
28
- literal_expression = Dmn::LiteralExpression.new(text: expression_text)
38
+ literal_expression = DMN::LiteralExpression.new(text: expression_text)
29
39
  raise SyntaxError, "Expression is not valid" unless literal_expression.valid?
30
40
  literal_expression.evaluate(variables)
31
41
  end
32
42
 
33
43
  def self.test(input, unary_tests_text, variables: {})
34
- unary_tests = Dmn::UnaryTests.new(text: unary_tests_text)
44
+ unary_tests = DMN::UnaryTests.new(text: unary_tests_text)
35
45
  raise SyntaxError, "Unary tests are not valid" unless unary_tests.valid?
36
46
  unary_tests.test(input, variables)
37
47
  end
38
48
 
39
49
  def self.decide(decision_id, definitions: nil, definitions_json: nil, definitions_xml: nil, variables: {})
40
50
  if definitions_xml.present?
41
- definitions = Dmn::Definitions.from_xml(definitions_xml)
51
+ definitions = DMN::Definitions.from_xml(definitions_xml)
42
52
  elsif definitions_json.present?
43
- definitions = Dmn::Definitions.from_json(definitions_json)
53
+ definitions = DMN::Definitions.from_json(definitions_json)
44
54
  end
45
55
  definitions.evaluate(decision_id, variables: variables)
46
56
  end
47
57
 
48
58
  def self.definitions_from_xml(xml)
49
- Dmn::Definitions.from_xml(xml)
59
+ DMN::Definitions.from_xml(xml)
50
60
  end
51
61
 
52
62
  def self.definitions_from_json(json)
53
- Dmn::Definitions.from_json(json)
63
+ DMN::Definitions.from_json(json)
54
64
  end
55
65
 
56
66
  def self.config
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dmn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Connected Bits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-30 00:00:00.000000000 Z
11
+ date: 2024-12-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -271,29 +271,28 @@ extra_rdoc_files: []
271
271
  files:
272
272
  - README.md
273
273
  - Rakefile
274
- - lib/spot_feel.rb
275
- - lib/spot_feel/configuration.rb
276
- - lib/spot_feel/dmn.rb
277
- - lib/spot_feel/dmn/decision.rb
278
- - lib/spot_feel/dmn/decision_table.rb
279
- - lib/spot_feel/dmn/definitions.rb
280
- - lib/spot_feel/dmn/information_requirement.rb
281
- - lib/spot_feel/dmn/input.rb
282
- - lib/spot_feel/dmn/literal_expression.rb
283
- - lib/spot_feel/dmn/output.rb
284
- - lib/spot_feel/dmn/rule.rb
285
- - lib/spot_feel/dmn/unary_tests.rb
286
- - lib/spot_feel/dmn/variable.rb
287
- - lib/spot_feel/nodes.rb
288
- - lib/spot_feel/parser.rb
289
- - lib/spot_feel/spot_feel.treetop
290
- - lib/spot_feel/version.rb
274
+ - lib/dmn.rb
275
+ - lib/dmn/configuration.rb
276
+ - lib/dmn/decision.rb
277
+ - lib/dmn/decision_table.rb
278
+ - lib/dmn/definitions.rb
279
+ - lib/dmn/dmn.treetop
280
+ - lib/dmn/information_requirement.rb
281
+ - lib/dmn/input.rb
282
+ - lib/dmn/literal_expression.rb
283
+ - lib/dmn/nodes.rb
284
+ - lib/dmn/output.rb
285
+ - lib/dmn/parser.rb
286
+ - lib/dmn/rule.rb
287
+ - lib/dmn/unary_tests.rb
288
+ - lib/dmn/variable.rb
289
+ - lib/dmn/version.rb
291
290
  homepage: https://www.connectedbits.com
292
291
  licenses:
293
292
  - MIT
294
293
  metadata:
295
294
  homepage_uri: https://www.connectedbits.com
296
- source_code_uri: https://github.com/connectedbits/feel
295
+ source_code_uri: https://github.com/connectedbits/bpmn/feel
297
296
  post_install_message:
298
297
  rdoc_options: []
299
298
  require_paths:
@@ -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
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SpotFeel
4
- module Dmn
5
- class Input
6
- attr_reader :id, :label, :input_expression
7
-
8
- def self.from_json(json)
9
- input_expression = LiteralExpression.from_json(json[:input_expression]) if json[:input_expression]
10
- Input.new(id: json[:id], label: json[:label], input_expression:)
11
- end
12
-
13
- def initialize(id:, label:, input_expression:)
14
- @id = id
15
- @label = label
16
- @input_expression = input_expression
17
- end
18
-
19
- def as_json
20
- {
21
- id: id,
22
- label: label,
23
- input_expression: input_expression.as_json,
24
- }
25
- end
26
- end
27
- end
28
- end