nobrainer 0.10.0 → 0.12.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.
- checksums.yaml +4 -4
- data/LICENSE +169 -0
- data/lib/no_brainer/criteria/after_find.rb +24 -0
- data/lib/no_brainer/criteria/{termination/cache.rb → cache.rb} +1 -1
- data/lib/no_brainer/criteria/{chainable/core.rb → core.rb} +1 -1
- data/lib/no_brainer/criteria/{termination/count.rb → count.rb} +1 -1
- data/lib/no_brainer/criteria/{termination/delete.rb → delete.rb} +1 -1
- data/lib/no_brainer/criteria/{termination/enumerable.rb → enumerable.rb} +1 -1
- data/lib/no_brainer/criteria/{termination/first.rb → first.rb} +1 -1
- data/lib/no_brainer/criteria/{termination/inc.rb → inc.rb} +1 -1
- data/lib/no_brainer/criteria/{chainable/limit.rb → limit.rb} +1 -1
- data/lib/no_brainer/criteria/{chainable/order_by.rb → order_by.rb} +1 -1
- data/lib/no_brainer/criteria/preload.rb +55 -0
- data/lib/no_brainer/criteria/raw.rb +25 -0
- data/lib/no_brainer/criteria/{chainable/scope.rb → scope.rb} +1 -1
- data/lib/no_brainer/criteria/{termination/update.rb → update.rb} +1 -1
- data/lib/no_brainer/criteria/{chainable/where.rb → where.rb} +61 -25
- data/lib/no_brainer/criteria.rb +4 -16
- data/lib/no_brainer/document/association/belongs_to.rb +9 -2
- data/lib/no_brainer/document/association/core.rb +2 -2
- data/lib/no_brainer/document/association/eager_loader.rb +7 -7
- data/lib/no_brainer/document/association/has_many.rb +1 -1
- data/lib/no_brainer/document/attributes.rb +9 -23
- data/lib/no_brainer/document/callbacks.rb +37 -0
- data/lib/no_brainer/document/core.rb +0 -2
- data/lib/no_brainer/document/criteria.rb +1 -1
- data/lib/no_brainer/document/dirty.rb +43 -46
- data/lib/no_brainer/document/dynamic_attributes.rb +11 -3
- data/lib/no_brainer/document/id.rb +1 -1
- data/lib/no_brainer/document/index.rb +0 -5
- data/lib/no_brainer/document/persistance.rb +39 -39
- data/lib/no_brainer/document/serialization.rb +0 -1
- data/lib/no_brainer/document/timestamps.rb +10 -9
- data/lib/no_brainer/document/types.rb +42 -39
- data/lib/no_brainer/document/validation.rb +5 -0
- data/lib/no_brainer/document.rb +3 -3
- data/lib/no_brainer/document_with_timestamps.rb +6 -0
- data/lib/no_brainer/error.rb +26 -9
- data/lib/no_brainer/query_runner/missing_index.rb +1 -1
- data/lib/no_brainer/railtie.rb +10 -0
- data/lib/nobrainer.rb +3 -3
- metadata +37 -34
- data/LICENSE.md +0 -7
- data/lib/no_brainer/criteria/chainable/raw.rb +0 -33
- data/lib/no_brainer/criteria/termination/eager_loading.rb +0 -50
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module NoBrainer::Document::Attributes
|
|
2
|
-
VALID_FIELD_OPTIONS = [:index, :default, :type]
|
|
2
|
+
VALID_FIELD_OPTIONS = [:index, :default, :type, :type_cast_method, :validates]
|
|
3
3
|
RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations] + NoBrainer::DecoratedSymbol::MODIFIERS.keys
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
@@ -10,14 +10,13 @@ module NoBrainer::Document::Attributes
|
|
|
10
10
|
self.fields = {}
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
def
|
|
14
|
-
|
|
15
|
-
@attributes = {}
|
|
13
|
+
def _initialize(attrs={}, options={})
|
|
14
|
+
@_attributes = {}.with_indifferent_access
|
|
16
15
|
assign_attributes(attrs, options.reverse_merge(:pristine => true))
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
def attributes
|
|
20
|
-
@
|
|
19
|
+
Hash[@_attributes.keys.map { |k| [k, read_attribute(k)] }].with_indifferent_access.freeze
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
def read_attribute(name)
|
|
@@ -32,7 +31,7 @@ module NoBrainer::Document::Attributes
|
|
|
32
31
|
|
|
33
32
|
def assign_defaults
|
|
34
33
|
self.class.fields.each do |name, field_options|
|
|
35
|
-
if field_options.has_key?(:default) && !@
|
|
34
|
+
if field_options.has_key?(:default) && !@_attributes.has_key?(name)
|
|
36
35
|
default_value = field_options[:default]
|
|
37
36
|
default_value = default_value.call if default_value.is_a?(Proc)
|
|
38
37
|
self.write_attribute(name, default_value)
|
|
@@ -45,7 +44,7 @@ module NoBrainer::Document::Attributes
|
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
def assign_attributes(attrs, options={})
|
|
48
|
-
@
|
|
47
|
+
@_attributes.clear if options[:pristine]
|
|
49
48
|
_assign_attributes(attrs, options)
|
|
50
49
|
assign_defaults if options[:pristine]
|
|
51
50
|
self
|
|
@@ -54,7 +53,7 @@ module NoBrainer::Document::Attributes
|
|
|
54
53
|
|
|
55
54
|
def inspectable_attributes
|
|
56
55
|
# TODO test that thing
|
|
57
|
-
Hash[@
|
|
56
|
+
Hash[@_attributes.sort_by { |k,v| self.class.fields.keys.index(k.to_sym) || 2**10 }]
|
|
58
57
|
end
|
|
59
58
|
|
|
60
59
|
def inspect
|
|
@@ -88,28 +87,15 @@ module NoBrainer::Document::Attributes
|
|
|
88
87
|
# Using a layer so the user can use super when overriding these methods
|
|
89
88
|
inject_in_layer :attributes, <<-RUBY, __FILE__, __LINE__ + 1
|
|
90
89
|
def #{name}=(value)
|
|
91
|
-
@
|
|
90
|
+
@_attributes['#{name}'] = value
|
|
92
91
|
end
|
|
93
92
|
|
|
94
93
|
def #{name}
|
|
95
|
-
@
|
|
94
|
+
@_attributes['#{name}']
|
|
96
95
|
end
|
|
97
96
|
RUBY
|
|
98
97
|
end
|
|
99
98
|
|
|
100
|
-
def remove_field(name)
|
|
101
|
-
name = name.to_sym
|
|
102
|
-
|
|
103
|
-
([self] + descendants).each do |klass|
|
|
104
|
-
klass.fields.delete(name)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
inject_in_layer :attributes, <<-RUBY, __FILE__, __LINE__ + 1
|
|
108
|
-
undef #{name}=
|
|
109
|
-
undef #{name}
|
|
110
|
-
RUBY
|
|
111
|
-
end
|
|
112
|
-
|
|
113
99
|
def has_field?(name)
|
|
114
100
|
!!fields[name.to_sym]
|
|
115
101
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module NoBrainer::Document::Callbacks
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
extend ActiveModel::Callbacks
|
|
6
|
+
define_model_callbacks :initialize, :create, :update, :save, :destroy, :terminator => 'false'
|
|
7
|
+
define_model_callbacks :find, :only => [:after], :terminator => 'false'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(*args)
|
|
11
|
+
run_callbacks(:initialize) { _initialize(*args); true }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def _create(*args)
|
|
15
|
+
run_callbacks(:create) { super }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def update(*args, &block)
|
|
19
|
+
run_callbacks(:update) { super }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def replace(*args, &block)
|
|
23
|
+
run_callbacks(:update) { super }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def _update_changed(*args)
|
|
27
|
+
run_callbacks(:update) { super }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def save(*args)
|
|
31
|
+
run_callbacks(:save) { super }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def destroy(*args)
|
|
35
|
+
run_callbacks(:destroy) { super }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -16,7 +16,7 @@ module NoBrainer::Document::Criteria
|
|
|
16
16
|
:with_cache, :without_cache, # Cache
|
|
17
17
|
:count, :empty?, :any?, # Count
|
|
18
18
|
:delete_all, :destroy_all, # Delete
|
|
19
|
-
:includes,
|
|
19
|
+
:includes, :preload, # Preload
|
|
20
20
|
:each, :to_a, # Enumerable
|
|
21
21
|
:first, :last, :first!, :last!, # First
|
|
22
22
|
:inc_all, :dec_all, # Inc
|
|
@@ -5,84 +5,81 @@ module NoBrainer::Document::Dirty
|
|
|
5
5
|
# ActiveModel::AttributeMethods which gives pretty violent method_missing()
|
|
6
6
|
# capabilities, such as giving a getter/setter method for any keys within the
|
|
7
7
|
# attributes keys. We don't want that.
|
|
8
|
+
# Also it doesn't work properly with array and hashes
|
|
8
9
|
|
|
9
|
-
included
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
included { after_save { clear_dirtiness } }
|
|
11
|
+
|
|
12
|
+
def old_attributes_values
|
|
13
|
+
@old_attributes_values ||= {}.with_indifferent_access
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
def
|
|
15
|
-
@
|
|
16
|
+
def clear_dirtiness
|
|
17
|
+
@old_attributes_values.try(:clear)
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def _assign_attributes(attrs, options={})
|
|
19
21
|
super
|
|
20
|
-
clear_dirtiness if options[:
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def clear_dirtiness
|
|
24
|
-
self.previous_changes = changes
|
|
25
|
-
self.changed_attributes.clear
|
|
22
|
+
clear_dirtiness if options[:from_db]
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def changed?
|
|
29
|
-
|
|
26
|
+
changes.present?
|
|
30
27
|
end
|
|
31
28
|
|
|
32
29
|
def changed
|
|
33
|
-
|
|
30
|
+
changes.keys
|
|
34
31
|
end
|
|
35
32
|
|
|
36
33
|
def changes
|
|
37
|
-
|
|
34
|
+
result = {}.with_indifferent_access
|
|
35
|
+
old_attributes_values.each do |attr, old_value|
|
|
36
|
+
current_value = read_attribute(attr)
|
|
37
|
+
result[attr] = [old_value, current_value] if current_value != old_value
|
|
38
|
+
end
|
|
39
|
+
result
|
|
38
40
|
end
|
|
39
41
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# happened. Why is that?
|
|
45
|
-
value = read_attribute(attr)
|
|
46
|
-
value = value.clone if value.duplicable?
|
|
47
|
-
|
|
48
|
-
return if value == new_value
|
|
49
|
-
|
|
50
|
-
changed_attributes[attr] = value
|
|
42
|
+
def attribute_may_change(attr, current_value)
|
|
43
|
+
unless old_attributes_values.has_key?(attr)
|
|
44
|
+
old_attributes_values[attr] = current_value.deep_dup
|
|
45
|
+
end
|
|
51
46
|
end
|
|
52
47
|
|
|
53
48
|
module ClassMethods
|
|
54
49
|
def field(name, options={})
|
|
55
50
|
super
|
|
56
51
|
|
|
57
|
-
inject_in_layer :dirty_tracking
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
inject_in_layer :dirty_tracking do
|
|
53
|
+
define_method("#{name}_change") do
|
|
54
|
+
if old_attributes_values.has_key?(name)
|
|
55
|
+
result = [old_attributes_values[name], read_attribute(name)]
|
|
56
|
+
result = nil if result.first == result.last
|
|
57
|
+
result
|
|
58
|
+
end
|
|
60
59
|
end
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
define_method("#{name}_changed?") do
|
|
62
|
+
!!__send__("#{name}_change")
|
|
64
63
|
end
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
define_method("#{name}_was") do
|
|
66
|
+
old_attributes_values.has_key?(name) ?
|
|
67
|
+
old_attributes_values[name] : read_attribute(name)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
define_method("#{name}") do
|
|
71
|
+
super().tap do |value|
|
|
72
|
+
# This take care of string/arrays/hashes that could change without going
|
|
73
|
+
# through the setter.
|
|
74
|
+
attribute_may_change(name, value) if value.respond_to?(:size)
|
|
75
|
+
end
|
|
73
76
|
end
|
|
74
|
-
RUBY
|
|
75
|
-
end
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
undef #{name}_change
|
|
83
|
-
undef #{name}_was
|
|
84
|
-
undef #{name}=
|
|
85
|
-
RUBY
|
|
78
|
+
define_method("#{name}=") do |value|
|
|
79
|
+
attribute_may_change(name, read_attribute(name))
|
|
80
|
+
super(value)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
86
83
|
end
|
|
87
84
|
end
|
|
88
85
|
end
|
|
@@ -2,11 +2,19 @@ module NoBrainer::Document::DynamicAttributes
|
|
|
2
2
|
extend ActiveSupport::Concern
|
|
3
3
|
|
|
4
4
|
def read_attribute(name)
|
|
5
|
-
self.respond_to?("#{name}")
|
|
5
|
+
if self.respond_to?("#{name}")
|
|
6
|
+
super
|
|
7
|
+
else
|
|
8
|
+
@_attributes[name].tap { |value| attribute_may_change(name, value) if value.respond_to?(:size) }
|
|
9
|
+
end
|
|
6
10
|
end
|
|
7
11
|
|
|
8
12
|
def write_attribute(name, value)
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
if self.respond_to?("#{name}=")
|
|
14
|
+
super
|
|
15
|
+
else
|
|
16
|
+
attribute_may_change(name, read_attribute(name))
|
|
17
|
+
@_attributes[name] = value
|
|
18
|
+
end
|
|
11
19
|
end
|
|
12
20
|
end
|
|
@@ -59,11 +59,6 @@ module NoBrainer::Document::Index
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
def remove_field(name)
|
|
63
|
-
remove_index(name) if fields[name.to_sym][:index]
|
|
64
|
-
super
|
|
65
|
-
end
|
|
66
|
-
|
|
67
62
|
def perform_create_index(index_name, options={})
|
|
68
63
|
index_name = index_name.to_sym
|
|
69
64
|
index_args = self.indexes[index_name]
|
|
@@ -6,8 +6,7 @@ module NoBrainer::Document::Persistance
|
|
|
6
6
|
define_model_callbacks :create, :update, :save, :destroy, :terminator => 'false'
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
def initialize(attrs={}, options={})
|
|
9
|
+
def _initialize(attrs={}, options={})
|
|
11
10
|
super
|
|
12
11
|
@new_record = !options[:from_db]
|
|
13
12
|
end
|
|
@@ -25,56 +24,57 @@ module NoBrainer::Document::Persistance
|
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
def reload(options={})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@attributes = {}
|
|
32
|
-
self.id = id
|
|
33
|
-
end
|
|
34
|
-
assign_attributes(selector.raw.first!, :pristine => true, :from_db => true)
|
|
27
|
+
attrs = selector.raw.first!
|
|
28
|
+
instance_variables.each { |ivar| remove_instance_variable(ivar) } unless options[:keep_ivars]
|
|
29
|
+
initialize(attrs, :pristine => true, :from_db => true)
|
|
35
30
|
self
|
|
36
31
|
end
|
|
37
32
|
|
|
38
33
|
def _create(options={})
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
self.id ||= keys.first
|
|
45
|
-
@new_record = false
|
|
46
|
-
true
|
|
47
|
-
end
|
|
48
|
-
end
|
|
34
|
+
return false if options[:validate] && !valid?
|
|
35
|
+
keys = self.class.insert_all(@_attributes)
|
|
36
|
+
self.id ||= keys.first
|
|
37
|
+
@new_record = false
|
|
38
|
+
true
|
|
49
39
|
end
|
|
50
40
|
|
|
51
41
|
def update(options={}, &block)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
else
|
|
56
|
-
selector.update_all(&block)
|
|
57
|
-
true
|
|
58
|
-
end
|
|
59
|
-
end
|
|
42
|
+
return false if options[:validate] && !valid?
|
|
43
|
+
selector.update_all(&block)
|
|
44
|
+
true
|
|
60
45
|
end
|
|
61
46
|
|
|
62
47
|
def replace(options={}, &block)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
48
|
+
return false if options[:validate] && !valid?
|
|
49
|
+
selector.replace_all(&block)
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def _update_changed_attributes(changed_attrs)
|
|
54
|
+
# If we have a hash to save, we replace the entire document
|
|
55
|
+
# instead of doing some smart update. This is because RethinkDB
|
|
56
|
+
# will merge the existing hash with the given hash. If the
|
|
57
|
+
# user has deleted some keys, we won't remove them.
|
|
58
|
+
if changed_attrs.values.any? { |v| v.is_a?(Hash) }
|
|
59
|
+
selector.replace_all { @_attributes }
|
|
60
|
+
else
|
|
61
|
+
selector.update_all { changed_attrs }
|
|
70
62
|
end
|
|
71
63
|
end
|
|
72
64
|
|
|
65
|
+
def _update_changed(options={})
|
|
66
|
+
return false if options[:validate] && !valid?
|
|
67
|
+
|
|
68
|
+
# We won't be using the `changes` values, because they went through
|
|
69
|
+
# read_attribute(), and we want the raw values.
|
|
70
|
+
changed_attrs = Hash[self.changed.map { |k| [k, @_attributes[k]] }]
|
|
71
|
+
_update_changed_attributes(changed_attrs) if changed_attrs.present?
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
73
75
|
def save(options={})
|
|
74
76
|
options = options.reverse_merge(:validate => true)
|
|
75
|
-
|
|
76
|
-
new_record? ? _create(options) : replace(options) { attributes }
|
|
77
|
-
end
|
|
77
|
+
new_record? ? _create(options) : _update_changed(options)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def save!(*args)
|
|
@@ -95,12 +95,12 @@ module NoBrainer::Document::Persistance
|
|
|
95
95
|
selector.delete_all
|
|
96
96
|
@destroyed = true
|
|
97
97
|
end
|
|
98
|
-
|
|
98
|
+
@_attributes.freeze
|
|
99
99
|
true
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
def destroy
|
|
103
|
-
|
|
103
|
+
delete
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
module ClassMethods
|
|
@@ -2,17 +2,18 @@ module NoBrainer::Document::Timestamps
|
|
|
2
2
|
extend ActiveSupport::Concern
|
|
3
3
|
|
|
4
4
|
included do
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
field :created_at, :type => Time
|
|
6
|
+
field :updated_at, :type => Time
|
|
7
7
|
|
|
8
|
-
before_create { self.created_at =
|
|
9
|
-
|
|
8
|
+
before_create { self.created_at = self.updated_at = Time.now }
|
|
9
|
+
# Not using the before_update callback as it would mess
|
|
10
|
+
# with the dirty tracking. We want to bypass the database
|
|
11
|
+
# call if nothing has changed.
|
|
10
12
|
end
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
end
|
|
14
|
+
def _update_changed_attributes(changed_attrs)
|
|
15
|
+
self.updated_at = Time.now
|
|
16
|
+
changed_attrs['updated_at'] = @_attributes['updated_at']
|
|
17
|
+
super
|
|
17
18
|
end
|
|
18
19
|
end
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
module NoBrainer::Document::Types
|
|
2
2
|
extend ActiveSupport::Concern
|
|
3
3
|
|
|
4
|
-
included do
|
|
5
|
-
# We namespace our fake Boolean class to avoid polluting the global namespace
|
|
6
|
-
class_exec { class Boolean; def initialize; raise; end; end }
|
|
7
|
-
before_validation :add_type_errors
|
|
8
|
-
end
|
|
9
|
-
|
|
10
4
|
module CastingRules
|
|
11
5
|
extend self
|
|
6
|
+
InvalidType = NoBrainer::Error::InvalidType
|
|
12
7
|
|
|
13
8
|
def String(value)
|
|
14
9
|
case value
|
|
@@ -63,67 +58,75 @@ module NoBrainer::Document::Types
|
|
|
63
58
|
else raise InvalidType
|
|
64
59
|
end
|
|
65
60
|
end
|
|
66
|
-
end
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def self.lookup_cast_method(type)
|
|
74
|
-
type = type.to_s
|
|
75
|
-
type = 'Boolean' if type == 'NoBrainer::Document::Types::Boolean'
|
|
76
|
-
CastingRules.method(type)
|
|
77
|
-
rescue NameError
|
|
78
|
-
proc { raise InvalidType }
|
|
79
|
-
end
|
|
62
|
+
def lookup(type)
|
|
63
|
+
CastingRules.method(type.to_s)
|
|
64
|
+
rescue NameError
|
|
65
|
+
proc { raise InvalidType }
|
|
66
|
+
end
|
|
80
67
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@type = type
|
|
68
|
+
def cast(value, type, type_cast_method)
|
|
69
|
+
return value if value.nil? || type.nil? || value.is_a?(type)
|
|
70
|
+
type_cast_method.call(value)
|
|
85
71
|
end
|
|
72
|
+
end
|
|
86
73
|
|
|
87
|
-
|
|
88
|
-
|
|
74
|
+
included do
|
|
75
|
+
# We namespace our fake Boolean class to avoid polluting the global namespace
|
|
76
|
+
class_exec do
|
|
77
|
+
class Boolean
|
|
78
|
+
def initialize; raise; end
|
|
79
|
+
def self.inspect; 'Boolean'; end
|
|
80
|
+
def self.to_s; inspect; end
|
|
81
|
+
def self.name; inspect; end
|
|
82
|
+
end
|
|
89
83
|
end
|
|
84
|
+
before_validation :add_type_errors
|
|
90
85
|
end
|
|
91
86
|
|
|
92
87
|
def add_type_errors
|
|
93
88
|
return unless @pending_type_errors
|
|
94
89
|
@pending_type_errors.each do |name, error|
|
|
95
|
-
errors.add(name,
|
|
90
|
+
errors.add(name, :invalid_type, :type => error.human_type_name)
|
|
96
91
|
end
|
|
97
92
|
end
|
|
98
93
|
|
|
99
94
|
module ClassMethods
|
|
95
|
+
def cast_value_for(name, value)
|
|
96
|
+
name = name.to_sym
|
|
97
|
+
field_def = fields[name]
|
|
98
|
+
return value unless field_def && field_def[:type]
|
|
99
|
+
NoBrainer::Document::Types::CastingRules.cast(value, field_def[:type], field_def[:type_cast_method])
|
|
100
|
+
rescue NoBrainer::Error::InvalidType => error
|
|
101
|
+
error.type = field_def[:type]
|
|
102
|
+
error.value = value
|
|
103
|
+
error.attr_name = name
|
|
104
|
+
raise error
|
|
105
|
+
end
|
|
106
|
+
|
|
100
107
|
def field(name, options={})
|
|
101
|
-
super
|
|
102
|
-
|
|
108
|
+
return super unless options.has_key?(:type)
|
|
109
|
+
|
|
103
110
|
name = name.to_sym
|
|
104
111
|
type = options[:type]
|
|
105
|
-
|
|
112
|
+
options[:type_cast_method] = NoBrainer::Document::Types::CastingRules.lookup(type)
|
|
113
|
+
|
|
114
|
+
super
|
|
106
115
|
|
|
107
116
|
inject_in_layer :types do
|
|
108
117
|
define_method("#{name}=") do |value|
|
|
109
118
|
begin
|
|
110
|
-
value =
|
|
119
|
+
value = self.class.cast_value_for(name, value)
|
|
111
120
|
@pending_type_errors.try(:delete, name)
|
|
112
|
-
rescue NoBrainer::
|
|
113
|
-
error.type ||= type
|
|
121
|
+
rescue NoBrainer::Error::InvalidType => error
|
|
114
122
|
@pending_type_errors ||= {}
|
|
115
123
|
@pending_type_errors[name] = error
|
|
116
124
|
end
|
|
117
125
|
super(value)
|
|
118
126
|
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
inject_in_layer :types, <<-RUBY, __FILE__, __LINE__ + 1
|
|
125
|
-
undef #{name}=
|
|
126
|
-
RUBY
|
|
128
|
+
define_method("#{name}?") { !!read_attribute(name) } if type == Boolean
|
|
129
|
+
end
|
|
127
130
|
end
|
|
128
131
|
end
|
|
129
132
|
end
|
|
@@ -8,6 +8,11 @@ module NoBrainer::Document::Validation
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
module ClassMethods
|
|
11
|
+
def field(name, options={})
|
|
12
|
+
super
|
|
13
|
+
validates(name.to_sym, options[:validates]) if options[:validates]
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
def validates_uniqueness_of(*attr_names)
|
|
12
17
|
validates_with UniquenessValidator, _merge_attributes(attr_names)
|
|
13
18
|
end
|
data/lib/no_brainer/document.rb
CHANGED
|
@@ -5,10 +5,10 @@ module NoBrainer::Document
|
|
|
5
5
|
extend NoBrainer::Autoload
|
|
6
6
|
|
|
7
7
|
autoload_and_include :Core, :StoreIn, :InjectionLayer, :Attributes, :Validation, :Types,
|
|
8
|
-
:Persistance, :Dirty, :Id, :Association, :Serialization,
|
|
9
|
-
:
|
|
8
|
+
:Persistance, :Callbacks, :Dirty, :Id, :Association, :Serialization,
|
|
9
|
+
:Criteria, :Polymorphic, :Index
|
|
10
10
|
|
|
11
|
-
autoload :DynamicAttributes
|
|
11
|
+
autoload :DynamicAttributes, :Timestamps
|
|
12
12
|
|
|
13
13
|
singleton_class.delegate :all, :to => Core
|
|
14
14
|
end
|
data/lib/no_brainer/error.rb
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
module NoBrainer::Error
|
|
2
|
-
class Connection <
|
|
3
|
-
class DocumentNotFound <
|
|
4
|
-
class DocumentInvalid <
|
|
5
|
-
class DocumentNotSaved <
|
|
6
|
-
class ChildrenExist <
|
|
7
|
-
class CannotUseIndex <
|
|
8
|
-
class MissingIndex <
|
|
9
|
-
class InvalidType <
|
|
10
|
-
class AssociationNotSaved <
|
|
2
|
+
class Connection < RuntimeError; end
|
|
3
|
+
class DocumentNotFound < RuntimeError; end
|
|
4
|
+
class DocumentInvalid < RuntimeError; end
|
|
5
|
+
class DocumentNotSaved < RuntimeError; end
|
|
6
|
+
class ChildrenExist < RuntimeError; end
|
|
7
|
+
class CannotUseIndex < RuntimeError; end
|
|
8
|
+
class MissingIndex < RuntimeError; end
|
|
9
|
+
class InvalidType < RuntimeError; end
|
|
10
|
+
class AssociationNotSaved < RuntimeError; end
|
|
11
|
+
|
|
12
|
+
class InvalidType < RuntimeError
|
|
13
|
+
attr_accessor :attr_name, :value, :type
|
|
14
|
+
def initialize(options={})
|
|
15
|
+
@attr_name = options[:attr_name]
|
|
16
|
+
@value = options[:value]
|
|
17
|
+
@type = options[:type]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def human_type_name
|
|
21
|
+
type.to_s.underscore.humanize.downcase
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def message
|
|
25
|
+
"#{attr_name} should be used with a #{human_type_name}. Got `#{value}`"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
11
28
|
end
|
|
@@ -3,7 +3,7 @@ class NoBrainer::QueryRunner::MissingIndex < NoBrainer::QueryRunner::Middleware
|
|
|
3
3
|
@runner.call(env)
|
|
4
4
|
rescue RethinkDB::RqlRuntimeError => e
|
|
5
5
|
if e.message =~ /^Index `(.+)` was not found\.$/
|
|
6
|
-
raise NoBrainer::Error::MissingIndex.new("Please run \"rake db:update_indexes\" to create index `#{$1}`\n" +
|
|
6
|
+
raise NoBrainer::Error::MissingIndex.new("Please run \"rake db:update_indexes\" to create the index `#{$1}`\n" +
|
|
7
7
|
"--> Read http://nobrainer.io/docs/indexes for more information.")
|
|
8
8
|
end
|
|
9
9
|
raise
|