activecouch 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README +42 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/lib/active_couch.rb +18 -0
- data/lib/active_couch/associations.rb +1 -0
- data/lib/active_couch/associations/has_many_association.rb +35 -0
- data/lib/active_couch/attribute.rb +46 -0
- data/lib/active_couch/base.rb +458 -0
- data/lib/active_couch/callbacks.rb +81 -0
- data/lib/active_couch/connection.rb +155 -0
- data/lib/active_couch/errors.rb +17 -0
- data/lib/active_couch/migrations.rb +3 -0
- data/lib/active_couch/migrations/errors.rb +4 -0
- data/lib/active_couch/migrations/migration.rb +77 -0
- data/lib/active_couch/migrations/migrator.rb +53 -0
- data/lib/active_couch/support.rb +2 -0
- data/lib/active_couch/support/extensions.rb +92 -0
- data/lib/active_couch/support/inflections.rb +52 -0
- data/lib/active_couch/support/inflector.rb +280 -0
- data/spec/attribute/initialize_spec.rb +138 -0
- data/spec/base/after_delete_spec.rb +107 -0
- data/spec/base/after_save_spec.rb +99 -0
- data/spec/base/before_delete_spec.rb +106 -0
- data/spec/base/before_save_spec.rb +98 -0
- data/spec/base/create_spec.rb +27 -0
- data/spec/base/database_spec.rb +65 -0
- data/spec/base/delete_spec.rb +78 -0
- data/spec/base/find_spec.rb +165 -0
- data/spec/base/from_json_spec.rb +48 -0
- data/spec/base/has_many_spec.rb +81 -0
- data/spec/base/has_spec.rb +76 -0
- data/spec/base/id_spec.rb +22 -0
- data/spec/base/initialize_spec.rb +43 -0
- data/spec/base/module_spec.rb +18 -0
- data/spec/base/nested_class_spec.rb +19 -0
- data/spec/base/rev_spec.rb +16 -0
- data/spec/base/save_spec.rb +65 -0
- data/spec/base/site_spec.rb +41 -0
- data/spec/base/to_json_spec.rb +64 -0
- data/spec/connection/initialize_spec.rb +28 -0
- data/spec/has_many_association/initialize_spec.rb +33 -0
- data/spec/migration/define_spec.rb +29 -0
- data/spec/migration/include_attributes_spec.rb +30 -0
- data/spec/migration/view_js_spec.rb +46 -0
- data/spec/migration/with_filter_spec.rb +13 -0
- data/spec/migration/with_key_spec.rb +13 -0
- data/spec/migrator/create_database_spec.rb +48 -0
- data/spec/migrator/delete_database_spec.rb +46 -0
- data/spec/migrator/migrate_spec.rb +99 -0
- data/spec/spec_helper.rb +9 -0
- 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,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,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
|