duck_record 0.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.
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