couch_surfer 0.3.2
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.
- data/CHANGELOG.md +27 -0
- data/LICENSE +176 -0
- data/README.md +168 -0
- data/Rakefile +56 -0
- data/lib/couch_surfer.rb +18 -0
- data/lib/couch_surfer/associations.rb +83 -0
- data/lib/couch_surfer/attachments.rb +27 -0
- data/lib/couch_surfer/model.rb +687 -0
- data/lib/couch_surfer/query.rb +34 -0
- data/lib/couch_surfer/validations.rb +49 -0
- data/lib/couchrest/extensions.rb +51 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/lib/associations_spec.rb +187 -0
- data/spec/lib/attachments_spec.rb +52 -0
- data/spec/lib/extensions_spec.rb +29 -0
- data/spec/lib/model_spec.rb +932 -0
- data/spec/lib/query_spec.rb +57 -0
- data/spec/lib/validations_spec.rb +93 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +29 -0
- metadata +118 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module CouchSurfer
|
|
2
|
+
module Query
|
|
3
|
+
module ClassMethods
|
|
4
|
+
def query_processor(name)
|
|
5
|
+
self.external = name
|
|
6
|
+
end
|
|
7
|
+
def query(view_name, query_string, query_options = {})
|
|
8
|
+
payload = {
|
|
9
|
+
:design => self.send(:design_doc_slug),
|
|
10
|
+
:view => set_view_options(view_name, query_options),
|
|
11
|
+
:external => set_external_options(query_string),
|
|
12
|
+
}
|
|
13
|
+
result = CouchRest.post "http://#{database}/_mix", payload
|
|
14
|
+
result['rows'].collect{|r|new(r['doc'])} if result['rows']
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def set_view_options(view_name, query_options)
|
|
20
|
+
{:name => view_name, :query => {:include_docs => true}.merge(query_options)}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def set_external_options(query_string)
|
|
24
|
+
{:name => external, :query => {:q => query_string}, :include_docs => true}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
def self.included(receiver)
|
|
28
|
+
receiver.extend ClassMethods
|
|
29
|
+
receiver.class_eval do
|
|
30
|
+
class_inheritable_accessor :external
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'validatable'
|
|
2
|
+
|
|
3
|
+
module CouchSurfer
|
|
4
|
+
module Validations
|
|
5
|
+
module ClassMethods
|
|
6
|
+
def validates_uniqueness_of *args
|
|
7
|
+
# add view, validation and before callbacks
|
|
8
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
9
|
+
field = args.first
|
|
10
|
+
class_eval do
|
|
11
|
+
#view_by *args
|
|
12
|
+
validates_true_for args.first, :logic => lambda { is_unique?(field, options) }, :message => options[:message] || "is taken"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module InstanceMethods
|
|
18
|
+
def is_unique?(field, options)
|
|
19
|
+
if options[:view]
|
|
20
|
+
view_name = options[:view]
|
|
21
|
+
query = options[:query].is_a?(Proc) ? self.instance_eval(&options[:query]) : nil
|
|
22
|
+
end
|
|
23
|
+
view_name ||= "by_#{field}"
|
|
24
|
+
query ||= {:key => self.send(field)}
|
|
25
|
+
result = self.class.send(view_name, query)
|
|
26
|
+
if result.blank?
|
|
27
|
+
return true
|
|
28
|
+
else
|
|
29
|
+
return !id.blank? && (id == result.first.id)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_instance
|
|
34
|
+
throw(:halt, false) unless valid?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.included(receiver)
|
|
39
|
+
receiver.extend ClassMethods
|
|
40
|
+
receiver.send :include, Validatable
|
|
41
|
+
receiver.send :include, InstanceMethods
|
|
42
|
+
receiver.class_eval do
|
|
43
|
+
[:save].each do |method|
|
|
44
|
+
before method, :validate_instance
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module CouchRest
|
|
2
|
+
class Database
|
|
3
|
+
# Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
|
|
4
|
+
# paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
|
|
5
|
+
#/db/_design/examples/_list/index-posts/posts-by-date
|
|
6
|
+
|
|
7
|
+
def list(list_name, view_name, doc_name, params = {})
|
|
8
|
+
url = CouchRest.paramify_url "#{@root}/_design/#{doc_name}/_list/#{list_name}/#{view_name}", params
|
|
9
|
+
response = RestClient.get(url)
|
|
10
|
+
JSON.parse(response)
|
|
11
|
+
rescue
|
|
12
|
+
response
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Design
|
|
17
|
+
# Dispatches to any named view.
|
|
18
|
+
# (using the database where this design doc was saved)
|
|
19
|
+
def list(list_name, view_name, query = {})
|
|
20
|
+
database.list(list_name, view_name.to_s, name, query)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Dispatches to any named view.
|
|
24
|
+
# (using the database where this design doc was saved)
|
|
25
|
+
def view view_name, query={}, &block
|
|
26
|
+
view_on database, view_name, query, &block
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Dispatches to any named view in a specific database
|
|
30
|
+
def view_on db, view_name, query={}, &block
|
|
31
|
+
view_name = view_name.to_s
|
|
32
|
+
view_slug = "#{name}/#{view_name}"
|
|
33
|
+
defaults = (self['views'][view_name] && self['views'][view_name]["couchrest-defaults"]) || {}
|
|
34
|
+
db.view(view_slug, defaults.merge(query), &block)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# returns stored defaults if the there is a view named this in the design doc
|
|
40
|
+
def has_view?(view)
|
|
41
|
+
view = view.to_s
|
|
42
|
+
self['views'][view] &&
|
|
43
|
+
(self['views'][view]["couchrest-defaults"]||{})
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def fetch_view view_name, opts, &block
|
|
47
|
+
database.view(view_name, opts, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
|
2
|
+
|
|
3
|
+
class Account
|
|
4
|
+
include CouchSurfer::Model
|
|
5
|
+
include CouchSurfer::Associations
|
|
6
|
+
|
|
7
|
+
key_accessor :name
|
|
8
|
+
|
|
9
|
+
has_many :employees,
|
|
10
|
+
:view => :by_account_id_and_role,
|
|
11
|
+
:query => lambda{ {:startkey => [id, nil], :endkey => [id, {}]} }
|
|
12
|
+
|
|
13
|
+
has_many :programmers, :class_name => :employee,
|
|
14
|
+
:view => :by_account_id_and_role,
|
|
15
|
+
:query => lambda{ {:startkey => [id, "Programmer"], :endkey => [id, "Programmer"]} }
|
|
16
|
+
|
|
17
|
+
has_many :projects
|
|
18
|
+
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Project
|
|
22
|
+
include CouchSurfer::Model
|
|
23
|
+
include CouchSurfer::Associations
|
|
24
|
+
|
|
25
|
+
key_accessor :name, :account_id
|
|
26
|
+
|
|
27
|
+
belongs_to :account
|
|
28
|
+
has_many :members, :through => :memberships, :class_name => :employee
|
|
29
|
+
|
|
30
|
+
view_by :account_id
|
|
31
|
+
view_by :id
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Membership
|
|
35
|
+
include CouchSurfer::Model
|
|
36
|
+
include CouchSurfer::Associations
|
|
37
|
+
|
|
38
|
+
key_accessor :project_id, :employee_id
|
|
39
|
+
belongs_to :project
|
|
40
|
+
belongs_to :employee
|
|
41
|
+
|
|
42
|
+
view_by :project_id
|
|
43
|
+
view_by :employee_id
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Employee
|
|
48
|
+
include CouchSurfer::Model
|
|
49
|
+
include CouchSurfer::Associations
|
|
50
|
+
|
|
51
|
+
key_accessor :email, :account_id, :role
|
|
52
|
+
|
|
53
|
+
belongs_to :employer, :class_name => :account
|
|
54
|
+
has_many :shirts, :inline => true
|
|
55
|
+
has_many :projects, :through => :memberships
|
|
56
|
+
|
|
57
|
+
view_by :account_id, :role
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class Shirt
|
|
61
|
+
include CouchSurfer::Model
|
|
62
|
+
include CouchSurfer::Associations
|
|
63
|
+
|
|
64
|
+
key_accessor :color, :bought_on
|
|
65
|
+
cast :bought_on, :as => "Time"
|
|
66
|
+
|
|
67
|
+
def change_color(new_color)
|
|
68
|
+
self.color = new_color
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe CouchSurfer::Associations do
|
|
73
|
+
before(:all) do
|
|
74
|
+
db = CouchRest.database!('couch_surfer-test')
|
|
75
|
+
db.delete!
|
|
76
|
+
CouchSurfer::Model.default_database = CouchRest.database!('http://127.0.0.1:5984/couch_surfer-test')
|
|
77
|
+
@account = Account.create(:name => "My Account")
|
|
78
|
+
2.times do |i|
|
|
79
|
+
Employee.create(:email => "foo#{i+1}@bar.com", :account_id => @account.id, :role => "Programmer")
|
|
80
|
+
Employee.create(:email => "foo#{i+3}@bar.com", :account_id => @account.id, :role => "Engineer")
|
|
81
|
+
Project.create(:name => "Project No. #{i}", :account_id => @account.id)
|
|
82
|
+
end
|
|
83
|
+
@employee = @account.employees.first
|
|
84
|
+
@project = @account.projects.first
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe "belongs_to" do
|
|
88
|
+
it "should return it's parent" do
|
|
89
|
+
@project.account.should == @account
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "should take the class_name into consideration" do
|
|
93
|
+
@employee.employer.should == @account
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe "has_many" do
|
|
98
|
+
describe "vanilla" do
|
|
99
|
+
it "should return it's children" do
|
|
100
|
+
@account.employees.length.should == 4
|
|
101
|
+
@account.employees.map{|employee| employee.email}.sort.should == ["foo1@bar.com", "foo2@bar.com", "foo3@bar.com", "foo4@bar.com"]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe ":class_name" do
|
|
106
|
+
it "should return it's children" do
|
|
107
|
+
@account.programmers.length.should == 2
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
describe ":inline" do
|
|
111
|
+
before(:all) do
|
|
112
|
+
@employee.shirts.clear
|
|
113
|
+
@white_shirt = Shirt.new(:color => "White")
|
|
114
|
+
@pink_shirt = Shirt.new(:color => "Pink")
|
|
115
|
+
@employee.shirts << {:color => "Blue"}
|
|
116
|
+
@employee.shirts << @white_shirt
|
|
117
|
+
@employee.save
|
|
118
|
+
@employee = Employee.get(@employee.id)
|
|
119
|
+
@employee.shirts << @pink_shirt
|
|
120
|
+
@employee.save
|
|
121
|
+
end
|
|
122
|
+
it "should return it's children" do
|
|
123
|
+
employee = Employee.get(@employee.id)
|
|
124
|
+
employee.shirts.each do |shirt|
|
|
125
|
+
shirt.should be_kind_of(Shirt)
|
|
126
|
+
end
|
|
127
|
+
employee.shirts.map{|shirt| shirt.color}.should == %w(Blue White Pink)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "should be able to delete a child" do
|
|
131
|
+
employee = Employee.get(@employee.id)
|
|
132
|
+
employee.shirts.delete(@pink_shirt)
|
|
133
|
+
employee.save
|
|
134
|
+
employee = Employee.get(@employee.id)
|
|
135
|
+
employee.shirts.map{|shirt| shirt.color}.should == %w(Blue White)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "should have an append method" do
|
|
139
|
+
employee = Employee.get(@employee.id)
|
|
140
|
+
employee.shirts << {:color => "Black"}
|
|
141
|
+
employee.save
|
|
142
|
+
employee = Employee.get(@employee.id)
|
|
143
|
+
employee.shirts.map{|shirt| shirt.color}.should == %w(Blue White Black)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "should cast the childrens methods appropriately" do
|
|
147
|
+
employee = Employee.new(:email => "foofoo@bar.com", :account_id => @account.id, :role => "Programmer")
|
|
148
|
+
employee.save
|
|
149
|
+
employee.shirts << Shirt.new(:color => "Mauve", :bought_on => Time.now)
|
|
150
|
+
employee.shirts.last.bought_on.should be_an_instance_of(Time)
|
|
151
|
+
employee.shirts.last.color.should == "Mauve"
|
|
152
|
+
# Save ..
|
|
153
|
+
employee.save
|
|
154
|
+
employee.shirts.last.bought_on.should be_an_instance_of(Time)
|
|
155
|
+
employee.shirts.last.color.should == "Mauve"
|
|
156
|
+
# Reload ..
|
|
157
|
+
employee = Employee.get(employee.id)
|
|
158
|
+
employee.shirts.last.bought_on.should be_an_instance_of(Time)
|
|
159
|
+
employee.shirts.last.color.should == "Mauve"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
describe ":through" do
|
|
165
|
+
it "should return it's 'through' children" do
|
|
166
|
+
@employee.memberships.should be_empty
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "should return it's children" do
|
|
170
|
+
3.times do |i|
|
|
171
|
+
p = Project.create(:name => "Project with Invitations No. #{i}", :account_id => @account.id)
|
|
172
|
+
Membership.create(:employee_id => @employee.id, :project_id => p.id)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
7.times do |i|
|
|
176
|
+
e = Employee.create(:email => "bar#{i}@bar.com", :account_id => @account.id)
|
|
177
|
+
Membership.create(:employee_id => e.id, :project_id => @project.id)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
@employee.memberships.size.should == 3
|
|
181
|
+
@employee.projects.size.should == 3
|
|
182
|
+
@project.memberships.size.should == 7
|
|
183
|
+
@project.members.size.should == 7
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
|
2
|
+
|
|
3
|
+
class Person
|
|
4
|
+
include CouchSurfer::Model
|
|
5
|
+
include CouchSurfer::Attachments
|
|
6
|
+
|
|
7
|
+
key_accessor :name, :email
|
|
8
|
+
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
describe CouchSurfer::Attachments do
|
|
12
|
+
before(:all) do
|
|
13
|
+
db = CouchRest.database!('couch_surfer-test')
|
|
14
|
+
db.delete!
|
|
15
|
+
CouchSurfer::Model.default_database = CouchRest.database!('http://127.0.0.1:5984/couch_surfer-test')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
before do
|
|
19
|
+
@person = Person.create(:name => "John", :email => "john@mail.com")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "attaching a file" do
|
|
23
|
+
it "should return true" do
|
|
24
|
+
@person.put_attachment("couchdb.png", open_fixture("attachments/couchdb.png")).should be_true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "should raise an exception if the record is not yet saved" do
|
|
28
|
+
person = Person.new(:name => "John", :email => "john@mail.com")
|
|
29
|
+
lambda {person.put_attachment("couchdb.png", open_fixture("attachments/couchdb.png"))}.should raise_error(ArgumentError)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe "fetching a file" do
|
|
34
|
+
it "should return the file" do
|
|
35
|
+
@person.put_attachment("couchdb.png", open_fixture("attachments/couchdb.png")).should be_true
|
|
36
|
+
@person.fetch_attachment("couchdb.png").size.should >= 3000
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "should raise an exception if the record is not yet saved" do
|
|
40
|
+
person = Person.new(:name => "John", :email => "john@mail.com")
|
|
41
|
+
lambda {person.fetch_attachment("couchdb.png")}.should raise_error(ArgumentError)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe "deleting a file" do
|
|
46
|
+
it "should return true" do
|
|
47
|
+
@person.put_attachment("couchdb.png", open_fixture("attachments/couchdb.png")).should be_true
|
|
48
|
+
@person.delete_attachment("couchdb.png").should be_true
|
|
49
|
+
lambda {@person.fetch_attachment("couchdb.png")}.should raise_error(RestClient::ResourceNotFound)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
|
2
|
+
|
|
3
|
+
describe "Extensions" do
|
|
4
|
+
before(:all) do
|
|
5
|
+
@cr = CouchRest.new(COUCHHOST)
|
|
6
|
+
@db = @cr.database(TESTDB)
|
|
7
|
+
@db.delete! rescue nil
|
|
8
|
+
@db = @cr.create_db(TESTDB) rescue nil
|
|
9
|
+
@adb = @cr.database('couchrest-model-test')
|
|
10
|
+
@adb.delete! rescue nil
|
|
11
|
+
CouchSurfer::Model.default_database = CouchRest.database!('http://127.0.0.1:5984/couch_surfer-test')
|
|
12
|
+
@des = CouchRest::Design.new
|
|
13
|
+
@des.database = @db
|
|
14
|
+
@des.name = "MyDesignDoc"
|
|
15
|
+
@des.save
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe CouchRest::Database do
|
|
19
|
+
it "should have a list method" do
|
|
20
|
+
@db.should respond_to(:list)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe CouchRest::Document do
|
|
25
|
+
it "should have a list method" do
|
|
26
|
+
@des.should respond_to(:list)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|