redtape 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/redtape/attribute_whitelist.rb +54 -0
- data/lib/redtape/model_factory.rb +189 -0
- data/lib/redtape/populator/abstract.rb +65 -0
- data/lib/redtape/populator/has_many.rb +9 -0
- data/lib/redtape/populator/has_one.rb +10 -0
- data/lib/redtape/populator/root.rb +9 -0
- data/lib/redtape/version.rb +3 -0
- metadata +10 -3
@@ -0,0 +1,54 @@
|
|
1
|
+
module Redtape
|
2
|
+
class AttributeWhitelist
|
3
|
+
attr_reader :whitelisted_attrs
|
4
|
+
|
5
|
+
def initialize(whitelisted_attrs)
|
6
|
+
@whitelisted_attrs = whitelisted_attrs
|
7
|
+
end
|
8
|
+
|
9
|
+
def top_level_name
|
10
|
+
whitelisted_attrs.try(:keys).try(:first)
|
11
|
+
end
|
12
|
+
|
13
|
+
def allows?(args = {})
|
14
|
+
allowed_attrs = whitelisted_attrs_for(args[:association_name]) || []
|
15
|
+
allowed_attrs = allowed_attrs.map(&:to_s)
|
16
|
+
allowed_attrs << "id"
|
17
|
+
allowed_attrs.include?(args[:attr].to_s)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Locate whitelisted attributes for the supplied association name
|
23
|
+
def whitelisted_attrs_for(assoc_name, attr_hash = whitelisted_attrs)
|
24
|
+
if assoc_name.to_s == attr_hash.keys.first.to_s
|
25
|
+
return attr_hash.values.first.reject { |v| v.is_a? Hash }
|
26
|
+
end
|
27
|
+
|
28
|
+
scoped_whitelisted_attrs = attr_hash.values.first
|
29
|
+
scoped_whitelisted_attrs.reject { |v|
|
30
|
+
!v.is_a? Hash
|
31
|
+
}.find { |v|
|
32
|
+
whitelisted_attrs_for(assoc_name, v)
|
33
|
+
}.try(:values).try(:first)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class NullAttrWhitelist
|
38
|
+
def top_level_name
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def present?
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
def nil?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def allows?(args = {})
|
51
|
+
false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
module Redtape
|
2
|
+
class ModelFactory
|
3
|
+
attr_reader :top_level_name, :records_to_save, :model, :controller, :attr_whitelist, :attrs
|
4
|
+
|
5
|
+
def initialize(args = {})
|
6
|
+
assert_inputs(args)
|
7
|
+
|
8
|
+
@attrs = args[:attrs]
|
9
|
+
@attr_whitelist = NullAttrWhitelist.new
|
10
|
+
@controller = args[:controller]
|
11
|
+
@records_to_save = []
|
12
|
+
@top_level_name =
|
13
|
+
@attr_whitelist.top_level_name ||
|
14
|
+
args[:top_level_name] ||
|
15
|
+
default_top_level_name_from(controller)
|
16
|
+
if args[:whitelisted_attrs]
|
17
|
+
@attr_whitelist = AttributeWhitelist.new(args[:whitelisted_attrs])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def populate_model
|
22
|
+
@model = find_or_create_root_model
|
23
|
+
|
24
|
+
populators = [ Populator::Root.new(root_populator_args) ]
|
25
|
+
populators.concat(
|
26
|
+
create_populators_for(model, attrs.values.first).flatten
|
27
|
+
)
|
28
|
+
|
29
|
+
populators.each do |p|
|
30
|
+
p.call
|
31
|
+
end
|
32
|
+
|
33
|
+
violations = populators.map(&:whitelist_failures).flatten
|
34
|
+
if violations.present?
|
35
|
+
errors = violations.join(", ")
|
36
|
+
fail WhitelistViolationError, "Form supplied non-whitelisted attrs #{errors}"
|
37
|
+
end
|
38
|
+
|
39
|
+
@model
|
40
|
+
end
|
41
|
+
|
42
|
+
def save!
|
43
|
+
model.save!
|
44
|
+
records_to_save.each(&:save!)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def default_top_level_name_from(controller)
|
50
|
+
if controller.class.to_s =~ /(\w+)Controller/
|
51
|
+
$1.singularize.downcase.to_sym
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def root_populator_args
|
56
|
+
root_populator_args = {
|
57
|
+
:model => model,
|
58
|
+
:attrs => params_for_current_scope(attrs.values.first),
|
59
|
+
:association_name => attrs.keys.first
|
60
|
+
}.tap do |r|
|
61
|
+
if attr_whitelist.present? && controller.respond_to?(:populate_individual_record)
|
62
|
+
fail ArgumentError, "Expected either controller to respond_to #populate_individual_record or :whitelisted_attrs but not both"
|
63
|
+
elsif controller.respond_to?(:populate_individual_record)
|
64
|
+
r[:data_mapper] = controller
|
65
|
+
elsif attr_whitelist
|
66
|
+
r[:attr_whitelist] = attr_whitelist
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def find_associated_model(attrs, args = {})
|
72
|
+
case args[:with_macro]
|
73
|
+
when :has_many
|
74
|
+
args[:on_association].find(attrs[:id])
|
75
|
+
when :has_one
|
76
|
+
args[:on_model].send(args[:for_association_name])
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_or_create_root_model
|
81
|
+
model_class = top_level_name.to_s.camelize.constantize
|
82
|
+
root_object_id = attrs.values.first[:id]
|
83
|
+
if root_object_id
|
84
|
+
model_class.send(:find, root_object_id)
|
85
|
+
else
|
86
|
+
model_class.new
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def create_populators_for(model, attributes)
|
91
|
+
attributes.each_with_object([]) do |key_value, association_populators|
|
92
|
+
next unless key_value[1].is_a?(Hash)
|
93
|
+
|
94
|
+
key, value = key_value
|
95
|
+
macro = macro_for_attribute_key(key)
|
96
|
+
associated_attrs =
|
97
|
+
case macro
|
98
|
+
when :has_many
|
99
|
+
value.values
|
100
|
+
when :has_one
|
101
|
+
[value]
|
102
|
+
end
|
103
|
+
|
104
|
+
associated_attrs.inject(association_populators) do |populators, record_attrs|
|
105
|
+
assoc_name = find_association_name_in(key)
|
106
|
+
current_scope_attrs = params_for_current_scope(record_attrs)
|
107
|
+
|
108
|
+
associated_model = find_or_initialize_associated_model(
|
109
|
+
current_scope_attrs,
|
110
|
+
:for_association_name => assoc_name,
|
111
|
+
:on_model => model,
|
112
|
+
:with_macro => macro
|
113
|
+
)
|
114
|
+
|
115
|
+
populator_class = "Redtape::Populator::#{macro.to_s.camelize}".constantize
|
116
|
+
|
117
|
+
populator_args = {
|
118
|
+
:model => associated_model,
|
119
|
+
:association_name => assoc_name,
|
120
|
+
:attrs => current_scope_attrs,
|
121
|
+
:parent => model
|
122
|
+
}
|
123
|
+
if controller.respond_to?(:populate_individual_record) && attr_whitelist.present?
|
124
|
+
fail ArgumentError, "Expected either controller to respond_to #populate_individual_record or :whitelisted_attrs but not both"
|
125
|
+
elsif controller.respond_to?(:populate_individual_record)
|
126
|
+
populator_args[:data_mapper] = controller
|
127
|
+
elsif attr_whitelist
|
128
|
+
populator_args[:attr_whitelist] = attr_whitelist
|
129
|
+
end
|
130
|
+
|
131
|
+
populators << populator_class.new(populator_args)
|
132
|
+
populators.concat(
|
133
|
+
create_populators_for(associated_model, record_attrs)
|
134
|
+
)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def find_or_initialize_associated_model(attrs, args = {})
|
140
|
+
association_name, macro, model = args.values_at(:for_association_name, :with_macro, :on_model)
|
141
|
+
|
142
|
+
association = model.send(association_name)
|
143
|
+
if attrs[:id]
|
144
|
+
find_associated_model(
|
145
|
+
attrs,
|
146
|
+
:on_model => model,
|
147
|
+
:with_macro => macro,
|
148
|
+
:on_association => association
|
149
|
+
).tap do |record|
|
150
|
+
records_to_save << record
|
151
|
+
end
|
152
|
+
else
|
153
|
+
case macro
|
154
|
+
when :has_many
|
155
|
+
model.send(association_name).build
|
156
|
+
when :has_one
|
157
|
+
model.send("build_#{association_name}")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def macro_for_attribute_key(key)
|
163
|
+
association_name = find_association_name_in(key).to_sym
|
164
|
+
association_reflection = model.class.reflect_on_association(association_name)
|
165
|
+
association_reflection.macro
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
def params_for_current_scope(attrs)
|
170
|
+
attrs.dup.reject { |_, v| v.is_a? Hash }
|
171
|
+
end
|
172
|
+
|
173
|
+
ATTRIBUTES_KEY_REGEXP = /^(.+)_attributes$/
|
174
|
+
|
175
|
+
def has_many_association_attrs?(key)
|
176
|
+
key =~ ATTRIBUTES_KEY_REGEXP
|
177
|
+
end
|
178
|
+
|
179
|
+
def find_association_name_in(key)
|
180
|
+
ATTRIBUTES_KEY_REGEXP.match(key)[1]
|
181
|
+
end
|
182
|
+
|
183
|
+
def assert_inputs(args)
|
184
|
+
if args[:top_level_name] && args[:whitelisted_attrs].present?
|
185
|
+
fail ArgumentError, ":top_level_name is redundant as it is already present as the key in :whitelisted_attrs"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Redtape
|
2
|
+
module Populator
|
3
|
+
class Abstract
|
4
|
+
attr_reader :association_name, :model, :pending_attributes, :parent, :data_mapper, :attr_whitelist, :whitelist_failures
|
5
|
+
|
6
|
+
def initialize(args = {})
|
7
|
+
@model = args[:model]
|
8
|
+
@association_name = args[:association_name]
|
9
|
+
@pending_attributes = args[:attrs]
|
10
|
+
@parent = args[:parent]
|
11
|
+
@data_mapper = args[:data_mapper]
|
12
|
+
@attr_whitelist = args[:attr_whitelist]
|
13
|
+
@whitelist_failures = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
populate_model_attributes(model, pending_attributes)
|
18
|
+
|
19
|
+
if model.new_record?
|
20
|
+
assign_to_parent
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def assign_to_parent
|
27
|
+
fail NotImplementedError, "You have to implement this in your subclass"
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def populate_model_attributes(model, attributes)
|
33
|
+
msg_target =
|
34
|
+
if data_mapper && data_mapper.respond_to?(:populate_individual_record)
|
35
|
+
data_mapper
|
36
|
+
else
|
37
|
+
self
|
38
|
+
end
|
39
|
+
msg_target.send(
|
40
|
+
:populate_individual_record,
|
41
|
+
model,
|
42
|
+
attributes
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def populate_individual_record(record, attrs)
|
47
|
+
assert_against_whitelisted(attrs.keys)
|
48
|
+
|
49
|
+
# #merge! didn't work here....
|
50
|
+
record.attributes = record.attributes.merge(attrs)
|
51
|
+
end
|
52
|
+
|
53
|
+
def assert_against_whitelisted(attrs)
|
54
|
+
return unless attr_whitelist.present?
|
55
|
+
return if model.new_record?
|
56
|
+
|
57
|
+
attrs.each do |a|
|
58
|
+
unless attr_whitelist.allows?(:association_name => association_name, :attr => a)
|
59
|
+
whitelist_failures << %{"#{association_name}##{a}"}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redtape
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -130,6 +130,13 @@ executables: []
|
|
130
130
|
extensions: []
|
131
131
|
extra_rdoc_files: []
|
132
132
|
files:
|
133
|
+
- lib/redtape/attribute_whitelist.rb
|
134
|
+
- lib/redtape/model_factory.rb
|
135
|
+
- lib/redtape/populator/abstract.rb
|
136
|
+
- lib/redtape/populator/has_many.rb
|
137
|
+
- lib/redtape/populator/has_one.rb
|
138
|
+
- lib/redtape/populator/root.rb
|
139
|
+
- lib/redtape/version.rb
|
133
140
|
- lib/redtape.rb
|
134
141
|
homepage: http://github.com/ClearFit/redtape
|
135
142
|
licenses: []
|
@@ -145,7 +152,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
145
152
|
version: '0'
|
146
153
|
segments:
|
147
154
|
- 0
|
148
|
-
hash:
|
155
|
+
hash: 4035707631943862781
|
149
156
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
157
|
none: false
|
151
158
|
requirements:
|
@@ -154,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
154
161
|
version: '0'
|
155
162
|
segments:
|
156
163
|
- 0
|
157
|
-
hash:
|
164
|
+
hash: 4035707631943862781
|
158
165
|
requirements: []
|
159
166
|
rubyforge_project:
|
160
167
|
rubygems_version: 1.8.24
|