marty 1.2.0 → 1.2.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -1
  3. data/Gemfile.lock +2 -2
  4. data/app/components/marty/base_rule_view.rb +279 -0
  5. data/app/components/marty/delorean_rule_view.rb +26 -0
  6. data/app/components/marty/extras/layout.rb +22 -7
  7. data/app/components/marty/log_view.rb +1 -1
  8. data/app/components/marty/mcfly_grid_panel.rb +53 -0
  9. data/app/components/marty/mcfly_grid_panel/client/dup_in_form.js +20 -0
  10. data/app/models/marty/base_rule.rb +126 -0
  11. data/app/models/marty/delorean_rule.rb +121 -0
  12. data/lib/marty/data_importer.rb +0 -1
  13. data/lib/marty/rule_script_set.rb +176 -0
  14. data/lib/marty/version.rb +1 -1
  15. data/lib/tasks/marty_tasks.rake +42 -0
  16. data/spec/dummy/app/components/gemini/cm_auth_app.rb +18 -0
  17. data/spec/dummy/app/components/gemini/my_rule_view.rb +63 -0
  18. data/spec/dummy/app/components/gemini/xyz_rule_view.rb +25 -0
  19. data/spec/dummy/app/models/gemini/guard_one.rb +5 -0
  20. data/spec/dummy/app/models/gemini/guard_two.rb +5 -0
  21. data/spec/dummy/app/models/gemini/my_rule.rb +46 -0
  22. data/spec/dummy/app/models/gemini/my_rule_type.rb +5 -0
  23. data/spec/dummy/app/models/gemini/xyz_enum.rb +5 -0
  24. data/spec/dummy/app/models/gemini/xyz_rule.rb +63 -0
  25. data/spec/dummy/app/models/gemini/xyz_rule_type.rb +5 -0
  26. data/spec/dummy/config/locales/en.yml +10 -0
  27. data/spec/dummy/db/migrate/20171220150101_add_rule_type_enums.rb +14 -0
  28. data/spec/dummy/db/migrate/20171221095312_create_gemini_my_rules.rb +22 -0
  29. data/spec/dummy/db/migrate/20171221095359_create_gemini_xyz_rules.rb +21 -0
  30. data/spec/dummy/db/migrate/20171222150100_add_rule_indices.rb +34 -0
  31. data/spec/dummy/db/seeds.rb +1 -1
  32. data/spec/dummy/delorean/base_code.dl +6 -0
  33. data/spec/dummy/lib/gemini/my_rule_script_set.rb +13 -0
  34. data/spec/dummy/lib/gemini/xyz_rule_script_set.rb +22 -0
  35. data/spec/features/rule_spec.rb +265 -0
  36. data/spec/fixtures/csv/rule/DataGrid.csv +6 -0
  37. data/spec/fixtures/csv/rule/MyRule.csv +14 -0
  38. data/spec/fixtures/csv/rule/XyzRule.csv +6 -0
  39. data/spec/models/rule_spec.rb +322 -0
  40. data/spec/support/integration_helpers.rb +1 -0
  41. metadata +29 -2
@@ -0,0 +1,20 @@
1
+ {
2
+ // copied from basepack grid's onEditInForm
3
+ netzkeOnDupInForm: function(){
4
+ var selModel = this.getSelectionModel();
5
+ var recordId = selModel.selected.first().getId();
6
+ this.netzkeLoadComponent("edit_window", {
7
+ title: "Duplicate in Form",
8
+ serverConfig: {record_id: recordId},
9
+ callback: function(w){
10
+ w.show();
11
+ var form = w.items.first();
12
+ form.baseParams = {dup: true};
13
+ w.on('close', function(){
14
+ if (w.closeRes === "ok") {
15
+ this.store.load();
16
+ }
17
+ }, this);
18
+ }, scope: this});
19
+ }
20
+ }
@@ -0,0 +1,126 @@
1
+ class Marty::BaseRule < Marty::Base
2
+ self.abstract_class = true
3
+ has_mcfly
4
+
5
+ def self.guard_info
6
+ {}
7
+ end
8
+
9
+ def chkrange(v)
10
+ v.match(/\A(\[|\()([0-9\.-]*),([0-9\.-]*)(\]|\))\z/)
11
+ end
12
+ def gettypes(v)
13
+ types = []
14
+ types << :string if v.is_a?(String)
15
+ types += [:int, :integer] if Integer(v) rescue nil
16
+ types << :float if Float(v) rescue nil
17
+ types << :date if Date.parse(v) rescue nil
18
+ types << :datetime if DateTime.parse(v) rescue nil
19
+ types << :range if chkrange(v) rescue nil
20
+ types << :boolean if [true, false].include?(v)
21
+ types
22
+ end
23
+ def check(name, h)
24
+ multi, type, enum, values, req = h.values_at(:multi, :type, :enum, :values,
25
+ :required)
26
+ ns = "'#{name}'"
27
+ expmulti = multi ? 'multi' : 'single'
28
+ errtype = :guards
29
+ v = simple_guards[name]
30
+ type ||= :string
31
+ return errors[errtype] << "- Required field #{ns} is missing" if
32
+ v.blank? && req
33
+ return if v.blank?
34
+ gotmulti = v.is_a?(Array) ? 'multi' : 'single'
35
+ return errors[errtype] << "- Wrong arity for #{ns} (expected #{expmulti} "\
36
+ "got #{gotmulti})" if expmulti != gotmulti
37
+ vs = [v].flatten.to_set
38
+ vs.each do |vv|
39
+ return errors[errtype] << "- Wrong type for #{ns}" unless
40
+ gettypes(vv).member?(type)
41
+ end
42
+ return unless enum || values
43
+ vals = enum && enum::VALUES || values.to_set
44
+ bad = (vs - vals)
45
+ p = bad.count > 1 ? 's' : ''
46
+ return errors[errtype] <<
47
+ %Q(- Bad value#{p} '#{bad.to_a.join("', '")}' for #{ns}) if bad.present?
48
+ end
49
+ def validate
50
+ self.class.guard_info.each { |name, h| check(name, h) }
51
+ grids.each do |vn, gn|
52
+ return errors[:grids] << "- Bad grid name '#{gn}' for '#{vn}'" unless
53
+ gn.blank? || Marty::DataGrid.lookup('infinity', gn)
54
+ end
55
+ cg_err = computed_guards.delete("~~ERROR~~")
56
+ errors[:computed] <<
57
+ "- Error in rule '#{name}' field 'computed_guards': #{cg_err.capitalize}" if cg_err
58
+ res_err = results.delete("~~ERROR~~")
59
+ errors[:computed] <<
60
+ "- Error in rule '#{name}' field 'results': #{res_err.capitalize}" if res_err
61
+
62
+ same_name_diff_guards = self.class.
63
+ where(obsoleted_dt: 'infinity', name: self.name).
64
+ # id is nil on new rules
65
+ where.not(id: self.id).
66
+ where("simple_guards != '#{self.simple_guards.to_json}'")
67
+
68
+ errors[:base] =
69
+ "Can't have rule with same name and different type/guards" +
70
+ " - #{self.name}" if same_name_diff_guards.exists?
71
+
72
+ end
73
+
74
+ validates_presence_of :name
75
+ validate :validate
76
+
77
+ before_validation(on: [:create, :update]) do
78
+ self.simple_guards ||= {}
79
+ self.computed_guards ||= {}
80
+ self.grids ||= {}
81
+ self.results ||= {}
82
+ end
83
+
84
+ before_create do
85
+ self.class.guard_info.each do |k,v|
86
+ next if v[:default].blank? || self.simple_guards.include?(k)
87
+ self.simple_guards[k] = v[:default]
88
+ end
89
+ end
90
+
91
+ def self.get_subq(field, subfield, multi, type, vraw)
92
+ arrow = multi || ![:range, :string, :date, :datetime].include?(type) ?
93
+ "->" : "->>"
94
+ op = multi || type == :range ? "@>" : "="
95
+ value0 = [:string, :date, :datetime].include?(type) ?
96
+ ActiveRecord::Base.connection.quote(vraw) :
97
+ type == :range ? vraw.to_f :
98
+ "'#{vraw}'::jsonb"
99
+ value = multi ? %Q('["%s"]') % value0[1..-2] : value0
100
+ fieldcast = type == :range ? "::numrange" : ''
101
+ "(#{field}#{arrow}'#{subfield}')#{fieldcast} #{op} #{value}"
102
+ end
103
+
104
+ def self.get_matches_(pt, attrs, params)
105
+
106
+ q = select("DISTINCT ON (name) *").where(attrs)
107
+
108
+ params.each do |k, vraw|
109
+ h = guard_info
110
+ use_k = (h[k] && k) ||
111
+ (h[k+"_array"] && k+"_array") ||
112
+ (h[k+"_range"] && k+"_range")
113
+ next unless use_k
114
+ multi, type = h[use_k].values_at(:multi, :type)
115
+ filts = [vraw].flatten.map do |v|
116
+ qstr = get_subq('simple_guards', use_k, multi, type, v)
117
+ end.join(" OR ")
118
+ isn = "simple_guards->'#{use_k}' IS NULL OR"
119
+
120
+ q = q.where("(#{isn} #{filts})")
121
+ end
122
+ # print q.to_sql
123
+ q.order(:name)
124
+ end
125
+
126
+ end
@@ -0,0 +1,121 @@
1
+ class Marty::DeloreanRule < Marty::BaseRule
2
+ self.abstract_class = true
3
+
4
+ validates_presence_of :rule_type, :start_dt
5
+
6
+ def validate
7
+ super
8
+ if self.class.where(obsoleted_dt: 'infinity', name: name).
9
+ where.not(id: id).
10
+ where("(start_dt, coalesce(end_dt, 'infinity')) OVERLAPS (?, ?)",
11
+ start_dt, end_dt || 'infinity').exists?
12
+ return errors[:base] <<
13
+ "Can't have rule with same name and overlapping start/end dates"\
14
+ " - #{name}"
15
+ end
16
+
17
+ return errors[:base] = "Start date must be before end date" if
18
+ start_dt && end_dt && start_dt >= end_dt
19
+
20
+ if computed_guards.present? || results.present?
21
+ begin
22
+ eclass = engine && engine.constantize || Marty::RuleScriptSet
23
+ eng = eclass.new('infinity').get_engine(self)
24
+ rescue => e
25
+ return errors[:computed] = "- " + e.message
26
+ end
27
+ end
28
+ end
29
+
30
+ def self.find_fixed(results)
31
+ results.each_with_object({}) do |(k, v), h|
32
+ v_wo_comment = /\A([^#]+)/.match(v)[1] if v.include?("#")
33
+ # if v contains a #, try cut it there and attempt parse that way first
34
+ jp = (v_wo_comment && JSON.parse("[#{v_wo_comment}]") rescue nil) ||
35
+ (JSON.parse("[#{v}]") rescue nil)
36
+ next h[k] = jp[0] if jp
37
+ # json doesn't like single quotes, so handle those manually
38
+ m = %r{\A'(.*)'\z}.match(v)
39
+ next unless m
40
+ h[k] = m[1]
41
+ end
42
+ end
43
+
44
+ before_validation(on: [:create, :update]) do
45
+ # identify result values that are fixed, stash them (removing quotes)
46
+ self.fixed_results = self.class.find_fixed(self.results)
47
+ end
48
+
49
+ def self.results_cfg_var
50
+ "NOT DEFINED"
51
+ end
52
+
53
+ def compg_keys
54
+ computed_guards.keys
55
+ end
56
+
57
+ def compres_keys
58
+ defkeys = (Marty::Config[self.class.results_cfg_var] || {}).keys +
59
+ ["adjustment", "breakeven"]
60
+ results.keys.select{|k| defkeys.include?(k)} + grid_keys
61
+ end
62
+ def grid_keys
63
+ grids.keys.map{|k|k.ends_with?("_grid") ? k : k + "_grid"}
64
+ end
65
+ def base_compute(params, dgparams=params)
66
+ eclass = engine && engine.constantize || Marty::RuleScriptSet
67
+ engine = eclass.new(params["pt"]).get_engine(self) if
68
+ computed_guards.present? || results.present?
69
+
70
+ if computed_guards.present?
71
+ begin
72
+ res = engine.evaluate(eclass.node_name,
73
+ compg_keys,
74
+ params.clone)
75
+ rescue => e
76
+ raise e, "Error (guard) in rule '#{id}:#{name}': #{e}", e.backtrace
77
+ end
78
+ return Hash[compg_keys.zip(res).select{|k,v| !v}] unless res.all?
79
+ end
80
+ grids_computed = false
81
+ grid_results = {}
82
+ if (results.keys - fixed_results.keys).present?
83
+ begin
84
+ eval_result = engine.evaluate(
85
+ eclass.node_name,
86
+ compres_keys,
87
+ params + {
88
+ "dgparams__" => dgparams,
89
+ })
90
+ grids_computed = true
91
+ rescue => e
92
+ raise e, "Error (results) in rule '#{id}:#{name}': #{e}", e.backtrace
93
+ end
94
+ result = Hash[compres_keys.zip(eval_result)]
95
+ elsif fixed_results.keys.sort == results.keys.sort
96
+ result = fixed_results
97
+ end
98
+ if grids.present? && !grids_computed
99
+ pt = params['pt']
100
+ gres = {}
101
+ grid_results = grids.each_with_object({}) do |(gvar, gname), h|
102
+ usename = gvar.ends_with?("_grid") ? gvar : gvar + "_grid"
103
+ next h[usename] = gres[gname] if gres[gname]
104
+ dg = Marty::DataGrid.lookup(pt,gname)
105
+ dgr = dg && dg.lookup_grid_distinct_entry(pt, dgparams)
106
+ h[usename] = gres[gname] = dgr["result"] if dgr
107
+ end
108
+ end
109
+ result + grid_results
110
+ end
111
+
112
+ def self.get_matches_(pt, attrs, params)
113
+ q = super(pt, attrs.except("rule_dt"), params)
114
+ rule_dt = attrs["rule_dt"]
115
+ q=q.where("start_dt <= ?", rule_dt).
116
+ where("end_dt >= ? OR end_dt IS NULL", rule_dt) if rule_dt
117
+ #puts q.to_sql
118
+ q
119
+ end
120
+
121
+ end
@@ -85,7 +85,6 @@ module Marty
85
85
  end
86
86
 
87
87
  ids = {}
88
-
89
88
  # raise an error if record referenced more than once.
90
89
  res.each_with_index do
91
90
  |(op, id), line|
@@ -0,0 +1,176 @@
1
+ class Marty::RuleScriptSet < Delorean::AbstractContainer
2
+ attr_reader :sset, :pt
3
+
4
+ def self.clear_cache
5
+ @@engines, @@dengines, @@dengines_dt = {}, {}, nil
6
+ end
7
+
8
+ clear_cache
9
+
10
+ def self.node_name
11
+ "Node"
12
+ end
13
+ def self.body_start
14
+ "#{node_name}:\n"
15
+ end
16
+ def self.body_lines
17
+ self.body_start.count("\n")
18
+ end
19
+
20
+ def initialize(pt)
21
+ @pt = Mcfly.normalize_infinity(pt)
22
+
23
+ # if pt is Infinity, we get a DEV Tag
24
+ tag = Marty::Tag.cached_find_match(pt)
25
+ @sset = Marty::ScriptSet.new(tag)
26
+ super()
27
+ end
28
+
29
+ def parse_check(sname, body)
30
+ sset.parse_check(sname, body)
31
+ end
32
+
33
+ def write_attr(k, v)
34
+ k + (v == :parameter ? " =?" : " = #{v}")
35
+ end
36
+
37
+ def paramify_h(h)
38
+ "{" + h.keys.reject{|k|k.ends_with?("__")}.
39
+ map {|k| %Q("#{k}": #{k}) }.join(",\n") + "}"
40
+ end
41
+
42
+ def expand_grid_code(h, dgid, dgname, cache, extra_params)
43
+ final_name = dgid.ends_with?("_grid") ? dgid : dgid + "_grid"
44
+ if cache[dgname]
45
+ h[final_name] = "#{cache[dgname]}"
46
+ else
47
+ h["#{dgid}_dg__"] = "Marty::DataGrid.lookup(pt,'#{dgname}')"
48
+ h["#{dgid}_dgp__"] = "dgparams__ + \n" + self.class.indent(paramify_h(h))
49
+ lgde = "lookup_grid_distinct_entry"
50
+ h["#{dgid}_h__"] = "#{dgid}_dg__.#{lgde}(pt,#{dgid}_dgp__)"
51
+ h[final_name] = "#{dgid}_h__ && #{dgid}_h__.result"
52
+ cache[dgname] = final_name
53
+ end
54
+ end
55
+
56
+ def write_code(attrs)
57
+ return '' if attrs.blank?
58
+ newh = attrs.each_with_object({}) do |(k, v), h|
59
+ if k.ends_with?("_grid")
60
+ expand_grid_code(h, k, v, {}, h)
61
+ else
62
+ h[k] = v
63
+ end
64
+ end
65
+ newh.map { |k, v| write_attr(k, v) }.join("\n") + "\n"
66
+ end
67
+
68
+ def grid_code(rule)
69
+ dgcache = {}
70
+ h = {}
71
+ rule.grids.each do |k, v|
72
+ expand_grid_code(h, k, v, dgcache, {})
73
+ end
74
+ h.map { |k, v| write_attr(k, v) }.join("\n") + "\n"
75
+ end
76
+
77
+ def guard_code(rule)
78
+ write_code(rule.computed_guards)
79
+ end
80
+
81
+ def result_code(rule)
82
+ write_code(rule.results)
83
+ end
84
+
85
+ def grid_init(rule)
86
+ if rule.grids.present? || rule.results.keys.any?{|k|k.ends_with?("_grid")}
87
+ write_code({ "pt" => :parameter,
88
+ "dgparams__" => :parameter,
89
+ })
90
+ else
91
+ ''
92
+ end
93
+ end
94
+ def get_code(rule)
95
+ grid_i = grid_init(rule)
96
+ grid_c = grid_code(rule)
97
+ result_c = result_code(rule)
98
+ guard_c = guard_code(rule)
99
+
100
+ code = self.class.body_start + self.class.indent(grid_i +
101
+ guard_c +
102
+ grid_c +
103
+ result_c)
104
+ #puts '='*40
105
+ #puts rule.name
106
+ #puts '-'
107
+ #puts code
108
+ #puts '-'*10
109
+ code
110
+ end
111
+
112
+ def code_section_counts(rule)
113
+ errs = {}
114
+ errs[:grid_params] = grid_init(rule).count("\n")
115
+ errs[:computed_guards] = guard_code(rule).count("\n")
116
+ errs[:grids] = grid_code(rule).count("\n")
117
+ errs[:results] = result_code(rule).count("\n")
118
+ errs
119
+ end
120
+ def get_parse_error_field(rule, exc)
121
+ line = exc.line ? exc.line - self.class.body_lines : 0
122
+ errs = code_section_counts(rule)
123
+ line_count = 0
124
+ errs.each do |k,v|
125
+ line_count += v
126
+ return k if line <= line_count
127
+ end
128
+ errs.keys.last
129
+ end
130
+
131
+ def get_engine(rule)
132
+ begin
133
+ # if rule is a str => importing a regular Script (i.e. not rule)
134
+ return sset.get_engine(rule) if rule.is_a? String
135
+
136
+ # on create rule doesn't have an id => don't cache
137
+ return sset.parse_check("New RULE #{rule.name}", get_code(rule)) unless
138
+ rule.id
139
+
140
+ rule_pfx = rule.class.name.demodulize
141
+
142
+ # unique name for specific version of rule
143
+ sname = "#{rule_pfx}_#{rule.group_id}_#{rule.created_dt.to_f}"
144
+
145
+ # is it a dev posting?
146
+ if Mcfly.is_infinity(pt)
147
+ max_dt = rule.class.
148
+ order("created_dt DESC").limit(1).pluck(:created_dt).first
149
+
150
+ @@dengines_dt ||= max_dt
151
+
152
+ # reset dengine cache if an rule has changed
153
+ @@dengines = {} if max_dt > @@dengines_dt
154
+
155
+ engine = @@dengines[sname]
156
+
157
+ return engine if engine
158
+
159
+ @@dengines[sname] = sset.parse_check(sname, get_code(rule))
160
+ else
161
+ engine = @@engines[[pt, sname]]
162
+
163
+ return engine if engine
164
+
165
+ @@engines[[pt, sname]] = sset.parse_check(sname, get_code(rule))
166
+ end
167
+ rescue Delorean::ParseError => e
168
+ f = get_parse_error_field(rule, e)
169
+ raise "Error in rule '#{rule.name}' field '#{f}': #{e}"
170
+ end
171
+ end
172
+
173
+ def self.indent(s)
174
+ s.gsub(/^/, ' '*4)
175
+ end
176
+ end