redtape 1.0.0 → 1.0.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.
- 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
|