activeentity 0.0.1.beta17 → 6.3.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.
@@ -1,13 +1,505 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/map"
3
4
  require "mutex_m"
4
5
  require "active_support/core_ext/enumerable"
5
6
 
6
7
  module ActiveEntity
8
+ module AMAttributeMethods
9
+ extend ActiveSupport::Concern
10
+
11
+ NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
12
+ CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
13
+
14
+ included do
15
+ class_attribute :attribute_aliases, instance_writer: false, default: {}
16
+ class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
17
+ end
18
+
19
+ module ClassMethods
20
+ # Declares a method available for all attributes with the given prefix.
21
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
22
+ #
23
+ # #{prefix}#{attr}(*args, &block)
24
+ #
25
+ # to
26
+ #
27
+ # #{prefix}attribute(#{attr}, *args, &block)
28
+ #
29
+ # An instance method <tt>#{prefix}attribute</tt> must exist and accept
30
+ # at least the +attr+ argument.
31
+ #
32
+ # class Person
33
+ # include ActiveModel::AttributeMethods
34
+ #
35
+ # attr_accessor :name
36
+ # attribute_method_prefix 'clear_'
37
+ # define_attribute_methods :name
38
+ #
39
+ # private
40
+ #
41
+ # def clear_attribute(attr)
42
+ # send("#{attr}=", nil)
43
+ # end
44
+ # end
45
+ #
46
+ # person = Person.new
47
+ # person.name = 'Bob'
48
+ # person.name # => "Bob"
49
+ # person.clear_name
50
+ # person.name # => nil
51
+ def attribute_method_prefix(*prefixes)
52
+ self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix }
53
+ undefine_attribute_methods
54
+ end
55
+
56
+ # Declares a method available for all attributes with the given suffix.
57
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
58
+ #
59
+ # #{attr}#{suffix}(*args, &block)
60
+ #
61
+ # to
62
+ #
63
+ # attribute#{suffix}(#{attr}, *args, &block)
64
+ #
65
+ # An <tt>attribute#{suffix}</tt> instance method must exist and accept at
66
+ # least the +attr+ argument.
67
+ #
68
+ # class Person
69
+ # include ActiveModel::AttributeMethods
70
+ #
71
+ # attr_accessor :name
72
+ # attribute_method_suffix '_short?'
73
+ # define_attribute_methods :name
74
+ #
75
+ # private
76
+ #
77
+ # def attribute_short?(attr)
78
+ # send(attr).length < 5
79
+ # end
80
+ # end
81
+ #
82
+ # person = Person.new
83
+ # person.name = 'Bob'
84
+ # person.name # => "Bob"
85
+ # person.name_short? # => true
86
+ def attribute_method_suffix(*suffixes)
87
+ self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix }
88
+ undefine_attribute_methods
89
+ end
90
+
91
+ # Declares a method available for all attributes with the given prefix
92
+ # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
93
+ # the method.
94
+ #
95
+ # #{prefix}#{attr}#{suffix}(*args, &block)
96
+ #
97
+ # to
98
+ #
99
+ # #{prefix}attribute#{suffix}(#{attr}, *args, &block)
100
+ #
101
+ # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
102
+ # accept at least the +attr+ argument.
103
+ #
104
+ # class Person
105
+ # include ActiveModel::AttributeMethods
106
+ #
107
+ # attr_accessor :name
108
+ # attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
109
+ # define_attribute_methods :name
110
+ #
111
+ # private
112
+ #
113
+ # def reset_attribute_to_default!(attr)
114
+ # send("#{attr}=", 'Default Name')
115
+ # end
116
+ # end
117
+ #
118
+ # person = Person.new
119
+ # person.name # => 'Gem'
120
+ # person.reset_name_to_default!
121
+ # person.name # => 'Default Name'
122
+ def attribute_method_affix(*affixes)
123
+ self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] }
124
+ undefine_attribute_methods
125
+ end
126
+
127
+ # Allows you to make aliases for attributes.
128
+ #
129
+ # class Person
130
+ # include ActiveModel::AttributeMethods
131
+ #
132
+ # attr_accessor :name
133
+ # attribute_method_suffix '_short?'
134
+ # define_attribute_methods :name
135
+ #
136
+ # alias_attribute :nickname, :name
137
+ #
138
+ # private
139
+ #
140
+ # def attribute_short?(attr)
141
+ # send(attr).length < 5
142
+ # end
143
+ # end
144
+ #
145
+ # person = Person.new
146
+ # person.name = 'Bob'
147
+ # person.name # => "Bob"
148
+ # person.nickname # => "Bob"
149
+ # person.name_short? # => true
150
+ # person.nickname_short? # => true
151
+ def alias_attribute(new_name, old_name)
152
+ self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
153
+ CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
154
+ attribute_method_matchers.each do |matcher|
155
+ matcher_new = matcher.method_name(new_name).to_s
156
+ matcher_old = matcher.method_name(old_name).to_s
157
+ define_proxy_call false, owner, matcher_new, matcher_old
158
+ end
159
+ end
160
+ end
161
+
162
+ # Is +new_name+ an alias?
163
+ def attribute_alias?(new_name)
164
+ attribute_aliases.key? new_name.to_s
165
+ end
166
+
167
+ # Returns the original name for the alias +name+
168
+ def attribute_alias(name)
169
+ attribute_aliases[name.to_s]
170
+ end
171
+
172
+ # Declares the attributes that should be prefixed and suffixed by
173
+ # <tt>ActiveModel::AttributeMethods</tt>.
174
+ #
175
+ # To use, pass attribute names (as strings or symbols). Be sure to declare
176
+ # +define_attribute_methods+ after you define any prefix, suffix or affix
177
+ # methods, or they will not hook in.
178
+ #
179
+ # class Person
180
+ # include ActiveModel::AttributeMethods
181
+ #
182
+ # attr_accessor :name, :age, :address
183
+ # attribute_method_prefix 'clear_'
184
+ #
185
+ # # Call to define_attribute_methods must appear after the
186
+ # # attribute_method_prefix, attribute_method_suffix or
187
+ # # attribute_method_affix declarations.
188
+ # define_attribute_methods :name, :age, :address
189
+ #
190
+ # private
191
+ #
192
+ # def clear_attribute(attr)
193
+ # send("#{attr}=", nil)
194
+ # end
195
+ # end
196
+ def define_attribute_methods(*attr_names)
197
+ CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
198
+ attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
199
+ end
200
+ end
201
+
202
+ # Declares an attribute that should be prefixed and suffixed by
203
+ # <tt>ActiveModel::AttributeMethods</tt>.
204
+ #
205
+ # To use, pass an attribute name (as string or symbol). Be sure to declare
206
+ # +define_attribute_method+ after you define any prefix, suffix or affix
207
+ # method, or they will not hook in.
208
+ #
209
+ # class Person
210
+ # include ActiveModel::AttributeMethods
211
+ #
212
+ # attr_accessor :name
213
+ # attribute_method_suffix '_short?'
214
+ #
215
+ # # Call to define_attribute_method must appear after the
216
+ # # attribute_method_prefix, attribute_method_suffix or
217
+ # # attribute_method_affix declarations.
218
+ # define_attribute_method :name
219
+ #
220
+ # private
221
+ #
222
+ # def attribute_short?(attr)
223
+ # send(attr).length < 5
224
+ # end
225
+ # end
226
+ #
227
+ # person = Person.new
228
+ # person.name = 'Bob'
229
+ # person.name # => "Bob"
230
+ # person.name_short? # => true
231
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods)
232
+ CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
233
+ attribute_method_matchers.each do |matcher|
234
+ method_name = matcher.method_name(attr_name)
235
+
236
+ unless instance_method_already_implemented?(method_name)
237
+ generate_method = "define_method_#{matcher.target}"
238
+
239
+ if respond_to?(generate_method, true)
240
+ send(generate_method, attr_name.to_s, owner: owner)
241
+ else
242
+ define_proxy_call true, owner, method_name, matcher.target, attr_name.to_s
243
+ end
244
+ end
245
+ end
246
+ attribute_method_matchers_cache.clear
247
+ end
248
+ end
249
+
250
+ # Removes all the previously dynamically defined methods from the class.
251
+ #
252
+ # class Person
253
+ # include ActiveModel::AttributeMethods
254
+ #
255
+ # attr_accessor :name
256
+ # attribute_method_suffix '_short?'
257
+ # define_attribute_method :name
258
+ #
259
+ # private
260
+ #
261
+ # def attribute_short?(attr)
262
+ # send(attr).length < 5
263
+ # end
264
+ # end
265
+ #
266
+ # person = Person.new
267
+ # person.name = 'Bob'
268
+ # person.name_short? # => true
269
+ #
270
+ # Person.undefine_attribute_methods
271
+ #
272
+ # person.name_short? # => NoMethodError
273
+ def undefine_attribute_methods
274
+ generated_attribute_methods.module_eval do
275
+ undef_method(*instance_methods)
276
+ end
277
+ attribute_method_matchers_cache.clear
278
+ end
279
+
280
+ private
281
+ class CodeGenerator
282
+ class << self
283
+ def batch(owner, path, line)
284
+ if owner.is_a?(CodeGenerator)
285
+ yield owner
286
+ else
287
+ instance = new(owner, path, line)
288
+ result = yield instance
289
+ instance.execute
290
+ result
291
+ end
292
+ end
293
+ end
294
+
295
+ def initialize(owner, path, line)
296
+ @owner = owner
297
+ @path = path
298
+ @line = line
299
+ @sources = ["# frozen_string_literal: true\n"]
300
+ @renames = {}
301
+ end
302
+
303
+ def <<(source_line)
304
+ @sources << source_line
305
+ end
306
+
307
+ def rename_method(old_name, new_name)
308
+ @renames[old_name] = new_name
309
+ end
310
+
311
+ def execute
312
+ @owner.module_eval(@sources.join(";"), @path, @line - 1)
313
+ @renames.each do |old_name, new_name|
314
+ @owner.alias_method new_name, old_name
315
+ @owner.undef_method old_name
316
+ end
317
+ end
318
+ end
319
+ private_constant :CodeGenerator
320
+
321
+ def generated_attribute_methods
322
+ @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
323
+ end
324
+
325
+ def instance_method_already_implemented?(method_name)
326
+ generated_attribute_methods.method_defined?(method_name)
327
+ end
328
+
329
+ # The methods +method_missing+ and +respond_to?+ of this module are
330
+ # invoked often in a typical rails, both of which invoke the method
331
+ # +matched_attribute_method+. The latter method iterates through an
332
+ # array doing regular expression matches, which results in a lot of
333
+ # object creations. Most of the time it returns a +nil+ match. As the
334
+ # match result is always the same given a +method_name+, this cache is
335
+ # used to alleviate the GC, which ultimately also speeds up the app
336
+ # significantly (in our case our test suite finishes 10% faster with
337
+ # this cache).
338
+ def attribute_method_matchers_cache
339
+ @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
340
+ end
341
+
342
+ def attribute_method_matchers_matching(method_name)
343
+ attribute_method_matchers_cache.compute_if_absent(method_name) do
344
+ attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
345
+ end
346
+ end
347
+
348
+ # Define a method `name` in `mod` that dispatches to `send`
349
+ # using the given `extra` args. This falls back on `define_method`
350
+ # and `send` if the given names cannot be compiled.
351
+ def define_proxy_call(include_private, code_generator, name, target, *extra)
352
+ defn = if NAME_COMPILABLE_REGEXP.match?(name)
353
+ "def #{name}(*args)"
354
+ else
355
+ "define_method(:'#{name}') do |*args|"
356
+ end
357
+
358
+ extra = (extra.map!(&:inspect) << "*args").join(", ")
359
+
360
+ body = if CALL_COMPILABLE_REGEXP.match?(target)
361
+ "#{"self." unless include_private}#{target}(#{extra})"
362
+ else
363
+ "send(:'#{target}', #{extra})"
364
+ end
365
+
366
+ code_generator <<
367
+ defn <<
368
+ body <<
369
+ "end" <<
370
+ "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
371
+ end
372
+
373
+ class AttributeMethodMatcher #:nodoc:
374
+ attr_reader :prefix, :suffix, :target
375
+
376
+ AttributeMethodMatch = Struct.new(:target, :attr_name)
377
+
378
+ def initialize(options = {})
379
+ @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
380
+ @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
381
+ @target = "#{@prefix}attribute#{@suffix}"
382
+ @method_name = "#{prefix}%s#{suffix}"
383
+ end
384
+
385
+ def match(method_name)
386
+ if @regex =~ method_name
387
+ AttributeMethodMatch.new(target, $1)
388
+ end
389
+ end
390
+
391
+ def method_name(attr_name)
392
+ @method_name % attr_name
393
+ end
394
+ end
395
+ end
396
+
397
+ # Allows access to the object attributes, which are held in the hash
398
+ # returned by <tt>attributes</tt>, as though they were first-class
399
+ # methods. So a +Person+ class with a +name+ attribute can for example use
400
+ # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
401
+ # the attributes hash -- except for multiple assignments with
402
+ # <tt>ActiveRecord::Base#attributes=</tt>.
403
+ #
404
+ # It's also possible to instantiate related objects, so a <tt>Client</tt>
405
+ # class belonging to the +clients+ table with a +master_id+ foreign key
406
+ # can instantiate master through <tt>Client#master</tt>.
407
+ def method_missing(method, *args, &block)
408
+ if respond_to_without_attributes?(method, true)
409
+ super
410
+ else
411
+ match = matched_attribute_method(method.to_s)
412
+ match ? attribute_missing(match, *args, &block) : super
413
+ end
414
+ end
415
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
416
+
417
+ # +attribute_missing+ is like +method_missing+, but for attributes. When
418
+ # +method_missing+ is called we check to see if there is a matching
419
+ # attribute method. If so, we tell +attribute_missing+ to dispatch the
420
+ # attribute. This method can be overloaded to customize the behavior.
421
+ def attribute_missing(match, *args, &block)
422
+ __send__(match.target, match.attr_name, *args, &block)
423
+ end
424
+
425
+ # A +Person+ instance with a +name+ attribute can ask
426
+ # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
427
+ # and <tt>person.respond_to?(:name?)</tt> which will all return +true+.
428
+ alias :respond_to_without_attributes? :respond_to?
429
+ def respond_to?(method, include_private_methods = false)
430
+ if super
431
+ true
432
+ elsif !include_private_methods && super(method, true)
433
+ # If we're here then we haven't found among non-private methods
434
+ # but found among all methods. Which means that the given method is private.
435
+ false
436
+ else
437
+ !matched_attribute_method(method.to_s).nil?
438
+ end
439
+ end
440
+
441
+ private
442
+ def attribute_method?(attr_name)
443
+ respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
444
+ end
445
+
446
+ # Returns a struct representing the matching attribute method.
447
+ # The struct's attributes are prefix, base and suffix.
448
+ def matched_attribute_method(method_name)
449
+ matches = self.class.send(:attribute_method_matchers_matching, method_name)
450
+ matches.detect { |match| attribute_method?(match.attr_name) }
451
+ end
452
+
453
+ def missing_attribute(attr_name, stack)
454
+ raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
455
+ end
456
+
457
+ def _read_attribute(attr)
458
+ __send__(attr)
459
+ end
460
+
461
+ module AttrNames # :nodoc:
462
+ DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
463
+
464
+ # We want to generate the methods via module_eval rather than
465
+ # define_method, because define_method is slower on dispatch.
466
+ # Evaluating many similar methods may use more memory as the instruction
467
+ # sequences are duplicated and cached (in MRI). define_method may
468
+ # be slower on dispatch, but if you're careful about the closure
469
+ # created, then define_method will consume much less memory.
470
+ #
471
+ # But sometimes the database might return columns with
472
+ # characters that are not allowed in normal method names (like
473
+ # 'my_column(omg)'. So to work around this we first define with
474
+ # the __temp__ identifier, and then use alias method to rename
475
+ # it to what we want.
476
+ #
477
+ # We are also defining a constant to hold the frozen string of
478
+ # the attribute name. Using a constant means that we do not have
479
+ # to allocate an object on each call to the attribute method.
480
+ # Making it frozen means that it doesn't get duped when used to
481
+ # key the @attributes in read_attribute.
482
+ def self.define_attribute_accessor_method(owner, attr_name, writer: false)
483
+ method_name = "#{attr_name}#{'=' if writer}"
484
+ if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
485
+ yield method_name, "'#{attr_name}'"
486
+ else
487
+ safe_name = attr_name.unpack1("h*")
488
+ const_name = "ATTR_#{safe_name}"
489
+ const_set(const_name, attr_name) unless const_defined?(const_name)
490
+ temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
491
+ attr_name_expr = "::ActiveEntity::AMAttributeMethods::AttrNames::#{const_name}"
492
+ yield temp_method_name, attr_name_expr
493
+ owner.rename_method(temp_method_name, method_name)
494
+ end
495
+ end
496
+ end
497
+ end
498
+
7
499
  # = Active Entity Attribute Methods
8
500
  module AttributeMethods
9
501
  extend ActiveSupport::Concern
10
- include ActiveModel::AttributeMethods
502
+ include AMAttributeMethods # Some AM modules will include AM::AttributeMethods, we want override it
11
503
 
12
504
  included do
13
505
  initialize_generated_modules
@@ -27,6 +519,17 @@ module ActiveEntity
27
519
  include Mutex_m
28
520
  end
29
521
 
522
+ class << self
523
+ def dangerous_attribute_methods # :nodoc:
524
+ @dangerous_attribute_methods ||= (
525
+ Base.instance_methods +
526
+ Base.private_instance_methods -
527
+ Base.superclass.instance_methods -
528
+ Base.superclass.private_instance_methods
529
+ ).map { |m| -m.to_s }.to_set.freeze
530
+ end
531
+ end
532
+
30
533
  module ClassMethods
31
534
  def inherited(child_class) #:nodoc:
32
535
  child_class.initialize_generated_modules
@@ -96,7 +599,7 @@ module ActiveEntity
96
599
  # A method name is 'dangerous' if it is already (re)defined by Active Entity, but
97
600
  # not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
98
601
  def dangerous_attribute_method?(name) # :nodoc:
99
- method_defined_within?(name, Base)
602
+ ::ActiveEntity::AttributeMethods.dangerous_attribute_methods.include?(name.to_s)
100
603
  end
101
604
 
102
605
  def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
@@ -148,25 +651,52 @@ module ActiveEntity
148
651
  # class Person < ActiveEntity::Base
149
652
  # end
150
653
  #
151
- # Person.has_attribute?('name') # => true
152
- # Person.has_attribute?(:age) # => true
153
- # Person.has_attribute?(:nothing) # => false
654
+ # Person.has_attribute?('name') # => true
655
+ # Person.has_attribute?('new_name') # => true
656
+ # Person.has_attribute?(:age) # => true
657
+ # Person.has_attribute?(:nothing) # => false
154
658
  def has_attribute?(attr_name)
155
- attribute_types.key?(attr_name.to_s)
659
+ attr_name = attr_name.to_s
660
+ attr_name = attribute_aliases[attr_name] || attr_name
661
+ attribute_types.key?(attr_name)
662
+ end
663
+
664
+ def _has_attribute?(attr_name) # :nodoc:
665
+ attribute_types.key?(attr_name)
666
+ end
667
+
668
+ # https://github.com/rails/rails/blob/df475877efdcf74d7524f734ab8ad1d4704fd187/activemodel/lib/active_model/attribute_methods.rb#L208-L217
669
+ def alias_attribute(new_name, old_name)
670
+ self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
671
+ CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
672
+ attribute_method_matchers.each do |matcher|
673
+ matcher_new = matcher.method_name(new_name).to_s
674
+ matcher_old = matcher.method_name(old_name).to_s
675
+ define_proxy_call false, owner, matcher_new, matcher_old
676
+ end
677
+ end
156
678
  end
157
679
  end
158
680
 
159
681
  # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
160
682
  #
161
683
  # class Person < ActiveEntity::Base
684
+ # alias_attribute :new_name, :name
162
685
  # end
163
686
  #
164
687
  # person = Person.new
165
- # person.has_attribute?(:name) # => true
166
- # person.has_attribute?('age') # => true
167
- # person.has_attribute?(:nothing) # => false
688
+ # person.has_attribute?(:name) # => true
689
+ # person.has_attribute?(:new_name) # => true
690
+ # person.has_attribute?('age') # => true
691
+ # person.has_attribute?(:nothing) # => false
168
692
  def has_attribute?(attr_name)
169
- @attributes.key?(attr_name.to_s)
693
+ attr_name = attr_name.to_s
694
+ attr_name = self.class.attribute_aliases[attr_name] || attr_name
695
+ @attributes.key?(attr_name)
696
+ end
697
+
698
+ def _has_attribute?(attr_name) # :nodoc:
699
+ @attributes.key?(attr_name)
170
700
  end
171
701
 
172
702
  # Returns an array of names for the attributes available on this object.
@@ -210,6 +740,7 @@ module ActiveEntity
210
740
  # person.attribute_for_inspect(:tag_ids)
211
741
  # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]"
212
742
  def attribute_for_inspect(attr_name)
743
+ attr_name = attr_name.to_s
213
744
  value = _read_attribute(attr_name)
214
745
  format_for_inspect(value)
215
746
  end
@@ -229,8 +760,9 @@ module ActiveEntity
229
760
  # task.is_done = true
230
761
  # task.attribute_present?(:title) # => true
231
762
  # task.attribute_present?(:is_done) # => true
232
- def attribute_present?(attribute)
233
- value = _read_attribute(attribute)
763
+ def attribute_present?(attr_name)
764
+ attr_name = attr_name.to_s
765
+ value = _read_attribute(attr_name)
234
766
  !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
235
767
  end
236
768
 
@@ -201,6 +201,7 @@ module ActiveEntity
201
201
  )
202
202
  attribute_types[name] = cast_type
203
203
  define_default_attribute(name, default, cast_type)
204
+ define_attribute_method(name)
204
205
  end
205
206
 
206
207
  def load_schema! # :nodoc:
@@ -317,6 +317,7 @@ module ActiveEntity
317
317
  end
318
318
 
319
319
  def init_internals
320
+ @primary_key = self.class.primary_key
320
321
  @readonly = false
321
322
  @marked_for_destruction = false
322
323
 
@@ -7,10 +7,10 @@ module ActiveEntity
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 0
11
- MINOR = 0
12
- TINY = 1
13
- PRE = "beta17"
10
+ MAJOR = 6
11
+ MINOR = 3
12
+ TINY = 0
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -151,7 +151,7 @@ module ActiveEntity
151
151
  ActiveSupport::Dependencies.constantize(type_name)
152
152
  else
153
153
  type_candidate = @_type_candidates_cache[type_name]
154
- if type_candidate && type_constant = ActiveSupport::Dependencies.safe_constantize(type_candidate)
154
+ if type_candidate && type_constant = type_candidate.safe_constantize
155
155
  return type_constant
156
156
  end
157
157
 
@@ -161,7 +161,7 @@ module ActiveEntity
161
161
  candidates << type_name
162
162
 
163
163
  candidates.each do |candidate|
164
- constant = ActiveSupport::Dependencies.safe_constantize(candidate)
164
+ constant = candidate.safe_constantize
165
165
  if candidate == constant.to_s
166
166
  @_type_candidates_cache[type_name] = candidate
167
167
  return constant