couchmodel 0.1.0.beta3 → 0.1.0.beta4

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.
Files changed (36) hide show
  1. data/README.rdoc +22 -7
  2. data/lib/core_extension/array.rb +18 -2
  3. data/lib/core_extension/string.rb +12 -2
  4. data/lib/couch_model/active_model.rb +31 -8
  5. data/lib/couch_model/base/accessor.rb +8 -5
  6. data/lib/couch_model/base/association.rb +36 -15
  7. data/lib/couch_model/base/finder.rb +1 -0
  8. data/lib/couch_model/base/setup.rb +6 -1
  9. data/lib/couch_model/base.rb +55 -17
  10. data/lib/couch_model/collection.rb +41 -21
  11. data/lib/couch_model/configuration.rb +42 -46
  12. data/lib/couch_model/database.rb +2 -2
  13. data/lib/couch_model/design.rb +18 -14
  14. data/lib/couch_model/row.rb +34 -0
  15. data/lib/couch_model/server.rb +2 -2
  16. data/lib/couch_model/transport.rb +55 -34
  17. data/lib/couch_model/view.rb +9 -5
  18. data/spec/fake_transport.yml +84 -29
  19. data/spec/fake_transport_helper.rb +5 -3
  20. data/spec/integration/basic_spec.rb +26 -12
  21. data/spec/integration/design/membership.design +1 -1
  22. data/spec/integration/design/user.design +12 -0
  23. data/spec/lib/core_extension/array_spec.rb +24 -0
  24. data/spec/lib/couch_model/active_model_spec.rb +51 -0
  25. data/spec/lib/couch_model/base_spec.rb +30 -1
  26. data/spec/lib/couch_model/collection_spec.rb +31 -7
  27. data/spec/lib/couch_model/configuration_spec.rb +2 -2
  28. data/spec/lib/couch_model/core/accessor_spec.rb +11 -3
  29. data/spec/lib/couch_model/core/association_spec.rb +26 -0
  30. data/spec/lib/couch_model/core/setup_spec.rb +8 -0
  31. data/spec/lib/couch_model/design_spec.rb +1 -5
  32. data/spec/lib/couch_model/row_spec.rb +71 -0
  33. data/spec/lib/couch_model/transport_spec.rb +18 -1
  34. data/spec/lib/couch_model/view_spec.rb +2 -2
  35. data/spec/spec_helper.rb +1 -1
  36. metadata +6 -3
data/README.rdoc CHANGED
@@ -36,6 +36,7 @@ To define a model, it's necessary to create a subclass of <tt>CouchModel::Base</
36
36
 
37
37
  key_accessor :name
38
38
  key_accessor :email
39
+ key_accessor :language, :default => "en"
39
40
 
40
41
  end
41
42
 
@@ -49,7 +50,8 @@ manually by calling <tt>CouchModel::Configuration.setup_databases</tt> and
49
50
  <tt>CouchModel::Configuration.setup_designs</tt>.
50
51
 
51
52
  The method <tt>key_accessor</tt> defined access methods to the given keys of the CouchDB document. It's also possible
52
- to use <tt>key_reader</tt> and <tt>key_writer</tt> here.
53
+ to use <tt>key_reader</tt> and <tt>key_writer</tt> here. If the <tt>:default</tt> option is passed, the key will get
54
+ a default value assigned during initialization of the class.
53
55
 
54
56
  == Design documents
55
57
 
@@ -67,7 +69,7 @@ A design document should look like this
67
69
  :id: "test_design"
68
70
  :language: "javascript"
69
71
  :views:
70
- "view_name_1":
72
+ :view_name_1:
71
73
  :map:
72
74
  function(document) {
73
75
  ...
@@ -76,7 +78,7 @@ A design document should look like this
76
78
  function(key, values, rereduce) {
77
79
  ...
78
80
  };
79
- "view_name_2":
81
+ :view_name_2:
80
82
  :keys: [ "key_one", "key_two" ]
81
83
  ...
82
84
 
@@ -113,12 +115,25 @@ generates getters and setters for the session object itself (<tt>session</tt> an
113
115
 
114
116
  The <tt>has_many</tt> acts as a wrapper for the specified view. The previously defined view
115
117
  <tt>by_user_id_and_created_at</tt> emits membership-documents by thier <tt>user_id</tt> and the <tt>created_at</tt>
116
- date. The given query option specifes a method that returns a query hash for the specifed view. The arguments for this
117
- method can be passed membership association method.
118
+ date.
119
+ Basically, the association can be accessed by a reader method. Options for querying the view can be passed by a hash.
118
120
 
119
- user.membership(created_at)
121
+ user.membership(:startkey => [ ... ], :endkey => [ ... ], :descending => false)
120
122
 
121
- The returned <tt>CouchModel::Collection</tt> can be treated as an (read-only) array.
123
+ The possible keys for that query hash can be taken from http://wiki.apache.org/couchdb/HTTP_view_API (Section Querying
124
+ Options).
125
+
126
+ If a <tt>:query</tt> option is defined (like in the example above), the given method is used to generate this query
127
+ hash. When querying a view, the first arguments will be passed to that method and the result of the generator-method
128
+ will be merged with the additionally given query hash.
129
+
130
+ user.membership(created_at, :returns => :rows)
131
+
132
+ The <tt>:returns</tt> option extends the possible keys defined by CouchDB. If not given or specified as
133
+ <tt>:models</tt>, CouchModel will try to cast the returned rows into model classes. It also automatically passes the
134
+ <tt>:include_docs</tt> option to CouchDB. If this option is specified as <tt>:rows</tt>, a collection of
135
+ <tt>CouchModel::Row</tt> objects is returned that wraps the CouchDB result rows. That's maybe useful for views with a
136
+ reduce function.
122
137
 
123
138
  == Rails integration
124
139
 
@@ -1,13 +1,29 @@
1
1
 
2
+ # Extention of ruby's standard array.
2
3
  class Array
3
4
 
5
+ def resize(length, element = nil)
6
+ case size <=> length
7
+ when 1
8
+ slice 0, length
9
+ when -1
10
+ result = self.dup
11
+ result << element while result.size < length
12
+ result
13
+ when 0
14
+ self
15
+ end
16
+ end
17
+
18
+ # This wrap method is taken from ActiveSupport and simply
19
+ # wraps an object into an array.
4
20
  def self.wrap(object)
5
21
  if object.nil?
6
- []
22
+ [ ]
7
23
  elsif object.respond_to?(:to_ary)
8
24
  object.to_ary
9
25
  else
10
- [object]
26
+ [ object ]
11
27
  end
12
28
  end
13
29
 
@@ -1,12 +1,22 @@
1
1
 
2
- class String
2
+ class String # :nodoc:
3
3
 
4
+ # This method converts a CamelCaseString into an underscore_string.
4
5
  def underscore
5
6
  self.gsub(/([a-z][A-Z])/){ |match| "#{match[0]}_#{match[1]}" }.downcase
6
7
  end
7
8
 
9
+ # This method converts an underscore_string into a CamelCaseString.
8
10
  def camelize
9
- self.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
11
+ self.camelize_path.camelize_name
12
+ end
13
+
14
+ def camelize_path
15
+ self.gsub(/\/(.?)/) { "::#{$1.upcase}" }
16
+ end
17
+
18
+ def camelize_name
19
+ self.gsub(/(?:^|_)(.)/) { $1.upcase }
10
20
  end
11
21
 
12
22
  end
@@ -5,6 +5,7 @@ require 'active_model'
5
5
 
6
6
  module CouchModel
7
7
 
8
+ # Extension of class Base to implement the ActiveModel interface.
8
9
  class Base
9
10
  extend ::ActiveModel::Naming
10
11
  extend ::ActiveModel::Callbacks
@@ -15,13 +16,16 @@ module CouchModel
15
16
  include ::ActiveModel::Serializers::JSON
16
17
  include ::ActiveModel::Serializers::Xml
17
18
 
19
+ # The InvalidModelError is raised, e. g. if the save! method is called on an invalid model.
20
+ class InvalidModelError < StandardError; end
21
+
18
22
  CALLBACKS = [ :initialize, :save, :create, :update, :destroy ].freeze unless defined?(CALLBACKS)
19
23
 
20
24
  define_model_callbacks *CALLBACKS
21
25
 
22
26
  CALLBACKS.each do |method_name|
23
27
 
24
- alias :"#{method_name}_without_callbacks" :"#{method_name}"
28
+ alias_method :"#{method_name}_without_callbacks", :"#{method_name}"
25
29
 
26
30
  define_method :"#{method_name}" do |*arguments|
27
31
  send :"_run_#{method_name}_callbacks" do
@@ -35,6 +39,10 @@ module CouchModel
35
39
 
36
40
  alias destroyed? new?
37
41
 
42
+ def to_param
43
+ id
44
+ end
45
+
38
46
  alias save_without_active_model save
39
47
 
40
48
  def save
@@ -44,6 +52,12 @@ module CouchModel
44
52
  result
45
53
  end
46
54
 
55
+ def save!
56
+ raise InvalidModelError, "errors: #{errors.full_messages.join(' / ')}" unless valid?
57
+ raise StandardError, "unknown error while saving model" unless save
58
+ true
59
+ end
60
+
47
61
  private
48
62
 
49
63
  def discard_changes!
@@ -55,17 +69,18 @@ module CouchModel
55
69
 
56
70
  alias key_accessor_without_dirty key_accessor
57
71
 
58
- def key_accessor(key)
72
+ def key_accessor(key, options = { })
59
73
  add_key key
60
74
  redefine_attribute_methods
61
75
 
62
- key_accessor_without_dirty key
76
+ key_accessor_without_dirty key, options
77
+ redefine_key_writer key
78
+ end
63
79
 
64
- alias_method :"#{key}_without_dirty=", :"#{key}="
65
- define_method :"#{key}=" do |value|
66
- send :"#{key}_will_change!"
67
- send :"#{key}_without_dirty=", value
68
- end
80
+ def create!(*arguments)
81
+ model = new *arguments
82
+ model.save!
83
+ model
69
84
  end
70
85
 
71
86
  private
@@ -80,6 +95,14 @@ module CouchModel
80
95
  define_attribute_methods @keys
81
96
  end
82
97
 
98
+ def redefine_key_writer(key)
99
+ alias_method :"#{key}_without_dirty=", :"#{key}="
100
+ define_method :"#{key}=" do |value|
101
+ send :"#{key}_will_change!"
102
+ send :"#{key}_without_dirty=", value
103
+ end
104
+ end
105
+
83
106
  end
84
107
 
85
108
  end
@@ -1,6 +1,7 @@
1
1
 
2
2
  module CouchModel
3
3
 
4
+ # This should extend the Base class to provide key_accessor methods.
4
5
  class Base
5
6
 
6
7
  module Accessor
@@ -13,21 +14,23 @@ module CouchModel
13
14
 
14
15
  module ClassMethods
15
16
 
16
- def key_reader(key)
17
+ def key_reader(key, options = { })
18
+ set_default key, options[:default] if options.has_key?(:default)
17
19
  define_method :"#{key}" do
18
20
  @attributes[key.to_s]
19
21
  end
20
22
  end
21
23
 
22
- def key_writer(key)
24
+ def key_writer(key, options = { })
25
+ set_default key, options[:default] if options.has_key?(:default)
23
26
  define_method :"#{key}=" do |value|
24
27
  @attributes[key.to_s] = value
25
28
  end
26
29
  end
27
30
 
28
- def key_accessor(key)
29
- key_reader key
30
- key_writer key
31
+ def key_accessor(*arguments)
32
+ key_reader *arguments
33
+ key_writer *arguments
31
34
  end
32
35
 
33
36
  end
@@ -1,6 +1,8 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "core_extension", "array"))
1
2
 
2
3
  module CouchModel
3
4
 
5
+ # This should extend the Base class to provide association methods.
4
6
  class Base
5
7
 
6
8
  module Association
@@ -18,13 +20,30 @@ module CouchModel
18
20
  key = options[:key] || "#{name}_id"
19
21
 
20
22
  key_accessor key
23
+ define_belongs_to_reader name, class_name, key
24
+ define_belongs_to_writer name, class_name, key
25
+ end
26
+
27
+ def has_many(name, options = { })
28
+ class_name = options[:class_name] || name.to_s.camelize
29
+ view_name = options[:view_name] || raise(ArgumentError, "no view_name is given")
30
+ query = options[:query]
31
+
32
+ define_has_many_query name, query
33
+ define_has_many_reader name, class_name, view_name
34
+ end
35
+
36
+ private
21
37
 
22
- define_method :"#{name}" do
38
+ def define_belongs_to_reader(reader_name, class_name, key)
39
+ define_method :"#{reader_name}" do
23
40
  klass = Object.const_get class_name
24
41
  klass.find self.send(key)
25
- end
42
+ end
43
+ end
26
44
 
27
- define_method :"#{name}=" do |value|
45
+ def define_belongs_to_writer(writer_name, class_name, key)
46
+ define_method :"#{writer_name}=" do |value|
28
47
  klass = Object.const_get class_name
29
48
  if value
30
49
  raise ArgumentError, "only objects of class #{klass} are accepted" unless value.is_a?(klass)
@@ -35,22 +54,24 @@ module CouchModel
35
54
  end
36
55
  end
37
56
 
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]
57
+ def define_has_many_query(query_name, query)
58
+ define_method :"#{query_name}_query", &query if query.is_a?(Proc)
59
+ end
42
60
 
43
- define_method :"#{name}_query", &query if query.is_a?(Proc)
61
+ def define_has_many_reader(reader_name, class_name, view_name)
62
+ define_method :"#{reader_name}" do |*arguments|
63
+ last_argument = arguments.last
64
+ hash_argument = last_argument.is_a?(Hash) && last_argument
44
65
 
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
66
+ arity = 0
67
+ query = if self.respond_to?(:"#{reader_name}_query")
68
+ arity = self.method(:"#{reader_name}_query").arity
69
+ self.send :"#{reader_name}_query", *arguments.resize(arity)
50
70
  else
51
- { :key => "\"#{self.id}\"" }
71
+ hash_argument || { :key => self.id }
52
72
  end
53
- klass.send :"#{view_name}", query
73
+ query.merge! hash_argument if hash_argument && arity < arguments.size
74
+ Object.const_get(class_name).send :"#{view_name}", query
54
75
  end
55
76
  end
56
77
 
@@ -1,6 +1,7 @@
1
1
 
2
2
  module CouchModel
3
3
 
4
+ # This should extend the Base class to provide basic find methods.
4
5
  class Base
5
6
 
6
7
  module Finder
@@ -1,6 +1,7 @@
1
1
 
2
2
  module CouchModel
3
3
 
4
+ # This should extend the Base class to provide setup methods.
4
5
  class Base
5
6
 
6
7
  module Setup
@@ -37,12 +38,16 @@ module CouchModel
37
38
  @design || raise(StandardError, "no database defined!")
38
39
  end
39
40
 
41
+ def count
42
+ all.total_count
43
+ end
44
+
40
45
  def method_missing(method_name, *arguments, &block)
41
46
  view = find_view method_name
42
47
  view ? view.collection(*arguments) : super
43
48
  end
44
49
 
45
- def respond_to?(method_name)
50
+ def respond_to?(method_name, *arguments)
46
51
  view = find_view method_name
47
52
  view ? true : super
48
53
  end
@@ -12,20 +12,28 @@ require 'uri'
12
12
 
13
13
  module CouchModel
14
14
 
15
+ # Base is the main super class of all models that should be stored in CouchDB.
16
+ # See the README file for more informations.
15
17
  class Base
16
18
  include CouchModel::Base::Setup
17
19
  include CouchModel::Base::Accessor
18
20
  include CouchModel::Base::Finder
19
21
  include CouchModel::Base::Association
20
22
 
21
- class Error < StandardError; end
23
+ # The NotFoundError will be raised if an operation is tried on a document that
24
+ # dosen't exists.
22
25
  class NotFoundError < StandardError; end
23
26
 
24
27
  attr_reader :attributes
25
28
 
26
29
  def initialize(attributes = { })
27
- @attributes = { Configuration::CLASS_KEY => self.class.to_s }
30
+ klass = self.class
31
+ @attributes = { Configuration::CLASS_KEY => klass.to_s }
28
32
  self.attributes = attributes
33
+
34
+ klass.defaults.each do |key, value|
35
+ @attributes[key] = value unless @attributes.has_key?(key)
36
+ end
29
37
  end
30
38
 
31
39
  def attributes=(attributes)
@@ -55,29 +63,25 @@ module CouchModel
55
63
  end
56
64
 
57
65
  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
66
+ load_response Transport.request(:get, url, :expected_status_code => 200)
63
67
  true
64
- rescue Transport::UnexpectedStatusCodeError => e
65
- raise NotFoundError if e.status_code == 404
66
- raise e
68
+ rescue Transport::UnexpectedStatusCodeError => error
69
+ upgrade_unexpected_status_error error
67
70
  end
68
71
 
72
+ alias reload load
73
+
69
74
  def save
70
75
  new? ? create : update
71
76
  end
72
77
 
73
78
  def destroy
74
79
  return false if new?
75
- Transport.request :delete, self.url, :parameters => { "rev" => self.rev }, :expected_status_code => 200
76
- self.rev = nil
80
+ Transport.request :delete, self.url, :headers => { "If-Match" => self.rev }, :expected_status_code => 200
81
+ clear_rev
77
82
  true
78
- rescue Transport::UnexpectedStatusCodeError => e
79
- raise NotFoundError if e.status_code == 404
80
- raise e
83
+ rescue Transport::UnexpectedStatusCodeError => error
84
+ upgrade_unexpected_status_error error
81
85
  end
82
86
 
83
87
  def url
@@ -95,8 +99,13 @@ module CouchModel
95
99
  @attributes["_rev"] = value
96
100
  end
97
101
 
102
+ def load_response(response)
103
+ self.rev = response["_rev"]
104
+ self.attributes = response
105
+ end
106
+
98
107
  def create
99
- response = Transport.request :post, self.database.url, :json => self.attributes, :expected_status_code => 201
108
+ response = Transport.request :post, self.database.url, :body => self.attributes, :expected_status_code => 201
100
109
  self.id = response["id"]
101
110
  self.rev = response["rev"]
102
111
  true
@@ -105,13 +114,42 @@ module CouchModel
105
114
  end
106
115
 
107
116
  def update
108
- response = Transport.request :put, self.url, :json => self.attributes, :expected_status_code => 201
117
+ response = Transport.request :put, self.url, :body => self.attributes, :expected_status_code => 201
109
118
  self.rev = response["rev"]
110
119
  true
111
120
  rescue Transport::UnexpectedStatusCodeError
112
121
  false
113
122
  end
114
123
 
124
+ def clear_rev
125
+ self.rev = nil
126
+ end
127
+
128
+ def upgrade_unexpected_status_error(error)
129
+ raise NotFoundError if error.status_code == 404
130
+ raise error
131
+ end
132
+
133
+ def self.set_default(key, value)
134
+ @defaults ||= { }
135
+ @defaults[key.to_s] = value
136
+ end
137
+
138
+ def self.defaults
139
+ @defaults || { }
140
+ end
141
+
142
+ def self.create(*arguments)
143
+ model = new *arguments
144
+ model.save ? model : nil
145
+ end
146
+
147
+ def self.destroy_all
148
+ all.each do |model|
149
+ model.destroy
150
+ end
151
+ end
152
+
115
153
  end
116
154
 
117
155
  end
@@ -1,9 +1,11 @@
1
- require File.join(File.dirname(__FILE__), "configuration")
2
1
  require File.join(File.dirname(__FILE__), "transport")
3
- require 'json'
2
+ require File.join(File.dirname(__FILE__), "row")
4
3
 
5
4
  module CouchModel
6
5
 
6
+ # Collection is a proxy class for the resultset of a CouchDB view. It provides
7
+ # all read-only methods of an array. The loading of content is lazy and
8
+ # will be triggerd on the first request.
7
9
  class Collection
8
10
 
9
11
  REQUEST_PARAMETER_KEYS = [
@@ -25,10 +27,11 @@ module CouchModel
25
27
 
26
28
  def initialize(url, options = { })
27
29
  @url, @options = url, options
30
+ @options[:returns] ||= :models
28
31
  end
29
32
 
30
33
  def total_count
31
- fetch :meta => true unless @total_count
34
+ fetch_meta unless @total_count
32
35
  @total_count
33
36
  end
34
37
 
@@ -47,38 +50,55 @@ module CouchModel
47
50
 
48
51
  private
49
52
 
50
- def fetch(options = { })
51
- meta = options[:meta] || false
53
+ def fetch
54
+ fetch_response
55
+ evaluate_total_count
56
+ evaluate_entries
57
+ true
58
+ end
52
59
 
53
- evaluate Transport.request(
60
+ def fetch_meta
61
+ fetch_meta_response
62
+ evaluate_total_count
63
+ true
64
+ end
65
+
66
+ def fetch_response
67
+ @response = Transport.request(
54
68
  :get, url,
55
- :parameters => request_parameters.merge(meta ? { "limit" => "0" } : { }),
69
+ :parameters => request_parameters,
56
70
  :expected_status_code => 200
57
71
  )
58
-
59
- true
60
72
  end
61
73
 
62
- def evaluate(response)
63
- @total_count = response["total_rows"]
64
- @entries = response["rows"].select do |row|
65
- row["doc"].has_key?(Configuration::CLASS_KEY) && Object.const_defined?(row["doc"][Configuration::CLASS_KEY])
66
- end.map do |row|
67
- model_class = Object.const_get row["doc"][Configuration::CLASS_KEY]
68
- model = model_class.new
69
- model.instance_variable_set :@attributes, row["doc"]
70
- model
71
- end
74
+ def fetch_meta_response
75
+ @response = Transport.request(
76
+ :get, url,
77
+ :parameters => request_parameters.merge(:limit => 0),
78
+ :expected_status_code => 200
79
+ )
72
80
  end
73
81
 
74
82
  def request_parameters
75
- parameters = { "include_docs" => "true" }
83
+ parameters = @options[:returns] == :models ? { :include_docs => true } : { }
76
84
  REQUEST_PARAMETER_KEYS.each do |key|
77
- parameters[ key.to_s ] = @options[key].is_a?(Array) ? JSON.dump(@options[key]) : @options[key].to_s if @options[key]
85
+ parameters[ key ] = @options[key] if @options.has_key?(key)
78
86
  end
79
87
  parameters
80
88
  end
81
89
 
90
+ def evaluate_total_count
91
+ @total_count = @response["total_rows"]
92
+ end
93
+
94
+ def evaluate_entries
95
+ returns = @options[:returns]
96
+ @entries = @response["rows"].map do |row_hash|
97
+ row = CouchModel::Row.new row_hash
98
+ returns == :models ? row.model : row
99
+ end
100
+ end
101
+
82
102
  end
83
103
 
84
104
  end