couchmodel 0.1.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +156 -0
- data/Rakefile +20 -0
- data/lib/core_extension/array.rb +14 -0
- data/lib/core_extension/string.rb +12 -0
- data/lib/couch_model/active_model.rb +86 -0
- data/lib/couch_model/base/accessor.rb +39 -0
- data/lib/couch_model/base/association.rb +63 -0
- data/lib/couch_model/base/finder.rb +28 -0
- data/lib/couch_model/base/setup.rb +88 -0
- data/lib/couch_model/base.rb +117 -0
- data/lib/couch_model/collection.rb +84 -0
- data/lib/couch_model/configuration.rb +68 -0
- data/lib/couch_model/database.rb +64 -0
- data/lib/couch_model/design.rb +92 -0
- data/lib/couch_model/server.rb +44 -0
- data/lib/couch_model/transport.rb +68 -0
- data/lib/couch_model/view.rb +52 -0
- data/lib/couch_model.rb +15 -0
- data/spec/fake_transport.yml +202 -0
- data/spec/fake_transport_helper.rb +27 -0
- data/spec/integration/basic_spec.rb +125 -0
- data/spec/integration/design/membership.design +5 -0
- data/spec/integration/design/user.design +2 -0
- data/spec/lib/core_extension/array_spec.rb +24 -0
- data/spec/lib/core_extension/string_spec.rb +22 -0
- data/spec/lib/couch_model/active_model_spec.rb +228 -0
- data/spec/lib/couch_model/base_spec.rb +169 -0
- data/spec/lib/couch_model/collection_spec.rb +100 -0
- data/spec/lib/couch_model/configuration_spec.rb +117 -0
- data/spec/lib/couch_model/core/accessor_spec.rb +59 -0
- data/spec/lib/couch_model/core/association_spec.rb +114 -0
- data/spec/lib/couch_model/core/finder_spec.rb +24 -0
- data/spec/lib/couch_model/core/setup_spec.rb +88 -0
- data/spec/lib/couch_model/database_spec.rb +165 -0
- data/spec/lib/couch_model/design/association_test_model_one.design +5 -0
- data/spec/lib/couch_model/design/base_test_model.design +10 -0
- data/spec/lib/couch_model/design/setup_test_model.design +10 -0
- data/spec/lib/couch_model/design_spec.rb +144 -0
- data/spec/lib/couch_model/server_spec.rb +64 -0
- data/spec/lib/couch_model/transport_spec.rb +44 -0
- data/spec/lib/couch_model/view_spec.rb +166 -0
- data/spec/lib/couch_model_spec.rb +3 -0
- data/spec/spec_helper.rb +27 -0
- metadata +128 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Philipp Brüll
|
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.rdoc
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
|
2
|
+
= CouchModel
|
3
|
+
|
4
|
+
The intent of CouchModel is, to provide an easy interface handle CouchDB documents. It also comes with a ActiveModel
|
5
|
+
implementation to integrate into an Rails 3 application.
|
6
|
+
|
7
|
+
The current version is under development and open for everyone to find bugs and post them into the issue tracker.
|
8
|
+
|
9
|
+
The code has been tested Ruby 1.9.1, CouchDB 0.10.0 and Rails 3.0.0.beta.
|
10
|
+
|
11
|
+
== Dependencies
|
12
|
+
|
13
|
+
If CouchModel is used without Rails, the ruby standard library (tested with 1.9.1) if the only requirement.
|
14
|
+
|
15
|
+
If the activemodel gem is installed, CouchModel automatically provides an interface to Rails 3.
|
16
|
+
|
17
|
+
To run the test suite, <tt>rspec</tt> (tested with 1.2.9) is required. A CouchDB instance is just required for the
|
18
|
+
integration tests (task <tt>spec:integration</tt>).
|
19
|
+
|
20
|
+
== Installation
|
21
|
+
|
22
|
+
The gem is part of the gemcutter archive. It can be installed by simply type
|
23
|
+
|
24
|
+
gem install couchmodel
|
25
|
+
|
26
|
+
== Defining a model
|
27
|
+
|
28
|
+
To define a model, it's necessary to create a subclass of <tt>CouchModel::Base</tt>
|
29
|
+
|
30
|
+
class User < CouchModel::Base
|
31
|
+
|
32
|
+
setup_database :url => "http://localhost:5984/test",
|
33
|
+
:setup_on_initialization => true,
|
34
|
+
:delete_if_exists => false
|
35
|
+
|
36
|
+
key_accessor :name
|
37
|
+
key_accessor :email
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
The <tt>setup_database</tt> method defines a database for the model. The +url+ option is required and specifies the url
|
42
|
+
of the database in the scheme <em>http://[host]:[port]/[database_name]</em>. If the option
|
43
|
+
<tt>setup_on_initialization</tt> is set to true, CouchModel will try to create the database when the model is
|
44
|
+
initialized. If the option <tt>delete_if_exists</tt> is specified, the database will be deleted and re-created. If the
|
45
|
+
option <tt>setup_on_initialization</tt> is not specified or false, the database setup be done manually by calling
|
46
|
+
<tt>CouchModel::Configuration.setup_databases</tt> and <tt>CouchModel::Configuration.setup_designs</tt>.
|
47
|
+
|
48
|
+
The method <tt>key_accessor</tt> defined access methods to the given keys of the CouchDB document. It's also possible
|
49
|
+
to use <tt>key_reader</tt> and <tt>key_writer</tt> here.
|
50
|
+
|
51
|
+
== Design documents
|
52
|
+
|
53
|
+
Each defined model has a realted design document, that keeps all the views for that model. Via the command
|
54
|
+
|
55
|
+
CouchModel::Configuration.design_directory = "[directory]"
|
56
|
+
|
57
|
+
a directory is specfied that keeps all the design document. CouchModel will watch out for a file with the name
|
58
|
+
<em>[design directory]/[model_name].design</em> and will use it as the related design document. If no such file exists,
|
59
|
+
a design document will be created (but not saved to the file). The design ducument can be asscessed via
|
60
|
+
<tt>Model.design</tt>.
|
61
|
+
|
62
|
+
A design document should look like this
|
63
|
+
|
64
|
+
:id: "test_design"
|
65
|
+
:language: "javascript"
|
66
|
+
:views:
|
67
|
+
"view_name_1":
|
68
|
+
:map:
|
69
|
+
function(document) {
|
70
|
+
...
|
71
|
+
};
|
72
|
+
:reduce:
|
73
|
+
function(key, values, rereduce) {
|
74
|
+
...
|
75
|
+
};
|
76
|
+
"view_name_2":
|
77
|
+
:keys: [ "key_one", "key_two" ]
|
78
|
+
...
|
79
|
+
|
80
|
+
It will create the methods <tt>Model.view_name_1</tt> and <tt>Model.view_name_2</tt>, which returns the result of the
|
81
|
+
related view. It's also possible to pass some extra options like <tt>startkey</tt> or <tt>key</tt> to these methods.
|
82
|
+
|
83
|
+
The view can be defined by write down a map and a reduce function or provide the <tt>keys</tt> array. If the
|
84
|
+
<tt>keys</tt> array is given, CouchModel will generate a map function that emits the given array of document keys. The
|
85
|
+
reduce function will be set to null.
|
86
|
+
|
87
|
+
CouchModel also creates by default a class view. This view simply selects all documents from the corresponding model
|
88
|
+
and is assigned to the method <tt>Model.all</tt>.
|
89
|
+
|
90
|
+
== Associations
|
91
|
+
|
92
|
+
CouchModel provides support for simple association definition. Currently, the method <tt>belongs_to</tt> and
|
93
|
+
<tt>has_many</tt> are implmented.
|
94
|
+
|
95
|
+
class User < CouchModel::Base
|
96
|
+
|
97
|
+
...
|
98
|
+
|
99
|
+
belongs_to :session, :class_name => "UserSession"
|
100
|
+
|
101
|
+
has_many :memberships,
|
102
|
+
:class_name => "Membership",
|
103
|
+
:view_name => :by_user_id_and_created_at
|
104
|
+
:query => proc { |created_at| { :startkey => [ self.id, (create_at || nil) ], :endkey => [ self.id, (created_at || { }) ] } }
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
In this example, the <tt>belongs_to</tt> adds a <tt>key_accessor</tt> named <tt>session_id</tt> to the user and also
|
109
|
+
generates getters and setters for the session object itself (<tt>session</tt> and <tt>session=</tt>).
|
110
|
+
|
111
|
+
The <tt>has_many</tt> acts as a wrapper for the specified view. The previously defined view
|
112
|
+
<em>by_user_id_and_created_at</em> emits membership-documents by thier <tt>user_id</tt> and the <tt>created_at</tt>
|
113
|
+
date. The given query option specifes a method that returns a query hash for the specifed view. The arguments for this
|
114
|
+
method can be passed membership association method.
|
115
|
+
|
116
|
+
user.membership(created_at)
|
117
|
+
|
118
|
+
The returned <tt>CouchModel::Collection</tt> can be treated as an (read-only) array.
|
119
|
+
|
120
|
+
== Rails integration
|
121
|
+
|
122
|
+
The following steps has been tested with the first beta version of Rails 3 (activemodel-3.0.0.beta).
|
123
|
+
|
124
|
+
First of all, the <tt>couchmodel</tt> gem has to added to the dependencies. This can be done by adding
|
125
|
+
|
126
|
+
gem "couchmodel", :require => "couch_model"
|
127
|
+
|
128
|
+
to the <tt>Gemfile</tt>.
|
129
|
+
|
130
|
+
The configuration can be done by creating an initializer. Here is an example file
|
131
|
+
(e.g. <tt>config/initializer/couch_model.rb</tt>).
|
132
|
+
|
133
|
+
CouchModel::Configuration.design_directory = File.join(Rails.root, "app", "models", "designs")
|
134
|
+
|
135
|
+
DATABASE = {
|
136
|
+
:test => { :url => "http://localhost:5984/test", :setup_on_initialization => true, :delete_if_exists => true }
|
137
|
+
:development => { :url => "http://localhost:5984/development", :setup_on_initialization => true, :delete_if_exists => false },
|
138
|
+
:production => { :url => "http://localhost:5984/production", :setup_on_initialization => true, :delete_if_exists => false }
|
139
|
+
}[Rails.env.to_sym] unless defined?(DATABASE)
|
140
|
+
|
141
|
+
This example uses an sub-directory of <tt>app/models</tt> to search for the design documents. It also defined a constant
|
142
|
+
named <tt>DATABASE</tt> that is initialized with the right database setup for the each environment. This constant can
|
143
|
+
then be used to define the models.
|
144
|
+
|
145
|
+
class User < CouchModel::Base
|
146
|
+
|
147
|
+
setup_database DATABASE
|
148
|
+
|
149
|
+
...
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
== Development
|
154
|
+
|
155
|
+
CouchModel is still under development and needs to be tested. Any contribution is welcome!
|
156
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'rspec'
|
3
|
+
require 'spec'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
desc "Run all specs in spec directory"
|
9
|
+
Spec::Rake::SpecTask.new do |task|
|
10
|
+
task.spec_files = FileList["spec/lib/**/*_spec.rb"]
|
11
|
+
end
|
12
|
+
|
13
|
+
namespace :spec do
|
14
|
+
|
15
|
+
desc "Run all integration specs in spec/integration directory"
|
16
|
+
Spec::Rake::SpecTask.new(:integration) do |task|
|
17
|
+
task.spec_files = FileList["spec/integration/**/*_spec.rb"]
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "core_extension", "array"))
|
2
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "base"))
|
3
|
+
gem 'activemodel'
|
4
|
+
require 'active_model'
|
5
|
+
|
6
|
+
module CouchModel
|
7
|
+
|
8
|
+
class Base
|
9
|
+
extend ::ActiveModel::Naming
|
10
|
+
extend ::ActiveModel::Callbacks
|
11
|
+
extend ::ActiveModel::Translation
|
12
|
+
include ::ActiveModel::Conversion
|
13
|
+
include ::ActiveModel::Dirty
|
14
|
+
include ::ActiveModel::Validations
|
15
|
+
include ::ActiveModel::Serializers::JSON
|
16
|
+
include ::ActiveModel::Serializers::Xml
|
17
|
+
|
18
|
+
CALLBACKS = [ :initialize, :save, :create, :update, :destroy ].freeze unless defined?(CALLBACKS)
|
19
|
+
|
20
|
+
define_model_callbacks *CALLBACKS
|
21
|
+
|
22
|
+
CALLBACKS.each do |method_name|
|
23
|
+
|
24
|
+
alias :"#{method_name}_without_callbacks" :"#{method_name}"
|
25
|
+
|
26
|
+
define_method :"#{method_name}" do |*arguments|
|
27
|
+
send :"_run_#{method_name}_callbacks" do
|
28
|
+
send :"#{method_name}_without_callbacks", *arguments
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
alias new_record? new?
|
35
|
+
|
36
|
+
alias destroyed? new?
|
37
|
+
|
38
|
+
alias save_without_dirty save
|
39
|
+
|
40
|
+
def save
|
41
|
+
result = save_without_dirty
|
42
|
+
discard_changes!
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def discard_changes!
|
49
|
+
@previously_changed = changes
|
50
|
+
@changed_attributes = { }
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
|
55
|
+
alias key_accessor_without_dirty key_accessor
|
56
|
+
|
57
|
+
def key_accessor(key)
|
58
|
+
add_key key
|
59
|
+
redefine_attribute_methods
|
60
|
+
|
61
|
+
key_accessor_without_dirty key
|
62
|
+
|
63
|
+
alias_method :"#{key}_without_dirty=", :"#{key}="
|
64
|
+
define_method :"#{key}=" do |value|
|
65
|
+
send :"#{key}_will_change!"
|
66
|
+
send :"#{key}_without_dirty=", value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def add_key(key)
|
73
|
+
@keys ||= [ ]
|
74
|
+
@keys << key
|
75
|
+
end
|
76
|
+
|
77
|
+
def redefine_attribute_methods
|
78
|
+
undefine_attribute_methods
|
79
|
+
define_attribute_methods @keys
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
module CouchModel
|
3
|
+
|
4
|
+
class Base
|
5
|
+
|
6
|
+
module Accessor
|
7
|
+
|
8
|
+
def self.included(base_class)
|
9
|
+
base_class.class_eval do
|
10
|
+
extend ClassMethods
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def key_reader(key)
|
17
|
+
define_method :"#{key}" do
|
18
|
+
@attributes[key.to_s]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def key_writer(key)
|
23
|
+
define_method :"#{key}=" do |value|
|
24
|
+
@attributes[key.to_s] = value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def key_accessor(key)
|
29
|
+
key_reader key
|
30
|
+
key_writer key
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
|
2
|
+
module CouchModel
|
3
|
+
|
4
|
+
class Base
|
5
|
+
|
6
|
+
module Association
|
7
|
+
|
8
|
+
def self.included(base_class)
|
9
|
+
base_class.class_eval do
|
10
|
+
extend ClassMethods
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def belongs_to(name, options = { })
|
17
|
+
class_name = options[:class_name] || name.to_s.camelize
|
18
|
+
key = options[:key] || "#{name}_id"
|
19
|
+
|
20
|
+
key_accessor key
|
21
|
+
|
22
|
+
define_method :"#{name}" do
|
23
|
+
klass = Object.const_get class_name
|
24
|
+
klass.find self.send(key)
|
25
|
+
end
|
26
|
+
|
27
|
+
define_method :"#{name}=" do |value|
|
28
|
+
klass = Object.const_get class_name
|
29
|
+
if value
|
30
|
+
raise ArgumentError, "only objects of class #{klass} are accepted" unless value.is_a?(klass)
|
31
|
+
self.send :"#{key}=", value.id
|
32
|
+
else
|
33
|
+
self.send :"#{key}=", nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def has_many(name, options = { })
|
39
|
+
class_name = options[:class_name] || name.to_s.camelize
|
40
|
+
view_name = options[:view_name] || raise(ArgumentError, "no view_name is given")
|
41
|
+
query = options[:query]
|
42
|
+
|
43
|
+
define_method :"#{name}_query", &query if query.is_a?(Proc)
|
44
|
+
|
45
|
+
define_method :"#{name}" do |*arguments|
|
46
|
+
klass = Object.const_get class_name
|
47
|
+
query = if self.respond_to?(:"#{name}_query")
|
48
|
+
arguments << nil while arguments.length < self.method(:"#{name}_query").arity
|
49
|
+
self.send :"#{name}_query", *arguments
|
50
|
+
else
|
51
|
+
{ :key => "\"#{self.id}\"" }
|
52
|
+
end
|
53
|
+
klass.send :"#{view_name}", query
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
module CouchModel
|
3
|
+
|
4
|
+
class Base
|
5
|
+
|
6
|
+
module Finder
|
7
|
+
|
8
|
+
def self.included(base_class)
|
9
|
+
base_class.class_eval do
|
10
|
+
extend ClassMethods
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
def find(id)
|
17
|
+
document = new :id => id
|
18
|
+
document.load
|
19
|
+
document
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
|
2
|
+
module CouchModel
|
3
|
+
|
4
|
+
class Base
|
5
|
+
|
6
|
+
module Setup
|
7
|
+
|
8
|
+
def self.included(base_class)
|
9
|
+
base_class.class_eval do
|
10
|
+
include InstanceMethods
|
11
|
+
extend ClassMethods
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
|
17
|
+
def database
|
18
|
+
self.class.database
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
|
25
|
+
def setup_database(options = { })
|
26
|
+
initialize_database options
|
27
|
+
initialize_design
|
28
|
+
generate_class_view
|
29
|
+
push_design options
|
30
|
+
end
|
31
|
+
|
32
|
+
def database
|
33
|
+
@database || raise(StandardError, "no database defined!")
|
34
|
+
end
|
35
|
+
|
36
|
+
def design
|
37
|
+
@design || raise(StandardError, "no database defined!")
|
38
|
+
end
|
39
|
+
|
40
|
+
def method_missing(method_name, *arguments, &block)
|
41
|
+
view = find_view method_name
|
42
|
+
view ? view.collection(*arguments) : super
|
43
|
+
end
|
44
|
+
|
45
|
+
def respond_to?(method_name)
|
46
|
+
view = find_view method_name
|
47
|
+
view ? true : super
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def initialize_database(options)
|
53
|
+
url = options[:url] || raise(ArgumentError, "no url was given to define the database")
|
54
|
+
setup_on_initialization = options[:setup_on_initialization] || false
|
55
|
+
|
56
|
+
uri = URI.parse url
|
57
|
+
server = Server.new :host => uri.host, :port => uri.port
|
58
|
+
database = Database.new :server => server, :name => uri.path.gsub("/", "")
|
59
|
+
@database = Configuration.register_database database
|
60
|
+
|
61
|
+
@database.setup! options if setup_on_initialization && @database === database
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize_design
|
65
|
+
@design = Design.new @database, self, :id => self.to_s.underscore
|
66
|
+
Configuration.register_design @design
|
67
|
+
end
|
68
|
+
|
69
|
+
def generate_class_view
|
70
|
+
@design.generate_view Configuration::CLASS_VIEW_NAME
|
71
|
+
end
|
72
|
+
|
73
|
+
def push_design(options)
|
74
|
+
setup_on_initialization = options[:setup_on_initialization] || false
|
75
|
+
@design.push if setup_on_initialization
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_view(name)
|
79
|
+
@design ? @design.views.select{ |view| view.name == name.to_s }.first : nil
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "core_extension", "string"))
|
2
|
+
require File.join(File.dirname(__FILE__), "configuration")
|
3
|
+
require File.join(File.dirname(__FILE__), "transport")
|
4
|
+
require File.join(File.dirname(__FILE__), "server")
|
5
|
+
require File.join(File.dirname(__FILE__), "database")
|
6
|
+
require File.join(File.dirname(__FILE__), "design")
|
7
|
+
require File.join(File.dirname(__FILE__), "base", "setup")
|
8
|
+
require File.join(File.dirname(__FILE__), "base", "accessor")
|
9
|
+
require File.join(File.dirname(__FILE__), "base", "finder")
|
10
|
+
require File.join(File.dirname(__FILE__), "base", "association")
|
11
|
+
require 'uri'
|
12
|
+
|
13
|
+
module CouchModel
|
14
|
+
|
15
|
+
class Base
|
16
|
+
include CouchModel::Base::Setup
|
17
|
+
include CouchModel::Base::Accessor
|
18
|
+
include CouchModel::Base::Finder
|
19
|
+
include CouchModel::Base::Association
|
20
|
+
|
21
|
+
class Error < StandardError; end
|
22
|
+
class NotFoundError < StandardError; end
|
23
|
+
|
24
|
+
attr_reader :attributes
|
25
|
+
|
26
|
+
def initialize(attributes = { })
|
27
|
+
@attributes = { Configuration::CLASS_KEY => self.class.to_s }
|
28
|
+
self.attributes = attributes
|
29
|
+
end
|
30
|
+
|
31
|
+
def attributes=(attributes)
|
32
|
+
attributes.each { |key, value| self.send :"#{key}=", value if self.respond_to?(:"#{key}=") }
|
33
|
+
end
|
34
|
+
|
35
|
+
def id
|
36
|
+
@attributes["_id"]
|
37
|
+
end
|
38
|
+
alias :_id :id
|
39
|
+
|
40
|
+
def id=(value)
|
41
|
+
@attributes["_id"] = value
|
42
|
+
end
|
43
|
+
|
44
|
+
def rev
|
45
|
+
@attributes["_rev"]
|
46
|
+
end
|
47
|
+
alias :_rev :rev
|
48
|
+
|
49
|
+
def ==(other)
|
50
|
+
self.id == other.id
|
51
|
+
end
|
52
|
+
|
53
|
+
def new?
|
54
|
+
self.rev.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
def load
|
58
|
+
response = Transport.request :get, url, :expected_status_code => 200
|
59
|
+
|
60
|
+
self.rev = response["_rev"]
|
61
|
+
[ "_id", "_rev", Configuration::CLASS_KEY ].each { |key| response.delete key }
|
62
|
+
self.attributes = response
|
63
|
+
true
|
64
|
+
rescue Transport::UnexpectedStatusCodeError => e
|
65
|
+
raise NotFoundError if e.status_code == 404
|
66
|
+
raise e
|
67
|
+
end
|
68
|
+
|
69
|
+
def save
|
70
|
+
new? ? create : update
|
71
|
+
end
|
72
|
+
|
73
|
+
def destroy
|
74
|
+
return false if new?
|
75
|
+
Transport.request :delete, self.url, :parameters => { "rev" => self.rev }, :expected_status_code => 200
|
76
|
+
self.rev = nil
|
77
|
+
true
|
78
|
+
rescue Transport::UnexpectedStatusCodeError => e
|
79
|
+
raise NotFoundError if e.status_code == 404
|
80
|
+
raise e
|
81
|
+
end
|
82
|
+
|
83
|
+
def url
|
84
|
+
"#{self.database.url}/#{self.id}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def method_missing(method_name, *arguments, &block)
|
88
|
+
return @attributes[Configuration::CLASS_KEY] if Configuration::CLASS_KEY == method_name.to_s
|
89
|
+
super
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def rev=(value)
|
95
|
+
@attributes["_rev"] = value
|
96
|
+
end
|
97
|
+
|
98
|
+
def create
|
99
|
+
response = Transport.request :post, self.database.url, :json => self.attributes, :expected_status_code => 201
|
100
|
+
self.id = response["id"]
|
101
|
+
self.rev = response["rev"]
|
102
|
+
true
|
103
|
+
rescue Transport::UnexpectedStatusCodeError
|
104
|
+
false
|
105
|
+
end
|
106
|
+
|
107
|
+
def update
|
108
|
+
response = Transport.request :put, self.url, :json => self.attributes, :expected_status_code => 200
|
109
|
+
self.rev = response["rev"]
|
110
|
+
true
|
111
|
+
rescue Transport::UnexpectedStatusCodeError
|
112
|
+
false
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|