couch_view 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/features/couch_view.array.feature +28 -0
- data/features/couch_view.config.feature +396 -0
- data/features/couch_view.count.proxy.feature +284 -0
- data/features/couch_view.feature +359 -0
- data/features/couch_view.map.feature +117 -0
- data/features/couch_view.proxy.feature +236 -0
- data/features/setup/env.rb +15 -0
- data/features/setup/hooks.rb +7 -0
- data/features/step_definitions/couch_view.array.rb +7 -0
- data/features/step_definitions/couch_view.config.rb +11 -0
- data/features/step_definitions/couch_view.count.proxy.rb +23 -0
- data/features/step_definitions/couch_view.map.rb +19 -0
- data/features/step_definitions/couch_view.proxy.rb +15 -0
- data/features/step_definitions/couch_view.rb +87 -0
- data/lib/couch_view/array.rb +9 -0
- data/lib/couch_view/config.rb +89 -0
- data/lib/couch_view/couch_view.rb +61 -0
- data/lib/couch_view/count_proxy.rb +27 -0
- data/lib/couch_view/map.rb +45 -0
- data/lib/couch_view/map_property.rb +7 -0
- data/lib/couch_view/proxy.rb +70 -0
- data/lib/couch_view/query_options.rb +43 -0
- data/lib/couch_view.rb +10 -0
- data/readme.markdown +307 -0
- metadata +174 -0
@@ -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,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,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,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'
|