activeentity 0.0.1.beta1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +42 -0
  3. data/README.md +145 -0
  4. data/Rakefile +29 -0
  5. data/lib/active_entity.rb +73 -0
  6. data/lib/active_entity/aggregations.rb +276 -0
  7. data/lib/active_entity/associations.rb +146 -0
  8. data/lib/active_entity/associations/embedded/association.rb +134 -0
  9. data/lib/active_entity/associations/embedded/builder/association.rb +100 -0
  10. data/lib/active_entity/associations/embedded/builder/collection_association.rb +69 -0
  11. data/lib/active_entity/associations/embedded/builder/embedded_in.rb +38 -0
  12. data/lib/active_entity/associations/embedded/builder/embeds_many.rb +13 -0
  13. data/lib/active_entity/associations/embedded/builder/embeds_one.rb +16 -0
  14. data/lib/active_entity/associations/embedded/builder/singular_association.rb +28 -0
  15. data/lib/active_entity/associations/embedded/collection_association.rb +188 -0
  16. data/lib/active_entity/associations/embedded/collection_proxy.rb +310 -0
  17. data/lib/active_entity/associations/embedded/embedded_in_association.rb +31 -0
  18. data/lib/active_entity/associations/embedded/embeds_many_association.rb +15 -0
  19. data/lib/active_entity/associations/embedded/embeds_one_association.rb +19 -0
  20. data/lib/active_entity/associations/embedded/singular_association.rb +35 -0
  21. data/lib/active_entity/attribute_assignment.rb +85 -0
  22. data/lib/active_entity/attribute_decorators.rb +90 -0
  23. data/lib/active_entity/attribute_methods.rb +330 -0
  24. data/lib/active_entity/attribute_methods/before_type_cast.rb +78 -0
  25. data/lib/active_entity/attribute_methods/primary_key.rb +98 -0
  26. data/lib/active_entity/attribute_methods/query.rb +35 -0
  27. data/lib/active_entity/attribute_methods/read.rb +47 -0
  28. data/lib/active_entity/attribute_methods/serialization.rb +90 -0
  29. data/lib/active_entity/attribute_methods/time_zone_conversion.rb +91 -0
  30. data/lib/active_entity/attribute_methods/write.rb +63 -0
  31. data/lib/active_entity/attributes.rb +165 -0
  32. data/lib/active_entity/base.rb +303 -0
  33. data/lib/active_entity/coders/json.rb +15 -0
  34. data/lib/active_entity/coders/yaml_column.rb +50 -0
  35. data/lib/active_entity/core.rb +281 -0
  36. data/lib/active_entity/define_callbacks.rb +17 -0
  37. data/lib/active_entity/enum.rb +234 -0
  38. data/lib/active_entity/errors.rb +80 -0
  39. data/lib/active_entity/gem_version.rb +17 -0
  40. data/lib/active_entity/inheritance.rb +278 -0
  41. data/lib/active_entity/integration.rb +78 -0
  42. data/lib/active_entity/locale/en.yml +45 -0
  43. data/lib/active_entity/model_schema.rb +115 -0
  44. data/lib/active_entity/nested_attributes.rb +592 -0
  45. data/lib/active_entity/readonly_attributes.rb +47 -0
  46. data/lib/active_entity/reflection.rb +441 -0
  47. data/lib/active_entity/serialization.rb +25 -0
  48. data/lib/active_entity/store.rb +242 -0
  49. data/lib/active_entity/translation.rb +24 -0
  50. data/lib/active_entity/type.rb +73 -0
  51. data/lib/active_entity/type/date.rb +9 -0
  52. data/lib/active_entity/type/date_time.rb +9 -0
  53. data/lib/active_entity/type/decimal_without_scale.rb +15 -0
  54. data/lib/active_entity/type/hash_lookup_type_map.rb +25 -0
  55. data/lib/active_entity/type/internal/timezone.rb +17 -0
  56. data/lib/active_entity/type/json.rb +30 -0
  57. data/lib/active_entity/type/modifiers/array.rb +72 -0
  58. data/lib/active_entity/type/registry.rb +92 -0
  59. data/lib/active_entity/type/serialized.rb +71 -0
  60. data/lib/active_entity/type/text.rb +11 -0
  61. data/lib/active_entity/type/time.rb +21 -0
  62. data/lib/active_entity/type/type_map.rb +62 -0
  63. data/lib/active_entity/type/unsigned_integer.rb +17 -0
  64. data/lib/active_entity/validate_embedded_association.rb +305 -0
  65. data/lib/active_entity/validations.rb +50 -0
  66. data/lib/active_entity/validations/absence.rb +25 -0
  67. data/lib/active_entity/validations/associated.rb +60 -0
  68. data/lib/active_entity/validations/length.rb +26 -0
  69. data/lib/active_entity/validations/presence.rb +68 -0
  70. data/lib/active_entity/validations/subset.rb +76 -0
  71. data/lib/active_entity/validations/uniqueness_in_embedding.rb +99 -0
  72. data/lib/active_entity/version.rb +10 -0
  73. data/lib/tasks/active_entity_tasks.rake +6 -0
  74. metadata +155 -0
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Associations
5
+ module Embedded
6
+ # = Active Entity Belongs To Association
7
+ class EmbeddedInAssociation < SingularAssociation #:nodoc:
8
+ def default(&block)
9
+ writer(owner.instance_exec(&block)) if reader.nil?
10
+ end
11
+
12
+ private
13
+
14
+ def replace(record)
15
+ if record
16
+ raise_on_type_mismatch!(record)
17
+ set_inverse_instance(record)
18
+ end
19
+
20
+ self.target = record
21
+ end
22
+ # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
23
+ # has_one associations.
24
+ def invertible_for?(record)
25
+ inverse = inverse_reflection_for(record)
26
+ inverse&.embeds_one?
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Associations
5
+ module Embedded
6
+ # = Active Entity Has Many Association
7
+ # This is the proxy that handles a has many association.
8
+ #
9
+ # If the association has a <tt>:through</tt> option further specialization
10
+ # is provided by its child HasManyThroughAssociation.
11
+ class EmbedsManyAssociation < CollectionAssociation #:nodoc:
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Associations
5
+ module Embedded
6
+ # = Active Entity Has One Association
7
+ class EmbedsOneAssociation < SingularAssociation #:nodoc:
8
+ private
9
+ def replace(record)
10
+ raise_on_type_mismatch!(record) if record
11
+
12
+ return target unless record
13
+
14
+ self.target = record
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module Associations
5
+ module Embedded
6
+ class SingularAssociation < Association #:nodoc:
7
+ # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
8
+ def reader
9
+ target
10
+ end
11
+
12
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
13
+ def writer(record)
14
+ replace(record)
15
+ end
16
+
17
+ def build(attributes = {}, &block)
18
+ record = build_record(attributes, &block)
19
+ set_new_record(record)
20
+ record
21
+ end
22
+
23
+ private
24
+
25
+ def replace(_record)
26
+ raise NotImplementedError, "Subclasses must implement a replace(record) method"
27
+ end
28
+
29
+ def set_new_record(record)
30
+ replace(record)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/forbidden_attributes_protection"
4
+
5
+ module ActiveEntity
6
+ module AttributeAssignment
7
+ include ActiveModel::AttributeAssignment
8
+
9
+ private
10
+
11
+ def _assign_attributes(attributes)
12
+ multi_parameter_attributes = {}
13
+ nested_parameter_attributes = {}
14
+
15
+ attributes.each do |k, v|
16
+ if k.to_s.include?("(")
17
+ multi_parameter_attributes[k] = attributes.delete(k)
18
+ elsif v.is_a?(Hash)
19
+ nested_parameter_attributes[k] = attributes.delete(k)
20
+ end
21
+ end
22
+ super(attributes)
23
+
24
+ assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
25
+ assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
26
+ end
27
+
28
+ # Assign any deferred nested attributes after the base attributes have been set.
29
+ def assign_nested_parameter_attributes(pairs)
30
+ pairs.each { |k, v| _assign_attribute(k, v) }
31
+ end
32
+
33
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
34
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
35
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
36
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
37
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and
38
+ # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
39
+ def assign_multiparameter_attributes(pairs)
40
+ execute_callstack_for_multiparameter_attributes(
41
+ extract_callstack_for_multiparameter_attributes(pairs)
42
+ )
43
+ end
44
+
45
+ def execute_callstack_for_multiparameter_attributes(callstack)
46
+ errors = []
47
+ callstack.each do |name, values_with_empty_parameters|
48
+ if values_with_empty_parameters.each_value.all?(&:nil?)
49
+ values = nil
50
+ else
51
+ values = values_with_empty_parameters
52
+ end
53
+ send("#{name}=", values)
54
+ rescue => ex
55
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
56
+ end
57
+ unless errors.empty?
58
+ error_descriptions = errors.map(&:message).join(",")
59
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
60
+ end
61
+ end
62
+
63
+ def extract_callstack_for_multiparameter_attributes(pairs)
64
+ attributes = {}
65
+
66
+ pairs.each do |(multiparameter_name, value)|
67
+ attribute_name = multiparameter_name.split("(").first
68
+ attributes[attribute_name] ||= {}
69
+
70
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
71
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
72
+ end
73
+
74
+ attributes
75
+ end
76
+
77
+ def type_cast_attribute_value(multiparameter_name, value)
78
+ multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
79
+ end
80
+
81
+ def find_parameter_position(multiparameter_name)
82
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveEntity
4
+ module AttributeDecorators # :nodoc:
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :attribute_type_decorations, instance_accessor: false, default: TypeDecorator.new # :internal:
9
+ end
10
+
11
+ module ClassMethods # :nodoc:
12
+ # This method is an internal API used to create class macros such as
13
+ # +serialize+, and features like time zone aware attributes.
14
+ #
15
+ # Used to wrap the type of an attribute in a new type.
16
+ # When the schema for a model is loaded, attributes with the same name as
17
+ # +column_name+ will have their type yielded to the given block. The
18
+ # return value of that block will be used instead.
19
+ #
20
+ # Subsequent calls where +column_name+ and +decorator_name+ are the same
21
+ # will override the previous decorator, not decorate twice. This can be
22
+ # used to create idempotent class macros like +serialize+
23
+ def decorate_attribute_type(column_name, decorator_name, &block)
24
+ matcher = ->(name, _) { name == column_name.to_s }
25
+ key = "_#{column_name}_#{decorator_name}"
26
+ decorate_matching_attribute_types(matcher, key, &block)
27
+ end
28
+
29
+ # This method is an internal API used to create higher level features like
30
+ # time zone aware attributes.
31
+ #
32
+ # When the schema for a model is loaded, +matcher+ will be called for each
33
+ # attribute with its name and type. If the matcher returns a truthy value,
34
+ # the type will then be yielded to the given block, and the return value
35
+ # of that block will replace the type.
36
+ #
37
+ # Subsequent calls to this method with the same value for +decorator_name+
38
+ # will replace the previous decorator, not decorate twice. This can be
39
+ # used to ensure that class macros are idempotent.
40
+ def decorate_matching_attribute_types(matcher, decorator_name, &block)
41
+ reload_schema_from_cache
42
+ decorator_name = decorator_name.to_s
43
+
44
+ # Create new hashes so we don't modify parent classes
45
+ self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
46
+ end
47
+
48
+ private
49
+
50
+ def load_schema!
51
+ super
52
+ attribute_types.each do |name, type|
53
+ decorated_type = attribute_type_decorations.apply(name, type)
54
+ define_attribute(name, decorated_type)
55
+ end
56
+ end
57
+ end
58
+
59
+ class TypeDecorator # :nodoc:
60
+ delegate :clear, to: :@decorations
61
+
62
+ def initialize(decorations = {})
63
+ @decorations = decorations
64
+ end
65
+
66
+ def merge(*args)
67
+ TypeDecorator.new(@decorations.merge(*args))
68
+ end
69
+
70
+ def apply(name, type)
71
+ decorations = decorators_for(name, type)
72
+ decorations.inject(type) do |new_type, block|
73
+ block.call(new_type)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def decorators_for(name, type)
80
+ matching(name, type).map(&:last)
81
+ end
82
+
83
+ def matching(name, type)
84
+ @decorations.values.select do |(matcher, _)|
85
+ matcher.call(name, type)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,330 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mutex_m"
4
+
5
+ module ActiveEntity
6
+ # = Active Entity Attribute Methods
7
+ module AttributeMethods
8
+ extend ActiveSupport::Concern
9
+ include ActiveModel::AttributeMethods
10
+
11
+ included do
12
+ initialize_generated_modules
13
+ include Read
14
+ include Write
15
+ include BeforeTypeCast
16
+ include Query
17
+ include PrimaryKey
18
+ include TimeZoneConversion
19
+ include Serialization
20
+ end
21
+
22
+ RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
23
+
24
+ class GeneratedAttributeMethods < Module #:nodoc:
25
+ include Mutex_m
26
+ end
27
+
28
+ module ClassMethods
29
+ def inherited(child_class) #:nodoc:
30
+ child_class.initialize_generated_modules
31
+ super
32
+ end
33
+
34
+ def initialize_generated_modules # :nodoc:
35
+ @generated_attribute_methods = GeneratedAttributeMethods.new
36
+ @attribute_methods_generated = false
37
+ include @generated_attribute_methods
38
+
39
+ super
40
+ end
41
+
42
+ # Generates all the attribute related methods for columns in the database
43
+ # accessors, mutators and query methods.
44
+ def define_attribute_methods # :nodoc:
45
+ return false if @attribute_methods_generated
46
+ # Use a mutex; we don't want two threads simultaneously trying to define
47
+ # attribute methods.
48
+ generated_attribute_methods.synchronize do
49
+ return false if @attribute_methods_generated
50
+ superclass.define_attribute_methods unless base_class?
51
+ super(attribute_names)
52
+ @attribute_methods_generated = true
53
+ end
54
+ end
55
+
56
+ def undefine_attribute_methods # :nodoc:
57
+ generated_attribute_methods.synchronize do
58
+ super if defined?(@attribute_methods_generated) && @attribute_methods_generated
59
+ @attribute_methods_generated = false
60
+ end
61
+ end
62
+
63
+ # Raises an ActiveEntity::DangerousAttributeError exception when an
64
+ # \Active \Record method is defined in the model, otherwise +false+.
65
+ #
66
+ # class Person < ActiveEntity::Base
67
+ # def save
68
+ # 'already defined by Active Entity'
69
+ # end
70
+ # end
71
+ #
72
+ # Person.instance_method_already_implemented?(:save)
73
+ # # => ActiveEntity::DangerousAttributeError: save is defined by Active Entity. Check to make sure that you don't have an attribute or method with the same name.
74
+ #
75
+ # Person.instance_method_already_implemented?(:name)
76
+ # # => false
77
+ def instance_method_already_implemented?(method_name)
78
+ if dangerous_attribute_method?(method_name)
79
+ raise DangerousAttributeError, "#{method_name} is defined by Active Entity. Check to make sure that you don't have an attribute or method with the same name."
80
+ end
81
+
82
+ if superclass == Base
83
+ super
84
+ else
85
+ # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
86
+ # defines its own attribute method, then we don't want to overwrite that.
87
+ defined = method_defined_within?(method_name, superclass, Base) &&
88
+ ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
89
+ defined || super
90
+ end
91
+ end
92
+
93
+ # A method name is 'dangerous' if it is already (re)defined by Active Entity, but
94
+ # not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
95
+ def dangerous_attribute_method?(name) # :nodoc:
96
+ method_defined_within?(name, Base)
97
+ end
98
+
99
+ def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
100
+ if klass.method_defined?(name) || klass.private_method_defined?(name)
101
+ if superklass.method_defined?(name) || superklass.private_method_defined?(name)
102
+ klass.instance_method(name).owner != superklass.instance_method(name).owner
103
+ else
104
+ true
105
+ end
106
+ else
107
+ false
108
+ end
109
+ end
110
+
111
+ # A class method is 'dangerous' if it is already (re)defined by Active Entity, but
112
+ # not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
113
+ def dangerous_class_method?(method_name)
114
+ RESTRICTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
115
+ end
116
+
117
+ def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
118
+ if klass.respond_to?(name, true)
119
+ if superklass.respond_to?(name, true)
120
+ klass.method(name).owner != superklass.method(name).owner
121
+ else
122
+ true
123
+ end
124
+ else
125
+ false
126
+ end
127
+ end
128
+
129
+ # Returns an array of column names as strings if it's not an abstract class and
130
+ # table exists. Otherwise it returns an empty array.
131
+ #
132
+ # class Person < ActiveEntity::Base
133
+ # end
134
+ #
135
+ # Person.attribute_names
136
+ # # => ["id", "created_at", "updated_at", "name", "age"]
137
+ def attribute_names
138
+ @attribute_names ||= if !abstract_class?
139
+ attribute_types.keys
140
+ else
141
+ []
142
+ end
143
+ end
144
+
145
+ # Returns true if the given attribute exists, otherwise false.
146
+ #
147
+ # class Person < ActiveEntity::Base
148
+ # end
149
+ #
150
+ # Person.has_attribute?('name') # => true
151
+ # Person.has_attribute?(:age) # => true
152
+ # Person.has_attribute?(:nothing) # => false
153
+ def has_attribute?(attr_name)
154
+ attribute_types.key?(attr_name.to_s)
155
+ end
156
+ end
157
+
158
+ # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
159
+ #
160
+ # class Person < ActiveEntity::Base
161
+ # end
162
+ #
163
+ # person = Person.new
164
+ # person.has_attribute?(:name) # => true
165
+ # person.has_attribute?('age') # => true
166
+ # person.has_attribute?(:nothing) # => false
167
+ def has_attribute?(attr_name)
168
+ @attributes.key?(attr_name.to_s)
169
+ end
170
+
171
+ # Returns an array of names for the attributes available on this object.
172
+ #
173
+ # class Person < ActiveEntity::Base
174
+ # end
175
+ #
176
+ # person = Person.new
177
+ # person.attribute_names
178
+ # # => ["id", "created_at", "updated_at", "name", "age"]
179
+ def attribute_names
180
+ @attributes.keys
181
+ end
182
+
183
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
184
+ #
185
+ # class Person < ActiveEntity::Base
186
+ # end
187
+ #
188
+ # person = Person.create(name: 'Francesco', age: 22)
189
+ # person.attributes
190
+ # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
191
+ def attributes
192
+ @attributes.to_hash
193
+ end
194
+
195
+ # Returns an <tt>#inspect</tt>-like string for the value of the
196
+ # attribute +attr_name+. String attributes are truncated up to 50
197
+ # characters, Date and Time attributes are returned in the
198
+ # <tt>:db</tt> format. Other attributes return the value of
199
+ # <tt>#inspect</tt> without modification.
200
+ #
201
+ # person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
202
+ #
203
+ # person.attribute_for_inspect(:name)
204
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
205
+ #
206
+ # person.attribute_for_inspect(:created_at)
207
+ # # => "\"2012-10-22 00:15:07\""
208
+ #
209
+ # person.attribute_for_inspect(:tag_ids)
210
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]"
211
+ def attribute_for_inspect(attr_name)
212
+ value = _read_attribute(attr_name)
213
+ format_for_inspect(value)
214
+ end
215
+
216
+ # Returns +true+ if the specified +attribute+ has been set by the user or by a
217
+ # database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies
218
+ # to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+.
219
+ # Note that it always returns +true+ with boolean attributes.
220
+ #
221
+ # class Task < ActiveEntity::Base
222
+ # end
223
+ #
224
+ # task = Task.new(title: '', is_done: false)
225
+ # task.attribute_present?(:title) # => false
226
+ # task.attribute_present?(:is_done) # => true
227
+ # task.title = 'Buy milk'
228
+ # task.is_done = true
229
+ # task.attribute_present?(:title) # => true
230
+ # task.attribute_present?(:is_done) # => true
231
+ def attribute_present?(attribute)
232
+ value = _read_attribute(attribute)
233
+ !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
234
+ end
235
+
236
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
237
+ # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
238
+ # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
239
+ #
240
+ # Note: +:id+ is always present.
241
+ #
242
+ # class Person < ActiveEntity::Base
243
+ # belongs_to :organization
244
+ # end
245
+ #
246
+ # person = Person.new(name: 'Francesco', age: '22')
247
+ # person[:name] # => "Francesco"
248
+ # person[:age] # => 22
249
+ #
250
+ # person = Person.select('id').first
251
+ # person[:name] # => ActiveModel::MissingAttributeError: missing attribute: name
252
+ # person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute: organization_id
253
+ def [](attr_name)
254
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
255
+ end
256
+
257
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
258
+ # (Alias for the protected #write_attribute method).
259
+ #
260
+ # class Person < ActiveEntity::Base
261
+ # end
262
+ #
263
+ # person = Person.new
264
+ # person[:age] = '22'
265
+ # person[:age] # => 22
266
+ # person[:age].class # => Integer
267
+ def []=(attr_name, value)
268
+ write_attribute(attr_name, value)
269
+ end
270
+
271
+ # Returns the name of all database fields which have been read from this
272
+ # model. This can be useful in development mode to determine which fields
273
+ # need to be selected. For performance critical pages, selecting only the
274
+ # required fields can be an easy performance win (assuming you aren't using
275
+ # all of the fields on the model).
276
+ #
277
+ # For example:
278
+ #
279
+ # class PostsController < ActionController::Base
280
+ # after_action :print_accessed_fields, only: :index
281
+ #
282
+ # def index
283
+ # @posts = Post.all
284
+ # end
285
+ #
286
+ # private
287
+ #
288
+ # def print_accessed_fields
289
+ # p @posts.first.accessed_fields
290
+ # end
291
+ # end
292
+ #
293
+ # Which allows you to quickly change your code to:
294
+ #
295
+ # class PostsController < ActionController::Base
296
+ # def index
297
+ # @posts = Post.select(:id, :title, :author_id, :updated_at)
298
+ # end
299
+ # end
300
+ def accessed_fields
301
+ @attributes.accessed
302
+ end
303
+
304
+ private
305
+ def attribute_method?(attr_name)
306
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
307
+ defined?(@attributes) && @attributes.key?(attr_name)
308
+ end
309
+
310
+ def attributes_with_values(attribute_names)
311
+ attribute_names.each_with_object({}) do |name, attrs|
312
+ attrs[name] = _read_attribute(name)
313
+ end
314
+ end
315
+
316
+ def format_for_inspect(value)
317
+ if value.is_a?(String) && value.length > 50
318
+ "#{value[0, 50]}...".inspect
319
+ elsif value.is_a?(Date) || value.is_a?(Time)
320
+ %("#{value.to_s(:db)}")
321
+ else
322
+ value.inspect
323
+ end
324
+ end
325
+
326
+ def readonly_attribute?(name)
327
+ self.class.readonly_attributes.include?(name)
328
+ end
329
+ end
330
+ end