activecouch 0.1.0

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 (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