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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +29 -0
- data/lib/duck_record/attribute/user_provided_default.rb +30 -0
- data/lib/duck_record/attribute.rb +221 -0
- data/lib/duck_record/attribute_assignment.rb +91 -0
- data/lib/duck_record/attribute_methods/before_type_cast.rb +76 -0
- data/lib/duck_record/attribute_methods/dirty.rb +124 -0
- data/lib/duck_record/attribute_methods/read.rb +78 -0
- data/lib/duck_record/attribute_methods/write.rb +65 -0
- data/lib/duck_record/attribute_methods.rb +332 -0
- data/lib/duck_record/attribute_mutation_tracker.rb +113 -0
- data/lib/duck_record/attribute_set/builder.rb +124 -0
- data/lib/duck_record/attribute_set/yaml_encoder.rb +41 -0
- data/lib/duck_record/attribute_set.rb +99 -0
- data/lib/duck_record/attributes.rb +262 -0
- data/lib/duck_record/base.rb +296 -0
- data/lib/duck_record/callbacks.rb +324 -0
- data/lib/duck_record/core.rb +253 -0
- data/lib/duck_record/define_callbacks.rb +23 -0
- data/lib/duck_record/errors.rb +44 -0
- data/lib/duck_record/inheritance.rb +130 -0
- data/lib/duck_record/locale/en.yml +48 -0
- data/lib/duck_record/model_schema.rb +64 -0
- data/lib/duck_record/serialization.rb +19 -0
- data/lib/duck_record/translation.rb +22 -0
- data/lib/duck_record/type/array.rb +36 -0
- data/lib/duck_record/type/decimal_without_scale.rb +13 -0
- data/lib/duck_record/type/internal/abstract_json.rb +33 -0
- data/lib/duck_record/type/json.rb +6 -0
- data/lib/duck_record/type/registry.rb +97 -0
- data/lib/duck_record/type/serialized.rb +63 -0
- data/lib/duck_record/type/text.rb +9 -0
- data/lib/duck_record/type/unsigned_integer.rb +15 -0
- data/lib/duck_record/type.rb +66 -0
- data/lib/duck_record/validations.rb +40 -0
- data/lib/duck_record/version.rb +3 -0
- data/lib/duck_record.rb +47 -0
- data/lib/tasks/acts_as_record_tasks.rake +4 -0
- 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
|