couchmodel 0.1.0.beta3 → 0.1.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
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