nobrainer 0.10.0 → 0.13.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +169 -0
  3. data/lib/no_brainer/autoload.rb +1 -1
  4. data/lib/no_brainer/criteria/after_find.rb +24 -0
  5. data/lib/no_brainer/criteria/{termination/cache.rb → cache.rb} +1 -1
  6. data/lib/no_brainer/criteria/{chainable/core.rb → core.rb} +2 -2
  7. data/lib/no_brainer/criteria/{termination/count.rb → count.rb} +1 -1
  8. data/lib/no_brainer/criteria/{termination/delete.rb → delete.rb} +2 -2
  9. data/lib/no_brainer/criteria/{termination/enumerable.rb → enumerable.rb} +1 -1
  10. data/lib/no_brainer/criteria/{termination/first.rb → first.rb} +1 -1
  11. data/lib/no_brainer/criteria/{chainable/limit.rb → limit.rb} +1 -1
  12. data/lib/no_brainer/criteria/{chainable/order_by.rb → order_by.rb} +1 -1
  13. data/lib/no_brainer/criteria/preload.rb +55 -0
  14. data/lib/no_brainer/criteria/raw.rb +25 -0
  15. data/lib/no_brainer/criteria/{chainable/scope.rb → scope.rb} +1 -1
  16. data/lib/no_brainer/criteria/update.rb +11 -0
  17. data/lib/no_brainer/criteria/{chainable/where.rb → where.rb} +61 -25
  18. data/lib/no_brainer/criteria.rb +3 -16
  19. data/lib/no_brainer/document/association/belongs_to.rb +10 -2
  20. data/lib/no_brainer/document/association/core.rb +2 -2
  21. data/lib/no_brainer/document/association/eager_loader.rb +7 -7
  22. data/lib/no_brainer/document/association/has_many.rb +1 -1
  23. data/lib/no_brainer/document/attributes.rb +30 -44
  24. data/lib/no_brainer/document/callbacks.rb +29 -0
  25. data/lib/no_brainer/document/core.rb +0 -2
  26. data/lib/no_brainer/document/criteria.rb +1 -2
  27. data/lib/no_brainer/document/dirty.rb +52 -52
  28. data/lib/no_brainer/document/dynamic_attributes.rb +11 -3
  29. data/lib/no_brainer/document/id.rb +1 -1
  30. data/lib/no_brainer/document/index.rb +7 -14
  31. data/lib/no_brainer/document/injection_layer.rb +2 -3
  32. data/lib/no_brainer/document/persistance.rb +29 -43
  33. data/lib/no_brainer/document/readonly.rb +19 -0
  34. data/lib/no_brainer/document/serialization.rb +0 -1
  35. data/lib/no_brainer/document/timestamps.rb +9 -9
  36. data/lib/no_brainer/document/types.rb +46 -41
  37. data/lib/no_brainer/document/validation.rb +6 -0
  38. data/lib/no_brainer/document.rb +4 -4
  39. data/lib/no_brainer/error.rb +37 -9
  40. data/lib/no_brainer/query_runner/connection.rb +36 -9
  41. data/lib/no_brainer/query_runner/missing_index.rb +1 -1
  42. data/lib/no_brainer/query_runner/run_options.rb +4 -2
  43. data/lib/no_brainer/railtie.rb +10 -0
  44. data/lib/nobrainer.rb +3 -7
  45. metadata +54 -44
  46. data/LICENSE.md +0 -7
  47. data/lib/no_brainer/criteria/chainable/raw.rb +0 -33
  48. data/lib/no_brainer/criteria/termination/eager_loading.rb +0 -50
  49. data/lib/no_brainer/criteria/termination/inc.rb +0 -14
  50. data/lib/no_brainer/criteria/termination/update.rb +0 -13
@@ -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, :required, :readonly]
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
+ 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) && !@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)
@@ -40,13 +39,13 @@ module NoBrainer::Document::Attributes
40
39
  end
41
40
  end
42
41
 
43
- def _assign_attributes(attrs, options={})
44
- attrs.each { |k,v| self.write_attribute(k,v) }
45
- end
46
-
47
42
  def assign_attributes(attrs, options={})
48
- @attributes.clear if options[:pristine]
49
- _assign_attributes(attrs, options)
43
+ @_attributes.clear if options[:pristine]
44
+ if options[:from_db]
45
+ @_attributes.merge!(attrs)
46
+ else
47
+ attrs.each { |k,v| self.write_attribute(k,v) }
48
+ end
50
49
  assign_defaults if options[:pristine]
51
50
  self
52
51
  end
@@ -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
@@ -72,46 +71,33 @@ module NoBrainer::Document::Attributes
72
71
  subclass.fields = self.fields.dup
73
72
  end
74
73
 
75
- def field(name, options={})
76
- name = name.to_sym
77
-
78
- options.assert_valid_keys(*VALID_FIELD_OPTIONS)
79
- if name.in?(RESERVED_FIELD_NAMES)
80
- raise "Cannot use a reserved field name: #{name}"
81
- end
82
-
83
- ([self] + descendants).each do |klass|
84
- klass.fields[name] ||= {}
85
- klass.fields[name].merge!(options)
86
- end
87
-
74
+ def _field(attr, options={})
88
75
  # Using a layer so the user can use super when overriding these methods
89
- inject_in_layer :attributes, <<-RUBY, __FILE__, __LINE__ + 1
90
- def #{name}=(value)
91
- @attributes['#{name}'] = value
92
- end
93
-
94
- def #{name}
95
- @attributes['#{name}']
96
- end
97
- RUBY
76
+ attr = attr.to_s
77
+ inject_in_layer :attributes do
78
+ define_method("#{attr}=") { |value| @_attributes[attr] = value }
79
+ define_method("#{attr}") { @_attributes[attr] }
80
+ end
98
81
  end
99
82
 
100
- def remove_field(name)
101
- name = name.to_sym
83
+ def field(attr, options={})
84
+ attr = attr.to_sym
85
+
86
+ options.assert_valid_keys(*VALID_FIELD_OPTIONS)
87
+ if attr.in?(RESERVED_FIELD_NAMES)
88
+ raise "Cannot use a reserved field attr: #{attr}"
89
+ end
102
90
 
103
91
  ([self] + descendants).each do |klass|
104
- klass.fields.delete(name)
92
+ klass.fields[attr] ||= {}
93
+ klass.fields[attr].deep_merge!(options)
105
94
  end
106
95
 
107
- inject_in_layer :attributes, <<-RUBY, __FILE__, __LINE__ + 1
108
- undef #{name}=
109
- undef #{name}
110
- RUBY
96
+ _field(attr, self.fields[attr])
111
97
  end
112
98
 
113
- def has_field?(name)
114
- !!fields[name.to_sym]
99
+ def has_field?(attr)
100
+ !!fields[attr.to_sym]
115
101
  end
116
102
  end
117
103
  end
@@ -0,0 +1,29 @@
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, &block)
11
+ run_callbacks(:initialize) { _initialize(*args); true }
12
+ end
13
+
14
+ def _create(*args, &block)
15
+ run_callbacks(:create) { super }
16
+ end
17
+
18
+ def _update_only_changed_attrs(*args, &block)
19
+ run_callbacks(:update) { super }
20
+ end
21
+
22
+ def save(*args, &block)
23
+ run_callbacks(:save) { super }
24
+ end
25
+
26
+ def destroy(*args, &block)
27
+ run_callbacks(:destroy) { super }
28
+ end
29
+ 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,10 +16,9 @@ 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
- :inc_all, :dec_all, # Inc
23
22
  :update_all, :replace_all, # Update
24
23
  :to => :all
25
24
 
@@ -1,88 +1,88 @@
1
1
  module NoBrainer::Document::Dirty
2
2
  extend ActiveSupport::Concern
3
+ # We need to save the changes as seen through read_attribute because
4
+ # the user sees attributes through the read_attribute getters.
5
+ # But we want to detect changes based on @_attributes to track
6
+ # things like undefined -> nil. Going through the getters will
7
+ # not give us that.
3
8
 
4
- # We are not using ActiveModel::Dirty because it's using
5
- # ActiveModel::AttributeMethods which gives pretty violent method_missing()
6
- # capabilities, such as giving a getter/setter method for any keys within the
7
- # attributes keys. We don't want that.
8
-
9
- included do
10
- attr_accessor :previous_changes
11
- after_save { clear_dirtiness }
9
+ def assign_attributes(attrs, options={})
10
+ clear_dirtiness if options[:pristine]
11
+ super
12
12
  end
13
13
 
14
- def changed_attributes
15
- @changed_attributes ||= {}
14
+ def _create(*args)
15
+ super.tap { clear_dirtiness }
16
16
  end
17
17
 
18
- def _assign_attributes(attrs, options={})
19
- super
20
- clear_dirtiness if options[:pristine]
18
+ def _update(*args)
19
+ super.tap { clear_dirtiness }
21
20
  end
22
21
 
23
22
  def clear_dirtiness
24
- self.previous_changes = changes
25
- self.changed_attributes.clear
23
+ @_old_attributes = {}.with_indifferent_access
24
+ @_old_attributes_keys = @_attributes.keys # to track undefined -> nil changes
26
25
  end
27
26
 
28
27
  def changed?
29
- changed_attributes.present?
28
+ changes.present?
30
29
  end
31
30
 
32
31
  def changed
33
- changed_attributes.keys
32
+ changes.keys
34
33
  end
35
34
 
36
35
  def changes
37
- Hash[changed_attributes.map { |k,v| [k, [v, read_attribute(k)]] }]
36
+ result = {}.with_indifferent_access
37
+ @_old_attributes.each do |attr, old_value|
38
+ current_value = read_attribute(attr)
39
+ if current_value != old_value || !@_old_attributes_keys.include?(attr)
40
+ result[attr] = [old_value, current_value]
41
+ end
42
+ end
43
+ result
38
44
  end
39
45
 
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
46
+ def attribute_may_change(attr, current_value)
47
+ unless @_old_attributes.has_key?(attr)
48
+ @_old_attributes[attr] = current_value.deep_dup
49
+ end
51
50
  end
52
51
 
53
52
  module ClassMethods
54
- def field(name, options={})
53
+ def _field(attr, options={})
55
54
  super
56
-
57
- inject_in_layer :dirty_tracking, <<-RUBY, __FILE__, __LINE__ + 1
58
- def #{name}_changed?
59
- changed_attributes.include?(:#{name})
55
+ attr = attr.to_s
56
+
57
+ inject_in_layer :dirty_tracking do
58
+ define_method("#{attr}_change") do
59
+ if @_old_attributes.has_key?(attr)
60
+ result = [@_old_attributes[attr], read_attribute(attr)]
61
+ result if result.first != result.last || !@_old_attributes_keys.include?(attr)
62
+ end
60
63
  end
61
64
 
62
- def #{name}_change
63
- [changed_attributes[:#{name}], #{name}] if #{name}_changed?
65
+ define_method("#{attr}_changed?") do
66
+ !!__send__("#{attr}_change")
64
67
  end
65
68
 
66
- def #{name}_was
67
- #{name}_changed? ? changed_attributes[:#{name}] : #{name}
69
+ define_method("#{attr}_was") do
70
+ @_old_attributes.has_key?(attr) ? @_old_attributes[attr] : read_attribute(attr)
68
71
  end
69
72
 
70
- def #{name}=(value)
71
- attribute_will_change!(:#{name}, value)
72
- super
73
+ define_method("#{attr}") do
74
+ super().tap do |value|
75
+ # This take care of string/arrays/hashes that could change without going
76
+ # through the setter.
77
+ attribute_may_change(attr, value) if value.respond_to?(:size)
78
+ end
73
79
  end
74
- RUBY
75
- end
76
80
 
77
- def remove_field(name)
78
- super
79
-
80
- inject_in_layer :dirty_tracking, <<-RUBY, __FILE__, __LINE__ + 1
81
- undef #{name}_changed?
82
- undef #{name}_change
83
- undef #{name}_was
84
- undef #{name}=
85
- RUBY
81
+ define_method("#{attr}=") do |value|
82
+ attribute_may_change(attr, read_attribute(attr))
83
+ super(value)
84
+ end
85
+ end
86
86
  end
87
87
  end
88
88
  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,7 +6,7 @@ module NoBrainer::Document::Id
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- self.field :id, :default => ->{ NoBrainer::Document::Id.generate }
9
+ self.field :id, :type => String, :default => ->{ NoBrainer::Document::Id.generate }, :readonly => true
10
10
  end
11
11
 
12
12
  def ==(other)
@@ -41,29 +41,22 @@ module NoBrainer::Document::Index
41
41
  !!indexes[name.to_sym]
42
42
  end
43
43
 
44
- def field(name, options={})
45
- name = name.to_sym
46
-
47
- if has_index?(name) && indexes[name][:kind] != :single
48
- raise "Cannot reuse index name #{name}"
44
+ def _field(attr, options={})
45
+ if has_index?(attr) && indexes[attr][:kind] != :single
46
+ raise "Cannot reuse index attr #{attr}"
49
47
  end
50
48
 
51
49
  super
52
50
 
53
51
  case options[:index]
54
52
  when nil then
55
- when Hash then index(name, options[:index])
56
- when Symbol then index(name, options[:index] => true)
57
- when true then index(name)
58
- when false then remove_index(name)
53
+ when Hash then index(attr, options[:index])
54
+ when Symbol then index(attr, options[:index] => true)
55
+ when true then index(attr)
56
+ when false then remove_index(attr)
59
57
  end
60
58
  end
61
59
 
62
- def remove_field(name)
63
- remove_index(name) if fields[name.to_sym][:index]
64
- super
65
- end
66
-
67
60
  def perform_create_index(index_name, options={})
68
61
  index_name = index_name.to_sym
69
62
  index_args = self.indexes[index_name]
@@ -2,10 +2,9 @@ module NoBrainer::Document::InjectionLayer
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  module ClassMethods
5
- def inject_in_layer(name, code=nil, file=nil, line=nil, &block)
5
+ def inject_in_layer(name, &block)
6
6
  mod = class_eval "module NoBrainerLayer; module #{name.to_s.camelize}; self; end; end"
7
- mod.module_eval(code, file, line) if code
8
- mod.module_exec(&block) if block
7
+ mod.module_exec(&block)
9
8
  include mod
10
9
  end
11
10
  end
@@ -6,10 +6,9 @@ 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={})
11
- super
9
+ def _initialize(attrs={}, options={})
12
10
  @new_record = !options[:from_db]
11
+ super
13
12
  end
14
13
 
15
14
  def new_record?
@@ -25,56 +24,43 @@ 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
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
- 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
59
- end
41
+ def _update(attrs)
42
+ selector.update_all(attrs)
60
43
  end
61
44
 
62
- 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
70
- end
45
+ def _update_only_changed_attrs(options={})
46
+ return false if options[:validate] && !valid?
47
+
48
+ # We won't be using the `changes` values, because they went through
49
+ # read_attribute(), and we want the raw values.
50
+ attrs = Hash[self.changed.map do |k|
51
+ attr = @_attributes[k]
52
+ # If we have a hash to save, we need to specify r.literal(),
53
+ # otherwise, the hash would just get merged with the existing one.
54
+ attr = RethinkDB::RQL.new.literal(attr) if attr.is_a?(Hash)
55
+ [k, attr]
56
+ end]
57
+ _update(attrs) if attrs.present?
58
+ true
71
59
  end
72
60
 
73
61
  def save(options={})
74
62
  options = options.reverse_merge(:validate => true)
75
- run_callbacks :save do
76
- new_record? ? _create(options) : replace(options) { attributes }
77
- end
63
+ new_record? ? _create(options) : _update_only_changed_attrs(options)
78
64
  end
79
65
 
80
66
  def save!(*args)
@@ -95,12 +81,12 @@ module NoBrainer::Document::Persistance
95
81
  selector.delete_all
96
82
  @destroyed = true
97
83
  end
98
- # TODO freeze attributes
84
+ @_attributes.freeze
99
85
  true
100
86
  end
101
87
 
102
88
  def destroy
103
- run_callbacks(:destroy) { delete }
89
+ delete
104
90
  end
105
91
 
106
92
  module ClassMethods
@@ -0,0 +1,19 @@
1
+ module NoBrainer::Document::Readonly
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def _field(attr, options={})
6
+ super
7
+ inject_in_layer :readonly do
8
+ if options[:readonly]
9
+ define_method("#{attr}=") do |value|
10
+ raise NoBrainer::Error::ReadonlyField.new("#{attr} is readonly") unless new_record?
11
+ super(value)
12
+ end
13
+ else
14
+ remove_method("#{attr}=") if method_defined?("#{attr}=")
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -3,7 +3,6 @@ module NoBrainer::Document::Serialization
3
3
 
4
4
  include ActiveModel::Serialization
5
5
  include ActiveModel::Serializers::JSON
6
- include ActiveModel::Serializers::Xml
7
6
 
8
7
  included { self.include_root_in_json = false }
9
8
  end
@@ -2,17 +2,17 @@ module NoBrainer::Document::Timestamps
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
- self.field :created_at, :type => Time
6
- self.field :updated_at, :type => Time
5
+ field :created_at, :type => Time
6
+ field :updated_at, :type => Time
7
+ end
7
8
 
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=) }
9
+ def _create(options={})
10
+ self.created_at = self.updated_at = Time.now
11
+ super
10
12
  end
11
13
 
12
- module ClassMethods
13
- def disable_timestamps
14
- self.remove_field :created_at
15
- self.remove_field :updated_at
16
- end
14
+ def _update(attrs)
15
+ self.updated_at = Time.now
16
+ super(attrs.merge('updated_at' => @_attributes['updated_at']))
17
17
  end
18
18
  end