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.
@@ -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