dmn 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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