nobrainer 0.10.0 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0fa1301301888d07f8c607cfd1102555aad40a92
4
- data.tar.gz: f05a189ae40c4e6b1f2d420dd84a4928a6f65203
3
+ metadata.gz: 56f5bfdef46fb94e140efe80af84031e02238d07
4
+ data.tar.gz: aa5965b806a43a8727be98ef2a7a9c370fe94cfc
5
5
  SHA512:
6
- metadata.gz: 1bfa4718cb043f4847f972d46b2728c772475b8d07683a591d38d6ec039d06c09508d2df855791e935bbaa85f5d11780a367a383e9ebae0c19dce278dab7c83d
7
- data.tar.gz: 4584d38e6646c426672cea00c0f9aa58f05a6df4778586ae2d699662d927984b1f32ba3b87ebc3b787d4ad9181b19e9e4ecf3f4cfafdbc116f85920566534ed0
6
+ metadata.gz: 0285cc6834d21a4e973fe293adf54ada0bcce15424a605719a32db2445a909793ebab766549a9d8b31b42ced44e6d3ef5f1cfecd980b33b4c4c1792af94a74c4
7
+ data.tar.gz: 3ed81e4508bc4528142b7049db0e1464927f7dfcad9c459853593c70a9ebbed95c47b3bd634d03f0bc2e9ebcc358a2d72cbcd7b7c73b4c46ca37a518a23c7c56
@@ -11,7 +11,7 @@ class NoBrainer::Criteria
11
11
  module Termination
12
12
  extend NoBrainer::Autoload
13
13
  extend ActiveSupport::Concern
14
- autoload_and_include :Count, :Delete, :Enumerable, :First, :EagerLoading,
14
+ autoload_and_include :Count, :Delete, :Enumerable, :First, :Preload,
15
15
  :Inc, :Update, :Cache
16
16
  end
17
17
 
@@ -56,14 +56,21 @@ module NoBrainer::Criteria::Chainable::Where
56
56
 
57
57
  class BinaryOperator < Struct.new(:key, :op, :value)
58
58
  def simplify
59
- # TODO Simplify the in uniq
60
- self
59
+ case op
60
+ when :in then
61
+ case value
62
+ when Range then BinaryOperator.new(key, :between, value)
63
+ when Array then BinaryOperator.new(key, :in, value.uniq)
64
+ else raise ArgumentError.new ":in takes an array/range, not #{value}"
65
+ end
66
+ else self
67
+ end
61
68
  end
62
69
 
63
70
  def to_rql(doc)
64
71
  case op
65
72
  when :between then (doc[key] >= value.min) & (doc[key] <= value.max)
66
- when :in then value.map { |v| doc[key].eq(v) }.reduce { |a,b| a | b }
73
+ when :in then RethinkDB::RQL.new.expr(value).contains(doc[key])
67
74
  else doc[key].__send__(op, value)
68
75
  end
69
76
  end
@@ -130,7 +137,7 @@ module NoBrainer::Criteria::Chainable::Where
130
137
  self.with_index_name == false
131
138
  end
132
139
 
133
- class IndexFinder < Struct.new(:criteria, :index_name, :indexed_values, :ast)
140
+ class IndexFinder < Struct.new(:criteria, :index_name, :rql_proc, :ast)
134
141
  def initialize(*args)
135
142
  super
136
143
  find_index
@@ -143,9 +150,7 @@ module NoBrainer::Criteria::Chainable::Where
143
150
  private
144
151
 
145
152
  def get_candidate_clauses(*types)
146
- Hash[criteria.where_ast.clauses
147
- .select { |c| c.is_a?(BinaryOperator) && types.include?(c.op) }
148
- .map { |c| [c.key, c] }]
153
+ criteria.where_ast.clauses.select { |c| c.is_a?(BinaryOperator) && types.include?(c.op) }
149
154
  end
150
155
 
151
156
  def get_usable_indexes(*types)
@@ -156,19 +161,24 @@ module NoBrainer::Criteria::Chainable::Where
156
161
  end
157
162
 
158
163
  def find_index_canonical
159
- clauses = get_candidate_clauses(:eq, :in)
164
+ clauses = Hash[get_candidate_clauses(:eq, :in, :between).map { |c| [c.key, c] }]
160
165
  return unless clauses.present?
161
166
 
162
167
  if index_name = (get_usable_indexes.keys & clauses.keys).first
163
168
  clause = clauses[index_name]
164
169
  self.index_name = index_name
165
- self.indexed_values = clause.op == :in ? clause.value : [clause.value]
166
170
  self.ast = MultiOperator.new(:and, criteria.where_ast.clauses - [clause])
171
+ self.rql_proc = case clause.op
172
+ when :eq then ->(rql){ rql.get_all(clause.value, :index => index_name) }
173
+ when :in then ->(rql){ rql.get_all(*clause.value, :index => index_name) }
174
+ when :between then ->(rql){ rql.between(clause.value.min, clause.value.max, :index => index_name,
175
+ :left_bound => :closed, :right_bound => :closed) }
176
+ end
167
177
  end
168
178
  end
169
179
 
170
180
  def find_index_compound
171
- clauses = get_candidate_clauses(:eq)
181
+ clauses = Hash[get_candidate_clauses(:eq).map { |c| [c.key, c] }]
172
182
  return unless clauses.present?
173
183
 
174
184
  index_name, index_values = get_usable_indexes(:compound)
@@ -179,14 +189,33 @@ module NoBrainer::Criteria::Chainable::Where
179
189
  if index_name
180
190
  indexed_clauses = index_values.map { |field| clauses[field] }
181
191
  self.index_name = index_name
182
- self.indexed_values = [indexed_clauses.map { |c| c.value }]
183
192
  self.ast = MultiOperator.new(:and, criteria.where_ast.clauses - indexed_clauses)
193
+ self.rql_proc = ->(rql){ rql.get_all(indexed_clauses.map { |c| c.value }, :index => index_name) }
194
+ end
195
+ end
196
+
197
+ def find_index_hidden_between
198
+ clauses = get_candidate_clauses(:gt, :ge, :lt, :le).group_by(&:key)
199
+ return unless clauses.present?
200
+
201
+ if index_name = (get_usable_indexes.keys & clauses.keys).first
202
+ op_clauses = Hash[clauses[index_name].map { |c| [c.op, c] }]
203
+ left_bound = op_clauses[:gt] || op_clauses[:ge]
204
+ right_bound = op_clauses[:lt] || op_clauses[:le]
205
+
206
+ self.index_name = index_name
207
+ self.ast = MultiOperator.new(:and, criteria.where_ast.clauses - [left_bound, right_bound].compact)
208
+
209
+ options = {:index => index_name}
210
+ options[:left_bound] = {:gt => :open, :ge => :closed}[left_bound.op] if left_bound
211
+ options[:right_bound] = {:lt => :open, :le => :closed}[right_bound.op] if right_bound
212
+ self.rql_proc = ->(rql){ rql.between(left_bound.try(:value), right_bound.try(:value), options) }
184
213
  end
185
214
  end
186
215
 
187
216
  def find_index
188
217
  return if criteria.__send__(:without_index?)
189
- find_index_canonical || find_index_compound
218
+ find_index_canonical || find_index_compound || find_index_hidden_between
190
219
  if criteria.with_index_name && !could_find_index?
191
220
  raise NoBrainer::Error::CannotUseIndex.new("Cannot use index #{criteria.with_index_name}")
192
221
  end
@@ -200,9 +229,7 @@ module NoBrainer::Criteria::Chainable::Where
200
229
 
201
230
  def compile_rql_pass1
202
231
  rql = super
203
- if index_finder.could_find_index?
204
- rql = rql.get_all(*index_finder.indexed_values, :index => index_finder.index_name)
205
- end
232
+ rql = index_finder.rql_proc.call(rql) if index_finder.could_find_index?
206
233
  rql
207
234
  end
208
235
 
@@ -0,0 +1,55 @@
1
+ module NoBrainer::Criteria::Termination::Preload
2
+ extend ActiveSupport::Concern
3
+
4
+ included { attr_accessor :_preloads }
5
+
6
+ def initialize(options={})
7
+ super
8
+ self._preloads = []
9
+ end
10
+
11
+ def preload(*values)
12
+ chain(:keep_cache => true) { |criteria| criteria._preloads = values }
13
+ end
14
+
15
+ def includes(*values)
16
+ NoBrainer.logger.warn "[NoBrainer] includes() is deprecated and will be removed, use preload() instead."
17
+ preload(*values)
18
+ end
19
+
20
+ def merge!(criteria, options={})
21
+ super
22
+ self._preloads = self._preloads + criteria._preloads
23
+
24
+ # XXX Not pretty hack
25
+ if criteria._preloads.present? && cached?
26
+ perform_preloads(@cache)
27
+ end
28
+ end
29
+
30
+ def each(options={}, &block)
31
+ return super unless should_preloads? && !options[:no_preloadsing] && block
32
+
33
+ docs = []
34
+ super(options.merge(:no_preloadsing => true)) { |doc| docs << doc }
35
+ perform_preloads(docs)
36
+ docs.each(&block)
37
+ self
38
+ end
39
+
40
+ private
41
+
42
+ def should_preloads?
43
+ self._preloads.present? && !raw?
44
+ end
45
+
46
+ def get_one(criteria)
47
+ super.tap { |doc| perform_preloads([doc]) }
48
+ end
49
+
50
+ def perform_preloads(docs)
51
+ if should_preloads? && docs.present?
52
+ NoBrainer::Document::Association::EagerLoader.new.eager_load(docs, self._preloads)
53
+ end
54
+ end
55
+ end
@@ -5,8 +5,8 @@ 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, :Criteria,
9
- :Polymorphic, :Index, :Timestamps
8
+ :Persistance, :Callbacks, :Dirty, :Id, :Association, :Serialization,
9
+ :Criteria, :Polymorphic, :Index, :Timestamps
10
10
 
11
11
  autoload :DynamicAttributes
12
12
 
@@ -2,7 +2,7 @@ class NoBrainer::Document::Association::BelongsTo
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:foreign_key, :class_name, :index]
5
+ VALID_OPTIONS = [:foreign_key, :class_name, :index, :validates]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
7
  extend NoBrainer::Document::Association::EagerLoader::Generic
8
8
 
@@ -19,7 +19,9 @@ class NoBrainer::Document::Association::BelongsTo
19
19
  def hook
20
20
  super
21
21
 
22
- owner_klass.field foreign_key, :index => options[:index]
22
+ owner_klass.field(foreign_key, :index => options[:index])
23
+ owner_klass.validates(target_name, options[:validates]) if options[:validates]
24
+
23
25
  delegate("#{foreign_key}=", :assign_foreign_key, :call_super => true)
24
26
  add_callback_for(:after_validation)
25
27
  # TODO test if we are not overstepping on another foreign_key
@@ -40,19 +40,19 @@ class NoBrainer::Document::Association::EagerLoader
40
40
  association.eager_load(docs, criteria)
41
41
  end
42
42
 
43
- def eager_load(docs, includes)
44
- case includes
45
- when Hash then includes.each do |k,v|
43
+ def eager_load(docs, preloads)
44
+ case preloads
45
+ when Hash then preloads.each do |k,v|
46
46
  if v.is_a?(NoBrainer::Criteria)
47
47
  v = v.dup
48
- nested_includes, v._includes = v._includes, []
49
- eager_load(eager_load_association(docs, k, v), nested_includes)
48
+ nested_preloads, v._preloads = v._preloads, []
49
+ eager_load(eager_load_association(docs, k, v), nested_preloads)
50
50
  else
51
51
  eager_load(eager_load_association(docs, k), v)
52
52
  end
53
53
  end
54
- when Array then includes.each { |v| eager_load(docs, v) }
55
- else eager_load_association(docs, includes)
54
+ when Array then preloads.each { |v| eager_load(docs, v) }
55
+ else eager_load_association(docs, preloads)
56
56
  end
57
57
  true
58
58
  end
@@ -1,5 +1,5 @@
1
1
  module NoBrainer::Document::Attributes
2
- VALID_FIELD_OPTIONS = [:index, :default, :type]
2
+ VALID_FIELD_OPTIONS = [:index, :default, :type, :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 initialize(attrs={}, options={})
14
- super
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
- @attributes.dup.freeze
19
+ @_attributes.dup.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) && !@attributes.has_key?(name.to_s)
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
- @attributes.clear if options[:pristine]
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[@attributes.sort_by { |k,v| self.class.fields.keys.index(k.to_sym) || 2**10 }]
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,11 +87,11 @@ 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
- @attributes['#{name}'] = value
90
+ @_attributes['#{name}'] = value
92
91
  end
93
92
 
94
93
  def #{name}
95
- @attributes['#{name}']
94
+ @_attributes['#{name}']
96
95
  end
97
96
  RUBY
98
97
  end
@@ -0,0 +1,32 @@
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
+ end
8
+
9
+ def initialize(*args)
10
+ run_callbacks(:initialize) { _initialize(*args); true }
11
+ end
12
+
13
+ def _create(*args)
14
+ run_callbacks(:create) { super }
15
+ end
16
+
17
+ def update(*args, &block)
18
+ run_callbacks(:update) { super }
19
+ end
20
+
21
+ def replace(*args, &block)
22
+ run_callbacks(:update) { super }
23
+ end
24
+
25
+ def save(*args)
26
+ run_callbacks(:save) { super }
27
+ end
28
+
29
+ def destroy(*args)
30
+ run_callbacks(:destroy) { super }
31
+ end
32
+ end
@@ -15,6 +15,4 @@ module NoBrainer::Document::Core
15
15
 
16
16
  NoBrainer::Document::Core.all << self
17
17
  end
18
-
19
- def initialize(attrs={}, options={}); end
20
18
  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, # EagerLoading
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,73 +5,87 @@ 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 do
10
- attr_accessor :previous_changes
11
- after_save { clear_dirtiness }
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 changed_attributes
15
- @changed_attributes ||= {}
16
+ def clear_dirtiness
17
+ @old_attributes_values.try(:clear)
16
18
  end
17
19
 
18
- def _assign_attributes(attrs, options={})
19
- super
20
- clear_dirtiness if options[:pristine]
20
+ def attributes
21
+ # If we leak all the attributes (rare), we perform a copy to ensure that we
22
+ # don't have issues with modified hashes/array. Devs are crazy.
23
+ super.tap { |attrs| @old_attributes_values = attrs.deep_dup }
21
24
  end
22
25
 
23
- def clear_dirtiness
24
- self.previous_changes = changes
25
- self.changed_attributes.clear
26
+ def _assign_attributes(attrs, options={})
27
+ super
28
+ clear_dirtiness if options[:from_db]
26
29
  end
27
30
 
28
31
  def changed?
29
- changed_attributes.present?
32
+ changes.present?
30
33
  end
31
34
 
32
35
  def changed
33
- changed_attributes.keys
36
+ changes.keys
34
37
  end
35
38
 
36
39
  def changes
37
- Hash[changed_attributes.map { |k,v| [k, [v, read_attribute(k)]] }]
40
+ result = {}.with_indifferent_access
41
+ old_attributes_values.each do |attr, old_value|
42
+ current_value = read_attribute(attr)
43
+ result[attr] = [old_value, current_value] if current_value != old_value
44
+ end
45
+ result
38
46
  end
39
47
 
40
- def attribute_will_change!(attr, new_value)
41
- return if changed_attributes.include?(attr)
42
-
43
- # ActiveModel ignores TypeError and NoMethodError exception as if nothng
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
48
+ def attribute_may_change(attr, current_value)
49
+ unless old_attributes_values.has_key?(attr)
50
+ old_attributes_values[attr] = current_value.deep_dup
51
+ end
51
52
  end
52
53
 
53
54
  module ClassMethods
54
55
  def field(name, options={})
55
56
  super
56
57
 
57
- inject_in_layer :dirty_tracking, <<-RUBY, __FILE__, __LINE__ + 1
58
- def #{name}_changed?
59
- changed_attributes.include?(:#{name})
58
+ inject_in_layer :dirty_tracking do
59
+ define_method("#{name}_change") do
60
+ if old_attributes_values.has_key?(name)
61
+ result = [old_attributes_values[name], read_attribute(name)]
62
+ result = nil if result.first == result.last
63
+ result
64
+ end
60
65
  end
61
66
 
62
- def #{name}_change
63
- [changed_attributes[:#{name}], #{name}] if #{name}_changed?
67
+ define_method("#{name}_changed?") do
68
+ !!__send__("#{name}_change")
64
69
  end
65
70
 
66
- def #{name}_was
67
- #{name}_changed? ? changed_attributes[:#{name}] : #{name}
71
+ define_method("#{name}_was") do
72
+ old_attributes_values.has_key?(name) ?
73
+ old_attributes_values[name] : read_attribute(name)
68
74
  end
69
75
 
70
- def #{name}=(value)
71
- attribute_will_change!(:#{name}, value)
72
- super
76
+ define_method("#{name}") do
77
+ super().tap do |value|
78
+ # This take care of string/arrays/hashes that could change without going
79
+ # through the setter.
80
+ attribute_may_change(name, value) if value.respond_to?(:size)
81
+ end
73
82
  end
74
- RUBY
83
+
84
+ define_method("#{name}=") do |value|
85
+ attribute_may_change(name, read_attribute(name))
86
+ super(value)
87
+ end
88
+ end
75
89
  end
76
90
 
77
91
  def remove_field(name)
@@ -81,6 +95,7 @@ module NoBrainer::Document::Dirty
81
95
  undef #{name}_changed?
82
96
  undef #{name}_change
83
97
  undef #{name}_was
98
+ undef #{name}
84
99
  undef #{name}=
85
100
  RUBY
86
101
  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}") ? super : @attributes[name.to_s]
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
- attribute_will_change!(name, value)
10
- self.respond_to?("#{name}=") ? super : @attributes[name.to_s] = value
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
@@ -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
- # TODO after_initialize, after_find callback
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,44 @@ module NoBrainer::Document::Persistance
25
24
  end
26
25
 
27
26
  def reload(options={})
28
- unless options[:keep_ivars]
29
- id = self.id
30
- instance_variables.each { |ivar| remove_instance_variable(ivar) }
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
- run_callbacks :create do
40
- if options[:validate] && !valid?
41
- false
42
- else
43
- keys = self.class.insert_all(attributes)
44
- self.id ||= keys.first
45
- @new_record = false
46
- true
47
- end
34
+ if options[:validate] && !valid?
35
+ false
36
+ else
37
+ keys = self.class.insert_all(attributes)
38
+ self.id ||= keys.first
39
+ @new_record = false
40
+ true
48
41
  end
49
42
  end
50
43
 
51
44
  def update(options={}, &block)
52
- run_callbacks :update do
53
- if options[:validate] && !valid?
54
- false
55
- else
56
- selector.update_all(&block)
57
- true
58
- end
45
+ if options[:validate] && !valid?
46
+ false
47
+ else
48
+ selector.update_all(&block)
49
+ true
59
50
  end
60
51
  end
61
52
 
62
53
  def replace(options={}, &block)
63
- run_callbacks :update do
64
- if options[:validate] && !valid?
65
- false
66
- else
67
- selector.replace_all(&block)
68
- true
69
- end
54
+ if options[:validate] && !valid?
55
+ false
56
+ else
57
+ selector.replace_all(&block)
58
+ true
70
59
  end
71
60
  end
72
61
 
73
62
  def save(options={})
74
63
  options = options.reverse_merge(:validate => true)
75
- run_callbacks :save do
76
- new_record? ? _create(options) : replace(options) { attributes }
77
- end
64
+ new_record? ? _create(options) : replace(options) { attributes }
78
65
  end
79
66
 
80
67
  def save!(*args)
@@ -95,12 +82,12 @@ module NoBrainer::Document::Persistance
95
82
  selector.delete_all
96
83
  @destroyed = true
97
84
  end
98
- # TODO freeze attributes
85
+ @_attributes.freeze
99
86
  true
100
87
  end
101
88
 
102
89
  def destroy
103
- run_callbacks(:destroy) { delete }
90
+ delete
104
91
  end
105
92
 
106
93
  module ClassMethods
@@ -2,15 +2,18 @@ module NoBrainer::Document::Timestamps
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
+ class_attribute :timestamps_disabled
6
+
5
7
  self.field :created_at, :type => Time
6
8
  self.field :updated_at, :type => Time
7
9
 
8
- before_create { self.created_at = Time.now if self.respond_to?(:created_at=) }
9
- before_save { self.updated_at = Time.now if self.respond_to?(:updated_at=) }
10
+ before_create { self.created_at = Time.now unless self.timestamps_disabled }
11
+ before_save { self.updated_at = Time.now unless self.timestamps_disabled }
10
12
  end
11
13
 
12
14
  module ClassMethods
13
15
  def disable_timestamps
16
+ self.timestamps_disabled = true
14
17
  self.remove_field :created_at
15
18
  self.remove_field :updated_at
16
19
  end
@@ -1,12 +1,6 @@
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
12
6
 
@@ -63,19 +57,30 @@ module NoBrainer::Document::Types
63
57
  else raise InvalidType
64
58
  end
65
59
  end
66
- end
67
60
 
68
- def self.cast(value, type, cast_method)
69
- return value if value.nil? || type.nil? || value.is_a?(type)
70
- cast_method.call(value)
61
+ def lookup(type)
62
+ CastingRules.method(type.to_s)
63
+ rescue NameError
64
+ proc { raise InvalidType }
65
+ end
66
+
67
+ def cast(value, type, cast_method)
68
+ return value if value.nil? || type.nil? || value.is_a?(type)
69
+ cast_method.call(value)
70
+ end
71
71
  end
72
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 }
73
+ included do
74
+ # We namespace our fake Boolean class to avoid polluting the global namespace
75
+ class_exec do
76
+ class Boolean
77
+ def initialize; raise; end
78
+ def self.inspect; 'Boolean'; end
79
+ def self.to_s; inspect; end
80
+ def self.name; inspect; end
81
+ end
82
+ end
83
+ before_validation :add_type_errors
79
84
  end
80
85
 
81
86
  class InvalidType < RuntimeError
@@ -102,12 +107,12 @@ module NoBrainer::Document::Types
102
107
  return unless options.has_key?(:type)
103
108
  name = name.to_sym
104
109
  type = options[:type]
105
- cast_method = NoBrainer::Document::Types.lookup_cast_method(type)
110
+ cast_method = NoBrainer::Document::Types::CastingRules.lookup(type)
106
111
 
107
112
  inject_in_layer :types do
108
113
  define_method("#{name}=") do |value|
109
114
  begin
110
- value = NoBrainer::Document::Types.cast(value, type, cast_method)
115
+ value = NoBrainer::Document::Types::CastingRules.cast(value, type, cast_method)
111
116
  @pending_type_errors.try(:delete, name)
112
117
  rescue NoBrainer::Document::Types::InvalidType => error
113
118
  error.type ||= type
@@ -116,11 +121,14 @@ module NoBrainer::Document::Types
116
121
  end
117
122
  super(value)
118
123
  end
124
+
125
+ define_method("#{name}?") { !!read_attribute(name) } if type == Boolean
119
126
  end
120
127
  end
121
128
 
122
129
  def remove_field(name)
123
130
  super
131
+ # TODO remove the name? if we added it. Low priority though.
124
132
  inject_in_layer :types, <<-RUBY, __FILE__, __LINE__ + 1
125
133
  undef #{name}=
126
134
  RUBY
@@ -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/nobrainer.rb CHANGED
@@ -4,7 +4,7 @@ end
4
4
 
5
5
  require 'active_support'
6
6
  %w(module/delegation module/attribute_accessors class/attribute object/blank object/inclusion
7
- object/duplicable object/try hash/keys hash/reverse_merge array/extract_options)
7
+ object/deep_dup object/try hash/keys hash/indifferent_access hash/reverse_merge array/extract_options)
8
8
  .each { |dep| require "active_support/core_ext/#{dep}" }
9
9
 
10
10
  module NoBrainer
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nobrainer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Viennot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-07 00:00:00.000000000 Z
11
+ date: 2014-01-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rethinkdb
@@ -66,36 +66,37 @@ extensions: []
66
66
  extra_rdoc_files: []
67
67
  files:
68
68
  - lib/no_brainer/document/id.rb
69
- - lib/no_brainer/document/association/belongs_to.rb
70
69
  - lib/no_brainer/document/association/has_many.rb
71
70
  - lib/no_brainer/document/association/has_one.rb
72
71
  - lib/no_brainer/document/association/has_one_through.rb
73
72
  - lib/no_brainer/document/association/has_many_through.rb
74
- - lib/no_brainer/document/association/eager_loader.rb
75
73
  - lib/no_brainer/document/association/core.rb
76
- - lib/no_brainer/document/dynamic_attributes.rb
77
- - lib/no_brainer/document/criteria.rb
74
+ - lib/no_brainer/document/association/belongs_to.rb
75
+ - lib/no_brainer/document/association/eager_loader.rb
78
76
  - lib/no_brainer/document/polymorphic.rb
79
77
  - lib/no_brainer/document/store_in.rb
80
- - lib/no_brainer/document/core.rb
81
78
  - lib/no_brainer/document/injection_layer.rb
82
79
  - lib/no_brainer/document/index.rb
83
80
  - lib/no_brainer/document/serialization.rb
84
81
  - lib/no_brainer/document/association.rb
85
- - lib/no_brainer/document/validation.rb
86
82
  - lib/no_brainer/document/timestamps.rb
83
+ - lib/no_brainer/document/core.rb
84
+ - lib/no_brainer/document/callbacks.rb
85
+ - lib/no_brainer/document/validation.rb
87
86
  - lib/no_brainer/document/dirty.rb
88
- - lib/no_brainer/document/persistance.rb
89
87
  - lib/no_brainer/document/attributes.rb
88
+ - lib/no_brainer/document/dynamic_attributes.rb
89
+ - lib/no_brainer/document/persistance.rb
90
+ - lib/no_brainer/document/criteria.rb
90
91
  - lib/no_brainer/document/types.rb
91
92
  - lib/no_brainer/query_runner/connection.rb
92
93
  - lib/no_brainer/query_runner/database_on_demand.rb
93
- - lib/no_brainer/query_runner/logger.rb
94
94
  - lib/no_brainer/query_runner/table_on_demand.rb
95
95
  - lib/no_brainer/query_runner/write_error.rb
96
96
  - lib/no_brainer/query_runner/driver.rb
97
97
  - lib/no_brainer/query_runner/missing_index.rb
98
98
  - lib/no_brainer/query_runner/run_options.rb
99
+ - lib/no_brainer/query_runner/logger.rb
99
100
  - lib/no_brainer/railtie/database.rake
100
101
  - lib/no_brainer/criteria/chainable/limit.rb
101
102
  - lib/no_brainer/criteria/chainable/order_by.rb
@@ -109,15 +110,13 @@ files:
109
110
  - lib/no_brainer/criteria/termination/enumerable.rb
110
111
  - lib/no_brainer/criteria/termination/update.rb
111
112
  - lib/no_brainer/criteria/termination/delete.rb
112
- - lib/no_brainer/criteria/termination/eager_loading.rb
113
113
  - lib/no_brainer/criteria/termination/cache.rb
114
+ - lib/no_brainer/criteria/termination/preload.rb
114
115
  - lib/no_brainer/decorated_symbol.rb
115
116
  - lib/no_brainer/index_manager.rb
116
117
  - lib/no_brainer/loader.rb
117
118
  - lib/no_brainer/locale/en.yml
118
- - lib/no_brainer/util.rb
119
119
  - lib/no_brainer/fork.rb
120
- - lib/no_brainer/criteria.rb
121
120
  - lib/no_brainer/connection.rb
122
121
  - lib/no_brainer/query_runner.rb
123
122
  - lib/no_brainer/error.rb
@@ -125,6 +124,8 @@ files:
125
124
  - lib/no_brainer/config.rb
126
125
  - lib/no_brainer/autoload.rb
127
126
  - lib/no_brainer/document.rb
127
+ - lib/no_brainer/util.rb
128
+ - lib/no_brainer/criteria.rb
128
129
  - lib/rails/generators/nobrainer.rb
129
130
  - lib/rails/generators/nobrainer/model/model_generator.rb
130
131
  - lib/rails/generators/nobrainer/model/templates/model.rb.tt
@@ -1,50 +0,0 @@
1
- module NoBrainer::Criteria::Termination::EagerLoading
2
- extend ActiveSupport::Concern
3
-
4
- included { attr_accessor :_includes }
5
-
6
- def initialize(options={})
7
- super
8
- self._includes = []
9
- end
10
-
11
- def includes(*values)
12
- chain(:keep_cache => true) { |criteria| criteria._includes = values }
13
- end
14
-
15
- def merge!(criteria, options={})
16
- super
17
- self._includes = self._includes + criteria._includes
18
-
19
- # XXX Not pretty hack
20
- if criteria._includes.present? && cached?
21
- perform_eager_loading(@cache)
22
- end
23
- end
24
-
25
- def each(options={}, &block)
26
- return super unless should_eager_load? && !options[:no_eager_loading] && block
27
-
28
- docs = []
29
- super(options.merge(:no_eager_loading => true)) { |doc| docs << doc }
30
- perform_eager_loading(docs)
31
- docs.each(&block)
32
- self
33
- end
34
-
35
- private
36
-
37
- def should_eager_load?
38
- self._includes.present? && !raw?
39
- end
40
-
41
- def get_one(criteria)
42
- super.tap { |doc| perform_eager_loading([doc]) }
43
- end
44
-
45
- def perform_eager_loading(docs)
46
- if should_eager_load? && docs.present?
47
- NoBrainer::Document::Association::EagerLoader.new.eager_load(docs, self._includes)
48
- end
49
- end
50
- end