decision_agent 0.2.0 → 0.3.0

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -1
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/dmn/adapter.rb +135 -0
  5. data/lib/decision_agent/dmn/cache.rb +306 -0
  6. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  7. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  8. data/lib/decision_agent/dmn/errors.rb +30 -0
  9. data/lib/decision_agent/dmn/exporter.rb +217 -0
  10. data/lib/decision_agent/dmn/feel/evaluator.rb +797 -0
  11. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  12. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  13. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  14. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  15. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  16. data/lib/decision_agent/dmn/importer.rb +77 -0
  17. data/lib/decision_agent/dmn/model.rb +197 -0
  18. data/lib/decision_agent/dmn/parser.rb +191 -0
  19. data/lib/decision_agent/dmn/testing.rb +333 -0
  20. data/lib/decision_agent/dmn/validator.rb +315 -0
  21. data/lib/decision_agent/dmn/versioning.rb +229 -0
  22. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  23. data/lib/decision_agent/dsl/condition_evaluator.rb +3 -0
  24. data/lib/decision_agent/dsl/schema_validator.rb +2 -1
  25. data/lib/decision_agent/evaluators/dmn_evaluator.rb +221 -0
  26. data/lib/decision_agent/version.rb +1 -1
  27. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  28. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  29. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  30. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  31. data/lib/decision_agent/web/public/index.html +3 -0
  32. data/lib/decision_agent/web/public/styles.css +21 -0
  33. data/lib/decision_agent/web/server.rb +465 -0
  34. data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
  35. data/spec/auth/rbac_adapter_spec.rb +228 -0
  36. data/spec/dmn/decision_graph_spec.rb +282 -0
  37. data/spec/dmn/decision_tree_spec.rb +203 -0
  38. data/spec/dmn/feel/errors_spec.rb +18 -0
  39. data/spec/dmn/feel/functions_spec.rb +400 -0
  40. data/spec/dmn/feel/simple_parser_spec.rb +274 -0
  41. data/spec/dmn/feel/types_spec.rb +176 -0
  42. data/spec/dmn/feel_parser_spec.rb +489 -0
  43. data/spec/dmn/hit_policy_spec.rb +202 -0
  44. data/spec/dmn/integration_spec.rb +226 -0
  45. data/spec/examples.txt +1846 -1570
  46. data/spec/fixtures/dmn/complex_decision.dmn +81 -0
  47. data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
  48. data/spec/fixtures/dmn/simple_decision.dmn +40 -0
  49. data/spec/monitoring/metrics_collector_spec.rb +37 -35
  50. data/spec/monitoring/monitored_agent_spec.rb +14 -11
  51. data/spec/performance_optimizations_spec.rb +10 -3
  52. data/spec/thread_safety_spec.rb +10 -2
  53. data/spec/web_ui_rack_spec.rb +294 -0
  54. metadata +65 -1
@@ -0,0 +1,221 @@
1
+ require_relative "../dmn/adapter"
2
+ require_relative "../dmn/errors"
3
+ require_relative "base"
4
+ require_relative "json_rule_evaluator"
5
+
6
+ module DecisionAgent
7
+ module Evaluators
8
+ # Evaluates DMN decision models
9
+ class DmnEvaluator < Base
10
+ attr_reader :model, :decision_id
11
+
12
+ def initialize(model:, decision_id:, name: nil)
13
+ @model = model
14
+ @decision_id = decision_id.to_s
15
+ @name = name || "DmnEvaluator(#{@decision_id})"
16
+
17
+ # Find and validate decision
18
+ @decision = @model.find_decision(@decision_id)
19
+ raise Dmn::InvalidDmnModelError, "Decision '#{@decision_id}' not found" unless @decision
20
+ raise Dmn::InvalidDmnModelError, "Decision '#{@decision_id}' has no decision table" unless @decision.decision_table
21
+
22
+ # Convert to JSON rules for execution
23
+ adapter = Dmn::Adapter.new(@decision.decision_table)
24
+ @rules_json = adapter.to_json_rules
25
+
26
+ # Create internal JSON rule evaluator
27
+ @json_evaluator = JsonRuleEvaluator.new(
28
+ rules_json: @rules_json,
29
+ name: @name
30
+ )
31
+
32
+ # Freeze for thread safety
33
+ @model.freeze
34
+ @decision_id.freeze
35
+ @name.freeze
36
+ freeze
37
+ end
38
+
39
+ def evaluate(context, feedback: {})
40
+ hit_policy = @decision.decision_table.hit_policy
41
+
42
+ # Short-circuit for FIRST and PRIORITY policies
43
+ if %w[FIRST PRIORITY].include?(hit_policy)
44
+ first_match = find_first_matching_evaluation(context, feedback: feedback)
45
+ return first_match if first_match
46
+
47
+ # If no match found, return nil (consistent with apply_first_policy behavior)
48
+ return nil
49
+ end
50
+
51
+ # For UNIQUE, ANY, COLLECT - need all matches
52
+ matching_evaluations = find_all_matching_evaluations(context, feedback: feedback)
53
+
54
+ # Apply hit policy to select the appropriate evaluation
55
+ apply_hit_policy(matching_evaluations)
56
+ end
57
+
58
+ private
59
+
60
+ def evaluator_name
61
+ @name
62
+ end
63
+
64
+ # Find first matching rule (for short-circuiting)
65
+ def find_first_matching_evaluation(context, feedback: {})
66
+ ctx = context.is_a?(Context) ? context : Context.new(context)
67
+ rules = @rules_json["rules"] || []
68
+
69
+ rules.each do |rule|
70
+ if_clause = rule["if"]
71
+ next unless if_clause
72
+
73
+ next unless Dsl::ConditionEvaluator.evaluate(if_clause, ctx)
74
+
75
+ then_clause = rule["then"]
76
+ return Evaluation.new(
77
+ decision: then_clause["decision"],
78
+ weight: then_clause["weight"] || 1.0,
79
+ reason: then_clause["reason"] || "Rule matched",
80
+ evaluator_name: @name,
81
+ metadata: {
82
+ type: "dmn_rule",
83
+ rule_id: rule["id"],
84
+ ruleset: @rules_json["ruleset"],
85
+ hit_policy: @decision.decision_table.hit_policy
86
+ }
87
+ )
88
+ end
89
+
90
+ nil
91
+ end
92
+
93
+ # Find all matching rules (not just first)
94
+ def find_all_matching_evaluations(context, feedback: {})
95
+ ctx = context.is_a?(Context) ? context : Context.new(context)
96
+ rules = @rules_json["rules"] || []
97
+ matching = []
98
+
99
+ rules.each do |rule|
100
+ if_clause = rule["if"]
101
+ next unless if_clause
102
+
103
+ next unless Dsl::ConditionEvaluator.evaluate(if_clause, ctx)
104
+
105
+ then_clause = rule["then"]
106
+ matching << Evaluation.new(
107
+ decision: then_clause["decision"],
108
+ weight: then_clause["weight"] || 1.0,
109
+ reason: then_clause["reason"] || "Rule matched",
110
+ evaluator_name: @name,
111
+ metadata: {
112
+ type: "dmn_rule",
113
+ rule_id: rule["id"],
114
+ ruleset: @rules_json["ruleset"],
115
+ hit_policy: @decision.decision_table.hit_policy
116
+ }
117
+ )
118
+ end
119
+
120
+ matching
121
+ end
122
+
123
+ # Apply hit policy to matching evaluations
124
+ def apply_hit_policy(matching_evaluations)
125
+ hit_policy = @decision.decision_table.hit_policy
126
+
127
+ case hit_policy
128
+ when "UNIQUE"
129
+ apply_unique_policy(matching_evaluations)
130
+ when "FIRST"
131
+ apply_first_policy(matching_evaluations)
132
+ when "PRIORITY"
133
+ apply_priority_policy(matching_evaluations)
134
+ when "ANY"
135
+ apply_any_policy(matching_evaluations)
136
+ when "COLLECT"
137
+ apply_collect_policy(matching_evaluations)
138
+ else
139
+ # Default to FIRST if unknown policy
140
+ apply_first_policy(matching_evaluations)
141
+ end
142
+ end
143
+
144
+ # UNIQUE: Exactly one rule must match
145
+ def apply_unique_policy(matching_evaluations)
146
+ case matching_evaluations.size
147
+ when 0
148
+ raise Dmn::InvalidDmnModelError,
149
+ "UNIQUE hit policy requires exactly one matching rule, but none matched"
150
+ when 1
151
+ matching_evaluations.first
152
+ else
153
+ rule_ids = matching_evaluations.map { |e| e.metadata[:rule_id] }.join(", ")
154
+ raise Dmn::InvalidDmnModelError,
155
+ "UNIQUE hit policy requires exactly one matching rule, but #{matching_evaluations.size} matched: #{rule_ids}"
156
+ end
157
+ end
158
+
159
+ # FIRST: Return first matching rule (already in order)
160
+ def apply_first_policy(matching_evaluations)
161
+ return nil if matching_evaluations.empty?
162
+
163
+ matching_evaluations.first
164
+ end
165
+
166
+ # PRIORITY: Return rule with highest priority
167
+ # For now, we use rule order as priority (first rule = highest priority)
168
+ # In full DMN spec, outputs can have priority values defined
169
+ def apply_priority_policy(matching_evaluations)
170
+ return nil if matching_evaluations.empty?
171
+
172
+ # For now, return first match (rules are already in priority order)
173
+ # Future enhancement: check output priority values if defined
174
+ matching_evaluations.first
175
+ end
176
+
177
+ # ANY: All matching rules must have same output
178
+ def apply_any_policy(matching_evaluations)
179
+ return nil if matching_evaluations.empty?
180
+
181
+ # Check that all decisions are the same
182
+ first_decision = matching_evaluations.first.decision
183
+ all_same = matching_evaluations.all? { |e| e.decision == first_decision }
184
+
185
+ unless all_same
186
+ decisions = matching_evaluations.map(&:decision).uniq.join(", ")
187
+ rule_ids = matching_evaluations.map { |e| e.metadata[:rule_id] }.join(", ")
188
+ raise Dmn::InvalidDmnModelError,
189
+ "ANY hit policy requires all matching rules to have the same output, " \
190
+ "but found different outputs: #{decisions} (rules: #{rule_ids})"
191
+ end
192
+
193
+ matching_evaluations.first
194
+ end
195
+
196
+ # COLLECT: Return all matching rules
197
+ # Since Evaluation expects a single decision, we'll return the first one
198
+ # but include metadata about all matches
199
+ def apply_collect_policy(matching_evaluations)
200
+ return nil if matching_evaluations.empty?
201
+
202
+ # Return first evaluation but include all matches in metadata
203
+ first = matching_evaluations.first
204
+ all_decisions = matching_evaluations.map(&:decision)
205
+ all_rule_ids = matching_evaluations.map { |e| e.metadata[:rule_id] }
206
+
207
+ Evaluation.new(
208
+ decision: first.decision,
209
+ weight: first.weight,
210
+ reason: "COLLECT: #{matching_evaluations.size} rules matched",
211
+ evaluator_name: @name,
212
+ metadata: first.metadata.merge(
213
+ collect_count: matching_evaluations.size,
214
+ collect_decisions: all_decisions,
215
+ collect_rule_ids: all_rule_ids
216
+ )
217
+ )
218
+ end
219
+ end
220
+ end
221
+ end
@@ -3,7 +3,7 @@ module DecisionAgent
3
3
  # MAJOR: Incremented for incompatible API changes
4
4
  # MINOR: Incremented for backward-compatible functionality additions
5
5
  # PATCH: Incremented for backward-compatible bug fixes
6
- VERSION = "0.2.0".freeze
6
+ VERSION = "0.3.0".freeze
7
7
 
8
8
  # Validate version format (semantic versioning)
9
9
  unless VERSION.match?(/\A\d+\.\d+\.\d+(-[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?\z/)
@@ -0,0 +1,426 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../dmn/parser"
5
+ require_relative "../dmn/exporter"
6
+ require_relative "../dmn/importer"
7
+ require_relative "../dmn/validator"
8
+ require_relative "../dmn/model"
9
+ require_relative "../dmn/decision_tree"
10
+ require_relative "../dmn/decision_graph"
11
+ require_relative "../dmn/visualizer"
12
+
13
+ module DecisionAgent
14
+ module Web
15
+ # DMN Editor Backend
16
+ # Provides API endpoints for visual DMN modeling
17
+ class DmnEditor
18
+ attr_reader :storage
19
+
20
+ def initialize(storage: nil)
21
+ @storage = storage || {}
22
+ @storage_mutex = Mutex.new
23
+ end
24
+
25
+ # Create a new DMN model
26
+ def create_model(name:, namespace: nil)
27
+ model_id = generate_id
28
+ namespace ||= "http://decisonagent.com/dmn/#{model_id}"
29
+
30
+ model = Dmn::Model.new(
31
+ id: model_id,
32
+ name: name,
33
+ namespace: namespace
34
+ )
35
+
36
+ store_model(model_id, model)
37
+
38
+ {
39
+ id: model_id,
40
+ name: name,
41
+ namespace: namespace,
42
+ decisions: [],
43
+ created_at: Time.now.utc.iso8601
44
+ }
45
+ end
46
+
47
+ # Get a DMN model
48
+ def get_model(model_id)
49
+ model = retrieve_model(model_id)
50
+ return nil unless model
51
+
52
+ serialize_model(model)
53
+ end
54
+
55
+ # Update DMN model metadata
56
+ def update_model(model_id, name: nil, namespace: nil)
57
+ model = retrieve_model(model_id)
58
+ return nil unless model
59
+
60
+ model.instance_variable_set(:@name, name) if name
61
+ model.instance_variable_set(:@namespace, namespace) if namespace
62
+
63
+ store_model(model_id, model)
64
+ serialize_model(model)
65
+ end
66
+
67
+ # Delete a DMN model
68
+ # rubocop:disable Naming/PredicateMethod
69
+ def delete_model(model_id)
70
+ @storage_mutex.synchronize do
71
+ @storage.delete(model_id)
72
+ end
73
+ true
74
+ end
75
+ # rubocop:enable Naming/PredicateMethod
76
+
77
+ # Add a decision to a model
78
+ def add_decision(model_id:, decision_id:, name:, type: "decision_table")
79
+ model = retrieve_model(model_id)
80
+ return nil unless model
81
+
82
+ decision = Dmn::Decision.new(
83
+ id: decision_id,
84
+ name: name
85
+ )
86
+
87
+ # Initialize decision logic based on type
88
+ case type
89
+ when "decision_table"
90
+ decision.instance_variable_set(:@decision_table, Dmn::DecisionTable.new(
91
+ id: "#{decision_id}_table",
92
+ hit_policy: "FIRST"
93
+ ))
94
+ when "decision_tree"
95
+ decision.instance_variable_set(:@decision_tree, Dmn::DecisionTree.new(
96
+ id: "#{decision_id}_tree",
97
+ name: name
98
+ ))
99
+ when "literal"
100
+ decision.instance_variable_set(:@literal_expression, "")
101
+ end
102
+
103
+ model.add_decision(decision)
104
+ store_model(model_id, model)
105
+
106
+ serialize_decision(decision)
107
+ end
108
+
109
+ # Update a decision
110
+ def update_decision(model_id:, decision_id:, name: nil, logic: nil)
111
+ model = retrieve_model(model_id)
112
+ return nil unless model
113
+
114
+ decision = model.find_decision(decision_id)
115
+ return nil unless decision
116
+
117
+ decision.instance_variable_set(:@name, name) if name
118
+
119
+ update_decision_table(decision.decision_table, logic) if logic && decision.decision_table
120
+
121
+ store_model(model_id, model)
122
+ serialize_decision(decision)
123
+ end
124
+
125
+ # Delete a decision
126
+ # rubocop:disable Naming/PredicateMethod
127
+ def delete_decision(model_id:, decision_id:)
128
+ model = retrieve_model(model_id)
129
+ return false unless model
130
+
131
+ model.decisions.reject! { |d| d.id == decision_id }
132
+ store_model(model_id, model)
133
+ true
134
+ end
135
+ # rubocop:enable Naming/PredicateMethod
136
+
137
+ # Add input to decision table
138
+ def add_input(model_id:, decision_id:, input_id:, label:, type_ref: nil, expression: nil)
139
+ model = retrieve_model(model_id)
140
+ return nil unless model
141
+
142
+ decision = model.find_decision(decision_id)
143
+ return nil unless decision&.decision_table
144
+
145
+ input = Dmn::Input.new(
146
+ id: input_id,
147
+ label: label,
148
+ type_ref: type_ref,
149
+ expression: expression
150
+ )
151
+
152
+ decision.decision_table.inputs << input
153
+ store_model(model_id, model)
154
+
155
+ serialize_input(input)
156
+ end
157
+
158
+ # Add output to decision table
159
+ def add_output(model_id:, decision_id:, output_id:, label:, type_ref: nil, name: nil)
160
+ model = retrieve_model(model_id)
161
+ return nil unless model
162
+
163
+ decision = model.find_decision(decision_id)
164
+ return nil unless decision&.decision_table
165
+
166
+ output = Dmn::Output.new(
167
+ id: output_id,
168
+ label: label,
169
+ type_ref: type_ref,
170
+ name: name
171
+ )
172
+
173
+ decision.decision_table.outputs << output
174
+ store_model(model_id, model)
175
+
176
+ serialize_output(output)
177
+ end
178
+
179
+ # Add rule to decision table
180
+ def add_rule(model_id:, decision_id:, rule_id:, input_entries:, output_entries:, description: nil)
181
+ model = retrieve_model(model_id)
182
+ return nil unless model
183
+
184
+ decision = model.find_decision(decision_id)
185
+ return nil unless decision&.decision_table
186
+
187
+ rule = Dmn::Rule.new(id: rule_id)
188
+ rule.instance_variable_set(:@input_entries, input_entries)
189
+ rule.instance_variable_set(:@output_entries, output_entries)
190
+ rule.instance_variable_set(:@description, description) if description
191
+
192
+ decision.decision_table.rules << rule
193
+ store_model(model_id, model)
194
+
195
+ serialize_rule(rule)
196
+ end
197
+
198
+ # Update rule
199
+ def update_rule(model_id:, decision_id:, rule_id:, input_entries: nil, output_entries: nil, description: nil)
200
+ model = retrieve_model(model_id)
201
+ return nil unless model
202
+
203
+ decision = model.find_decision(decision_id)
204
+ return nil unless decision&.decision_table
205
+
206
+ rule = decision.decision_table.rules.find { |r| r.id == rule_id }
207
+ return nil unless rule
208
+
209
+ rule.instance_variable_set(:@input_entries, input_entries) if input_entries
210
+ rule.instance_variable_set(:@output_entries, output_entries) if output_entries
211
+ rule.instance_variable_set(:@description, description) if description
212
+
213
+ store_model(model_id, model)
214
+ serialize_rule(rule)
215
+ end
216
+
217
+ # Delete rule
218
+ # rubocop:disable Naming/PredicateMethod
219
+ def delete_rule(model_id:, decision_id:, rule_id:)
220
+ model = retrieve_model(model_id)
221
+ return false unless model
222
+
223
+ decision = model.find_decision(decision_id)
224
+ return false unless decision&.decision_table
225
+
226
+ decision.decision_table.rules.reject! { |r| r.id == rule_id }
227
+ store_model(model_id, model)
228
+ true
229
+ end
230
+ # rubocop:enable Naming/PredicateMethod
231
+
232
+ # Validate a DMN model
233
+ def validate_model(model_id)
234
+ model = retrieve_model(model_id)
235
+ return { valid: false, errors: ["Model not found"] } unless model
236
+
237
+ validator = Dmn::Validator.new
238
+ validator.validate(model)
239
+
240
+ {
241
+ valid: validator.valid?,
242
+ errors: validator.errors,
243
+ warnings: validator.warnings
244
+ }
245
+ end
246
+
247
+ # Export DMN model to XML
248
+ def export_to_xml(model_id)
249
+ model = retrieve_model(model_id)
250
+ return nil unless model
251
+
252
+ exporter = Dmn::Exporter.new
253
+ exporter.export(model)
254
+ end
255
+
256
+ # Import DMN model from XML
257
+ def import_from_xml(xml_content, name: nil)
258
+ parser = Dmn::Parser.new
259
+ model = parser.parse(xml_content)
260
+
261
+ # Generate new ID for imported model
262
+ model_id = generate_id
263
+ model.instance_variable_set(:@id, model_id)
264
+ model.instance_variable_set(:@name, name) if name
265
+
266
+ store_model(model_id, model)
267
+
268
+ serialize_model(model)
269
+ end
270
+
271
+ # Generate visualization for decision tree
272
+ def visualize_tree(model_id:, decision_id:, format: "svg")
273
+ model = retrieve_model(model_id)
274
+ return nil unless model
275
+
276
+ decision = model.find_decision(decision_id)
277
+ return nil unless decision || !decision.decision_tree
278
+
279
+ case format.to_s.downcase
280
+ when "svg"
281
+ Dmn::Visualizer.tree_to_svg(decision.decision_tree)
282
+ when "dot"
283
+ Dmn::Visualizer.tree_to_dot(decision.decision_tree)
284
+ when "mermaid"
285
+ Dmn::Visualizer.tree_to_mermaid(decision.decision_tree)
286
+ end
287
+ end
288
+
289
+ # Generate visualization for decision graph
290
+ def visualize_graph(model_id:, format: "svg")
291
+ model = retrieve_model(model_id)
292
+ return nil unless model
293
+
294
+ # Convert model to decision graph
295
+ graph = Dmn::DecisionGraph.new(id: model.id, name: model.name)
296
+ model.decisions.each do |decision|
297
+ node = Dmn::DecisionNode.new(
298
+ id: decision.id,
299
+ name: decision.name,
300
+ decision_logic: decision.decision_table || decision.decision_tree
301
+ )
302
+
303
+ # Add dependencies from information requirements
304
+ decision.information_requirements.each do |req|
305
+ node.add_dependency(req[:decision_id], req[:variable_name])
306
+ end
307
+
308
+ graph.add_decision(node)
309
+ end
310
+
311
+ case format.to_s.downcase
312
+ when "svg"
313
+ Dmn::Visualizer.graph_to_svg(graph)
314
+ when "dot"
315
+ Dmn::Visualizer.graph_to_dot(graph)
316
+ when "mermaid"
317
+ Dmn::Visualizer.graph_to_mermaid(graph)
318
+ end
319
+ end
320
+
321
+ # List all models
322
+ def list_models
323
+ @storage_mutex.synchronize do
324
+ @storage.map do |id, model|
325
+ {
326
+ id: id,
327
+ name: model.name,
328
+ namespace: model.namespace,
329
+ decision_count: model.decisions.size
330
+ }
331
+ end
332
+ end
333
+ end
334
+
335
+ private
336
+
337
+ def generate_id
338
+ "dmn_#{Time.now.to_i}_#{rand(10_000)}"
339
+ end
340
+
341
+ def store_model(model_id, model)
342
+ @storage_mutex.synchronize do
343
+ @storage[model_id] = model
344
+ end
345
+ end
346
+
347
+ def retrieve_model(model_id)
348
+ @storage_mutex.synchronize do
349
+ @storage[model_id]
350
+ end
351
+ end
352
+
353
+ def update_decision_table(table, logic)
354
+ table.instance_variable_set(:@hit_policy, logic[:hit_policy]) if logic[:hit_policy]
355
+ table.instance_variable_set(:@inputs, logic[:inputs]) if logic[:inputs]
356
+ table.instance_variable_set(:@outputs, logic[:outputs]) if logic[:outputs]
357
+ table.instance_variable_set(:@rules, logic[:rules]) if logic[:rules]
358
+ end
359
+
360
+ def serialize_model(model)
361
+ {
362
+ id: model.id,
363
+ name: model.name,
364
+ namespace: model.namespace,
365
+ decisions: model.decisions.map { |d| serialize_decision(d) }
366
+ }
367
+ end
368
+
369
+ def serialize_decision(decision)
370
+ result = {
371
+ id: decision.id,
372
+ name: decision.name
373
+ }
374
+
375
+ if decision.decision_table
376
+ result[:decision_table] = serialize_decision_table(decision.decision_table)
377
+ elsif decision.decision_tree
378
+ result[:decision_tree] = decision.decision_tree.to_h
379
+ elsif decision.instance_variable_get(:@literal_expression)
380
+ result[:literal_expression] = decision.instance_variable_get(:@literal_expression)
381
+ end
382
+
383
+ result[:information_requirements] = decision.information_requirements if decision.information_requirements.any?
384
+
385
+ result
386
+ end
387
+
388
+ def serialize_decision_table(table)
389
+ {
390
+ id: table.id,
391
+ hit_policy: table.hit_policy,
392
+ inputs: table.inputs.map { |i| serialize_input(i) },
393
+ outputs: table.outputs.map { |o| serialize_output(o) },
394
+ rules: table.rules.map { |r| serialize_rule(r) }
395
+ }
396
+ end
397
+
398
+ def serialize_input(input)
399
+ {
400
+ id: input.id,
401
+ label: input.label,
402
+ type_ref: input.type_ref,
403
+ expression: input.expression
404
+ }
405
+ end
406
+
407
+ def serialize_output(output)
408
+ {
409
+ id: output.id,
410
+ label: output.label,
411
+ type_ref: output.type_ref,
412
+ name: output.name
413
+ }
414
+ end
415
+
416
+ def serialize_rule(rule)
417
+ {
418
+ id: rule.id,
419
+ input_entries: rule.input_entries,
420
+ output_entries: rule.output_entries,
421
+ description: rule.description
422
+ }
423
+ end
424
+ end
425
+ end
426
+ end