peleteiro-activecouch 0.2.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.
Files changed (58) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README +28 -0
  3. data/Rakefile +52 -0
  4. data/VERSION +1 -0
  5. data/lib/active_couch.rb +12 -0
  6. data/lib/active_couch/base.rb +608 -0
  7. data/lib/active_couch/callbacks.rb +89 -0
  8. data/lib/active_couch/connection.rb +164 -0
  9. data/lib/active_couch/errors.rb +13 -0
  10. data/lib/active_couch/support.rb +3 -0
  11. data/lib/active_couch/support/exporter.rb +97 -0
  12. data/lib/active_couch/support/extensions.rb +86 -0
  13. data/lib/active_couch/support/inflections.rb +52 -0
  14. data/lib/active_couch/support/inflector.rb +279 -0
  15. data/lib/active_couch/views.rb +3 -0
  16. data/lib/active_couch/views/errors.rb +4 -0
  17. data/lib/active_couch/views/raw_view.rb +40 -0
  18. data/lib/active_couch/views/view.rb +85 -0
  19. data/lib/activecouch.rb +1 -0
  20. data/spec/base/after_delete_spec.rb +110 -0
  21. data/spec/base/after_save_spec.rb +102 -0
  22. data/spec/base/before_delete_spec.rb +109 -0
  23. data/spec/base/before_save_spec.rb +101 -0
  24. data/spec/base/count_all_spec.rb +29 -0
  25. data/spec/base/count_spec.rb +77 -0
  26. data/spec/base/create_spec.rb +28 -0
  27. data/spec/base/database_spec.rb +70 -0
  28. data/spec/base/delete_spec.rb +97 -0
  29. data/spec/base/find_from_url_spec.rb +55 -0
  30. data/spec/base/find_spec.rb +383 -0
  31. data/spec/base/from_json_spec.rb +54 -0
  32. data/spec/base/has_many_spec.rb +89 -0
  33. data/spec/base/has_spec.rb +88 -0
  34. data/spec/base/id_spec.rb +25 -0
  35. data/spec/base/initialize_spec.rb +91 -0
  36. data/spec/base/marshal_dump_spec.rb +64 -0
  37. data/spec/base/marshal_load_spec.rb +58 -0
  38. data/spec/base/module_spec.rb +18 -0
  39. data/spec/base/nested_class_spec.rb +19 -0
  40. data/spec/base/rev_spec.rb +20 -0
  41. data/spec/base/save_spec.rb +130 -0
  42. data/spec/base/site_spec.rb +62 -0
  43. data/spec/base/to_json_spec.rb +73 -0
  44. data/spec/connection/initialize_spec.rb +28 -0
  45. data/spec/exporter/all_databases_spec.rb +24 -0
  46. data/spec/exporter/create_database_spec.rb +47 -0
  47. data/spec/exporter/delete_database_spec.rb +45 -0
  48. data/spec/exporter/delete_spec.rb +36 -0
  49. data/spec/exporter/export_spec.rb +62 -0
  50. data/spec/exporter/export_with_raw_views_spec.rb +66 -0
  51. data/spec/spec_helper.rb +9 -0
  52. data/spec/views/define_spec.rb +34 -0
  53. data/spec/views/include_attributes_spec.rb +30 -0
  54. data/spec/views/raw_view_spec.rb +49 -0
  55. data/spec/views/to_json_spec.rb +58 -0
  56. data/spec/views/with_filter_spec.rb +13 -0
  57. data/spec/views/with_key_spec.rb +19 -0
  58. metadata +117 -0
@@ -0,0 +1,89 @@
1
+ module ActiveCouch
2
+ module Callbacks
3
+ CALLBACKS = %w(before_save after_save before_delete after_delete)
4
+
5
+ def self.included(base)
6
+ # Alias methods which will have callbacks, (for now only save and delete).
7
+ # This creates 2 pairs of methods: save_with_callbacks, save_without_callbacks,
8
+ # delete_with_callbacks, delete_without_callbacks
9
+ #
10
+ # save_without_callbacks and delete_without_callbacks
11
+ # have the same behaviour as the save and delete methods, respectively
12
+ [:save, :delete].each do |method|
13
+ base.send :alias_method_chain, method, :callbacks
14
+ end
15
+
16
+ CALLBACKS.each do |method|
17
+ base.class_eval <<-"end_eval"
18
+ def self.#{method}(*callbacks, &block)
19
+ callbacks << block if block_given?
20
+ # Assumes that the default value for the callbacks hash in the
21
+ # including class is an empty array
22
+ self.callbacks[#{method.to_sym.inspect}] = self.callbacks[#{method.to_sym.inspect}] + callbacks
23
+ end
24
+ end_eval
25
+ end
26
+ end # end method self.included
27
+
28
+ def before_save() end
29
+
30
+ def after_save() end
31
+
32
+ def before_delete() end
33
+
34
+ def after_delete() end
35
+
36
+ def save_with_callbacks(opts = {})
37
+ return false if callback(:before_save) == false
38
+ result = save_without_callbacks(opts)
39
+ callback(:after_save)
40
+ result
41
+ end
42
+ private :save_with_callbacks
43
+
44
+ def delete_with_callbacks(opts = {})
45
+ return false if callback(:before_delete) == false
46
+ result = delete_without_callbacks(opts)
47
+ callback(:after_delete)
48
+ result
49
+ end
50
+ private :delete_with_callbacks
51
+
52
+ def find_with_callbacks
53
+ return false if callback(:before_find) == false
54
+ result = find_without_callbacks
55
+ callback(:after_find)
56
+ result
57
+ end
58
+ private :find_with_callbacks
59
+
60
+ private
61
+ def callback(method)
62
+ callbacks_for(method).each do |callback|
63
+ result = case callback
64
+ when Symbol
65
+ self.send(callback)
66
+ when String
67
+ eval(callback, binding)
68
+ when Proc, Method
69
+ callback.call(self)
70
+ else
71
+ if callback.respond_to?(method)
72
+ callback.send(method, self)
73
+ else
74
+ raise ActiveCouchError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
75
+ end
76
+ end
77
+ return false if result == false
78
+ end
79
+
80
+ result = send(method) if respond_to?(method)
81
+
82
+ return result
83
+ end
84
+
85
+ def callbacks_for(method)
86
+ self.class.callbacks[method.to_sym]
87
+ end
88
+ end # end module Callbacks
89
+ end # end module ActiveCouch
@@ -0,0 +1,164 @@
1
+ # Connection class borrowed from ActiveResource
2
+ require 'net/https'
3
+ require 'date'
4
+ require 'time'
5
+ require 'uri'
6
+ require 'benchmark'
7
+
8
+ module ActiveCouch
9
+ class ConnectionError < StandardError # :nodoc:
10
+ attr_reader :response
11
+
12
+ def initialize(response, message = nil)
13
+ @response = response
14
+ @message = message
15
+ end
16
+
17
+ def to_s
18
+ "Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
19
+ end
20
+ end
21
+
22
+ # 3xx Redirection
23
+ class Redirection < ConnectionError # :nodoc:
24
+ def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
25
+ end
26
+
27
+ # 4xx Client Error
28
+ class ClientError < ConnectionError; end # :nodoc:
29
+
30
+ # 404 Not Found
31
+ class ResourceNotFound < ClientError; end # :nodoc:
32
+
33
+ # 409 Conflict
34
+ class ResourceConflict < ClientError; end # :nodoc:
35
+
36
+ # 412 Precondition Failed - this is returned when there is an update conflict (usually means revisions don't match).
37
+ class UpdateConflict < ClientError # :nodoc:
38
+ def to_s
39
+ "Failed with #{response.code} #{response.message if response.respond_to?(:message)}. Body: #{response.body}"
40
+ end
41
+ end
42
+
43
+ # 5xx Server Error
44
+ class ServerError < ConnectionError; end # :nodoc:
45
+
46
+ # 405 Method Not Allowed
47
+ class MethodNotAllowed < ClientError # :nodoc:
48
+ def allowed_methods
49
+ @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
50
+ end
51
+ end
52
+
53
+ # Class to handle connections to remote web services.
54
+ # This class is used by ActiveCouch::Base to interface with REST
55
+ # services.
56
+ class Connection
57
+ attr_reader :site
58
+
59
+ class << self
60
+ def requests
61
+ @@requests ||= []
62
+ end
63
+ end
64
+
65
+ # The +site+ parameter is required and will set the +site+
66
+ # attribute to the URI for the remote resource service.
67
+ def initialize(site)
68
+ raise ArgumentError, 'Missing site URI' unless site
69
+ init_site_with_path(site)
70
+ end
71
+
72
+ # Set URI for remote service.
73
+ def site=(site)
74
+ @site = site.is_a?(URI) ? site : URI.parse(site)
75
+ end
76
+
77
+ # Execute a GET request.
78
+ # Used to get (find) resources.
79
+ def get(path, headers = {})
80
+ request(:get, path, build_request_headers(headers)).body
81
+ end
82
+
83
+ # Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
84
+ # Used to delete resources.
85
+ def delete(path, headers = {})
86
+ request(:delete, path, build_request_headers(headers))
87
+ end
88
+
89
+ # Execute a PUT request (see HTTP protocol documentation if unfamiliar).
90
+ # Used to update resources.
91
+ def put(path, body = '', headers = {})
92
+ request(:put, path, body.to_s, build_request_headers(headers))
93
+ end
94
+
95
+ # Execute a POST request.
96
+ # Used to create new resources.
97
+ def post(path, body = '', headers = {})
98
+ request(:post, path, body.to_s, build_request_headers(headers))
99
+ end
100
+
101
+
102
+ private
103
+ # Makes request to remote service.
104
+ def request(method, path, *arguments)
105
+ result = nil
106
+ time = Benchmark.realtime { result = http.send(method, path, *arguments) }
107
+ handle_response(result)
108
+ end
109
+
110
+ # Handles response and error codes from remote service.
111
+ def handle_response(response)
112
+ case response.code.to_i
113
+ when 301,302
114
+ raise(Redirection.new(response))
115
+ when 200...400
116
+ response
117
+ when 404
118
+ raise(ResourceNotFound.new(response))
119
+ when 405
120
+ raise(MethodNotAllowed.new(response))
121
+ when 409
122
+ raise(ResourceConflict.new(response))
123
+ when 412
124
+ raise(UpdateConflict.new(response))
125
+ when 422
126
+ raise(ResourceInvalid.new(response))
127
+ when 401...500
128
+ raise(ClientError.new(response))
129
+ when 500...600
130
+ raise(ServerError.new(response))
131
+ else
132
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
133
+ end
134
+ end
135
+
136
+ # Creates new Net::HTTP instance for communication with
137
+ # remote service and resources.
138
+ def http
139
+ http = Net::HTTP.new(@site.host, @site.port)
140
+ http.use_ssl = @site.is_a?(URI::HTTPS)
141
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
142
+ http
143
+ end
144
+
145
+ def default_header
146
+ @default_header ||= { 'Content-Type' => 'application/json' }
147
+ end
148
+
149
+ # Builds headers for request to remote service.
150
+ def build_request_headers(headers)
151
+ authorization_header.update(default_header).update(headers)
152
+ end
153
+
154
+ # Sets authorization header; authentication information is pulled from credentials provided with site URI.
155
+ def authorization_header
156
+ (@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {})
157
+ end
158
+
159
+ def init_site_with_path(site)
160
+ site = "#{site}:5984" if site.is_a?(String) && (site =~ /http\:\/\/(.*?)\:(\d+)/).nil?
161
+ @site = URI.parse(site)
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveCouch
2
+ # Base exception class for all ActiveCouch errors.
3
+ class ActiveCouchError < StandardError
4
+ end
5
+
6
+ # Raised when there is a configuration error (duh).
7
+ class ConfigurationError < ActiveCouchError
8
+ end
9
+
10
+ # Raised when trying to get or set a non-existent attribute.
11
+ class AttributeMissingError < ActiveCouchError
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ require 'active_couch/support/inflector'
2
+ require 'active_couch/support/extensions'
3
+ require 'active_couch/support/exporter'
@@ -0,0 +1,97 @@
1
+ # TODO
2
+ # - might consider moving create_database and delete_database to an adapter type class to encapsulate CouchDB semantics
3
+ # and responses.
4
+ module ActiveCouch
5
+ class Exporter
6
+ class << self # Class methods
7
+
8
+ def all_databases(site)
9
+ conn = Connection.new(site)
10
+ JSON.parse(conn.get("/_all_dbs"))
11
+ end
12
+
13
+ def export(site, view, opts = {})
14
+ existing_view = {}
15
+ if view.name.nil? || (view.database.nil? && opts[:database].nil?)
16
+ raise ActiveCouch::ViewError, "Both the name and the database need to be defined in your view"
17
+ end
18
+ # If the database is not defined in the view, it can be supported
19
+ # as an option to the export method
20
+ database_name = opts[:database] || view.database
21
+ conn = Connection.new(site)
22
+ # The view function for a view with name 'by_name' and database 'activecouch_test' should be PUT to
23
+ # http://#{host}:#{port}/activecouch_test/_design/by_name.
24
+ if(view_json = exists?(site, "/#{database_name}/_design/#{view.name}"))
25
+ existing_view = JSON.parse(view_json)
26
+ end
27
+ response = conn.put("/#{database_name}/_design/#{view.name}", view.to_json(existing_view))
28
+ case response.code
29
+ when '201'
30
+ true # 201 = success
31
+ else
32
+ raise ActiveCouch::ViewError, "Error exporting view - got HTTP response #{response.code}"
33
+ end
34
+ end
35
+
36
+ def delete(site, view, opts = {})
37
+ rev = nil
38
+ if view.name.nil? || (view.database.nil? && opts[:database].nil?)
39
+ raise ActiveCouch::ViewError, "Both the name and the database need to be defined in your view"
40
+ end
41
+ # If the database is not defined in the view, it can be supported
42
+ # as an option to the export method
43
+ database_name = opts[:database] || view.database
44
+ conn = Connection.new(site)
45
+ if(view_json = exists?(site, "/#{database_name}/_design/#{view.name}"))
46
+ rev = JSON.parse(view_json)['_rev']
47
+ end
48
+ # The view function for a view with name 'by_name' and database 'activecouch_test' should be PUT to
49
+ # http://#{host}:#{port}/activecouch_test/_design/by_name.
50
+ response = conn.delete("/#{database_name}/_design/#{view.name}?rev=#{rev}")
51
+ if response.code =~ /20[0,2]/
52
+ true # 20[0,2] = success
53
+ else
54
+ raise ActiveCouch::ViewError, "Error deleting view - got HTTP response #{response.code}"
55
+ end
56
+ end
57
+
58
+ def exists?(site, name)
59
+ conn = Connection.new(site)
60
+ response = conn.get("#{name}")
61
+ response
62
+ rescue ActiveCouch::ResourceNotFound
63
+ false
64
+ end
65
+
66
+ def create_database(site, name)
67
+ conn = Connection.new(site)
68
+ response = conn.put("/#{name}", "{}")
69
+
70
+ case response.code
71
+ when '201' # 201 = success
72
+ true
73
+ when '409' # 409 = database already exists
74
+ raise ActiveCouch::ViewError, 'Database exists'
75
+ else
76
+ raise ActiveCouch::ViewError, "Error creating database - got HTTP response #{response.code}"
77
+ end
78
+ end
79
+
80
+ def delete_database(site, name)
81
+ conn = Connection.new(site)
82
+ response = conn.delete("/#{name}")
83
+
84
+ case response.code
85
+ when '200'
86
+ when '201'
87
+ when '202'
88
+ true # 201 = success
89
+ when '404'
90
+ raise ActiveCouch::ViewError, "Database '#{name}' does not exist" # 404 = database doesn't exist
91
+ else
92
+ raise ActiveCouch::ViewError, "Error creating database - got HTTP response #{response.code}"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,86 @@
1
+ module ActiveCouch
2
+
3
+ Symbol.class_eval do
4
+ def singularize; ActiveCouch::Inflector.singularize(self); end
5
+ end
6
+
7
+ String.class_eval do
8
+ require 'cgi'
9
+ def url_encode; CGI.escape("\"#{self.to_s}\""); end
10
+ # Delegate to Inflector
11
+ def singularize; ActiveCouch::Inflector.singularize(self); end
12
+ def demodulize; ActiveCouch::Inflector.demodulize(self); end
13
+ def pluralize; ActiveCouch::Inflector.pluralize(self); end
14
+ def underscore; ActiveCouch::Inflector.underscore(self); end
15
+ def classify; ActiveCouch::Inflector.classify(self); end
16
+ def constantize; ActiveCouch::Inflector.constantize(self); end
17
+ end
18
+
19
+ Array.class_eval do
20
+ def extract_options!
21
+ last.is_a?(::Hash) ? pop : {}
22
+ end
23
+ end
24
+
25
+ Hash.class_eval do
26
+ # Flatten on the array removes everything into *one* single array,
27
+ # so {}.to_a.flatten sometimes won't work nicely because a value might be an array
28
+ # So..introducing flatten for Hash, so that arrays which are values (to keys)
29
+ # are retained
30
+ def flatten
31
+ (0...self.size).inject([]) {|k,v| k << self.keys[v]; k << self.values[v]}
32
+ end
33
+ end
34
+
35
+ Object.class_eval do
36
+ def get_class(name)
37
+ # From 'The Ruby Way Second Edition' by Hal Fulton
38
+ # This is to get nested class for e.g. A::B::C
39
+ name.split("::").inject(Object) {|x,y| x.const_get(y)}
40
+ end
41
+
42
+ # The singleton class.
43
+ def metaclass; class << self; self; end; end
44
+ def meta_eval &blk; metaclass.instance_eval &blk; end
45
+
46
+ # Adds methods to a metaclass.
47
+ def meta_def name, &blk
48
+ meta_eval { define_method name, &blk }
49
+ end
50
+
51
+ # Defines an instance method within a class.
52
+ def class_def name, &blk
53
+ class_eval { define_method name, &blk }
54
+ end
55
+ end
56
+
57
+ Module.module_eval do
58
+ # Return the module which contains this one; if this is a root module, such as
59
+ # +::MyModule+, then Object is returned.
60
+ def parent
61
+ parent_name = name.split('::')[0..-2] * '::'
62
+ parent_name.empty? ? Object : ActiveCouch::Inflector.constantize(parent_name)
63
+ end
64
+
65
+ def alias_method_chain(target, feature)
66
+ # Strip out punctuation on predicates or bang methods since
67
+ # e.g. target?_without_feature is not a valid method name.
68
+ aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
69
+ yield(aliased_target, punctuation) if block_given?
70
+
71
+ with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"
72
+
73
+ alias_method without_method, target
74
+ alias_method target, with_method
75
+
76
+ case
77
+ when public_method_defined?(without_method)
78
+ public target
79
+ when protected_method_defined?(without_method)
80
+ protected target
81
+ when private_method_defined?(without_method)
82
+ private target
83
+ end
84
+ end
85
+ end
86
+ end