marty 1.2.0 → 1.2.1

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