langalex-couch_potato 0.1 → 0.1.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.
Files changed (38) hide show
  1. data/README.textile +340 -0
  2. data/VERSION.yml +4 -0
  3. data/lib/couch_potato/ordering.rb +5 -9
  4. data/lib/couch_potato/persistence.rb +32 -7
  5. data/lib/couch_potato/persistence/belongs_to_property.rb +17 -3
  6. data/lib/couch_potato/persistence/callbacks.rb +9 -0
  7. data/lib/couch_potato/persistence/custom_view.rb +41 -0
  8. data/lib/couch_potato/persistence/dirty_attributes.rb +19 -0
  9. data/lib/couch_potato/persistence/external_collection.rb +31 -2
  10. data/lib/couch_potato/persistence/external_has_many_property.rb +4 -0
  11. data/lib/couch_potato/persistence/finder.rb +21 -65
  12. data/lib/couch_potato/persistence/inline_has_many_property.rb +4 -0
  13. data/lib/couch_potato/persistence/json.rb +1 -1
  14. data/lib/couch_potato/persistence/simple_property.rb +29 -1
  15. data/lib/couch_potato/persistence/view_query.rb +81 -0
  16. data/lib/couch_potato/versioning.rb +1 -1
  17. data/spec/attributes_spec.rb +35 -15
  18. data/spec/belongs_to_spec.rb +18 -0
  19. data/spec/callbacks_spec.rb +31 -12
  20. data/spec/create_spec.rb +5 -0
  21. data/spec/custom_view_spec.rb +44 -0
  22. data/spec/destroy_spec.rb +4 -0
  23. data/spec/dirty_attributes_spec.rb +82 -0
  24. data/spec/find_spec.rb +11 -3
  25. data/spec/finder_spec.rb +10 -0
  26. data/spec/has_many_spec.rb +64 -1
  27. data/spec/ordering_spec.rb +1 -0
  28. data/spec/property_spec.rb +4 -0
  29. data/spec/reload_spec.rb +4 -0
  30. data/spec/spec_helper.rb +2 -1
  31. data/spec/unit/external_collection_spec.rb +84 -0
  32. data/spec/unit/finder_spec.rb +10 -0
  33. data/spec/unit/view_query_spec.rb +10 -0
  34. data/spec/update_spec.rb +6 -0
  35. data/spec/versioning_spec.rb +1 -0
  36. metadata +21 -48
  37. data/CREDITS +0 -3
  38. data/init.rb +0 -5
@@ -7,22 +7,36 @@ module CouchPotato
7
7
  self.name = name
8
8
  accessors = <<-ACCESSORS
9
9
  def #{name}
10
- @#{name} || @#{name}_id ? #{item_class_name}.find(@#{name}_id) : nil
10
+ return @#{name} if instance_variable_defined?(:@#{name})
11
+ @#{name} = @#{name}_id ? #{item_class_name}.find(@#{name}_id) : nil
11
12
  end
12
13
 
13
14
  def #{name}=(value)
14
15
  @#{name} = value
15
- @#{name}_id = value.id
16
+ if value.nil?
17
+ @#{name}_id = nil
18
+ else
19
+ @#{name}_id = value.id
20
+ end
21
+ end
22
+
23
+ def #{name}_id=(id)
24
+ remove_instance_variable(:@#{name}) if instance_variable_defined?(:@#{name})
25
+ @#{name}_id = id
16
26
  end
17
27
  ACCESSORS
18
28
  owner_clazz.class_eval accessors
19
- owner_clazz.send :attr_accessor, "#{name}_id"
29
+ owner_clazz.send :attr_reader, "#{name}_id"
20
30
  end
21
31
 
22
32
  def save(object)
23
33
 
24
34
  end
25
35
 
36
+ def dirty?(object)
37
+ false
38
+ end
39
+
26
40
  def destroy(object)
27
41
 
28
42
  end
@@ -5,6 +5,7 @@ module CouchPotato
5
5
  base.extend ClassMethods
6
6
 
7
7
  base.class_eval do
8
+ attr_accessor :skip_callbacks
8
9
  def self.callbacks
9
10
  @@callbacks ||= {}
10
11
  @@callbacks[self.name] ||= {:before_validation_on_create => [],
@@ -16,9 +17,17 @@ module CouchPotato
16
17
  end
17
18
  end
18
19
 
20
+ def save_without_callbacks
21
+ self.skip_callbacks = true
22
+ result = save
23
+ self.skip_callbacks = false
24
+ result
25
+ end
26
+
19
27
  private
20
28
 
21
29
  def run_callbacks(name)
30
+ return if skip_callbacks
22
31
  self.class.callbacks[name].each do |callback|
23
32
  self.send callback
24
33
  end
@@ -0,0 +1,41 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ module CustomView
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ def view(name, options)
12
+ (class << self; self; end).instance_eval do
13
+ if options[:properties]
14
+ define_method name do
15
+ ViewQuery.new(self.name.underscore, name, map_function(options[:key], options[:properties]), nil, {}, {}).query_view!['rows'].map{|doc| self.new(doc['value'].merge(:_id => doc['id']))}
16
+ end
17
+ else
18
+ define_method name do
19
+ ViewQuery.new(self.name.underscore, name, map_function(options[:key], options[:properties]), nil, {}, {:include_docs => true}).query_view!['rows'].map{|doc| self.new(doc['doc'])}
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def map_function(key, properties)
26
+ "function(doc) {
27
+ emit(doc.#{key}, #{properties_for_map(properties)});
28
+ }"
29
+ end
30
+
31
+ def properties_for_map(properties)
32
+ if properties.nil?
33
+ 'null'
34
+ else
35
+ '{' + properties.map{|p| "#{p}: doc.#{p}"}.join(', ') + '}'
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ module DirtyAttributes
4
+ def save
5
+ if dirty?
6
+ super
7
+ else
8
+ valid?
9
+ end
10
+ end
11
+
12
+ def dirty?
13
+ new_document? || self.class.properties.inject(false) do |res, property|
14
+ res || property.dirty?(self)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -13,6 +13,26 @@ module CouchPotato
13
13
  @owner_id_attribute_name = owner_id_attribute_name
14
14
  end
15
15
 
16
+ def all(options = {}, view_options = {})
17
+ if options.empty? && view_options.empty?
18
+ items
19
+ else
20
+ Finder.new.find @item_class, options.merge(@owner_id_attribute_name => @owner_id), view_options
21
+ end
22
+ end
23
+
24
+ def first(options = {}, view_options = {})
25
+ if options.empty? && view_options.empty?
26
+ items.first
27
+ else
28
+ Finder.new.find(@item_class, options.merge(@owner_id_attribute_name => @owner_id), view_options).first
29
+ end
30
+ end
31
+
32
+ def count(options = {}, view_options = {})
33
+ Finder.new.count @item_class, options.merge(@owner_id_attribute_name => @owner_id), view_options
34
+ end
35
+
16
36
  def build(attributes = {})
17
37
  item = @item_class.new(attributes)
18
38
  self.<< item
@@ -32,8 +52,17 @@ module CouchPotato
32
52
  item
33
53
  end
34
54
 
55
+ def dirty?
56
+ return unless @items
57
+ @original_item_ids != @items.map(&:_id) || @items.inject(false) {|res, item| res || item.dirty?}
58
+ end
59
+
35
60
  def items
36
- @items ||= Finder.new.find @item_class, @owner_id_attribute_name => owner_id
61
+ unless @items
62
+ @items = Finder.new.find @item_class, @owner_id_attribute_name => owner_id
63
+ @original_item_ids = @items.map(&:_id)
64
+ end
65
+ @items
37
66
  end
38
67
 
39
68
  def save
@@ -44,7 +73,7 @@ module CouchPotato
44
73
  end
45
74
 
46
75
  def destroy
47
- @items.each do |item|
76
+ items.each do |item|
48
77
  item.destroy
49
78
  end
50
79
  end
@@ -19,6 +19,10 @@ module CouchPotato
19
19
  owner_clazz.class_eval getter
20
20
  end
21
21
 
22
+ def dirty?(object)
23
+ object.send("#{name}").dirty?
24
+ end
25
+
22
26
  def save(object)
23
27
  object.send(name).owner_id = object._id
24
28
  object.send(name).each do |item|
@@ -8,97 +8,53 @@ module CouchPotato
8
8
  # value can also be a range which will do a range search with startkey/endkey
9
9
  # WARNING: calling this methods creates a new view in couchdb if it's not present already so don't overuse this
10
10
  def find(clazz, conditions = {}, view_options = {})
11
- params = view_parameters(clazz, conditions, view_options)
12
- to_instances clazz, query_view!(params)
11
+ to_instances clazz, ViewQuery.new(design_document(clazz), view(conditions), map_function(clazz, search_fields(conditions)), nil, conditions, view_options).query_view!
13
12
  end
14
13
 
15
14
  def count(clazz, conditions = {}, view_options = {})
16
- params = view_parameters(clazz, conditions, view_options)
17
- query_view!(params, '_count')['rows'].first.try(:[], 'value') || 0
15
+ ViewQuery.new(design_document(clazz), view(conditions) + '_count', map_function(clazz, search_fields(conditions)), count_reduce_function, conditions, view_options).query_view!['rows'].first.try(:[], 'value') || 0
18
16
  end
19
17
 
20
18
  private
21
19
 
22
- def query_view!(params, view_postfix = nil)
23
- begin
24
- query_view params, view_postfix
25
- rescue RestClient::ResourceNotFound => e
26
- create_view params
27
- query_view params, view_postfix
28
- end
29
- end
30
-
31
20
  def db(name = nil)
32
21
  ::CouchPotato::Persistence.Db(name)
33
22
  end
34
23
 
35
- def create_view(params)
36
- # in couchdb 0.9 we could use only 1 view and pass reduce=false for find and count with reduce
37
- design_doc = db.get "_design/#{params[:design_document]}" rescue nil
38
- db.save({
39
- "_id" => "_design/#{params[:design_document]}",
40
- :views => {
41
- params[:view] => {
42
- :map => map_function(params)
43
- },
44
- params[:view] + '_count' => {
45
- :map => map_function(params),
46
- :reduce => "function(keys, values) {
47
- return values.length;
48
- }"
49
- }
50
- }
51
- }.merge(design_doc ? {'_rev' => design_doc['_rev']} : {}))
24
+ def design_document(clazz)
25
+ clazz.name.underscore
52
26
  end
53
27
 
54
- def map_function(params)
28
+ def map_function(clazz, search_fields)
55
29
  "function(doc) {
56
- if(doc.ruby_class == '#{params[:class]}') {
30
+ if(doc.ruby_class == '#{clazz}') {
57
31
  emit(
58
- [#{params[:search_fields].map{|attr| "doc[\"#{attr}\"]"}.join(', ')}], doc
32
+ [#{search_fields.map{|attr| "doc[\"#{attr}\"]"}.join(', ')}], doc
59
33
  );
60
34
  }
61
35
  }"
62
36
  end
63
37
 
64
- def to_instances(clazz, query_result)
65
- query_result['rows'].map{|doc| doc['value']}.map{|json| clazz.json_create json}
66
- end
67
-
68
- def query_view(params, view_postfix)
69
- db.view params[:view_url] + view_postfix.to_s, search_keys(params)
38
+ def count_reduce_function
39
+ "function(keys, values, combine) {
40
+ if (combine) {
41
+ return sum(values);
42
+ } else {
43
+ return values.length;
44
+ }
45
+ }"
70
46
  end
71
47
 
72
- def search_keys(params)
73
- if params[:search_values].select{|v| v.is_a?(Range)}.any?
74
- {:startkey => params[:search_values].map{|v| v.is_a?(Range) ? v.first : v}, :endkey => params[:search_values].map{|v| v.is_a?(Range) ? v.last : v}}.merge(params[:view_options])
75
- elsif params[:search_values].select{|v| v.is_a?(Array)}.any?
76
- {:keys => prepare_multi_key_search(params[:search_values])}.merge(params[:view_options])
77
- else
78
- {:key => params[:search_values]}.merge(params[:view_options])
79
- end
48
+ def to_instances(clazz, query_result)
49
+ query_result['rows'].map{|doc| doc['value']}.map{|json| clazz.json_create json}
80
50
  end
81
51
 
82
- def prepare_multi_key_search(values)
83
- array = values.select{|v| v.is_a?(Array)}.first
84
- index = values.index array
85
- array.map do |item|
86
- copy = values.dup
87
- copy[index] = item
88
- copy
89
- end
52
+ def view(conditions)
53
+ "by_#{view_name(conditions)}"
90
54
  end
91
55
 
92
- def view_parameters(clazz, conditions, view_options)
93
- {
94
- :class => clazz,
95
- :design_document => clazz.name.underscore,
96
- :search_fields => conditions.to_a.sort_by{|f| f.first.to_s}.map(&:first),
97
- :search_values => conditions.to_a.sort_by{|f| f.first.to_s}.map(&:last),
98
- :view_options => view_options,
99
- :view => "by_#{view_name(conditions)}",
100
- :view_url => "#{clazz.name.underscore}/by_#{view_name(conditions)}"
101
- }
56
+ def search_fields(conditions)
57
+ conditions.to_a.sort_by{|f| f.first.to_s}.map(&:first)
102
58
  end
103
59
 
104
60
  def view_name(options)
@@ -19,6 +19,10 @@ class InlineHasManyProperty
19
19
  end
20
20
  end
21
21
 
22
+ def dirty?(object)
23
+ object.send("#{name}").dirty?
24
+ end
25
+
22
26
  def save(object)
23
27
 
24
28
  end
@@ -36,7 +36,7 @@ module CouchPotato
36
36
  instance._id = json['_id']
37
37
  instance._rev = json['_rev']
38
38
  properties.each do |property|
39
- property.build(instance, json)
39
+ property.build(instance, json) unless property.is_a?(ExternalHasManyProperty)
40
40
  end
41
41
  instance
42
42
  end
@@ -6,10 +6,34 @@ module CouchPotato
6
6
  def initialize(owner_clazz, name, options = {})
7
7
  self.name = name
8
8
  owner_clazz.class_eval do
9
- attr_accessor name
9
+ attr_reader name, "#{name}_was"
10
+
11
+ def initialize(attributes = {})
12
+ super attributes
13
+ attributes.each do |name, value|
14
+ self.instance_variable_set("@#{name}_was", value)
15
+ end if attributes
16
+ end
17
+
18
+ def self.json_create(json)
19
+ instance = super
20
+ instance.attributes.each do |name, value|
21
+ instance.instance_variable_set("@#{name}_was", value)
22
+ end
23
+ instance
24
+ end
25
+
26
+ define_method "#{name}=" do |value|
27
+ self.instance_variable_set("@#{name}", value)
28
+ end
29
+
10
30
  define_method "#{name}?" do
11
31
  !self.send(name).nil? && !self.send(name).try(:blank?)
12
32
  end
33
+
34
+ define_method "#{name}_changed?" do
35
+ self.send(name) != self.send("#{name}_was")
36
+ end
13
37
  end
14
38
  end
15
39
 
@@ -17,6 +41,10 @@ module CouchPotato
17
41
  object.send "#{name}=", json.stringify_keys[name.to_s]
18
42
  end
19
43
 
44
+ def dirty?(object)
45
+ object.send("#{name}_changed?")
46
+ end
47
+
20
48
  def save(object)
21
49
 
22
50
  end
@@ -0,0 +1,81 @@
1
+ module CouchPotato
2
+ module Persistence
3
+
4
+ class ViewQuery
5
+ def initialize(design_document_name, view_name, map_function, reduce_function = nil, conditions = {}, view_options = {})
6
+ @design_document_name = design_document_name
7
+ @view_name = view_name
8
+ @map_function = map_function
9
+ @reduce_function = reduce_function
10
+ @conditions = conditions
11
+ @view_options = view_options
12
+ end
13
+
14
+ def query_view!
15
+ begin
16
+ query_view
17
+ rescue RestClient::ResourceNotFound => e
18
+ create_view
19
+ query_view
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def create_view
26
+ # in couchdb 0.9 we could use only 1 view and pass reduce=false for find and count with reduce
27
+ design_doc = db.get "_design/#{@design_document_name}" rescue nil
28
+ design_doc ||= {'views' => {}, "_id" => "_design/#{@design_document_name}"}
29
+ design_doc['views'][@view_name.to_s] = {
30
+ 'map' => @map_function,
31
+ 'reduce' => @reduce_function
32
+ }
33
+ db.save(design_doc)
34
+ end
35
+
36
+ def db(name = nil)
37
+ ::CouchPotato::Persistence.Db(name)
38
+ end
39
+
40
+ def query_view
41
+ db.view view_url, search_keys
42
+ end
43
+
44
+ def view_url
45
+ "#{@design_document_name}/#{@view_name}"
46
+ end
47
+
48
+ def search_keys
49
+ if search_values.select{|v| v.is_a?(Range)}.any?
50
+ {:startkey => search_values.map{|v| v.is_a?(Range) ? v.first : v}, :endkey => search_values.map{|v| v.is_a?(Range) ? v.last : v}}.merge(view_options)
51
+ elsif search_values.select{|v| v.is_a?(Array)}.any?
52
+ {:keys => prepare_multi_key_search(search_values)}.merge(view_options)
53
+ else
54
+ view_options.merge(search_values.any? ? {:key => search_values} : {})
55
+ end
56
+ end
57
+
58
+ def search_values
59
+ conditions.to_a.sort_by{|f| f.first.to_s}.map(&:last)
60
+ end
61
+
62
+ def view_options
63
+ @view_options
64
+ end
65
+
66
+ def conditions
67
+ @conditions
68
+ end
69
+
70
+ def prepare_multi_key_search(values)
71
+ array = values.select{|v| v.is_a?(Array)}.first
72
+ index = values.index array
73
+ array.map do |item|
74
+ copy = values.dup
75
+ copy[index] = item
76
+ copy
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end