couch_view 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'