loose_change 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ module LooseChange
2
+ module Errors
3
+
4
+ attr_accessor :errors
5
+
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ module LooseChange
2
+ module Helpers
3
+ def default_headers
4
+ {
5
+ :content_type => :json,
6
+ :accept => :json
7
+ }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module LooseChange
2
+ module Naming
3
+ include ActiveModel::Naming
4
+ end
5
+
6
+ module NamingClassMethods
7
+ def model_name
8
+ self.class.model_name
9
+ end
10
+ end
11
+ end
@@ -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,11 @@
1
+ module LooseChange
2
+ class Server
3
+
4
+ attr_accessor :uri
5
+
6
+ def initialize(server = "http://127.0.0.1:5984")
7
+ @uri = server
8
+ end
9
+
10
+ end
11
+ 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
+