tallty_duck_record 1.0.0

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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +41 -0
  3. data/README.md +82 -0
  4. data/Rakefile +28 -0
  5. data/lib/core_ext/array_without_blank.rb +46 -0
  6. data/lib/duck_record.rb +65 -0
  7. data/lib/duck_record/associations.rb +130 -0
  8. data/lib/duck_record/associations/association.rb +271 -0
  9. data/lib/duck_record/associations/belongs_to_association.rb +71 -0
  10. data/lib/duck_record/associations/builder/association.rb +127 -0
  11. data/lib/duck_record/associations/builder/belongs_to.rb +44 -0
  12. data/lib/duck_record/associations/builder/collection_association.rb +45 -0
  13. data/lib/duck_record/associations/builder/embeds_many.rb +9 -0
  14. data/lib/duck_record/associations/builder/embeds_one.rb +9 -0
  15. data/lib/duck_record/associations/builder/has_many.rb +11 -0
  16. data/lib/duck_record/associations/builder/has_one.rb +20 -0
  17. data/lib/duck_record/associations/builder/singular_association.rb +33 -0
  18. data/lib/duck_record/associations/collection_association.rb +476 -0
  19. data/lib/duck_record/associations/collection_proxy.rb +1160 -0
  20. data/lib/duck_record/associations/embeds_association.rb +92 -0
  21. data/lib/duck_record/associations/embeds_many_association.rb +203 -0
  22. data/lib/duck_record/associations/embeds_many_proxy.rb +892 -0
  23. data/lib/duck_record/associations/embeds_one_association.rb +48 -0
  24. data/lib/duck_record/associations/foreign_association.rb +11 -0
  25. data/lib/duck_record/associations/has_many_association.rb +17 -0
  26. data/lib/duck_record/associations/has_one_association.rb +39 -0
  27. data/lib/duck_record/associations/singular_association.rb +73 -0
  28. data/lib/duck_record/attribute.rb +213 -0
  29. data/lib/duck_record/attribute/user_provided_default.rb +30 -0
  30. data/lib/duck_record/attribute_assignment.rb +118 -0
  31. data/lib/duck_record/attribute_decorators.rb +89 -0
  32. data/lib/duck_record/attribute_methods.rb +325 -0
  33. data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
  34. data/lib/duck_record/attribute_methods/dirty.rb +107 -0
  35. data/lib/duck_record/attribute_methods/read.rb +78 -0
  36. data/lib/duck_record/attribute_methods/serialization.rb +66 -0
  37. data/lib/duck_record/attribute_methods/write.rb +70 -0
  38. data/lib/duck_record/attribute_mutation_tracker.rb +108 -0
  39. data/lib/duck_record/attribute_set.rb +98 -0
  40. data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
  41. data/lib/duck_record/attributes.rb +262 -0
  42. data/lib/duck_record/base.rb +300 -0
  43. data/lib/duck_record/callbacks.rb +324 -0
  44. data/lib/duck_record/coders/json.rb +13 -0
  45. data/lib/duck_record/coders/yaml_column.rb +48 -0
  46. data/lib/duck_record/core.rb +262 -0
  47. data/lib/duck_record/define_callbacks.rb +23 -0
  48. data/lib/duck_record/enum.rb +139 -0
  49. data/lib/duck_record/errors.rb +71 -0
  50. data/lib/duck_record/inheritance.rb +130 -0
  51. data/lib/duck_record/locale/en.yml +46 -0
  52. data/lib/duck_record/model_schema.rb +71 -0
  53. data/lib/duck_record/nested_attributes.rb +555 -0
  54. data/lib/duck_record/nested_validate_association.rb +262 -0
  55. data/lib/duck_record/persistence.rb +39 -0
  56. data/lib/duck_record/readonly_attributes.rb +36 -0
  57. data/lib/duck_record/reflection.rb +650 -0
  58. data/lib/duck_record/serialization.rb +26 -0
  59. data/lib/duck_record/translation.rb +22 -0
  60. data/lib/duck_record/type.rb +77 -0
  61. data/lib/duck_record/type/array.rb +36 -0
  62. data/lib/duck_record/type/array_without_blank.rb +36 -0
  63. data/lib/duck_record/type/date.rb +7 -0
  64. data/lib/duck_record/type/date_time.rb +7 -0
  65. data/lib/duck_record/type/decimal_without_scale.rb +13 -0
  66. data/lib/duck_record/type/internal/abstract_json.rb +33 -0
  67. data/lib/duck_record/type/internal/timezone.rb +15 -0
  68. data/lib/duck_record/type/json.rb +6 -0
  69. data/lib/duck_record/type/registry.rb +97 -0
  70. data/lib/duck_record/type/serialized.rb +63 -0
  71. data/lib/duck_record/type/text.rb +9 -0
  72. data/lib/duck_record/type/time.rb +19 -0
  73. data/lib/duck_record/type/unsigned_integer.rb +15 -0
  74. data/lib/duck_record/validations.rb +67 -0
  75. data/lib/duck_record/validations/subset.rb +74 -0
  76. data/lib/duck_record/validations/uniqueness_on_real_record.rb +248 -0
  77. data/lib/duck_record/version.rb +3 -0
  78. data/lib/tasks/acts_as_record_tasks.rake +4 -0
  79. metadata +181 -0
@@ -0,0 +1,89 @@
1
+ module DuckRecord
2
+ module AttributeDecorators # :nodoc:
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :attribute_type_decorations, instance_accessor: false # :internal:
7
+ self.attribute_type_decorations = TypeDecorator.new
8
+ end
9
+
10
+ module ClassMethods # :nodoc:
11
+ # This method is an internal API used to create class macros such as
12
+ # +serialize+, and features like time zone aware attributes.
13
+ #
14
+ # Used to wrap the type of an attribute in a new type.
15
+ # When the schema for a model is loaded, attributes with the same name as
16
+ # +column_name+ will have their type yielded to the given block. The
17
+ # return value of that block will be used instead.
18
+ #
19
+ # Subsequent calls where +column_name+ and +decorator_name+ are the same
20
+ # will override the previous decorator, not decorate twice. This can be
21
+ # used to create idempotent class macros like +serialize+
22
+ def decorate_attribute_type(column_name, decorator_name, &block)
23
+ matcher = ->(name, _) { name == column_name.to_s }
24
+ key = "_#{column_name}_#{decorator_name}"
25
+ decorate_matching_attribute_types(matcher, key, &block)
26
+ end
27
+
28
+ # This method is an internal API used to create higher level features like
29
+ # time zone aware attributes.
30
+ #
31
+ # When the schema for a model is loaded, +matcher+ will be called for each
32
+ # attribute with its name and type. If the matcher returns a truthy value,
33
+ # the type will then be yielded to the given block, and the return value
34
+ # of that block will replace the type.
35
+ #
36
+ # Subsequent calls to this method with the same value for +decorator_name+
37
+ # will replace the previous decorator, not decorate twice. This can be
38
+ # used to ensure that class macros are idempotent.
39
+ def decorate_matching_attribute_types(matcher, decorator_name, &block)
40
+ reload_schema_from_cache
41
+ decorator_name = decorator_name.to_s
42
+
43
+ # Create new hashes so we don't modify parent classes
44
+ self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
45
+ end
46
+
47
+ private
48
+
49
+ def load_schema!
50
+ super
51
+ attribute_types.each do |name, type|
52
+ decorated_type = attribute_type_decorations.apply(name, type)
53
+ define_attribute(name, decorated_type)
54
+ end
55
+ end
56
+ end
57
+
58
+ class TypeDecorator # :nodoc:
59
+ delegate :clear, to: :@decorations
60
+
61
+ def initialize(decorations = {})
62
+ @decorations = decorations
63
+ end
64
+
65
+ def merge(*args)
66
+ TypeDecorator.new(@decorations.merge(*args))
67
+ end
68
+
69
+ def apply(name, type)
70
+ decorations = decorators_for(name, type)
71
+ decorations.inject(type) do |new_type, block|
72
+ block.call(new_type)
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def decorators_for(name, type)
79
+ matching(name, type).map(&:last)
80
+ end
81
+
82
+ def matching(name, type)
83
+ @decorations.values.select do |(matcher, _)|
84
+ matcher.call(name, type)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,325 @@
1
+ require "active_support/core_ext/enumerable"
2
+ require "active_support/core_ext/string/filters"
3
+ require "mutex_m"
4
+ require "concurrent/map"
5
+
6
+ module DuckRecord
7
+ # = Active Record Attribute Methods
8
+ module AttributeMethods
9
+ extend ActiveSupport::Concern
10
+ include ActiveModel::AttributeMethods
11
+
12
+ included do
13
+ initialize_generated_modules
14
+
15
+ include Read
16
+ include Write
17
+ include BeforeTypeCast
18
+ include Dirty
19
+ include Serialization
20
+ end
21
+
22
+ AttrNames = Module.new {
23
+ def self.set_name_cache(name, value)
24
+ const_name = "ATTR_#{name}"
25
+ unless const_defined? const_name
26
+ const_set const_name, value.dup.freeze
27
+ end
28
+ end
29
+ }
30
+
31
+ BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
32
+
33
+ class GeneratedAttributeMethods < Module; end # :nodoc:
34
+
35
+ module ClassMethods
36
+ def inherited(child_class) #:nodoc:
37
+ child_class.initialize_generated_modules
38
+ super
39
+ end
40
+
41
+ def initialize_generated_modules # :nodoc:
42
+ @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
43
+ @attribute_methods_generated = false
44
+ include @generated_attribute_methods
45
+
46
+ super
47
+ end
48
+
49
+ # Generates all the attribute related methods for columns in the database
50
+ # accessors, mutators and query methods.
51
+ def define_attribute_methods # :nodoc:
52
+ return false if @attribute_methods_generated
53
+ # Use a mutex; we don't want two threads simultaneously trying to define
54
+ # attribute methods.
55
+ generated_attribute_methods.synchronize do
56
+ return false if @attribute_methods_generated
57
+ superclass.define_attribute_methods unless self == base_class
58
+ super(attribute_names)
59
+ @attribute_methods_generated = true
60
+ end
61
+ true
62
+ end
63
+
64
+ def undefine_attribute_methods # :nodoc:
65
+ generated_attribute_methods.synchronize do
66
+ super if defined?(@attribute_methods_generated) && @attribute_methods_generated
67
+ @attribute_methods_generated = false
68
+ end
69
+ end
70
+
71
+ # Raises an DuckRecord::DangerousAttributeError exception when an
72
+ # \Active \Record method is defined in the model, otherwise +false+.
73
+ #
74
+ # class Person < DuckRecord::Base
75
+ # def save
76
+ # 'already defined by Active Record'
77
+ # end
78
+ # end
79
+ #
80
+ # Person.instance_method_already_implemented?(:save)
81
+ # # => DuckRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.
82
+ #
83
+ # Person.instance_method_already_implemented?(:name)
84
+ # # => false
85
+ def instance_method_already_implemented?(method_name)
86
+ if dangerous_attribute_method?(method_name)
87
+ raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name."
88
+ end
89
+
90
+ if superclass == Base
91
+ super
92
+ else
93
+ # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
94
+ # defines its own attribute method, then we don't want to overwrite that.
95
+ defined = method_defined_within?(method_name, superclass, Base) &&
96
+ ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
97
+ defined || super
98
+ end
99
+ end
100
+
101
+ # A method name is 'dangerous' if it is already (re)defined by Active Record, but
102
+ # not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
103
+ def dangerous_attribute_method?(name) # :nodoc:
104
+ method_defined_within?(name, Base)
105
+ end
106
+
107
+ def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
108
+ if klass.method_defined?(name) || klass.private_method_defined?(name)
109
+ if superklass.method_defined?(name) || superklass.private_method_defined?(name)
110
+ klass.instance_method(name).owner != superklass.instance_method(name).owner
111
+ else
112
+ true
113
+ end
114
+ else
115
+ false
116
+ end
117
+ end
118
+
119
+ # A class method is 'dangerous' if it is already (re)defined by Active Record, but
120
+ # not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
121
+ def dangerous_class_method?(method_name)
122
+ BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
123
+ end
124
+
125
+ def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
126
+ if klass.respond_to?(name, true)
127
+ if superklass.respond_to?(name, true)
128
+ klass.method(name).owner != superklass.method(name).owner
129
+ else
130
+ true
131
+ end
132
+ else
133
+ false
134
+ end
135
+ end
136
+
137
+ # Returns an array of column names as strings if it's not an abstract class and
138
+ # table exists. Otherwise it returns an empty array.
139
+ #
140
+ # class Person < DuckRecord::Base
141
+ # end
142
+ #
143
+ # Person.attribute_names
144
+ # # => ["id", "created_at", "updated_at", "name", "age"]
145
+ def attribute_names
146
+ @attribute_names ||= if !abstract_class?
147
+ attribute_types.keys
148
+ else
149
+ []
150
+ end
151
+ end
152
+
153
+ # Returns true if the given attribute exists, otherwise false.
154
+ #
155
+ # class Person < DuckRecord::Base
156
+ # end
157
+ #
158
+ # Person.has_attribute?('name') # => true
159
+ # Person.has_attribute?(:age) # => true
160
+ # Person.has_attribute?(:nothing) # => false
161
+ def has_attribute?(attr_name)
162
+ attribute_types.key?(attr_name.to_s)
163
+ end
164
+ end
165
+
166
+ # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
167
+ # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
168
+ # which will all return +true+. It also defines the attribute methods if they have
169
+ # not been generated.
170
+ #
171
+ # class Person < DuckRecord::Base
172
+ # end
173
+ #
174
+ # person = Person.new
175
+ # person.respond_to?(:name) # => true
176
+ # person.respond_to?(:name=) # => true
177
+ # person.respond_to?(:name?) # => true
178
+ # person.respond_to?('age') # => true
179
+ # person.respond_to?('age=') # => true
180
+ # person.respond_to?('age?') # => true
181
+ # person.respond_to?(:nothing) # => false
182
+ def respond_to?(name, include_private = false)
183
+ return false unless super
184
+
185
+ name = name.to_s
186
+
187
+ # If the result is true then check for the select case.
188
+ # For queries selecting a subset of columns, return false for unselected columns.
189
+ # We check defined?(@attributes) not to issue warnings if called on objects that
190
+ # have been allocated but not yet initialized.
191
+ if defined?(@attributes) && self.class.attribute_names.include?(name)
192
+ return has_attribute?(name)
193
+ end
194
+
195
+ true
196
+ end
197
+
198
+ # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
199
+ #
200
+ # class Person < DuckRecord::Base
201
+ # end
202
+ #
203
+ # person = Person.new
204
+ # person.has_attribute?(:name) # => true
205
+ # person.has_attribute?('age') # => true
206
+ # person.has_attribute?(:nothing) # => false
207
+ def has_attribute?(attr_name)
208
+ @attributes.key?(attr_name.to_s)
209
+ end
210
+
211
+ # Returns an array of names for the attributes available on this object.
212
+ #
213
+ # class Person < DuckRecord::Base
214
+ # end
215
+ #
216
+ # person = Person.new
217
+ # person.attribute_names
218
+ # # => ["id", "created_at", "updated_at", "name", "age"]
219
+ def attribute_names
220
+ @attributes.keys
221
+ end
222
+
223
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
224
+ #
225
+ # class Person < DuckRecord::Base
226
+ # end
227
+ #
228
+ # person = Person.create(name: 'Francesco', age: 22)
229
+ # person.attributes
230
+ # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
231
+ def attributes
232
+ @attributes.to_hash
233
+ end
234
+
235
+ # Returns an <tt>#inspect</tt>-like string for the value of the
236
+ # attribute +attr_name+. String attributes are truncated up to 50
237
+ # characters, Date and Time attributes are returned in the
238
+ # <tt>:db</tt> format. Other attributes return the value of
239
+ # <tt>#inspect</tt> without modification.
240
+ #
241
+ # person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
242
+ #
243
+ # person.attribute_for_inspect(:name)
244
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
245
+ #
246
+ # person.attribute_for_inspect(:created_at)
247
+ # # => "\"2012-10-22 00:15:07\""
248
+ #
249
+ # person.attribute_for_inspect(:tag_ids)
250
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]"
251
+ def attribute_for_inspect(attr_name)
252
+ value = read_attribute(attr_name)
253
+
254
+ if value.is_a?(String) && value.length > 50
255
+ "#{value[0, 50]}...".inspect
256
+ elsif value.is_a?(Date) || value.is_a?(Time)
257
+ %("#{value.to_s(:db)}")
258
+ else
259
+ value.inspect
260
+ end
261
+ end
262
+
263
+ # Returns +true+ if the specified +attribute+ has been set by the user or by a
264
+ # database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies
265
+ # to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+.
266
+ # Note that it always returns +true+ with boolean attributes.
267
+ #
268
+ # class Task < DuckRecord::Base
269
+ # end
270
+ #
271
+ # task = Task.new(title: '', is_done: false)
272
+ # task.attribute_present?(:title) # => false
273
+ # task.attribute_present?(:is_done) # => true
274
+ # task.title = 'Buy milk'
275
+ # task.is_done = true
276
+ # task.attribute_present?(:title) # => true
277
+ # task.attribute_present?(:is_done) # => true
278
+ def attribute_present?(attribute)
279
+ value = _read_attribute(attribute)
280
+ !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
281
+ end
282
+
283
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
284
+ # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
285
+ # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
286
+ #
287
+ # Note: +:id+ is always present.
288
+ #
289
+ # class Person < DuckRecord::Base
290
+ # belongs_to :organization
291
+ # end
292
+ #
293
+ # person = Person.new(name: 'Francesco', age: '22')
294
+ # person[:name] # => "Francesco"
295
+ # person[:age] # => 22
296
+ #
297
+ # person = Person.select('id').first
298
+ # person[:name] # => ActiveModel::MissingAttributeError: missing attribute: name
299
+ # person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute: organization_id
300
+ def [](attr_name)
301
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
302
+ end
303
+
304
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
305
+ # (Alias for the protected #write_attribute method).
306
+ #
307
+ # class Person < DuckRecord::Base
308
+ # end
309
+ #
310
+ # person = Person.new
311
+ # person[:age] = '22'
312
+ # person[:age] # => 22
313
+ # person[:age].class # => Integer
314
+ def []=(attr_name, value)
315
+ write_attribute(attr_name, value)
316
+ end
317
+
318
+ protected
319
+
320
+ def attribute_method?(attr_name) # :nodoc:
321
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
322
+ defined?(@attributes) && @attributes.key?(attr_name)
323
+ end
324
+ end
325
+ end
@@ -0,0 +1,76 @@
1
+ module DuckRecord
2
+ module AttributeMethods
3
+ # = Active Record Attribute Methods Before Type Cast
4
+ #
5
+ # DuckRecord::AttributeMethods::BeforeTypeCast provides a way to
6
+ # read the value of the attributes before typecasting and deserialization.
7
+ #
8
+ # class Task < DuckRecord::Base
9
+ # end
10
+ #
11
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
12
+ # task.id # => 1
13
+ # task.completed_on # => Sun, 21 Oct 2012
14
+ #
15
+ # task.attributes_before_type_cast
16
+ # # => {"id"=>"1", "completed_on"=>"2012-10-21", ... }
17
+ # task.read_attribute_before_type_cast('id') # => "1"
18
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
19
+ #
20
+ # In addition to #read_attribute_before_type_cast and #attributes_before_type_cast,
21
+ # it declares a method for all attributes with the <tt>*_before_type_cast</tt>
22
+ # suffix.
23
+ #
24
+ # task.id_before_type_cast # => "1"
25
+ # task.completed_on_before_type_cast # => "2012-10-21"
26
+ module BeforeTypeCast
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ attribute_method_suffix "_before_type_cast"
31
+ attribute_method_suffix "_came_from_user?"
32
+ end
33
+
34
+ # Returns the value of the attribute identified by +attr_name+ before
35
+ # typecasting and deserialization.
36
+ #
37
+ # class Task < DuckRecord::Base
38
+ # end
39
+ #
40
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
41
+ # task.read_attribute('id') # => 1
42
+ # task.read_attribute_before_type_cast('id') # => '1'
43
+ # task.read_attribute('completed_on') # => Sun, 21 Oct 2012
44
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
45
+ # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
46
+ def read_attribute_before_type_cast(attr_name)
47
+ @attributes[attr_name.to_s].value_before_type_cast
48
+ end
49
+
50
+ # Returns a hash of attributes before typecasting and deserialization.
51
+ #
52
+ # class Task < DuckRecord::Base
53
+ # end
54
+ #
55
+ # task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21')
56
+ # task.attributes
57
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil}
58
+ # task.attributes_before_type_cast
59
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
60
+ def attributes_before_type_cast
61
+ @attributes.values_before_type_cast
62
+ end
63
+
64
+ private
65
+
66
+ # Handle *_before_type_cast for method_missing.
67
+ def attribute_before_type_cast(attribute_name)
68
+ read_attribute_before_type_cast(attribute_name)
69
+ end
70
+
71
+ def attribute_came_from_user?(attribute_name)
72
+ @attributes[attribute_name].came_from_user?
73
+ end
74
+ end
75
+ end
76
+ end