dmn 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,374 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFeel
4
+ module Dmn
5
+ class LiteralExpression
6
+ attr_reader :id, :text
7
+
8
+ def self.from_json(json)
9
+ LiteralExpression.new(id: json[:id], text: json[:text])
10
+ end
11
+
12
+ def initialize(id: nil, text:)
13
+ @id = id
14
+ @text = text&.strip
15
+ end
16
+
17
+ def tree
18
+ @tree ||= SpotFeel::Parser.parse(text)
19
+ end
20
+
21
+ def valid?
22
+ return false if text.blank?
23
+ tree.present?
24
+ end
25
+
26
+ def evaluate(variables = {})
27
+ tree.eval(functions.merge(variables))
28
+ end
29
+
30
+ def functions
31
+ builtins = LiteralExpression.builtin_functions
32
+ custom = (SpotFeel.config.functions || {})
33
+ ActiveSupport::HashWithIndifferentAccess.new(builtins.merge(custom))
34
+ end
35
+
36
+ def named_functions
37
+ # Initialize a set to hold the qualified names
38
+ function_names = Set.new
39
+
40
+ # Define a lambda for the recursive function
41
+ walk_tree = lambda do |node|
42
+ # If the node is a qualified name, add it to the set
43
+ if node.is_a?(SpotFeel::FunctionInvocation)
44
+ function_names << node.fn_name.text_value
45
+ end
46
+
47
+ # Recursively walk the child nodes
48
+ node.elements&.each do |child|
49
+ walk_tree.call(child)
50
+ end
51
+ end
52
+
53
+ # Start walking the tree from the root
54
+ walk_tree.call(tree)
55
+
56
+ # Return the array of functions
57
+ function_names.to_a
58
+ end
59
+
60
+ def named_variables
61
+ # Initialize a set to hold the qualified names
62
+ qualified_names = Set.new
63
+
64
+ # Define a lambda for the recursive function
65
+ walk_tree = lambda do |node|
66
+ # If the node is a qualified name, add it to the set
67
+ if node.is_a?(SpotFeel::QualifiedName)
68
+ qualified_names << node.text_value
69
+ end
70
+
71
+ # Recursively walk the child nodes
72
+ node.elements&.each do |child|
73
+ walk_tree.call(child)
74
+ end
75
+ end
76
+
77
+ # Start walking the tree from the root
78
+ walk_tree.call(tree)
79
+
80
+ # Return the array of qualified names
81
+ qualified_names.to_a
82
+ end
83
+
84
+ def self.builtin_functions
85
+ HashWithIndifferentAccess.new({
86
+ # Conversion functions
87
+ "string": ->(from) {
88
+ return if from.nil?
89
+ from.to_s
90
+ },
91
+ "number": ->(from) {
92
+ return if from.nil?
93
+ from.include?(".") ? from.to_f : from.to_i
94
+ },
95
+ # Boolean functions
96
+ "not": ->(value) {
97
+ if value == true || value == false
98
+ !value
99
+ end
100
+ },
101
+ "is defined": ->(value) {
102
+ return if value.nil?
103
+ !value.nil?
104
+ },
105
+ "get or else": ->(value, default) {
106
+ value.nil? ? default : value
107
+ },
108
+ # String functions
109
+ "substring": ->(string, start, length) {
110
+ return if string.nil? || start.nil?
111
+ return "" if length.nil?
112
+ string[start - 1, length]
113
+ },
114
+ "substring before": ->(string, match) {
115
+ return if string.nil? || match.nil?
116
+ string.split(match).first
117
+ },
118
+ "substring after": ->(string, match) {
119
+ return if string.nil? || match.nil?
120
+ string.split(match).last
121
+ },
122
+ "string length": ->(string) {
123
+ return if string.nil?
124
+ string.length
125
+ },
126
+ "upper case": ->(string) {
127
+ return if string.nil?
128
+ string.upcase
129
+ },
130
+ "lower case": -> (string) {
131
+ return if string.nil?
132
+ string.downcase
133
+ },
134
+ "contains": ->(string, match) {
135
+ return if string.nil? || match.nil?
136
+ string.include?(match)
137
+ },
138
+ "starts with": ->(string, match) {
139
+ return if string.nil? || match.nil?
140
+ string.start_with?(match)
141
+ },
142
+ "ends with": ->(string, match) {
143
+ return if string.nil? || match.nil?
144
+ string.end_with?(match)
145
+ },
146
+ "matches": ->(string, match) {
147
+ return if string.nil? || match.nil?
148
+ string.match?(match)
149
+ },
150
+ "replace": ->(string, match, replacement) {
151
+ return if string.nil? || match.nil? || replacement.nil?
152
+ string.gsub(match, replacement)
153
+ },
154
+ "split": ->(string, match) {
155
+ return if string.nil? || match.nil?
156
+ string.split(match)
157
+ },
158
+ "strip": -> (string) {
159
+ return if string.nil?
160
+ string.strip
161
+ },
162
+ "extract": -> (string, pattern) {
163
+ return if string.nil? || pattern.nil?
164
+ string.match(pattern).captures
165
+ },
166
+ # Numeric functions
167
+ "decimal": ->(n, scale) {
168
+ return if n.nil? || scale.nil?
169
+ n.round(scale)
170
+ },
171
+ "floor": ->(n) {
172
+ return if n.nil?
173
+ n.floor
174
+ },
175
+ "ceiling": ->(n) {
176
+ return if n.nil?
177
+ n.ceil
178
+ },
179
+ "round up": ->(n) {
180
+ return if n.nil?
181
+ n.ceil
182
+ },
183
+ "round down": ->(n) {
184
+ return if n.nil?
185
+ n.floor
186
+ },
187
+ "abs": ->(n) {
188
+ return if n.nil?
189
+ n.abs
190
+ },
191
+ "modulo": ->(n, divisor) {
192
+ return if n.nil? || divisor.nil?
193
+ n % divisor
194
+ },
195
+ "sqrt": ->(n) {
196
+ return if n.nil?
197
+ Math.sqrt(n)
198
+ },
199
+ "log": ->(n) {
200
+ return if n.nil?
201
+ Math.log(n)
202
+ },
203
+ "exp": ->(n) {
204
+ return if n.nil?
205
+ Math.exp(n)
206
+ },
207
+ "odd": ->(n) {
208
+ return if n.nil?
209
+ n.odd?
210
+ },
211
+ "even": ->(n) {
212
+ return if n.nil?
213
+ n.even?
214
+ },
215
+ "random number": ->(n) {
216
+ return if n.nil?
217
+ rand(n)
218
+ },
219
+ # List functions
220
+ "list contains": ->(list, match) {
221
+ return if list.nil?
222
+ return false if match.nil?
223
+ list.include?(match)
224
+ },
225
+ "count": ->(list) {
226
+ return if list.nil?
227
+ return 0 if list.empty?
228
+ list.length
229
+ },
230
+ "min": ->(list) {
231
+ return if list.nil?
232
+ list.min
233
+ },
234
+ "max": ->(list) {
235
+ return if list.nil?
236
+ list.max
237
+ },
238
+ "sum": ->(list) {
239
+ return if list.nil?
240
+ list.sum
241
+ },
242
+ "product": ->(list) {
243
+ return if list.nil?
244
+ list.inject(:*)
245
+ },
246
+ "mean": ->(list) {
247
+ return if list.nil?
248
+ list.sum / list.length
249
+ },
250
+ "median": ->(list) {
251
+ return if list.nil?
252
+ list.sort[list.length / 2]
253
+ },
254
+ "stddev": ->(list) {
255
+ return if list.nil?
256
+ mean = list.sum / list.length.to_f
257
+ Math.sqrt(list.map { |n| (n - mean)**2 }.sum / list.length)
258
+ },
259
+ "mode": ->(list) {
260
+ return if list.nil?
261
+ list.group_by(&:itself).values.max_by(&:size).first
262
+ },
263
+ "all": ->(list) {
264
+ return if list.nil?
265
+ list.all?
266
+ },
267
+ "any": ->(list) {
268
+ return if list.nil?
269
+ list.any?
270
+ },
271
+ "sublist": ->(list, start, length) {
272
+ return if list.nil? || start.nil?
273
+ return [] if length.nil?
274
+ list[start - 1, length]
275
+ },
276
+ "append": ->(list, item) {
277
+ return if list.nil?
278
+ list + [item]
279
+ },
280
+ "concatenate": ->(list1, list2) {
281
+ return [nil, nil] if list1.nil? && list2.nil?
282
+ return [nil] + list2 if list1.nil?
283
+ return list1 + [nil] if list2.nil?
284
+ Array.wrap(list1) + Array.wrap(list2)
285
+ },
286
+ "insert before": ->(list, position, item) {
287
+ return if list.nil? || position.nil?
288
+ list.insert(position - 1, item)
289
+ },
290
+ "remove": ->(list, position) {
291
+ return if list.nil? || position.nil?
292
+ list.delete_at(position - 1); list
293
+ },
294
+ "reverse": ->(list) {
295
+ return if list.nil?
296
+ list.reverse
297
+ },
298
+ "index of": ->(list, match) {
299
+ return if list.nil?
300
+ return [] if match.nil?
301
+ list.index(match) + 1
302
+ },
303
+ "union": ->(list1, list2) {
304
+ return if list1.nil? || list2.nil?
305
+ list1 | list2
306
+ },
307
+ "distinct values": ->(list) {
308
+ return if list.nil?
309
+ list.uniq
310
+ },
311
+ "duplicate values": ->(list) {
312
+ return if list.nil?
313
+ list.select { |e| list.count(e) > 1 }.uniq
314
+ },
315
+ "flatten": ->(list) {
316
+ return if list.nil?
317
+ list.flatten
318
+ },
319
+ "sort": ->(list) {
320
+ return if list.nil?
321
+ list.sort
322
+ },
323
+ "string join": ->(list, separator) {
324
+ return if list.nil?
325
+ list.join(separator)
326
+ },
327
+ # Context functions
328
+ "get value": ->(context, name) {
329
+ return if context.nil? || name.nil?
330
+ context[name]
331
+ },
332
+ "context put": ->(context, name, value) {
333
+ return if context.nil? || name.nil?
334
+ context[name] = value; context
335
+ },
336
+ "context merge": ->(context1, context2) {
337
+ return if context1.nil? || context2.nil?
338
+ context1.merge(context2)
339
+ },
340
+ "get entries": ->(context) {
341
+ return if context.nil?
342
+ context.entries
343
+ },
344
+ # Temporal functions
345
+ "now": ->() { Time.now },
346
+ "today": ->() { Date.today },
347
+ "day of week": ->(date) {
348
+ return if date.nil?
349
+ date.wday
350
+ },
351
+ "day of year": ->(date) {
352
+ return if date.nil?
353
+ date.yday
354
+ },
355
+ "week of year": ->(date) {
356
+ return if date.nil?
357
+ date.cweek
358
+ },
359
+ "month of year": ->(date) {
360
+ return if date.nil?
361
+ date.month
362
+ },
363
+ })
364
+ end
365
+
366
+ def as_json
367
+ {
368
+ id: id,
369
+ text: text,
370
+ }
371
+ end
372
+ end
373
+ end
374
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFeel
4
+ module Dmn
5
+ class Output
6
+ attr_reader :id, :label, :name, :type_ref
7
+
8
+ def self.from_json(json)
9
+ Output.new(id: json[:id], label: json[:label], name: json[:name], type_ref: json[:type_ref])
10
+ end
11
+
12
+ def initialize(id:, label:, name:, type_ref:)
13
+ @id = id
14
+ @label = label
15
+ @name = name
16
+ @type_ref = type_ref
17
+ end
18
+
19
+ def as_json
20
+ {
21
+ id: id,
22
+ label: label,
23
+ name: name,
24
+ type_ref: type_ref,
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFeel
4
+ module Dmn
5
+ class Rule
6
+ attr_accessor :id, :input_entries, :output_entries, :description
7
+
8
+ def self.from_json(json)
9
+ input_entries = Array.wrap(json[:input_entry]).map { |input_entry| UnaryTests.from_json(input_entry) }
10
+ output_entries = Array.wrap(json[:output_entry]).map { |output_entry| LiteralExpression.from_json(output_entry) }
11
+ Rule.new(id: json[:id], input_entries:, output_entries:, description: json[:description])
12
+ end
13
+
14
+ def initialize(id:, input_entries:, output_entries:, description: nil)
15
+ @id = id
16
+ @input_entries = input_entries
17
+ @output_entries = output_entries
18
+ @description = description
19
+ end
20
+
21
+ def evaluate(input_values = [], variables = {})
22
+ [].tap do |test_results|
23
+ input_entries.each_with_index do |input_entry, index|
24
+ test_results.push input_entry.test(input_values[index], variables)
25
+ end
26
+ end
27
+ end
28
+
29
+ def as_json
30
+ {
31
+ id: id,
32
+ input_entries: input_entries.map(&:as_json),
33
+ output_entries: output_entries.map(&:as_json),
34
+ description: description,
35
+ }
36
+ end
37
+
38
+ def output_value(outputs, variables)
39
+ HashWithIndifferentAccess.new.tap do |ov|
40
+ output_entries.each_with_index do |output_entry, index|
41
+ if output_entry.valid?
42
+ val = output_entry.evaluate(variables)
43
+ nested_hash_value(ov, outputs[index].name, val)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def nested_hash_value(hash, key_string, value)
52
+ keys = key_string.split('.')
53
+ current = hash
54
+ keys[0...-1].each do |key|
55
+ current[key] ||= {}
56
+ current = current[key]
57
+ end
58
+ current[keys.last] = value
59
+ hash
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFeel
4
+ module Dmn
5
+ class UnaryTests < LiteralExpression
6
+ attr_reader :id, :text
7
+
8
+ def self.from_json(json)
9
+ UnaryTests.new(id: json[:id], text: json[:text])
10
+ end
11
+
12
+ def tree
13
+ @tree ||= Parser.parse_test(text)
14
+ end
15
+
16
+ def valid?
17
+ return true if text.nil? || text == '-'
18
+ tree.present?
19
+ end
20
+
21
+ def test(input, variables = {})
22
+ return true if text.nil? || text == '-'
23
+ tree.eval(functions.merge(variables)).call(input)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFeel
4
+ module Dmn
5
+ class Variable
6
+ attr_reader :id, :name, :type_ref
7
+
8
+ def self.from_json(json)
9
+ Variable.new(id: json[:id], name: json[:name], type_ref: json[:type_ref])
10
+ end
11
+
12
+ def initialize(id:, name:, type_ref:)
13
+ @id = id
14
+ @name = name
15
+ @type_ref = type_ref
16
+ end
17
+
18
+ def as_json
19
+ {
20
+ id: id,
21
+ name: name,
22
+ type_ref: type_ref,
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dmn/variable"
4
+ require_relative "dmn/literal_expression"
5
+ require_relative "dmn/unary_tests"
6
+ require_relative "dmn/input"
7
+ require_relative "dmn/output"
8
+ require_relative "dmn/rule"
9
+ require_relative "dmn/decision_table"
10
+ require_relative "dmn/information_requirement"
11
+ require_relative "dmn/decision"
12
+ require_relative "dmn/definitions"
13
+
14
+ module SpotFeel
15
+ module Dmn
16
+ end
17
+ end