duck_record 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +59 -0
  4. data/Rakefile +29 -0
  5. data/lib/duck_record/attribute/user_provided_default.rb +30 -0
  6. data/lib/duck_record/attribute.rb +221 -0
  7. data/lib/duck_record/attribute_assignment.rb +91 -0
  8. data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
  9. data/lib/duck_record/attribute_methods/dirty.rb +124 -0
  10. data/lib/duck_record/attribute_methods/read.rb +78 -0
  11. data/lib/duck_record/attribute_methods/write.rb +65 -0
  12. data/lib/duck_record/attribute_methods.rb +332 -0
  13. data/lib/duck_record/attribute_mutation_tracker.rb +113 -0
  14. data/lib/duck_record/attribute_set/builder.rb +124 -0
  15. data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
  16. data/lib/duck_record/attribute_set.rb +99 -0
  17. data/lib/duck_record/attributes.rb +262 -0
  18. data/lib/duck_record/base.rb +296 -0
  19. data/lib/duck_record/callbacks.rb +324 -0
  20. data/lib/duck_record/core.rb +253 -0
  21. data/lib/duck_record/define_callbacks.rb +23 -0
  22. data/lib/duck_record/errors.rb +44 -0
  23. data/lib/duck_record/inheritance.rb +130 -0
  24. data/lib/duck_record/locale/en.yml +48 -0
  25. data/lib/duck_record/model_schema.rb +64 -0
  26. data/lib/duck_record/serialization.rb +19 -0
  27. data/lib/duck_record/translation.rb +22 -0
  28. data/lib/duck_record/type/array.rb +36 -0
  29. data/lib/duck_record/type/decimal_without_scale.rb +13 -0
  30. data/lib/duck_record/type/internal/abstract_json.rb +33 -0
  31. data/lib/duck_record/type/json.rb +6 -0
  32. data/lib/duck_record/type/registry.rb +97 -0
  33. data/lib/duck_record/type/serialized.rb +63 -0
  34. data/lib/duck_record/type/text.rb +9 -0
  35. data/lib/duck_record/type/unsigned_integer.rb +15 -0
  36. data/lib/duck_record/type.rb +66 -0
  37. data/lib/duck_record/validations.rb +40 -0
  38. data/lib/duck_record/version.rb +3 -0
  39. data/lib/duck_record.rb +47 -0
  40. data/lib/tasks/acts_as_record_tasks.rake +4 -0
  41. metadata +126 -0
@@ -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
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/module/attribute_accessors'
3
+ require 'duck_record/attribute_mutation_tracker'
4
+
5
+ module DuckRecord
6
+ module AttributeMethods
7
+ module Dirty # :nodoc:
8
+ extend ActiveSupport::Concern
9
+
10
+ include ActiveModel::Dirty
11
+
12
+ included do
13
+ class_attribute :partial_writes, instance_writer: false
14
+ self.partial_writes = true
15
+ end
16
+
17
+ def initialize_dup(other) # :nodoc:
18
+ super
19
+ @attributes = self.class._default_attributes.map do |attr|
20
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
21
+ end
22
+ @mutation_tracker = nil
23
+ end
24
+
25
+ def changes_applied
26
+ @previous_mutation_tracker = mutation_tracker
27
+ @changed_attributes = HashWithIndifferentAccess.new
28
+ store_original_attributes
29
+ end
30
+
31
+ def clear_changes_information
32
+ @previous_mutation_tracker = nil
33
+ @changed_attributes = HashWithIndifferentAccess.new
34
+ store_original_attributes
35
+ end
36
+
37
+ def raw_write_attribute(attr_name, *)
38
+ result = super
39
+ clear_attribute_change(attr_name)
40
+ result
41
+ end
42
+
43
+ def clear_attribute_changes(attr_names)
44
+ super
45
+ attr_names.each do |attr_name|
46
+ clear_attribute_change(attr_name)
47
+ end
48
+ end
49
+
50
+ def changed_attributes
51
+ # This should only be set by methods which will call changed_attributes
52
+ # multiple times when it is known that the computed value cannot change.
53
+ if defined?(@cached_changed_attributes)
54
+ @cached_changed_attributes
55
+ else
56
+ super.reverse_merge(mutation_tracker.changed_values).freeze
57
+ end
58
+ end
59
+
60
+ def changes
61
+ cache_changed_attributes do
62
+ super
63
+ end
64
+ end
65
+
66
+ def previous_changes
67
+ previous_mutation_tracker.changes
68
+ end
69
+
70
+ def attribute_changed_in_place?(attr_name)
71
+ mutation_tracker.changed_in_place?(attr_name)
72
+ end
73
+
74
+ private
75
+
76
+ def mutation_tracker
77
+ unless defined?(@mutation_tracker)
78
+ @mutation_tracker = nil
79
+ end
80
+ @mutation_tracker ||= AttributeMutationTracker.new(@attributes)
81
+ end
82
+
83
+ def changes_include?(attr_name)
84
+ super || mutation_tracker.changed?(attr_name)
85
+ end
86
+
87
+ def clear_attribute_change(attr_name)
88
+ mutation_tracker.forget_change(attr_name)
89
+ end
90
+
91
+ def _update_record(*)
92
+ partial_writes? ? super(keys_for_partial_write) : super
93
+ end
94
+
95
+ def _create_record(*)
96
+ partial_writes? ? super(keys_for_partial_write) : super
97
+ end
98
+
99
+ def keys_for_partial_write
100
+ changed & self.class.column_names
101
+ end
102
+
103
+ def store_original_attributes
104
+ @attributes = @attributes.map(&:forgetting_assignment)
105
+ @mutation_tracker = nil
106
+ end
107
+
108
+ def previous_mutation_tracker
109
+ @previous_mutation_tracker ||= NullMutationTracker.instance
110
+ end
111
+
112
+ def cache_changed_attributes
113
+ @cached_changed_attributes = changed_attributes
114
+ yield
115
+ ensure
116
+ clear_changed_attributes_cache
117
+ end
118
+
119
+ def clear_changed_attributes_cache
120
+ remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,78 @@
1
+ module DuckRecord
2
+ module AttributeMethods
3
+ module Read
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ private
8
+
9
+ # We want to generate the methods via module_eval rather than
10
+ # define_method, because define_method is slower on dispatch.
11
+ # Evaluating many similar methods may use more memory as the instruction
12
+ # sequences are duplicated and cached (in MRI). define_method may
13
+ # be slower on dispatch, but if you're careful about the closure
14
+ # created, then define_method will consume much less memory.
15
+ #
16
+ # But sometimes the database might return columns with
17
+ # characters that are not allowed in normal method names (like
18
+ # 'my_column(omg)'. So to work around this we first define with
19
+ # the __temp__ identifier, and then use alias method to rename
20
+ # it to what we want.
21
+ #
22
+ # We are also defining a constant to hold the frozen string of
23
+ # the attribute name. Using a constant means that we do not have
24
+ # to allocate an object on each call to the attribute method.
25
+ # Making it frozen means that it doesn't get duped when used to
26
+ # key the @attributes in read_attribute.
27
+ def define_method_attribute(name)
28
+ safe_name = name.unpack("h*".freeze).first
29
+ temp_method = "__temp__#{safe_name}"
30
+
31
+ DuckRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
32
+
33
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
34
+ def #{temp_method}
35
+ name = ::DuckRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
36
+ _read_attribute(name) { |n| missing_attribute(n, caller) }
37
+ end
38
+ STR
39
+
40
+ generated_attribute_methods.module_eval do
41
+ alias_method name, temp_method
42
+ undef_method temp_method
43
+ end
44
+ end
45
+ end
46
+
47
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after
48
+ # it has been typecast (for example, "2004-12-12" in a date column is cast
49
+ # to a date object, like Date.new(2004, 12, 12)).
50
+ def read_attribute(attr_name, &block)
51
+ name = if self.class.attribute_alias?(attr_name)
52
+ self.class.attribute_alias(attr_name).to_s
53
+ else
54
+ attr_name.to_s
55
+ end
56
+
57
+ _read_attribute(name, &block)
58
+ end
59
+
60
+ # This method exists to avoid the expensive primary_key check internally, without
61
+ # breaking compatibility with the read_attribute API
62
+ if defined?(JRUBY_VERSION)
63
+ # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
64
+ # https://github.com/jruby/jruby/pull/2562
65
+ def _read_attribute(attr_name, &block) # :nodoc
66
+ @attributes.fetch_value(attr_name.to_s, &block)
67
+ end
68
+ else
69
+ def _read_attribute(attr_name) # :nodoc:
70
+ @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
71
+ end
72
+ end
73
+
74
+ alias :attribute :_read_attribute
75
+ private :attribute
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,65 @@
1
+ module DuckRecord
2
+ module AttributeMethods
3
+ module Write
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attribute_method_suffix '='
8
+ end
9
+
10
+ module ClassMethods
11
+ private
12
+
13
+ def define_method_attribute=(name)
14
+ safe_name = name.unpack("h*".freeze).first
15
+ DuckRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
16
+
17
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
18
+ def __temp__#{safe_name}=(value)
19
+ name = ::DuckRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
20
+ write_attribute(name, value)
21
+ end
22
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
23
+ undef_method :__temp__#{safe_name}=
24
+ STR
25
+ end
26
+ end
27
+
28
+ # Updates the attribute identified by <tt>attr_name</tt> with the
29
+ # specified +value+. Empty strings for Integer and Float columns are
30
+ # turned into +nil+.
31
+ def write_attribute(attr_name, value)
32
+ name = if self.class.attribute_alias?(attr_name)
33
+ self.class.attribute_alias(attr_name).to_s
34
+ else
35
+ attr_name.to_s
36
+ end
37
+
38
+ write_attribute_with_type_cast(name, value, true)
39
+ end
40
+
41
+ def raw_write_attribute(attr_name, value) # :nodoc:
42
+ write_attribute_with_type_cast(attr_name, value, false)
43
+ end
44
+
45
+ private
46
+
47
+ # Handle *= for method_missing.
48
+ def attribute=(attribute_name, value)
49
+ write_attribute(attribute_name, value)
50
+ end
51
+
52
+ def write_attribute_with_type_cast(attr_name, value, should_type_cast)
53
+ attr_name = attr_name.to_s
54
+
55
+ if should_type_cast
56
+ @attributes.write_from_user(attr_name, value)
57
+ else
58
+ @attributes.write_cast_value(attr_name, value)
59
+ end
60
+
61
+ value
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,332 @@
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
+ case name
186
+ when :to_partial_path
187
+ name = 'to_partial_path'.freeze
188
+ when :to_model
189
+ name = 'to_model'.freeze
190
+ else
191
+ name = name.to_s
192
+ end
193
+
194
+ # If the result is true then check for the select case.
195
+ # For queries selecting a subset of columns, return false for unselected columns.
196
+ # We check defined?(@attributes) not to issue warnings if called on objects that
197
+ # have been allocated but not yet initialized.
198
+ if defined?(@attributes) && self.class.attribute_names.include?(name)
199
+ return has_attribute?(name)
200
+ end
201
+
202
+ true
203
+ end
204
+
205
+ # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
206
+ #
207
+ # class Person < DuckRecord::Base
208
+ # end
209
+ #
210
+ # person = Person.new
211
+ # person.has_attribute?(:name) # => true
212
+ # person.has_attribute?('age') # => true
213
+ # person.has_attribute?(:nothing) # => false
214
+ def has_attribute?(attr_name)
215
+ @attributes.key?(attr_name.to_s)
216
+ end
217
+
218
+ # Returns an array of names for the attributes available on this object.
219
+ #
220
+ # class Person < DuckRecord::Base
221
+ # end
222
+ #
223
+ # person = Person.new
224
+ # person.attribute_names
225
+ # # => ["id", "created_at", "updated_at", "name", "age"]
226
+ def attribute_names
227
+ @attributes.keys
228
+ end
229
+
230
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
231
+ #
232
+ # class Person < DuckRecord::Base
233
+ # end
234
+ #
235
+ # person = Person.create(name: 'Francesco', age: 22)
236
+ # person.attributes
237
+ # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
238
+ def attributes
239
+ @attributes.to_hash
240
+ end
241
+
242
+ # Returns an <tt>#inspect</tt>-like string for the value of the
243
+ # attribute +attr_name+. String attributes are truncated up to 50
244
+ # characters, Date and Time attributes are returned in the
245
+ # <tt>:db</tt> format. Other attributes return the value of
246
+ # <tt>#inspect</tt> without modification.
247
+ #
248
+ # person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
249
+ #
250
+ # person.attribute_for_inspect(:name)
251
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
252
+ #
253
+ # person.attribute_for_inspect(:created_at)
254
+ # # => "\"2012-10-22 00:15:07\""
255
+ #
256
+ # person.attribute_for_inspect(:tag_ids)
257
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]"
258
+ def attribute_for_inspect(attr_name)
259
+ value = read_attribute(attr_name)
260
+
261
+ if value.is_a?(String) && value.length > 50
262
+ "#{value[0, 50]}...".inspect
263
+ elsif value.is_a?(Date) || value.is_a?(Time)
264
+ %("#{value.to_s(:db)}")
265
+ else
266
+ value.inspect
267
+ end
268
+ end
269
+
270
+ # Returns +true+ if the specified +attribute+ has been set by the user or by a
271
+ # database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies
272
+ # to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+.
273
+ # Note that it always returns +true+ with boolean attributes.
274
+ #
275
+ # class Task < DuckRecord::Base
276
+ # end
277
+ #
278
+ # task = Task.new(title: '', is_done: false)
279
+ # task.attribute_present?(:title) # => false
280
+ # task.attribute_present?(:is_done) # => true
281
+ # task.title = 'Buy milk'
282
+ # task.is_done = true
283
+ # task.attribute_present?(:title) # => true
284
+ # task.attribute_present?(:is_done) # => true
285
+ def attribute_present?(attribute)
286
+ value = _read_attribute(attribute)
287
+ !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
288
+ end
289
+
290
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
291
+ # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
292
+ # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
293
+ #
294
+ # Note: +:id+ is always present.
295
+ #
296
+ # class Person < DuckRecord::Base
297
+ # belongs_to :organization
298
+ # end
299
+ #
300
+ # person = Person.new(name: 'Francesco', age: '22')
301
+ # person[:name] # => "Francesco"
302
+ # person[:age] # => 22
303
+ #
304
+ # person = Person.select('id').first
305
+ # person[:name] # => ActiveModel::MissingAttributeError: missing attribute: name
306
+ # person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute: organization_id
307
+ def [](attr_name)
308
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
309
+ end
310
+
311
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
312
+ # (Alias for the protected #write_attribute method).
313
+ #
314
+ # class Person < DuckRecord::Base
315
+ # end
316
+ #
317
+ # person = Person.new
318
+ # person[:age] = '22'
319
+ # person[:age] # => 22
320
+ # person[:age].class # => Integer
321
+ def []=(attr_name, value)
322
+ write_attribute(attr_name, value)
323
+ end
324
+
325
+ protected
326
+
327
+ def attribute_method?(attr_name) # :nodoc:
328
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
329
+ defined?(@attributes) && @attributes.key?(attr_name)
330
+ end
331
+ end
332
+ end