couch_view 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.
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift './lib'
2
+
3
+ require 'couch_view'
4
+ require 'couchrest_model_config'
5
+ require 'cucumber/rspec/doubles'
6
+
7
+ CouchRest::Model::Config.edit do
8
+ database do
9
+ default "http://admin:password@localhost:5984/couch_view_test"
10
+ end
11
+ end
12
+
13
+ Before("@db") do
14
+ CouchRest::Model::Config.default_database.recreate!
15
+ end
@@ -0,0 +1,7 @@
1
+ Before do
2
+ Object.class_eval do
3
+ if defined? Article
4
+ remove_const "Article"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ Given /^an array of 3 elements:$/ do |code|
2
+ eval code
3
+ end
4
+
5
+ Then /^I should receive all of the subsets of my array:$/ do |code|
6
+ eval code
7
+ end
@@ -0,0 +1,11 @@
1
+ When /^I (?:create a |change the).*:$/ do |code|
2
+ eval code
3
+ end
4
+
5
+ When /^I add the Published and Visible conditions to it:$/ do |code|
6
+ eval code
7
+ end
8
+
9
+ When /^I give it a base name of .*:$/ do |code|
10
+ eval code
11
+ end
@@ -0,0 +1,23 @@
1
+ Given /^an Article model that maps ByLabel:$/ do |code|
2
+ eval code
3
+ end
4
+
5
+ Given /^several articles:$/ do |code|
6
+ eval code
7
+ end
8
+
9
+ When /^I set the CouchDB.*query option to.*:$/ do |code|
10
+ eval code
11
+ end
12
+
13
+ Then /^.* should raise an exception:$/ do |code|
14
+ eval code
15
+ end
16
+
17
+ Then /^.* should not raise an exception:$/ do |code|
18
+ eval code
19
+ end
20
+
21
+ Given /^there are .*articles:$/ do |code|
22
+ eval code
23
+ end
@@ -0,0 +1,19 @@
1
+ Given /^the .*$/ do |code|
2
+ eval code
3
+ end
4
+
5
+ When /^I instantiate .*$/ do |code|
6
+ @response = eval code
7
+ end
8
+
9
+ Then /^I should not be able to call .*$/ do |code|
10
+ eval code
11
+ end
12
+
13
+ Then /^I should receive a map proxy:$/ do |code|
14
+ eval code
15
+ end
16
+
17
+ Then /^I should receive (?:a CouchDB|the following).*$/ do |response|
18
+ @response.strip.gsub(/\n[\ ]*/, "").should == response.strip.gsub(/\n[\ ]*/, "")
19
+ end
@@ -0,0 +1,15 @@
1
+ Given /^an Article model with a view .*:$/ do |code|
2
+ eval code
3
+ end
4
+
5
+ When /^I (?:destructively )?limit the results to \d+:$/ do |code|
6
+ eval code
7
+ end
8
+
9
+ When /^I call the .*/ do |code|
10
+ eval code
11
+ end
12
+
13
+ Then /^@new_proxy should (?:not )?be a new object:$/ do |code|
14
+ eval code
15
+ end
@@ -0,0 +1,87 @@
1
+ When /^I mix .*:$/ do |code|
2
+ eval code
3
+ end
4
+
5
+ Then /^my model should respond to .*:$/ do |code|
6
+ eval code
7
+ end
8
+
9
+ When /^I create an Article:$/ do |code|
10
+ eval code
11
+ end
12
+
13
+ Then /^`.*` should return the article$/ do |code|
14
+ eval code
15
+ end
16
+
17
+ Then /^my proxy should map .*$/ do |code|
18
+ eval code
19
+ end
20
+
21
+ Then /^I should receive a.*proxy:$/ do |code|
22
+ eval code
23
+ end
24
+
25
+ When /^I create.*articles:$/ do |code|
26
+ eval code
27
+ end
28
+
29
+ Then /^`count_by_id!` should return .*:$/ do |code|
30
+ eval code
31
+ end
32
+
33
+ When /^I pass :label to the `map` class method:$/ do |code|
34
+ eval code
35
+ end
36
+
37
+ When /^I create some articles with labels:$/ do |code|
38
+ eval code
39
+ end
40
+
41
+ Then /^they should be indexed in my label map:$/ do |code|
42
+ eval code
43
+ end
44
+
45
+ When /^I add them as conditions to a map over my model's label property:$/ do |code|
46
+ eval code
47
+ end
48
+
49
+ When /^I create visible and published documents:$/ do |code|
50
+ eval code
51
+ end
52
+
53
+ Then /^I should be able to query them through my query proxy:$/ do |code|
54
+ eval code
55
+ end
56
+
57
+ Given /^a.*model:$/ do |code|
58
+ eval code
59
+ end
60
+
61
+ When /^I define a map over labels with a custom reduce that always returns .*:$/ do |code|
62
+ eval code
63
+ end
64
+
65
+ When /^I create two articles with the same label:$/ do |code|
66
+ eval code
67
+ end
68
+
69
+ Then /^`reduce_by_label` should return.*:$/ do |code|
70
+ eval code
71
+ end
72
+
73
+ When /^I call.*with.*:$/ do |code|
74
+ eval code
75
+ end
76
+
77
+ Then /^my model should not respond to .*:$/ do |code|
78
+ eval code
79
+ end
80
+
81
+ When /^I create two articles with labels:$/ do |code|
82
+ eval code
83
+ end
84
+
85
+ Then /^".*" should return.*:$/ do |code|
86
+ eval code
87
+ end
@@ -0,0 +1,9 @@
1
+ class Array
2
+ def all_combinations
3
+ (0..self.length).map do |i|
4
+ (combination i).to_a
5
+ end.inject([]) do |sum, value|
6
+ sum += value
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,89 @@
1
+ module CouchView
2
+ class Config
3
+ attr_reader :map_class, :model, :properties, :conditions
4
+
5
+ def initialize(model_class)
6
+ @model = model_class
7
+ @conditions = []
8
+ end
9
+
10
+ def map(*args, &block)
11
+ @map_class, @properties = extract_map_class_data args
12
+ self.instance_eval &block if block
13
+ end
14
+
15
+ def reduce(function=nil)
16
+ if function
17
+ @reduce = function
18
+ else
19
+ @reduce ||= "_count"
20
+ end
21
+ end
22
+
23
+ def conditions(*args)
24
+ if args.empty?
25
+ @conditions
26
+ else
27
+ @conditions += args
28
+ end
29
+ end
30
+
31
+ def view_names
32
+ views.keys.map &:to_s
33
+ end
34
+
35
+ def views
36
+ all_views = {}
37
+ all_views[base_view_name.to_sym] = {
38
+ :map => @map_class.new(@model, *@properties).map,
39
+ :reduce => reduce
40
+ }
41
+ all_views.merge! condition_views
42
+ all_views
43
+ end
44
+
45
+ def base_view_name(name=nil)
46
+ if name
47
+ @base_view_name = name
48
+ elsif @base_view_name
49
+ @base_view_name
50
+ elsif @properties.empty?
51
+ @base_view_name = @map_class.to_s.underscore
52
+ else
53
+ @base_view_name = "by_" + @properties.map(&:to_s).map(&:underscore).join("_and_")
54
+ end
55
+ end
56
+
57
+ private
58
+ def condition_views
59
+ all_condition_subsets = @conditions.all_combinations.reject &:empty?
60
+
61
+ all_condition_subsets.reject(&:empty?).inject({}) do |result, condition_combination|
62
+ condition_combination.sort! {|a,b| a.to_s <=> b.to_s}
63
+
64
+ view_name =
65
+ "#{base_view_name}_" +
66
+ condition_combination.map {|condition| condition.to_s.underscore}.join("_")
67
+
68
+ map_instance = @map_class.new @model, *@properties
69
+ condition_combination.map { |condition| map_instance.extend condition }
70
+
71
+ result[view_name.to_sym] = {
72
+ :map => map_instance.map,
73
+ :reduce => reduce
74
+ }
75
+
76
+ result
77
+ end
78
+ end
79
+
80
+ def extract_map_class_data(map)
81
+ if map.first.class == Symbol
82
+ properties = [map].flatten
83
+ return CouchView::Map::Property, properties
84
+ else
85
+ return map.first, []
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,61 @@
1
+ module CouchView
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def map(*args, &block)
6
+ couch_view do
7
+ map *args, &block
8
+ end
9
+ end
10
+
11
+ def couch_view(name=nil, &block)
12
+ view_config = CouchView::Config.new self
13
+ view_config.instance_eval &block
14
+ view_config.base_view_name name if name
15
+ view_config.views.each do |view_name, view|
16
+ view_by view_name, :map => view[:map], :reduce => view[:reduce]
17
+ end
18
+
19
+ base_view_name = view_config.base_view_name
20
+
21
+ instance_eval <<-METHODS
22
+ def map_#{base_view_name}!
23
+ generate_view_proxy_for("#{base_view_name}").get!
24
+ end
25
+
26
+ def map_#{base_view_name}
27
+ generate_view_proxy_for "#{base_view_name}"
28
+ end
29
+
30
+ def reduce_#{base_view_name}!
31
+ generate_view_proxy_for("#{base_view_name}").reduce(true).get!
32
+ end
33
+
34
+ def reduce_#{base_view_name}
35
+ generate_view_proxy_for("#{base_view_name}").reduce(true)
36
+ end
37
+ METHODS
38
+
39
+ if view_config.reduce == "_count"
40
+ instance_eval <<-METHODS
41
+ def count_#{base_view_name}!
42
+ generate_count_proxy_for("#{base_view_name}").get!
43
+ end
44
+
45
+ def count_#{base_view_name}
46
+ generate_count_proxy_for "#{base_view_name}"
47
+ end
48
+ METHODS
49
+ end
50
+ end
51
+
52
+ private
53
+ def generate_count_proxy_for(view)
54
+ CouchView::Count::Proxy.new self, "by_#{view}".to_sym
55
+ end
56
+
57
+ def generate_view_proxy_for(view)
58
+ CouchView::Proxy.new self, "by_#{view}".to_sym
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,27 @@
1
+ module CouchView
2
+ module Count
3
+ class Proxy < CouchView::Proxy
4
+ def each(&block)
5
+ raise "You can't call 'each' on a count proxy that doesn't set 'group' to 'true'." unless _options[:group]
6
+ results = self.get!['rows']
7
+ results.each do |row|
8
+ block.call row['key'], row['value']
9
+ end
10
+ end
11
+
12
+ def get!
13
+ if _options[:group]
14
+ super
15
+ else
16
+ result = super['rows'].first
17
+ result ? result['value'] : 0
18
+ end
19
+ end
20
+
21
+ private
22
+ def default_query_options
23
+ CouchView::QueryOptions.new self, :reduce => true
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ module CouchView
2
+ module Map
3
+ attr_accessor :model
4
+
5
+ def initialize(model=nil, *properties)
6
+ @model = model
7
+ @properties = properties.empty? ? ["_id"] : properties
8
+ end
9
+
10
+ def map
11
+ if conditions != "true"
12
+ "
13
+ function(doc){
14
+ if (#{conditions})
15
+ emit(#{key}, null)
16
+ }
17
+ "
18
+ else
19
+ "
20
+ function(doc){
21
+ emit(#{key}, null)
22
+ }
23
+ "
24
+ end
25
+ end
26
+
27
+ def conditions
28
+ if @model
29
+ "doc['couchrest-type'] == '#{@model}'"
30
+ else
31
+ "true"
32
+ end
33
+ end
34
+
35
+ private
36
+ def key
37
+ properties = @properties.map {|p| "doc.#{p}"}
38
+ if properties.length == 1
39
+ properties
40
+ else
41
+ "[#{properties.join ", "}]"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ module CouchView
2
+ module Map
3
+ class Property
4
+ include CouchView::Map
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,70 @@
1
+ module CouchView
2
+ class Proxy
3
+ extend Forwardable
4
+
5
+ attr_reader :_model, :_map
6
+ attr_accessor :_query_options
7
+
8
+ # Supported CouchDB Query Options
9
+ def_delegators :@_query_options,
10
+ :limit, :limit!,
11
+ :skip, :skip!,
12
+ :startkey, :startkey!,
13
+ :endkey, :endkey!,
14
+ :startkey_docid, :startkey_docid!,
15
+ :endkey_docid, :endkey_docid!,
16
+ :stale, :stale!,
17
+ :descending, :descending!,
18
+ :group, :group!,
19
+ :group_level, :group_level!,
20
+ :reduce, :reduce!,
21
+ :include_docs, :include_docs!,
22
+ :update_seq, :update_seq!
23
+
24
+ def initialize(model, map)
25
+ @_model = model
26
+ @_map = map
27
+ @_conditions = []
28
+ @_query_options = default_query_options
29
+ end
30
+
31
+ def _options
32
+ @_query_options.to_hash
33
+ end
34
+
35
+ def _map
36
+ map = [@_map.to_s, @_conditions.sort.join("_")]
37
+ map.reject! &:blank?
38
+ map.join("_").to_sym
39
+ end
40
+
41
+ def method_missing(condition, *args, &block)
42
+ condition = remove_exclamation(condition)
43
+ @_conditions << condition
44
+ self
45
+ end
46
+
47
+ def each(&block)
48
+ _model.send(_map, _options).each &block
49
+ end
50
+
51
+ def get!
52
+ _model.send _map, _options
53
+ end
54
+
55
+ private
56
+ def default_query_options
57
+ CouchView::QueryOptions.new self, :reduce => false
58
+ end
59
+
60
+ def remove_exclamation(condition)
61
+ condition = condition.to_s
62
+
63
+ if condition[-1..-1] == "!"
64
+ condition[0...-1]
65
+ else
66
+ condition
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,43 @@
1
+ module CouchView
2
+ class QueryOptions
3
+ attr_reader :options
4
+
5
+ def initialize(view_proxy, options={})
6
+ @view_proxy = view_proxy
7
+ @options = options
8
+ end
9
+
10
+ def method_missing(option_name, *args, &block)
11
+ set_query_option option_name.to_s, args.first
12
+ end
13
+
14
+ def to_hash
15
+ @options
16
+ end
17
+
18
+ private
19
+ def ends_in_exclamation?(option_name="")
20
+ option_name.to_s[-1..-1] == "!"
21
+ end
22
+
23
+ def set_query_option(option_name, option_value)
24
+ if ends_in_exclamation? option_name
25
+ destructively_update_option option_name, option_value
26
+ else
27
+ update_option_and_return_new_proxy option_name, option_value
28
+ end
29
+ end
30
+
31
+ def destructively_update_option(option_name, option_value)
32
+ @options[option_name[0...-1].to_sym] = option_value
33
+ @view_proxy
34
+ end
35
+
36
+ def update_option_and_return_new_proxy(option_name, option_value)
37
+ new_proxy = @view_proxy.dup
38
+ new_options = @options.dup.merge option_name.to_sym => option_value
39
+ new_proxy._query_options = CouchView::QueryOptions.new new_proxy, new_options
40
+ new_proxy
41
+ end
42
+ end
43
+ end
data/lib/couch_view.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'forwardable'
2
+ require 'couchrest_model'
3
+ require 'couch_view/map'
4
+ require 'couch_view/config'
5
+ require 'couch_view/couch_view'
6
+ require 'couch_view/proxy'
7
+ require 'couch_view/map_property'
8
+ require 'couch_view/count_proxy'
9
+ require 'couch_view/query_options'
10
+ require 'couch_view/array'