activecouch 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README +42 -0
  3. data/Rakefile +31 -0
  4. data/VERSION +1 -0
  5. data/lib/active_couch.rb +18 -0
  6. data/lib/active_couch/associations.rb +1 -0
  7. data/lib/active_couch/associations/has_many_association.rb +35 -0
  8. data/lib/active_couch/attribute.rb +46 -0
  9. data/lib/active_couch/base.rb +458 -0
  10. data/lib/active_couch/callbacks.rb +81 -0
  11. data/lib/active_couch/connection.rb +155 -0
  12. data/lib/active_couch/errors.rb +17 -0
  13. data/lib/active_couch/migrations.rb +3 -0
  14. data/lib/active_couch/migrations/errors.rb +4 -0
  15. data/lib/active_couch/migrations/migration.rb +77 -0
  16. data/lib/active_couch/migrations/migrator.rb +53 -0
  17. data/lib/active_couch/support.rb +2 -0
  18. data/lib/active_couch/support/extensions.rb +92 -0
  19. data/lib/active_couch/support/inflections.rb +52 -0
  20. data/lib/active_couch/support/inflector.rb +280 -0
  21. data/spec/attribute/initialize_spec.rb +138 -0
  22. data/spec/base/after_delete_spec.rb +107 -0
  23. data/spec/base/after_save_spec.rb +99 -0
  24. data/spec/base/before_delete_spec.rb +106 -0
  25. data/spec/base/before_save_spec.rb +98 -0
  26. data/spec/base/create_spec.rb +27 -0
  27. data/spec/base/database_spec.rb +65 -0
  28. data/spec/base/delete_spec.rb +78 -0
  29. data/spec/base/find_spec.rb +165 -0
  30. data/spec/base/from_json_spec.rb +48 -0
  31. data/spec/base/has_many_spec.rb +81 -0
  32. data/spec/base/has_spec.rb +76 -0
  33. data/spec/base/id_spec.rb +22 -0
  34. data/spec/base/initialize_spec.rb +43 -0
  35. data/spec/base/module_spec.rb +18 -0
  36. data/spec/base/nested_class_spec.rb +19 -0
  37. data/spec/base/rev_spec.rb +16 -0
  38. data/spec/base/save_spec.rb +65 -0
  39. data/spec/base/site_spec.rb +41 -0
  40. data/spec/base/to_json_spec.rb +64 -0
  41. data/spec/connection/initialize_spec.rb +28 -0
  42. data/spec/has_many_association/initialize_spec.rb +33 -0
  43. data/spec/migration/define_spec.rb +29 -0
  44. data/spec/migration/include_attributes_spec.rb +30 -0
  45. data/spec/migration/view_js_spec.rb +46 -0
  46. data/spec/migration/with_filter_spec.rb +13 -0
  47. data/spec/migration/with_key_spec.rb +13 -0
  48. data/spec/migrator/create_database_spec.rb +48 -0
  49. data/spec/migrator/delete_database_spec.rb +46 -0
  50. data/spec/migrator/migrate_spec.rb +99 -0
  51. data/spec/spec_helper.rb +9 -0
  52. metadata +104 -0
@@ -0,0 +1,81 @@
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 sets of methods: save_with_callbacks, save_without_callbacks,
8
+ # delete_with_callbacks, delete_without_callbacks
9
+ #
10
+ # save_without_callbacks and delete_without_callbacks have the same behaviour
11
+ # 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
37
+ return false if callback(:before_save) == false
38
+ result = save_without_callbacks
39
+ callback(:after_save)
40
+ result
41
+ end
42
+ private :save_with_callbacks
43
+
44
+ def delete_with_callbacks
45
+ return false if callback(:before_delete) == false
46
+ result = delete_without_callbacks
47
+ callback(:after_delete)
48
+ result
49
+ end
50
+ private :delete_with_callbacks
51
+
52
+ private
53
+ def callback(method)
54
+ callbacks_for(method).each do |callback|
55
+ result = case callback
56
+ when Symbol
57
+ self.send(callback)
58
+ when String
59
+ eval(callback, binding)
60
+ when Proc, Method
61
+ callback.call(self)
62
+ else
63
+ if callback.respond_to?(method)
64
+ callback.send(method, self)
65
+ else
66
+ 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."
67
+ end
68
+ end
69
+ return false if result == false
70
+ end
71
+
72
+ result = send(method) if respond_to?(method)
73
+
74
+ return result
75
+ end
76
+
77
+ def callbacks_for(method)
78
+ self.class.callbacks[method.to_sym]
79
+ end
80
+ end # end module Callbacks
81
+ end # end module ActiveCouch
@@ -0,0 +1,155 @@
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
+ # 5xx Server Error
37
+ class ServerError < ConnectionError; end # :nodoc:
38
+
39
+ # 405 Method Not Allowed
40
+ class MethodNotAllowed < ClientError # :nodoc:
41
+ def allowed_methods
42
+ @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
43
+ end
44
+ end
45
+
46
+ # Class to handle connections to remote web services.
47
+ # This class is used by ActiveCouch::Base to interface with REST
48
+ # services.
49
+ class Connection
50
+ attr_reader :site
51
+
52
+ class << self
53
+ def requests
54
+ @@requests ||= []
55
+ end
56
+ end
57
+
58
+ # The +site+ parameter is required and will set the +site+
59
+ # attribute to the URI for the remote resource service.
60
+ def initialize(site)
61
+ raise ArgumentError, 'Missing site URI' unless site
62
+ init_site_with_path(site)
63
+ end
64
+
65
+ # Set URI for remote service.
66
+ def site=(site)
67
+ @site = site.is_a?(URI) ? site : URI.parse(site)
68
+ end
69
+
70
+ # Execute a GET request.
71
+ # Used to get (find) resources.
72
+ def get(path, headers = {})
73
+ request(:get, path, build_request_headers(headers)).body
74
+ end
75
+
76
+ # Execute a DELETE request (see HTTP protocol documentation if unfamiliar).
77
+ # Used to delete resources.
78
+ def delete(path, headers = {})
79
+ request(:delete, path, build_request_headers(headers))
80
+ end
81
+
82
+ # Execute a PUT request (see HTTP protocol documentation if unfamiliar).
83
+ # Used to update resources.
84
+ def put(path, body = '', headers = {})
85
+ request(:put, path, body.to_s, build_request_headers(headers))
86
+ end
87
+
88
+ # Execute a POST request.
89
+ # Used to create new resources.
90
+ def post(path, body = '', headers = {})
91
+ request(:post, path, body.to_s, build_request_headers(headers))
92
+ end
93
+
94
+
95
+ private
96
+ # Makes request to remote service.
97
+ def request(method, path, *arguments)
98
+ result = nil
99
+ time = Benchmark.realtime { result = http.send(method, path, *arguments) }
100
+ handle_response(result)
101
+ end
102
+
103
+ # Handles response and error codes from remote service.
104
+ def handle_response(response)
105
+ case response.code.to_i
106
+ when 301,302
107
+ raise(Redirection.new(response))
108
+ when 200...400
109
+ response
110
+ when 404
111
+ raise(ResourceNotFound.new(response))
112
+ when 405
113
+ raise(MethodNotAllowed.new(response))
114
+ when 409
115
+ raise(ResourceConflict.new(response))
116
+ when 422
117
+ raise(ResourceInvalid.new(response))
118
+ when 401...500
119
+ raise(ClientError.new(response))
120
+ when 500...600
121
+ raise(ServerError.new(response))
122
+ else
123
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
124
+ end
125
+ end
126
+
127
+ # Creates new Net::HTTP instance for communication with
128
+ # remote service and resources.
129
+ def http
130
+ http = Net::HTTP.new(@site.host, @site.port)
131
+ http.use_ssl = @site.is_a?(URI::HTTPS)
132
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
133
+ http
134
+ end
135
+
136
+ def default_header
137
+ @default_header ||= { 'Content-Type' => 'application/json' }
138
+ end
139
+
140
+ # Builds headers for request to remote service.
141
+ def build_request_headers(headers)
142
+ authorization_header.update(default_header).update(headers)
143
+ end
144
+
145
+ # Sets authorization header; authentication information is pulled from credentials provided with site URI.
146
+ def authorization_header
147
+ (@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{ @site.password}"].pack('m').delete("\r\n") } : {})
148
+ end
149
+
150
+ def init_site_with_path(site)
151
+ site = "#{site}:5984" if site.is_a?(String) && (site =~ /http\:\/\/(.*?)\:(\d+)/).nil?
152
+ @site = URI.parse(site)
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,17 @@
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 assign a object of an invalid type as an ActiveCouch attribute.
11
+ class InvalidCouchTypeError < ActiveCouchError
12
+ end
13
+
14
+ # Raised when trying to get or set a non-existent attribute.
15
+ class AttributeMissingError < ActiveCouchError
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ require 'active_couch/migrations/errors.rb'
2
+ require 'active_couch/migrations/migration.rb'
3
+ require 'active_couch/migrations/migrator.rb'
@@ -0,0 +1,4 @@
1
+ module ActiveCouch
2
+ class MigrationError < StandardError; end
3
+ class InvalidFilter < MigrationError; end
4
+ end
@@ -0,0 +1,77 @@
1
+ require 'json'
2
+
3
+ module ActiveCouch
4
+ class Migration
5
+ class << self # Class Methods
6
+ # Class instance variables
7
+ @view = nil; @database = nil
8
+ # These are accessible only at class-scope
9
+ attr_accessor :view, :database
10
+ # Set the view name and database name in the define method and then execute
11
+ # the block
12
+ def define(*args)
13
+ # Borrowed from ActiveRecord::Base.find
14
+ first = args.slice!(0); second = args.slice!(0)
15
+ # Based on the classes of the arguments passed, set instance variables
16
+ case first.class.to_s
17
+ when 'String', 'Symbol' then view = first.to_s; options = second || {}
18
+ when 'Hash' then view = ''; options = first
19
+ else raise ArgumentError, "Wrong arguments used to define the view"
20
+ end
21
+ # Define the view and database instance variables based on the args passed
22
+ # Don't care if the key doesn't exist
23
+ @view, @database = get_view(view), options[:for_db]
24
+ # Block being called to set other parameters for the Migration
25
+ yield if block_given?
26
+ end
27
+
28
+ def with_key(key = "")
29
+ @key = key unless key.nil?
30
+ end
31
+
32
+ def with_filter(filter = "")
33
+ @filter = filter unless filter.nil?
34
+ end
35
+
36
+ def include_attributes(*attrs)
37
+ @attrs = attrs unless attrs.nil? || !attrs.is_a?(Array)
38
+ end
39
+
40
+ def view_js
41
+ results_hash = {"_id" => "_design/#{@view}", "language" => "text/javascript"}
42
+ results_hash["views"] = { @view => view_function }
43
+ # Returns the JSON format for the function
44
+ results_hash.to_json
45
+ end
46
+
47
+ private
48
+ def include_attrs
49
+ attrs = "doc"
50
+ unless @attrs.nil?
51
+ js = @attrs.inject([]) {|result, att| result << "#{att}: doc.#{att}"}
52
+ attrs = "{#{js.join(' , ')}}" if js.size > 0
53
+ end
54
+ attrs
55
+ end
56
+
57
+ def get_view(view)
58
+ view_name = view
59
+ view_name = Inflector.underscore("#{self}") if view.nil? || view.length == 0
60
+ view_name
61
+ end
62
+
63
+ def view_function
64
+ filter_present = !@filter.nil? && @filter.length > 0
65
+
66
+ js = "function(doc) { "
67
+ js << "if(#{@filter}) { " if filter_present
68
+ js << "map(doc.#{@key}, #{include_attrs});"
69
+ js << " } " if filter_present
70
+ js << " }"
71
+
72
+ js
73
+ end
74
+
75
+ end # End Class Methods
76
+ end # End Class Migration
77
+ end # End module ActiveCouch
@@ -0,0 +1,53 @@
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 Migrator
6
+ class << self # Class methods
7
+ def migrate(site, migration)
8
+ if migration.view.nil? || migration.database.nil?
9
+ raise ActiveCouch::MigrationError, "Both the view and the database need to be defined in your migration"
10
+ end
11
+
12
+ conn = Connection.new(site)
13
+ # Migration for a view with name 'by_name' and database 'activecouch_test' should be PUT to
14
+ # http://#{host}:#{port}/activecouch_test/_design/by_name.
15
+ response = conn.put("/#{migration.database}/_design/#{migration.view}", migration.view_js)
16
+ case response.code
17
+ when '201'
18
+ true # 201 = success
19
+ else
20
+ raise ActiveCouch::MigrationError, "Error migrating view - got HTTP response #{response.code}"
21
+ end
22
+ end
23
+
24
+ def create_database(site, name)
25
+ conn = Connection.new(site)
26
+ response = conn.put("/#{name}", "{}")
27
+
28
+ case response.code
29
+ when '201' # 201 = success
30
+ true
31
+ when '409' # 409 = database already exists
32
+ raise ActiveCouch::MigrationError, 'Database exists'
33
+ else
34
+ raise ActiveCouch::MigrationError, "Error creating database - got HTTP response #{response.code}"
35
+ end
36
+ end
37
+
38
+ def delete_database(site, name)
39
+ conn = Connection.new(site)
40
+ response = conn.delete("/#{name}")
41
+
42
+ case response.code
43
+ when '202'
44
+ true # 202 = success
45
+ when '404'
46
+ raise ActiveCouch::MigrationError, "Database '#{name}' does not exist" # 404 = database doesn't exist
47
+ else
48
+ raise ActiveCouch::MigrationError, "Error creating database - got HTTP response #{response.code}"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,2 @@
1
+ require 'active_couch/support/inflector'
2
+ require 'active_couch/support/extensions'
@@ -0,0 +1,92 @@
1
+ module ActiveCouch
2
+
3
+ String.class_eval do
4
+ require 'cgi'
5
+ def url_encode
6
+ CGI.escape("\"#{self.to_s}\"")
7
+ end
8
+ end
9
+
10
+ Hash.class_eval do
11
+ # Flatten on the array removes everything into *one* single array,
12
+ # so {}.to_a.flatten sometimes won't work nicely because a value might be an array
13
+ # So..introducing flatten for Hash, so that arrays which are values (to keys)
14
+ # are retained
15
+ def flatten
16
+ (0...self.size).inject([]) {|k,v| k << self.keys[v]; k << self.values[v]}
17
+ end
18
+ end
19
+
20
+ Object.class_eval do
21
+ def get_class(name)
22
+ # From 'The Ruby Way Second Edition' by Hal Fulton
23
+ # This is to get nested class for e.g. A::B::C
24
+ name.split("::").inject(Object) {|x,y| x.const_get(y)}
25
+ end
26
+
27
+ # The singleton class.
28
+ def metaclass; class << self; self; end; end
29
+ def meta_eval &blk; metaclass.instance_eval &blk; end
30
+
31
+ # Adds methods to a metaclass.
32
+ def meta_def name, &blk
33
+ meta_eval { define_method name, &blk }
34
+ end
35
+
36
+ # Defines an instance method within a class.
37
+ def class_def name, &blk
38
+ class_eval { define_method name, &blk }
39
+ end
40
+ end
41
+
42
+ Module.module_eval do
43
+ # Return the module which contains this one; if this is a root module, such as
44
+ # +::MyModule+, then Object is returned.
45
+ def parent
46
+ parent_name = name.split('::')[0..-2] * '::'
47
+ parent_name.empty? ? Object : Inflector.constantize(parent_name)
48
+ end
49
+
50
+ # Encapsulates the common pattern of:
51
+ #
52
+ # alias_method :foo_without_feature, :foo
53
+ # alias_method :foo, :foo_with_feature
54
+ #
55
+ # With this, you simply do:
56
+ #
57
+ # alias_method_chain :foo, :feature
58
+ #
59
+ # And both aliases are set up for you.
60
+ #
61
+ # Query and bang methods (foo?, foo!) keep the same punctuation:
62
+ #
63
+ # alias_method_chain :foo?, :feature
64
+ #
65
+ # is equivalent to
66
+ #
67
+ # alias_method :foo_without_feature?, :foo?
68
+ # alias_method :foo?, :foo_with_feature?
69
+ #
70
+ # so you can safely chain foo, foo?, and foo! with the same feature.
71
+ def alias_method_chain(target, feature)
72
+ # Strip out punctuation on predicates or bang methods since
73
+ # e.g. target?_without_feature is not a valid method name.
74
+ aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
75
+ yield(aliased_target, punctuation) if block_given?
76
+
77
+ with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"
78
+
79
+ alias_method without_method, target
80
+ alias_method target, with_method
81
+
82
+ case
83
+ when public_method_defined?(without_method)
84
+ public target
85
+ when protected_method_defined?(without_method)
86
+ protected target
87
+ when private_method_defined?(without_method)
88
+ private target
89
+ end
90
+ end
91
+ end
92
+ end