loose_change 0.3.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.
- data/.gitignore +2 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +41 -0
- data/LICENSE +20 -0
- data/README.md +39 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/lib/loose_change.rb +23 -0
- data/lib/loose_change/attachments.rb +60 -0
- data/lib/loose_change/attributes.rb +79 -0
- data/lib/loose_change/base.rb +71 -0
- data/lib/loose_change/database.rb +52 -0
- data/lib/loose_change/errors.rb +7 -0
- data/lib/loose_change/helpers.rb +10 -0
- data/lib/loose_change/naming.rb +11 -0
- data/lib/loose_change/pagination.rb +69 -0
- data/lib/loose_change/persistence.rb +154 -0
- data/lib/loose_change/server.rb +11 -0
- data/lib/loose_change/views.rb +99 -0
- data/loose_change.gemspec +89 -0
- data/test/attachment_test.rb +22 -0
- data/test/attributes_test.rb +37 -0
- data/test/base_test.rb +22 -0
- data/test/callback_test.rb +35 -0
- data/test/inheritance_test.rb +36 -0
- data/test/pagination_test.rb +35 -0
- data/test/persistence_test.rb +126 -0
- data/test/resources/couchdb.png +0 -0
- data/test/test_helper.rb +12 -0
- data/test/view_test.rb +59 -0
- metadata +162 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
module LooseChange
|
2
|
+
|
3
|
+
module Pagination
|
4
|
+
|
5
|
+
# Similar to <tt>#view_by</tt>, but adds a paginated view
|
6
|
+
# compatible with <tt>will_paginate</tt>.
|
7
|
+
def paginated_view_by(*keys)
|
8
|
+
view_name = "paginated_by_#{ keys.join('_and_') }"
|
9
|
+
map_code = "function(doc) {
|
10
|
+
if ((doc['model_name'] == '#{ model_name }') && #{ keys.map {|k| existence_check k}.join('&&') }) {
|
11
|
+
emit(#{ key_for(keys) }, 1)
|
12
|
+
}
|
13
|
+
}
|
14
|
+
"
|
15
|
+
reduce_code = "function(keys, values) { return sum(values); }"
|
16
|
+
add_view(view_name, map_code, reduce_code)
|
17
|
+
view_by(*keys)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns a <tt>will_paginate</tt>-compatible set of documents.
|
21
|
+
# In +opts+, <tt>:per_page</tt> is required, and <tt>:page</tt>
|
22
|
+
# will be set to 1 unless otherwise specified. All other +opts+
|
23
|
+
# will be passed to CouchDB as in <tt>:view_by</tt>.
|
24
|
+
def paginated_by(view_name, opts = {})
|
25
|
+
raise "You must include a per_page parameter" if opts[:per_page].nil?
|
26
|
+
|
27
|
+
opts[:page] ||= 1
|
28
|
+
|
29
|
+
WillPaginate::Collection.create( opts[:page], opts[:per_page] ) do |pager|
|
30
|
+
total_result = view("paginated_by_#{ view_name }", :reduce => true)
|
31
|
+
pager.total_entries = total_result ? total_result.first : 0
|
32
|
+
|
33
|
+
results = if view_name == :all
|
34
|
+
view(view_name, :limit => opts[:per_page], :skip => ((opts[:page].to_i - 1) * opts[:per_page].to_i), :include_docs => true)
|
35
|
+
else
|
36
|
+
view("by_#{ view_name }", :key => opts[:key], :limit => opts[:per_page], :skip => ((opts[:page].to_i - 1) * opts[:per_page].to_i), :include_docs => true)
|
37
|
+
end
|
38
|
+
pager.replace( results )
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Short for <tt>paginated_by(:all)</tt>; see
|
43
|
+
# <tt>#paginated_by</tt> for +opts+.
|
44
|
+
def paginate(opts = {})
|
45
|
+
paginated_by(:all, opts)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def paginated_view_by_all
|
51
|
+
view_name = "paginated_by_all"
|
52
|
+
map_code = "function(doc) {
|
53
|
+
if (doc['model_name'] == '#{ model_name }') {
|
54
|
+
emit(null, 1);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
"
|
58
|
+
reduce_code = "function(keys, values) { return sum(values); }"
|
59
|
+
add_view(view_name, map_code, reduce_code)
|
60
|
+
end
|
61
|
+
|
62
|
+
def existence_check(key)
|
63
|
+
"(#{ doc_key(key) } != null)"
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module LooseChange
|
4
|
+
module Persistence
|
5
|
+
|
6
|
+
# Set the database source for this model, named +db+ on +server+
|
7
|
+
# (http://localhost:5984 by default). If the database does not
|
8
|
+
# exist, it will be added.
|
9
|
+
def use_database(db, server = "http://127.0.0.1:5984")
|
10
|
+
self.database = Database.new(db, Server.new(server))
|
11
|
+
Database.setup_design(self.database, self.model_name)
|
12
|
+
view_by_all
|
13
|
+
paginated_view_by_all
|
14
|
+
end
|
15
|
+
|
16
|
+
# Retrieve a document from CouchDB with id +id+ as a Loose Change instance.
|
17
|
+
def find(id)
|
18
|
+
begin
|
19
|
+
result = JSON.parse(RestClient.get(self.database.uri + "/#{ id }"), default_headers)
|
20
|
+
rescue RestClient::ResourceNotFound
|
21
|
+
raise RecordNotFound
|
22
|
+
end
|
23
|
+
raise RecordNotFound unless result['model_name'] == model_name
|
24
|
+
instantiate_from_hash(result)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Instantiate a new Loose Change record with attributes +args+ and
|
28
|
+
# immediately save to the database.
|
29
|
+
def create(args = {})
|
30
|
+
model = new(args)
|
31
|
+
model.save
|
32
|
+
model
|
33
|
+
end
|
34
|
+
|
35
|
+
# Instantiate a new Loose Change record with attributes +args+ and
|
36
|
+
# immediately save to the database. If the record is invalid, a
|
37
|
+
# <tt>RecordInvalid</tt> error will be thrown.
|
38
|
+
def create!(args = {})
|
39
|
+
new(args).save!
|
40
|
+
end
|
41
|
+
|
42
|
+
# Build a Loose Change record from a hash of attributes +hash+ as
|
43
|
+
# returned by CouchDB.
|
44
|
+
def instantiate_from_hash(hash)
|
45
|
+
model = new(hash.reject {|k, _| 'model_name' == k || '_attachments' == k})
|
46
|
+
model.id = hash['_id']
|
47
|
+
model.new_record = false
|
48
|
+
if hash['_attachments']
|
49
|
+
attachment_names = hash['_attachments'].map {|name, _| name}
|
50
|
+
model.attachments = attachment_names.inject({}) {|acc, name| acc[name.to_sym] = {:content_type => hash['_attachments'][name]['content_type']}; acc}
|
51
|
+
end
|
52
|
+
model
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
module PersistenceClassMethods
|
58
|
+
|
59
|
+
attr_accessor :new_record, :destroyed, :database, :id, :_rev, :_id
|
60
|
+
|
61
|
+
def new_record?() @new_record end
|
62
|
+
def destroyed?() @destroyed end
|
63
|
+
|
64
|
+
def persisted?
|
65
|
+
!(new_record? || destroyed?)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Persist the record to CouchDB, including saving of any attachments.
|
69
|
+
def save
|
70
|
+
if new_record?
|
71
|
+
_run_create_callbacks { _run_save_callbacks { _save } }
|
72
|
+
else
|
73
|
+
_run_save_callbacks { _save }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Persist the record to CouchDB, including saving of any
|
78
|
+
# attachments. If the record is not valid, a
|
79
|
+
# <tt>RecordInvalid</tt> error will be thrown.
|
80
|
+
def save!
|
81
|
+
if new_record?
|
82
|
+
_run_create_callbacks { _run_save_callbacks { _save! } }
|
83
|
+
else
|
84
|
+
_run_save_callbacks { _save! }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Destroy the record on CouchDB by sending an HTTP DELETE request.
|
89
|
+
def destroy
|
90
|
+
_run_destroy_callbacks do
|
91
|
+
raise DatabaseNotSet.new("Cannot destroy without database set.") unless @database
|
92
|
+
result = JSON.parse(RestClient.delete("#{ database.uri }/#{ CGI.escape(id) }?rev=#{ @_rev }", default_headers))['ok']
|
93
|
+
@destroyed = result
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def _save
|
100
|
+
raise DatabaseNotSet.new("Cannot save without database set.") unless @database
|
101
|
+
apply_defaults
|
102
|
+
return false unless valid?
|
103
|
+
new_record? ? post_record : put_record
|
104
|
+
put_attachments
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
def _save!
|
109
|
+
raise RecordInvalid, self.errors.map {|k, v| "#{k.capitalize} #{v}"}.join(', ') unless _save
|
110
|
+
self
|
111
|
+
end
|
112
|
+
|
113
|
+
def uri
|
114
|
+
"#{database.uri}/#{ CGI.escape(id) }"
|
115
|
+
end
|
116
|
+
|
117
|
+
def post_record
|
118
|
+
result = JSON.parse(RestClient.post(database.uri, self.to_json(:methods => [:model_name]), default_headers))
|
119
|
+
@id = @_id = result['id']
|
120
|
+
@_rev = result['rev']
|
121
|
+
@new_record = false
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
def put_record
|
126
|
+
result = JSON.parse(RestClient.put(uri, self.to_json(:methods => [:model_name, :_rev, :_id], :except => [:id]), default_headers))
|
127
|
+
@_rev = result['rev']
|
128
|
+
result
|
129
|
+
end
|
130
|
+
|
131
|
+
def put_attachments
|
132
|
+
(@attachments || {}).each { |name, attachment| put_attachment(name) if attachment[:dirty] }
|
133
|
+
end
|
134
|
+
|
135
|
+
def attachment_ivar(name)
|
136
|
+
instance_variable_get("@#{ name }")
|
137
|
+
end
|
138
|
+
|
139
|
+
def attachment_content_type(name)
|
140
|
+
instance_variable_get("@_#{ name }_content_type")
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
class RecordNotFound < Exception
|
146
|
+
end
|
147
|
+
|
148
|
+
class DatabaseNotSet < Exception
|
149
|
+
end
|
150
|
+
|
151
|
+
class RecordInvalid < Exception
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module LooseChange
|
4
|
+
module Views
|
5
|
+
|
6
|
+
# Invoke a view identified by +view_name+ on this Loose Change
|
7
|
+
# model's design document on CouchDB. Options specified in the
|
8
|
+
# +opts+ hash will be passed along to CouchDB; for options see
|
9
|
+
# http://wiki.apache.org/couchdb/Introduction_to_CouchDB_views
|
10
|
+
def view(view_name, opts = {})
|
11
|
+
opts[:key] = opts[:key] ? CGI.escape(opts[:key].to_json) : nil
|
12
|
+
param_string = opts.reject {|k,v| v.nil?}.map {|k,v| "#{k}=#{v}"}.join('&')
|
13
|
+
JSON.parse(RestClient.get("#{ self.database.uri }/_design/#{ CGI.escape(self.model_name) }/_view/#{ view_name }?#{ param_string }", default_headers))['rows'].map do |row|
|
14
|
+
opts[:include_docs] ? instantiate_from_hash(row['doc']) : row['value']
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Set up a view that will allow you to query CouchDB by +keys+. A
|
19
|
+
# view will be added to the design document on CouchDB, and a
|
20
|
+
# method to retrieve documents will be added to the Loose Change
|
21
|
+
# model class.
|
22
|
+
#
|
23
|
+
# class Recipe < LooseChange::Base
|
24
|
+
# property :name
|
25
|
+
# property :popularity
|
26
|
+
# view_by :name
|
27
|
+
# view_by :name, :popularity
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Recipe.by_name("lasagne")
|
31
|
+
# Recipe.by_name_and_popularity("lasagne", 4)
|
32
|
+
def view_by(*keys)
|
33
|
+
view_name = "by_#{ keys.join('_and_') }"
|
34
|
+
view_code = "function(doc) {
|
35
|
+
if ((doc['model_name'] == '#{ model_name }') && #{ keys.map {|k| existence_check k}.join('&&') }) {
|
36
|
+
emit(#{ key_for(keys) }, null)
|
37
|
+
}
|
38
|
+
}
|
39
|
+
"
|
40
|
+
add_view(view_name, view_code)
|
41
|
+
self.class.send(:define_method, view_name.to_sym) do |*keys|
|
42
|
+
keys = keys.first if keys.length == 1
|
43
|
+
view(view_name, :key => keys, :include_docs => true)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Retrieve all of this model's documents currently stored on CouchDB.
|
48
|
+
def all(opts = {})
|
49
|
+
view(:all, opts.merge!(:include_docs => true))
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add a view to the this model's design document on CouchDB. The
|
53
|
+
# view is identified by +name+, defined by +map+ and optionally
|
54
|
+
# +reduce+, which are composed of JavaScript functions. For more
|
55
|
+
# information on CouchDB views, see
|
56
|
+
# http://wiki.apache.org/couchdb/Introduction_to_CouchDB_views
|
57
|
+
def add_view(name, map, reduce = nil)
|
58
|
+
design_doc = JSON.parse(RestClient.get("#{ self.database.uri }/_design/#{ CGI.escape(self.model_name) }"))
|
59
|
+
current_views = design_doc['views'] || {}
|
60
|
+
JSON.parse(RestClient.put("#{ self.database.uri }/_design/#{ self.model_name }",
|
61
|
+
{ '_id' => design_doc['_id'],
|
62
|
+
'_rev' => design_doc['_rev'],
|
63
|
+
'language' => 'javascript',
|
64
|
+
'views' => current_views.merge({name => {'map' => map, 'reduce' => reduce}})}.to_json, default_headers))
|
65
|
+
end
|
66
|
+
|
67
|
+
#:nodoc:
|
68
|
+
def view_by_all
|
69
|
+
view_name = "all"
|
70
|
+
view_code = "function(doc) {
|
71
|
+
if (doc['model_name'] == '#{ model_name }') {
|
72
|
+
emit(null);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
"
|
76
|
+
add_view(view_name, view_code)
|
77
|
+
end
|
78
|
+
|
79
|
+
#:nodoc:
|
80
|
+
def existence_check(key)
|
81
|
+
"(#{ doc_key(key) } != null)"
|
82
|
+
end
|
83
|
+
|
84
|
+
#:nodoc:
|
85
|
+
def key_for(keys)
|
86
|
+
if keys.length == 1
|
87
|
+
doc_key(keys.first)
|
88
|
+
else
|
89
|
+
"[#{keys.map {|k| doc_key(k) }.join(',')}]"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
#:nodoc:
|
94
|
+
def doc_key(key)
|
95
|
+
"doc['#{ key }']"
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{loose_change}
|
8
|
+
s.version = "0.3.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Joshua Miller"]
|
12
|
+
s.date = %q{2010-11-24}
|
13
|
+
s.email = %q{josh@joshinharrisburg.com}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE",
|
16
|
+
"README.md"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"Gemfile",
|
21
|
+
"Gemfile.lock",
|
22
|
+
"LICENSE",
|
23
|
+
"README.md",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"lib/loose_change.rb",
|
27
|
+
"lib/loose_change/attachments.rb",
|
28
|
+
"lib/loose_change/attributes.rb",
|
29
|
+
"lib/loose_change/base.rb",
|
30
|
+
"lib/loose_change/database.rb",
|
31
|
+
"lib/loose_change/errors.rb",
|
32
|
+
"lib/loose_change/helpers.rb",
|
33
|
+
"lib/loose_change/naming.rb",
|
34
|
+
"lib/loose_change/pagination.rb",
|
35
|
+
"lib/loose_change/persistence.rb",
|
36
|
+
"lib/loose_change/server.rb",
|
37
|
+
"lib/loose_change/views.rb",
|
38
|
+
"loose_change.gemspec",
|
39
|
+
"test/attachment_test.rb",
|
40
|
+
"test/attributes_test.rb",
|
41
|
+
"test/base_test.rb",
|
42
|
+
"test/callback_test.rb",
|
43
|
+
"test/inheritance_test.rb",
|
44
|
+
"test/pagination_test.rb",
|
45
|
+
"test/persistence_test.rb",
|
46
|
+
"test/resources/couchdb.png",
|
47
|
+
"test/test_helper.rb",
|
48
|
+
"test/view_test.rb"
|
49
|
+
]
|
50
|
+
s.homepage = %q{http://github.com/joshuamiller/loose_change}
|
51
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
52
|
+
s.require_paths = ["lib"]
|
53
|
+
s.rubygems_version = %q{1.3.7}
|
54
|
+
s.summary = %q{ActiveModel-compliant CouchDB ORM}
|
55
|
+
s.test_files = [
|
56
|
+
"test/attachment_test.rb",
|
57
|
+
"test/attributes_test.rb",
|
58
|
+
"test/base_test.rb",
|
59
|
+
"test/callback_test.rb",
|
60
|
+
"test/inheritance_test.rb",
|
61
|
+
"test/pagination_test.rb",
|
62
|
+
"test/persistence_test.rb",
|
63
|
+
"test/test_helper.rb",
|
64
|
+
"test/view_test.rb"
|
65
|
+
]
|
66
|
+
|
67
|
+
if s.respond_to? :specification_version then
|
68
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
69
|
+
s.specification_version = 3
|
70
|
+
|
71
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
72
|
+
s.add_runtime_dependency(%q<activesupport>, ["~> 3.0.0"])
|
73
|
+
s.add_runtime_dependency(%q<activemodel>, ["~> 3.0.0"])
|
74
|
+
s.add_runtime_dependency(%q<rest-client>, ["~> 1.6.0"])
|
75
|
+
s.add_runtime_dependency(%q<json>, ["~> 1.4.6"])
|
76
|
+
else
|
77
|
+
s.add_dependency(%q<activesupport>, ["~> 3.0.0"])
|
78
|
+
s.add_dependency(%q<activemodel>, ["~> 3.0.0"])
|
79
|
+
s.add_dependency(%q<rest-client>, ["~> 1.6.0"])
|
80
|
+
s.add_dependency(%q<json>, ["~> 1.4.6"])
|
81
|
+
end
|
82
|
+
else
|
83
|
+
s.add_dependency(%q<activesupport>, ["~> 3.0.0"])
|
84
|
+
s.add_dependency(%q<activemodel>, ["~> 3.0.0"])
|
85
|
+
s.add_dependency(%q<rest-client>, ["~> 1.6.0"])
|
86
|
+
s.add_dependency(%q<json>, ["~> 1.4.6"])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|