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