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
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
|
3
|
+
gem 'activesupport', '~> 3.0.0'
|
4
|
+
gem 'activemodel', '~> 3.0.0'
|
5
|
+
gem 'rest-client'
|
6
|
+
gem 'json'
|
7
|
+
gem 'will_paginate'
|
8
|
+
|
9
|
+
group :development do
|
10
|
+
gem 'jeweler'
|
11
|
+
gem 'rake'
|
12
|
+
end
|
13
|
+
|
14
|
+
group :test do
|
15
|
+
gem 'shoulda'
|
16
|
+
gem 'timecop'
|
17
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.0.0)
|
5
|
+
activesupport (= 3.0.0)
|
6
|
+
builder (~> 2.1.2)
|
7
|
+
i18n (~> 0.4.1)
|
8
|
+
activesupport (3.0.0)
|
9
|
+
builder (2.1.2)
|
10
|
+
gemcutter (0.6.1)
|
11
|
+
git (1.2.5)
|
12
|
+
i18n (0.4.1)
|
13
|
+
jeweler (1.4.0)
|
14
|
+
gemcutter (>= 0.1.0)
|
15
|
+
git (>= 1.2.5)
|
16
|
+
rubyforge (>= 2.0.0)
|
17
|
+
json (1.4.6)
|
18
|
+
json_pure (1.4.6)
|
19
|
+
mime-types (1.16)
|
20
|
+
rake (0.8.7)
|
21
|
+
rest-client (1.6.1)
|
22
|
+
mime-types (>= 1.16)
|
23
|
+
rubyforge (2.0.4)
|
24
|
+
json_pure (>= 1.1.7)
|
25
|
+
shoulda (2.11.3)
|
26
|
+
timecop (0.3.5)
|
27
|
+
will_paginate (3.0.pre2)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
ruby
|
31
|
+
|
32
|
+
DEPENDENCIES
|
33
|
+
activemodel (~> 3.0.0)
|
34
|
+
activesupport (~> 3.0.0)
|
35
|
+
jeweler
|
36
|
+
json
|
37
|
+
rake
|
38
|
+
rest-client
|
39
|
+
shoulda
|
40
|
+
timecop
|
41
|
+
will_paginate
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Joshua Miller
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Loose Change is a Ruby ORM for CouchDB
|
2
|
+
|
3
|
+
* where 'ORM' is as accurate as ['Holy Roman
|
4
|
+
Empire'](http://en.wikipedia.org/wiki/Holy_Roman_Empire#Analysis)
|
5
|
+
|
6
|
+
## Goals and Principles
|
7
|
+
|
8
|
+
* Take advantage of
|
9
|
+
[ActiveModel](http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/)
|
10
|
+
* Make common tasks easy; get out of your way if you need the metal
|
11
|
+
* Make working with [GeoCouch](http://github.com/vmx/couchdb) seamless
|
12
|
+
|
13
|
+
## Warnings
|
14
|
+
|
15
|
+
* This is pretty alpha at this point.
|
16
|
+
* Only tested on Ruby 1.9.2. I'm not intentionally breaking 1.8.x, but neither do I guarantee anything.
|
17
|
+
* The stuff about GeoCouch above was a goal I didn't get to yet.
|
18
|
+
|
19
|
+
## Help
|
20
|
+
|
21
|
+
* Accepted. Fork at
|
22
|
+
[Github](http://github.com/joshuamiller/loose_change)
|
23
|
+
|
24
|
+
## Shoulders of Giants
|
25
|
+
|
26
|
+
Inspiration and help from:
|
27
|
+
|
28
|
+
* [RestClient](http://github.com/archiloque/rest-client), for the
|
29
|
+
basic HTTP plumbing.
|
30
|
+
* [CouchRest](http://github.com/couchrest/couchrest), for the idea and
|
31
|
+
basic structure of using RestClient to talk to CouchDB.
|
32
|
+
* [CouchRest-Rails](http://github.com/hpoydar/couchrest-rails), for
|
33
|
+
implementation ideas for interacting with Rails.
|
34
|
+
* [will_paginate](http://github.com/mislave/will_paginate), for making
|
35
|
+
pagination a dead-simple addition.
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
* MIT. See LICENSE for more details.
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
Rake::TestTask.new(:test) do |t|
|
8
|
+
Bundler.require(:test)
|
9
|
+
t.pattern = 'test/**/*_test.rb'
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
Bundler.require :development
|
15
|
+
Jeweler::Tasks.new do |gemspec|
|
16
|
+
gemspec.name = "loose_change"
|
17
|
+
gemspec.summary = "ActiveModel-compliant CouchDB ORM"
|
18
|
+
gemspec.email = "josh@joshinharrisburg.com"
|
19
|
+
gemspec.homepage = "http://github.com/joshuamiller/loose_change"
|
20
|
+
gemspec.authors = ["Joshua Miller"]
|
21
|
+
# Dependencies
|
22
|
+
gemspec.add_dependency 'activesupport', '~> 3.0.0'
|
23
|
+
gemspec.add_dependency 'activemodel', '~> 3.0.0'
|
24
|
+
gemspec.add_dependency 'rest-client', '~> 1.6.0'
|
25
|
+
gemspec.add_dependency 'json', '~> 1.4.6'
|
26
|
+
end
|
27
|
+
Jeweler::GemcutterTasks.new
|
28
|
+
rescue LoadError
|
29
|
+
puts "Jeweler not available."
|
30
|
+
end
|
31
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.1
|
data/lib/loose_change.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
require 'active_model'
|
4
|
+
require 'rest-client'
|
5
|
+
require 'json'
|
6
|
+
require 'will_paginate'
|
7
|
+
require 'will_paginate/collection'
|
8
|
+
|
9
|
+
module LooseChange
|
10
|
+
extend ActiveSupport::Autoload
|
11
|
+
|
12
|
+
autoload :Attributes, File.dirname(__FILE__) + '/loose_change/attributes'
|
13
|
+
autoload :Attachments, File.dirname(__FILE__) + '/loose_change/attachments'
|
14
|
+
autoload :Errors, File.dirname(__FILE__) + '/loose_change/errors'
|
15
|
+
autoload :Naming, File.dirname(__FILE__) + '/loose_change/naming'
|
16
|
+
autoload :Base, File.dirname(__FILE__) + '/loose_change/base'
|
17
|
+
autoload :Persistence, File.dirname(__FILE__) + '/loose_change/persistence'
|
18
|
+
autoload :Database, File.dirname(__FILE__) + '/loose_change/database'
|
19
|
+
autoload :Server, File.dirname(__FILE__) + '/loose_change/server'
|
20
|
+
autoload :Views, File.dirname(__FILE__) + '/loose_change/views'
|
21
|
+
autoload :Pagination, File.dirname(__FILE__) + '/loose_change/pagination'
|
22
|
+
autoload :Helpers, File.dirname(__FILE__) + '/loose_change/helpers'
|
23
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module LooseChange
|
2
|
+
|
3
|
+
module Attachments
|
4
|
+
end
|
5
|
+
|
6
|
+
module AttachmentClassMethods
|
7
|
+
|
8
|
+
# Attach a file to this model, to be stored inline in
|
9
|
+
# CouchDB. Note that the file will not actually be transferred
|
10
|
+
# to CouchDB until #save is called. The +name+ parameter will be
|
11
|
+
# used on CouchDB and in Loose Change to retrieve the attachment
|
12
|
+
# later. If you set the <tt>:content_type</tt> key in the optional +args+
|
13
|
+
# hash, that content-type will be set on the attachment in CouchDB
|
14
|
+
# and available when the model is retrieved.
|
15
|
+
#
|
16
|
+
# recipe = Recipe.create!(:name => "Lasagne")
|
17
|
+
# recipe.attach(:photo, File.open("lasagne.png"), :content_type
|
18
|
+
# => 'image/png'
|
19
|
+
# recipe.save
|
20
|
+
def attach(name, file, args = {})
|
21
|
+
attachment = args.merge :file => file, :dirty => true
|
22
|
+
@attachments = (@attachments || {}).merge(name => attachment)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the file identified by +name+ on a Loose Change model
|
26
|
+
# instance, whether or not that file has been saved back to
|
27
|
+
# CouchDB. Will return nil if no attachment by that name exists.
|
28
|
+
def attachment(name)
|
29
|
+
return attachments[name][:file] if @attachments.try(:[], :name).try(:[], :file)
|
30
|
+
begin
|
31
|
+
result = retrieve_attachment(name)
|
32
|
+
@attachments = (@attachments || {}).merge(name => {:file => result[:file], :dirty => false, :content_type => result[:content_type]})
|
33
|
+
result[:file]
|
34
|
+
rescue RestClient::ResourceNotFound
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a hash composed of <tt>file</tt> and <tt>content_type</tt> as
|
40
|
+
# returned from CouchDB identified by +name+.
|
41
|
+
def retrieve_attachment(name)
|
42
|
+
{ :file => RestClient.get("#{ uri }/#{ name }"),
|
43
|
+
:content_type => JSON.parse(RestClient.get(uri))['_attachments']['name'] }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Explicitly transfers an attachment to CouchDB without saving the
|
47
|
+
# rest of the model.
|
48
|
+
#
|
49
|
+
# recipe.attach(:photo, File.open("lasagne.png"), :content_type
|
50
|
+
# => 'image/png'
|
51
|
+
# recipe.put_attachment(:photo)
|
52
|
+
def put_attachment(name)
|
53
|
+
return unless attachments[name]
|
54
|
+
result = JSON.parse(RestClient.put("#{ uri }/#{ name }#{ '?rev=' + @_rev if @_rev }", attachments[name][:file], {:content_type => attachments[name][:content_type], :accept => 'text/json'}))
|
55
|
+
@_rev = result['rev']
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module LooseChange
|
2
|
+
|
3
|
+
module Attributes
|
4
|
+
|
5
|
+
# Set a field to be stored in a CouchDB document. Any
|
6
|
+
# JSON-encodable value can be used; there is no explicit typing.
|
7
|
+
# The <tt>:default</tt> option can be used to always store a value
|
8
|
+
# on documents where this property has not been set.
|
9
|
+
#
|
10
|
+
# class Recipe < LooseChange::Base
|
11
|
+
# property :name
|
12
|
+
# property :popularity, :default => 0
|
13
|
+
# end
|
14
|
+
def property(name, opts = {})
|
15
|
+
attr_accessor name.to_sym
|
16
|
+
self.properties = ((self.properties || []) << name.to_sym)
|
17
|
+
default(name.to_sym, opts[:default]) if opts[:default]
|
18
|
+
define_attribute_methods [name]
|
19
|
+
end
|
20
|
+
|
21
|
+
#:nodoc:
|
22
|
+
def default(property, value)
|
23
|
+
self.defaults ||= {}
|
24
|
+
self.defaults[property] = value
|
25
|
+
end
|
26
|
+
|
27
|
+
# Automatically set up <tt>:created_at</tt> and
|
28
|
+
# <tt>:updated_at</tt> properties which are set on creation and
|
29
|
+
# save, respectively.
|
30
|
+
def timestamps!
|
31
|
+
property :created_at
|
32
|
+
property :updated_at
|
33
|
+
|
34
|
+
before_create :touch_created_at
|
35
|
+
before_create :touch_updated_at
|
36
|
+
before_save :touch_updated_at
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module AttributeClassMethods
|
41
|
+
|
42
|
+
# Returns a hash of property names and current values.
|
43
|
+
def attributes
|
44
|
+
(self.class.properties || []).inject({}) {|acc, key| acc[key] = send(key); acc}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Change the value of a property and save the result to CouchDB.
|
48
|
+
def update_attribute(name, value)
|
49
|
+
send("#{name}=", value)
|
50
|
+
save
|
51
|
+
end
|
52
|
+
|
53
|
+
# Change multiple properties at once with a hash of property names
|
54
|
+
# and values, then save the result to CouchDB.
|
55
|
+
def update_attributes(args = {})
|
56
|
+
args.each do |name, value|
|
57
|
+
send("#{name}=", value)
|
58
|
+
end
|
59
|
+
save
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def touch_created_at
|
65
|
+
self.created_at = Time.now
|
66
|
+
end
|
67
|
+
|
68
|
+
def touch_updated_at
|
69
|
+
self.updated_at = Time.now
|
70
|
+
end
|
71
|
+
|
72
|
+
def apply_defaults
|
73
|
+
(self.class.defaults || {}).each do |property, value|
|
74
|
+
self.send("#{property}=", self.send(property) || value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module LooseChange
|
2
|
+
class Base
|
3
|
+
|
4
|
+
include ActiveModel::AttributeMethods
|
5
|
+
include ActiveModel::Validations
|
6
|
+
include ActiveModel::Serialization
|
7
|
+
include ActiveModel::Serializers::JSON
|
8
|
+
include ActiveModel::Dirty
|
9
|
+
|
10
|
+
# Can't escape the following when trying to
|
11
|
+
# use a Callbacks module:
|
12
|
+
# undefined method `extlib_inheritable_reader' for LooseChange::Callbacks:Module
|
13
|
+
# so we'll throw it in here
|
14
|
+
extend ActiveModel::Callbacks
|
15
|
+
define_model_callbacks :create, :save, :destroy
|
16
|
+
|
17
|
+
extend Attributes
|
18
|
+
include AttributeClassMethods
|
19
|
+
extend Attachments
|
20
|
+
include AttachmentClassMethods
|
21
|
+
extend Naming
|
22
|
+
include NamingClassMethods
|
23
|
+
include Errors
|
24
|
+
extend Persistence
|
25
|
+
include PersistenceClassMethods
|
26
|
+
extend Views
|
27
|
+
extend Pagination
|
28
|
+
include Helpers
|
29
|
+
extend Helpers
|
30
|
+
|
31
|
+
class_inheritable_accessor :database, :properties, :defaults
|
32
|
+
attr_accessor :attachments
|
33
|
+
|
34
|
+
def to_key
|
35
|
+
persisted? ? [id] : nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_model
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_param
|
43
|
+
(to_key && persisted?) ? to_key.join('-') : nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def ==(other_model)
|
47
|
+
return false unless other_model && other_model.is_a?(self.class)
|
48
|
+
id == other_model.id &&
|
49
|
+
_rev == other_model._rev &&
|
50
|
+
!(changed? || other_model.changed?)
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method :eql?, :==
|
54
|
+
|
55
|
+
def hash
|
56
|
+
id.try(:hex) || super
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(args = {})
|
60
|
+
@errors = ActiveModel::Errors.new(self)
|
61
|
+
@database = self.database
|
62
|
+
@new_record = true unless args['_id']
|
63
|
+
args.each {|property, value| self.send("#{property}=".to_sym, value)}
|
64
|
+
apply_defaults
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
LooseChange::Base.include_root_in_json = false
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module LooseChange
|
4
|
+
class Database
|
5
|
+
include Helpers
|
6
|
+
extend Helpers
|
7
|
+
|
8
|
+
attr_reader :server, :name
|
9
|
+
|
10
|
+
# Build a new LooseChange::Database instance with a database named
|
11
|
+
# +name+. The +server+ is a URI string identifying the CouchDB
|
12
|
+
# server and port. If the database does not exist on the server,
|
13
|
+
# it will be created.
|
14
|
+
def initialize(name, server)
|
15
|
+
@name = name
|
16
|
+
@server = server
|
17
|
+
@uri = "/#{name.gsub('/','%2F')}"
|
18
|
+
create_database_unless_exists
|
19
|
+
end
|
20
|
+
|
21
|
+
def uri
|
22
|
+
server.uri + @uri
|
23
|
+
end
|
24
|
+
|
25
|
+
# Delete the database named +name+ on the server +server+.
|
26
|
+
def self.delete(name, server = "http://127.0.0.1:5984")
|
27
|
+
begin
|
28
|
+
RestClient.delete("#{ server }/#{ name }")
|
29
|
+
rescue RestClient::ResourceNotFound
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.setup_design(database, model_name)
|
34
|
+
begin
|
35
|
+
RestClient.get("#{ database.uri }/_design/#{ CGI.escape(model_name) }")
|
36
|
+
rescue
|
37
|
+
RestClient.put("#{ database.uri }/_design/#{ CGI.escape(model_name) }",
|
38
|
+
{ '_id' => "_design/#{ CGI.escape(model_name) }",
|
39
|
+
'language' => 'javascript'}.to_json, default_headers)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def create_database_unless_exists
|
46
|
+
unless JSON.parse(RestClient.get("#{server.uri}/_all_dbs", default_headers)).include?(name)
|
47
|
+
RestClient.put uri, "", default_headers
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|