spot_feel 0.0.1

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.
@@ -0,0 +1,370 @@
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) { value == true ? false : true },
97
+ "is defined": ->(value) {
98
+ return if value.nil?
99
+ !value.nil?
100
+ },
101
+ "get or else": ->(value, default) {
102
+ value.nil? ? default : value
103
+ },
104
+ # String functions
105
+ "substring": ->(string, start, length) {
106
+ return if string.nil? || start.nil?
107
+ return "" if length.nil?
108
+ string[start - 1, length]
109
+ },
110
+ "substring before": ->(string, match) {
111
+ return if string.nil? || match.nil?
112
+ string.split(match).first
113
+ },
114
+ "substring after": ->(string, match) {
115
+ return if string.nil? || match.nil?
116
+ string.split(match).last
117
+ },
118
+ "string length": ->(string) {
119
+ return if string.nil?
120
+ string.length
121
+ },
122
+ "upper case": ->(string) {
123
+ return if string.nil?
124
+ string.upcase
125
+ },
126
+ "lower case": -> (string) {
127
+ return if string.nil?
128
+ string.downcase
129
+ },
130
+ "contains": ->(string, match) {
131
+ return if string.nil? || match.nil?
132
+ string.include?(match)
133
+ },
134
+ "starts with": ->(string, match) {
135
+ return if string.nil? || match.nil?
136
+ string.start_with?(match)
137
+ },
138
+ "ends with": ->(string, match) {
139
+ return if string.nil? || match.nil?
140
+ string.end_with?(match)
141
+ },
142
+ "matches": ->(string, match) {
143
+ return if string.nil? || match.nil?
144
+ string.match?(match)
145
+ },
146
+ "replace": ->(string, match, replacement) {
147
+ return if string.nil? || match.nil? || replacement.nil?
148
+ string.gsub(match, replacement)
149
+ },
150
+ "split": ->(string, match) {
151
+ return if string.nil? || match.nil?
152
+ string.split(match)
153
+ },
154
+ "strip": -> (string) {
155
+ return if string.nil?
156
+ string.strip
157
+ },
158
+ "extract": -> (string, pattern) {
159
+ return if string.nil? || pattern.nil?
160
+ string.match(pattern).captures
161
+ },
162
+ # Numeric functions
163
+ "decimal": ->(n, scale) {
164
+ return if n.nil? || scale.nil?
165
+ n.round(scale)
166
+ },
167
+ "floor": ->(n) {
168
+ return if n.nil?
169
+ n.floor
170
+ },
171
+ "ceiling": ->(n) {
172
+ return if n.nil?
173
+ n.ceil
174
+ },
175
+ "round up": ->(n) {
176
+ return if n.nil?
177
+ n.ceil
178
+ },
179
+ "round down": ->(n) {
180
+ return if n.nil?
181
+ n.floor
182
+ },
183
+ "abs": ->(n) {
184
+ return if n.nil?
185
+ n.abs
186
+ },
187
+ "modulo": ->(n, divisor) {
188
+ return if n.nil? || divisor.nil?
189
+ n % divisor
190
+ },
191
+ "sqrt": ->(n) {
192
+ return if n.nil?
193
+ Math.sqrt(n)
194
+ },
195
+ "log": ->(n) {
196
+ return if n.nil?
197
+ Math.log(n)
198
+ },
199
+ "exp": ->(n) {
200
+ return if n.nil?
201
+ Math.exp(n)
202
+ },
203
+ "odd": ->(n) {
204
+ return if n.nil?
205
+ n.odd?
206
+ },
207
+ "even": ->(n) {
208
+ return if n.nil?
209
+ n.even?
210
+ },
211
+ "random number": ->(n) {
212
+ return if n.nil?
213
+ rand(n)
214
+ },
215
+ # List functions
216
+ "list contains": ->(list, match) {
217
+ return if list.nil?
218
+ return false if match.nil?
219
+ list.include?(match)
220
+ },
221
+ "count": ->(list) {
222
+ return if list.nil?
223
+ return 0 if list.empty?
224
+ list.length
225
+ },
226
+ "min": ->(list) {
227
+ return if list.nil?
228
+ list.min
229
+ },
230
+ "max": ->(list) {
231
+ return if list.nil?
232
+ list.max
233
+ },
234
+ "sum": ->(list) {
235
+ return if list.nil?
236
+ list.sum
237
+ },
238
+ "product": ->(list) {
239
+ return if list.nil?
240
+ list.inject(:*)
241
+ },
242
+ "mean": ->(list) {
243
+ return if list.nil?
244
+ list.sum / list.length
245
+ },
246
+ "median": ->(list) {
247
+ return if list.nil?
248
+ list.sort[list.length / 2]
249
+ },
250
+ "stddev": ->(list) {
251
+ return if list.nil?
252
+ mean = list.sum / list.length.to_f
253
+ Math.sqrt(list.map { |n| (n - mean)**2 }.sum / list.length)
254
+ },
255
+ "mode": ->(list) {
256
+ return if list.nil?
257
+ list.group_by(&:itself).values.max_by(&:size).first
258
+ },
259
+ "all": ->(list) {
260
+ return if list.nil?
261
+ list.all?
262
+ },
263
+ "any": ->(list) {
264
+ return if list.nil?
265
+ list.any?
266
+ },
267
+ "sublist": ->(list, start, length) {
268
+ return if list.nil? || start.nil?
269
+ return [] if length.nil?
270
+ list[start - 1, length]
271
+ },
272
+ "append": ->(list, item) {
273
+ return if list.nil?
274
+ list + [item]
275
+ },
276
+ "concatenate": ->(list1, list2) {
277
+ return [nil, nil] if list1.nil? && list2.nil?
278
+ return [nil] + list2 if list1.nil?
279
+ return list1 + [nil] if list2.nil?
280
+ Array.wrap(list1) + Array.wrap(list2)
281
+ },
282
+ "insert before": ->(list, position, item) {
283
+ return if list.nil? || position.nil?
284
+ list.insert(position - 1, item)
285
+ },
286
+ "remove": ->(list, position) {
287
+ return if list.nil? || position.nil?
288
+ list.delete_at(position - 1); list
289
+ },
290
+ "reverse": ->(list) {
291
+ return if list.nil?
292
+ list.reverse
293
+ },
294
+ "index of": ->(list, match) {
295
+ return if list.nil?
296
+ return [] if match.nil?
297
+ list.index(match) + 1
298
+ },
299
+ "union": ->(list1, list2) {
300
+ return if list1.nil? || list2.nil?
301
+ list1 | list2
302
+ },
303
+ "distinct values": ->(list) {
304
+ return if list.nil?
305
+ list.uniq
306
+ },
307
+ "duplicate values": ->(list) {
308
+ return if list.nil?
309
+ list.select { |e| list.count(e) > 1 }.uniq
310
+ },
311
+ "flatten": ->(list) {
312
+ return if list.nil?
313
+ list.flatten
314
+ },
315
+ "sort": ->(list) {
316
+ return if list.nil?
317
+ list.sort
318
+ },
319
+ "string join": ->(list, separator) {
320
+ return if list.nil?
321
+ list.join(separator)
322
+ },
323
+ # Context functions
324
+ "get value": ->(context, name) {
325
+ return if context.nil? || name.nil?
326
+ context[name]
327
+ },
328
+ "context put": ->(context, name, value) {
329
+ return if context.nil? || name.nil?
330
+ context[name] = value; context
331
+ },
332
+ "context merge": ->(context1, context2) {
333
+ return if context1.nil? || context2.nil?
334
+ context1.merge(context2)
335
+ },
336
+ "get entries": ->(context) {
337
+ return if context.nil?
338
+ context.entries
339
+ },
340
+ # Temporal functions
341
+ "now": ->() { Time.now },
342
+ "today": ->() { Date.today },
343
+ "day of week": ->(date) {
344
+ return if date.nil?
345
+ date.wday
346
+ },
347
+ "day of year": ->(date) {
348
+ return if date.nil?
349
+ date.yday
350
+ },
351
+ "week of year": ->(date) {
352
+ return if date.nil?
353
+ date.cweek
354
+ },
355
+ "month of year": ->(date) {
356
+ return if date.nil?
357
+ date.month
358
+ },
359
+ })
360
+ end
361
+
362
+ def as_json
363
+ {
364
+ id: id,
365
+ text: text,
366
+ }
367
+ end
368
+ end
369
+ end
370
+ 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