redtape 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,9 @@
1
+ module Redtape
2
+ module Populator
3
+ class HasMany < Abstract
4
+ def assign_to_parent
5
+ parent.send(association_name).send("<<", model)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module Redtape
2
+ module Populator
3
+ class HasOne < Abstract
4
+ def assign_to_parent
5
+ parent.send("#{association_name}=", model)
6
+ end
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,9 @@
1
+ module Redtape
2
+ module Populator
3
+ class Root < Abstract
4
+ def assign_to_parent
5
+ # no-op
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Redtape
2
+ VERSION = "1.0.1"
3
+ 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.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: -2628988932070424880
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: -2628988932070424880
164
+ hash: 4035707631943862781
158
165
  requirements: []
159
166
  rubyforge_project:
160
167
  rubygems_version: 1.8.24