marty 1.2.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -1
- data/Gemfile.lock +2 -2
- data/app/components/marty/base_rule_view.rb +279 -0
- data/app/components/marty/delorean_rule_view.rb +26 -0
- data/app/components/marty/extras/layout.rb +22 -7
- data/app/components/marty/log_view.rb +1 -1
- data/app/components/marty/mcfly_grid_panel.rb +53 -0
- data/app/components/marty/mcfly_grid_panel/client/dup_in_form.js +20 -0
- data/app/models/marty/base_rule.rb +126 -0
- data/app/models/marty/delorean_rule.rb +121 -0
- data/lib/marty/data_importer.rb +0 -1
- data/lib/marty/rule_script_set.rb +176 -0
- data/lib/marty/version.rb +1 -1
- data/lib/tasks/marty_tasks.rake +42 -0
- data/spec/dummy/app/components/gemini/cm_auth_app.rb +18 -0
- data/spec/dummy/app/components/gemini/my_rule_view.rb +63 -0
- data/spec/dummy/app/components/gemini/xyz_rule_view.rb +25 -0
- data/spec/dummy/app/models/gemini/guard_one.rb +5 -0
- data/spec/dummy/app/models/gemini/guard_two.rb +5 -0
- data/spec/dummy/app/models/gemini/my_rule.rb +46 -0
- data/spec/dummy/app/models/gemini/my_rule_type.rb +5 -0
- data/spec/dummy/app/models/gemini/xyz_enum.rb +5 -0
- data/spec/dummy/app/models/gemini/xyz_rule.rb +63 -0
- data/spec/dummy/app/models/gemini/xyz_rule_type.rb +5 -0
- data/spec/dummy/config/locales/en.yml +10 -0
- data/spec/dummy/db/migrate/20171220150101_add_rule_type_enums.rb +14 -0
- data/spec/dummy/db/migrate/20171221095312_create_gemini_my_rules.rb +22 -0
- data/spec/dummy/db/migrate/20171221095359_create_gemini_xyz_rules.rb +21 -0
- data/spec/dummy/db/migrate/20171222150100_add_rule_indices.rb +34 -0
- data/spec/dummy/db/seeds.rb +1 -1
- data/spec/dummy/delorean/base_code.dl +6 -0
- data/spec/dummy/lib/gemini/my_rule_script_set.rb +13 -0
- data/spec/dummy/lib/gemini/xyz_rule_script_set.rb +22 -0
- data/spec/features/rule_spec.rb +265 -0
- data/spec/fixtures/csv/rule/DataGrid.csv +6 -0
- data/spec/fixtures/csv/rule/MyRule.csv +14 -0
- data/spec/fixtures/csv/rule/XyzRule.csv +6 -0
- data/spec/models/rule_spec.rb +322 -0
- data/spec/support/integration_helpers.rb +1 -0
- 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
|
data/lib/marty/data_importer.rb
CHANGED
@@ -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
|