yssk22-couch_resource 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.
@@ -0,0 +1,103 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+
4
+ module CouchResource
5
+ # Callbacks are hooks similar to ActiveRecord::Callbacks
6
+ #
7
+ # * (-) <tt>save</tt>
8
+ # * (-) <tt>valid</tt>
9
+ # * (1) <tt>before_validation</tt>
10
+ # * (2) <tt>before_validation_on_create</tt>
11
+ # * (-) <tt>validate</tt>
12
+ # * (-) <tt>validate_on_create</tt>
13
+ # * (3) <tt>after_validation</tt>
14
+ # * (4) <tt>after_validation_on_create</tt>
15
+ # * (5) <tt>before_save</tt>
16
+ # * (6) <tt>before_create</tt>
17
+ # * (-) <tt>create</tt>
18
+ # * (7) <tt>after_create</tt>
19
+ # * (8) <tt>after_save</tt>
20
+ #
21
+ module Callbacks
22
+ CALLBACKS = %w(
23
+ after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
24
+ after_validation before_validation_on_create after_validation_on_create before_validation_on_update
25
+ after_validation_on_update before_destroy after_destroy
26
+ )
27
+
28
+ def self.included(base)
29
+ [:create_or_update, :valid?, :create, :update, :destroy].each do |method|
30
+ base.send :alias_method_chain, method, :callbacks
31
+ end
32
+
33
+ base.send(:include, ActiveSupport::Callbacks)
34
+ [:save, :create, :update, :validation, :validation_on_create, :validation_on_update, :destroy].each do |method|
35
+ base.define_callbacks "before_#{method}".to_sym, "after_#{method}".to_sym
36
+ end
37
+
38
+ end
39
+
40
+ [:save, :create, :update, :validation, :validation_on_create, :validation_on_update, :destroy].each do |method|
41
+ module_eval <<-EOS
42
+ def before_#{method}; end
43
+ def after_#{method}; end
44
+ EOS
45
+ end
46
+
47
+ private
48
+ def create_or_update_with_callbacks
49
+ return false if invoke_callbacks(:before_save) == false
50
+ result = create_or_update_without_callbacks
51
+ invoke_callbacks(:after_save)
52
+ result
53
+ end
54
+
55
+ def create_with_callbacks
56
+ return false if invoke_callbacks(:before_create) == false
57
+ result = create_without_callbacks
58
+ invoke_callbacks(:after_create)
59
+ result
60
+ end
61
+
62
+ def update_with_callbacks
63
+ return false if invoke_callbacks(:before_update) == false
64
+ result = update_without_callbacks
65
+ invoke_callbacks(:after_update)
66
+ result
67
+ end
68
+
69
+ def valid_with_callbacks?
70
+ return false if invoke_callbacks(:before_validation) == false
71
+ if new_record? then
72
+ return false if invoke_callbacks(:before_validation_on_create) == false
73
+ else
74
+ return false if invoke_callbacks(:before_validation_on_update) == false
75
+ end
76
+
77
+ result = valid_without_callbacks?
78
+
79
+ invoke_callbacks(:after_validation)
80
+ if new_record? then
81
+ invoke_callbacks(:after_validation_on_create)
82
+ else
83
+ invoke_callbacks(:after_validation_on_update)
84
+ end
85
+ return result
86
+ end
87
+
88
+ def destroy_with_callbacks
89
+ return false if invoke_callbacks(:before_destroy) == false
90
+ result = destroy_without_callbacks
91
+ invoke_callbacks(:after_destroy)
92
+ result
93
+ end
94
+
95
+ def invoke_callbacks(callback_name)
96
+ # invoke member method
97
+ return false if send(callback_name) == false
98
+ # invoke callbacks defined by using ActiveSupport::Callbacks
99
+ run_callbacks(callback_name)
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,194 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'json'
4
+ require 'net/https'
5
+ require 'uri'
6
+ require File.join(File.dirname(__FILE__), "error")
7
+
8
+ module CouchResource
9
+ class ConnectionError < CouchResourceError # :nodoc:
10
+ attr_reader :request
11
+ attr_reader :response
12
+ attr_reader :json
13
+
14
+ def initialize(request, response, message = nil)
15
+ @request = request
16
+ @response = response
17
+ @json = JSON(response.body) rescue nil
18
+ @message = message
19
+ end
20
+
21
+ def to_s
22
+ "Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
23
+ end
24
+ end
25
+
26
+ # 3xx Redirection
27
+ class Redirection < ConnectionError # :nodoc:
28
+ def to_s; response['Location'] ? "#{super} => #{response['Location']}" : super; end
29
+ end
30
+
31
+ # 4xx Client Error
32
+ class ClientError < ConnectionError; end # :nodoc:
33
+
34
+ # 400 Bad Request
35
+ class BadRequest < ClientError; end # :nodoc
36
+
37
+ # 401 Unauthorized
38
+ class UnauthorizedAccess < ClientError; end # :nodoc
39
+
40
+ # 403 Forbidden
41
+ class ForbiddenAccess < ClientError; end # :nodoc
42
+
43
+ # 404 Not Found
44
+ class ResourceNotFound < ClientError; end # :nodoc:
45
+
46
+ # 409 Conflict
47
+ class ResourceConflict < ClientError; end # :nodoc:
48
+
49
+ # 412 Precondition Failed
50
+ class PreconditionFailed < ClientError; end
51
+
52
+ # 5xx Server Error
53
+ class ServerError < ConnectionError; end # :nodoc:
54
+
55
+ # 405 Method Not Allowed
56
+ class MethodNotAllowed < ClientError # :nodoc:
57
+ def allowed_methods
58
+ @response['Allow'].split(',').map { |verb| verb.strip.downcase.to_sym }
59
+ end
60
+ end
61
+
62
+ class Connection
63
+ attr_reader :site, :user, :password, :timeout
64
+ attr_writer :user, :password, :timeout
65
+
66
+ def initialize(site)
67
+ raise ArgumentError, 'Missing site URI' unless site
68
+ @user = @password = nil
69
+ self.site = site
70
+ end
71
+
72
+ def site=(uri_string)
73
+ @site = uri_string.is_a?(URI) ? uri_string : URI.parse(uri_string)
74
+ @user = URI.decode(@site.user) if @site.user
75
+ @password = URI.decode(@site.password) if @site.password
76
+ end
77
+
78
+ def get(path, headers = {})
79
+ req = Net::HTTP::Get.new(path)
80
+ set_request_headers(req, headers)
81
+ request(req)
82
+ end
83
+
84
+ def delete(path, headers = {})
85
+ req = Net::HTTP::Delete.new(path)
86
+ set_request_headers(req, headers)
87
+ request(req)
88
+ end
89
+
90
+ def put(path, body='', headers = {})
91
+ req = Net::HTTP::Put.new(path)
92
+ set_request_headers(req, headers)
93
+ req.body = body
94
+ request(req)
95
+ end
96
+
97
+ def post(path, body='', headers = {})
98
+ req = Net::HTTP::Post.new(path)
99
+ set_request_headers(req, headers)
100
+ req.body = body
101
+ request(req)
102
+ end
103
+
104
+ def head(path, body='', headers = {})
105
+ req = Net::HTTP::Head.new(path)
106
+ request(req)
107
+ end
108
+
109
+ private
110
+ def request(req)
111
+ res = http.request(req)
112
+ handle_response(req, res)
113
+ end
114
+
115
+ # Handles response and error codes from remote service.
116
+ def handle_response(request, response)
117
+ case response.code.to_i
118
+ when 301,302
119
+ raise(Redirection.new(request, response))
120
+ when 200...400
121
+ response
122
+ when 400
123
+ raise(BadRequest.new(request, response))
124
+ when 401
125
+ raise(UnauthorizedAccess.new(request, response))
126
+ when 403
127
+ raise(ForbiddenAccess.new(request, response))
128
+ when 404
129
+ raise(ResourceNotFound.new(request, response))
130
+ when 405
131
+ raise(MethodNotAllowed.new(request, response))
132
+ when 409
133
+ raise(ResourceConflict.new(request, response))
134
+ when 412
135
+ raise(PreconditionFailed.new(request, response))
136
+ when 422
137
+ raise(ResourceInvalid.new(request, response))
138
+ when 401...500
139
+ raise(ClientError.new(request, response))
140
+ when 500...600
141
+ raise(ServerError.new(request, response))
142
+ else
143
+ raise(ConnectionError.new(request, response, "Unknown response code: #{response.code}"))
144
+ end
145
+ begin
146
+ if response.body.blank?
147
+ nil
148
+ else
149
+ hash = JSON(response.body)
150
+ normalize_hash(hash)
151
+ end
152
+ rescue JSON::ParserError => e
153
+ raise(ConnectionError.new(request, response, "Invalid json response: #{e.body}"))
154
+ end
155
+ end
156
+
157
+ def normalize_hash(hash)
158
+ hash.inject(HashWithIndifferentAccess.new({})) do |normalized, (k, v)|
159
+ v = normalize_hash(v) if v.is_a?(Hash)
160
+ normalized[k.to_sym] = v
161
+ normalized
162
+ end
163
+ end
164
+
165
+ def http
166
+ http = Net::HTTP.new(@site.host, @site.port)
167
+ http.use_ssl = @site.is_a?(URI::HTTPS)
168
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl
169
+ http.read_timeout = @timeout if @timeout
170
+ http
171
+ end
172
+
173
+ def default_header
174
+ @default_header ||= {
175
+ "Accept" => "application/json",
176
+ 'Content-Type' => "application/json"
177
+ }
178
+ end
179
+
180
+
181
+ def set_request_headers(request, headers={})
182
+ headers = authorization_header.update(default_header).update(headers)
183
+ headers.each do |k,v|
184
+ request[k.to_s] = v
185
+ end
186
+ end
187
+
188
+ # Sets authorization header
189
+ def authorization_header
190
+ (@user || @password ? { 'Authorization' => 'Basic ' + ["#{@user}:#{ @password}"].pack('m').delete("\r\n") } : {})
191
+ end
192
+
193
+ end
194
+ end
@@ -0,0 +1,3 @@
1
+ module CouchResource
2
+ class CouchResourceError < StandardError; end
3
+ end
@@ -0,0 +1,340 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'json'
4
+ module CouchResource
5
+ module Struct
6
+ def self.included(base)
7
+ base.send(:extend, ClassMethods)
8
+ base.send(:include, InstanceMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ #
13
+ # define a string attribtue
14
+ # options are :
15
+ # * <tt>:validates</tt> - see CouchResource::Validations
16
+ #
17
+ def string(name, option={})
18
+ option[:is_a] = :string
19
+ register_attribute_member(name, option)
20
+ define_attribute_accessor(name, option)
21
+ define_validations(name, option)
22
+ end
23
+
24
+ #
25
+ # define a number attribute
26
+ # options are :
27
+ # * <tt>:validates</tt> - see CouchResource::Validations
28
+ #
29
+ def number(name, option={})
30
+ option[:is_a] = :number
31
+ register_attribute_member(name, option)
32
+ define_attribute_accessor(name, option)
33
+ define_validations(name, option)
34
+ end
35
+
36
+ #
37
+ # define a boolean attribute
38
+ # options are :
39
+ # * <tt>:validates</tt> - see CouchResource::Validations
40
+ #
41
+ def boolean(name, option={})
42
+ option[:is_a] = :boolean
43
+ register_attribute_member(name, option)
44
+ define_attribute_accessor(name, option)
45
+ method = <<-EOS
46
+ def #{name}?
47
+ get_attribute(:#{name})
48
+ end
49
+ EOS
50
+ class_eval(method, __FILE__, __LINE__)
51
+ define_validations(name, option)
52
+ end
53
+
54
+ #
55
+ # define a array attribute, each of which elements is a primitive (one of string, number, array, boolean or hash) object
56
+ # options are :
57
+ #
58
+ def array(name, option={})
59
+ option[:is_a] = :array
60
+ register_attribute_member(name, option)
61
+ define_attribute_accessor(name, option)
62
+ define_validations(name, option)
63
+ end
64
+
65
+ #
66
+ # define a collection attribute, each of which elements is an object specified by the <tt>:is_a</tt> option.
67
+ # options are :
68
+ # * <tt>:each</tt> - set the class to encode/decode each of Hash object (default is :hash, which means no encoding/decoding will be processed)
69
+ # * <tt>:validates</tt> - see CouchResource::Validations
70
+ #
71
+ def collection(name, option={})
72
+ option = {
73
+ :each => :hash
74
+ }.update(option)
75
+ option[:is_a] = :collection
76
+ register_attribute_member(name, option)
77
+ define_attribute_accessor(name, option)
78
+ define_validations(name, option)
79
+ end
80
+
81
+ #
82
+ # define a object attribute
83
+ # <tt>options</tt> are :
84
+ # * <tt>:is_a</tt> - set the class to encode/decode Hash object (default is :hash, which means no encoding/decoding will be processed)
85
+ # * <tt>:validates</tt> - see CouchResource::Validations
86
+ #
87
+ def object(name, option={})
88
+ unless option.has_key?(:is_a)
89
+ option[:is_a] = :hash
90
+ end
91
+ register_attribute_member(name, option)
92
+ define_attribute_accessor(name, option)
93
+ define_validations(name, option)
94
+ end
95
+
96
+ #
97
+ # define a datetime object (extension of string)
98
+ # options are :
99
+ # * <tt>:validates</tt> - see CouchResource::Validations
100
+ #
101
+ def datetime(name, option={})
102
+ option[:is_a] = :datetime
103
+ register_attribute_member(name, option)
104
+ define_attribute_accessor(name, option)
105
+ define_validations(name, option)
106
+ end
107
+
108
+
109
+ def from_hash(hash)
110
+ hash ||= {}
111
+ hash.symbolize_keys!
112
+ instance = self.new
113
+ (read_inheritable_attribute(:attribute_members) || {}).each do |name, option|
114
+ instance.set_attribute(name, hash[name.to_sym])
115
+ end
116
+ instance
117
+ end
118
+
119
+ private
120
+ def register_attribute_member(name, option = {})
121
+ attribute_members = read_inheritable_attribute(:attribute_members)
122
+ attribute_members ||= HashWithIndifferentAccess.new({})
123
+ attribute_members[name] = option
124
+ write_inheritable_attribute(:attribute_members, attribute_members)
125
+ end
126
+
127
+ def define_attribute_accessor(name, option={})
128
+ define_attribute_read_accessor(name, option)
129
+ define_attribute_write_accessor(name, option)
130
+ end
131
+
132
+ def define_attribute_read_accessor(name, option={})
133
+ method = <<-EOS
134
+ def #{name}
135
+ get_attribute(:#{name})
136
+ end
137
+ def #{name}_before_type_cast
138
+ get_attribute_before_type_cast(:#{name})
139
+ end
140
+ EOS
141
+ class_eval(method, __FILE__, __LINE__)
142
+ end
143
+
144
+ def define_attribute_write_accessor(name, option={})
145
+ method = <<-EOS
146
+ def #{name}=(value)
147
+ set_attribute(:#{name}, value)
148
+ end
149
+ EOS
150
+ class_eval(method, __FILE__, __LINE__)
151
+ end
152
+
153
+ def define_validations(name, option={})
154
+ (option[:validates] || []).each do |validation_type, validate_option|
155
+ args = validate_option.nil? ? [name] : [name, validate_option]
156
+ case validation_type.to_sym
157
+ when :each
158
+ proc = validate_option[:proc]
159
+ send("validates_each", *args) do |record, attr, value|
160
+ proc.call(record, attr, value) if proc
161
+ end
162
+ when :confirmation_of, :presense_of, :length_of, :size_of, :format_of,
163
+ :inclusion_of, :exclusion_of, :numericality_of, :children_of
164
+ send("validates_#{validation_type}", *args)
165
+ else
166
+ raise ArgumentError, "invalid validation type (#{validation_type})"
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ module InstanceMethods
173
+ def [](name)
174
+ get_attribute(name)
175
+ end
176
+
177
+ def []=(name, value)
178
+ set_attribute(name, value)
179
+ end
180
+
181
+ def get_attribute_option(attr_name)
182
+ (self.class.read_inheritable_attribute(:attribute_members) || {})[attr_name]
183
+ end
184
+
185
+ def set_attribute(name, value)
186
+ @attributes ||= HashWithIndifferentAccess.new({})
187
+ # inplicit type cast
188
+ attribute_members = self.class.read_inheritable_attribute(:attribute_members) || {}
189
+ if attribute_members.has_key?(name)
190
+ option = attribute_members[name]
191
+ if value.nil?
192
+ @attributes[name] = nil
193
+ else
194
+ if option[:allow_nil] && value.blank?
195
+ @attributes[name] = nil
196
+ else
197
+ klass = option[:is_a]
198
+ @attributes[name] = case klass
199
+ when :string, :number, :boolean, :array, :hash, :datetime
200
+ self.send("type_cast_for_#{klass}_attributes", value)
201
+ when :collection
202
+ self.send("type_cast_for_collection_attributes", value, option[:each])
203
+ else
204
+ self.send("type_cast_for_object_attributes", value, klass)
205
+ end
206
+ end
207
+ end
208
+ else
209
+ @attributes[name] = nil
210
+ end
211
+ value
212
+ end
213
+
214
+
215
+ def get_attribute(name)
216
+ value = get_attribute_before_type_cast(name)
217
+ attribute_members = self.class.read_inheritable_attribute(:attribute_members) || {}
218
+ if attribute_members.has_key?(name)
219
+ value = @attributes[name]
220
+ option = attribute_members[name]
221
+ value
222
+ #if value.nil?
223
+ # nil
224
+ #else
225
+ # klass = option[:is_a]
226
+ # case klass
227
+ # when :string, :number, :boolean, :array, :hash, :datetime
228
+ # self.send("type_cast_for_#{klass}_attributes", value)
229
+ # when :collection
230
+ # self.send("type_cast_for_collection_attributes", value, option[:each])
231
+ # else
232
+ # self.send("type_cast_for_object_attributes", value, klass)
233
+ # end
234
+ #end
235
+ else
236
+ nil
237
+ end
238
+ end
239
+
240
+ def get_attribute_before_type_cast(name)
241
+ @attributes ||= HashWithIndifferentAccess.new({})
242
+ @attributes[name]
243
+ end
244
+
245
+ def to_hash
246
+ hash = HashWithIndifferentAccess.new({ :class => self.class.name })
247
+ (self.class.read_inheritable_attribute(:attribute_members) || {}).each do |name, option|
248
+ klass = option[:is_a]
249
+ value = get_attribute(name)
250
+ case klass
251
+ when :string, :number, :boolean, :array, :hash, :datetime
252
+ hash[name] = value
253
+ when :collection
254
+ hash[name] = value.map(&:to_hash)
255
+ else
256
+ if value
257
+ hash[name] = value.to_hash
258
+ else
259
+ hash[name] = nil
260
+ end
261
+ end
262
+ end
263
+ hash
264
+ end
265
+
266
+ private
267
+ def type_cast_for_string_attributes(value)
268
+ value.is_a?(String) ? value : value.to_s
269
+ end
270
+
271
+ def type_cast_for_number_attributes(value)
272
+ if value.is_a?(Numeric)
273
+ value
274
+ else
275
+ v = value.to_s
276
+ if v =~ /^\d+$/
277
+ v.to_i
278
+ else
279
+ v.to_f
280
+ end
281
+ end
282
+ end
283
+
284
+ def type_cast_for_boolean_attributes(value)
285
+ value && true
286
+ end
287
+
288
+ def type_cast_for_array_attributes(value)
289
+ if value.is_a?(Array)
290
+ value
291
+ else
292
+ if value.respond_to?(:to_a)
293
+ value.to_a
294
+ else
295
+ [value]
296
+ end
297
+ end
298
+ end
299
+
300
+ def type_cast_for_collection_attributes(v1, each)
301
+ type_cast_for_array_attributes(v1).map { |v2|
302
+ type_cast_for_object_attributes(v2, each)
303
+ }.reject { |v3|
304
+ v3.nil?
305
+ }
306
+ end
307
+
308
+ def type_cast_for_hash_attributes(value)
309
+ if value.is_a?(Hash)
310
+ value
311
+ else
312
+ if value.respond_to?(:to_hash)
313
+ value.to_hash
314
+ else
315
+ nil
316
+ end
317
+ end
318
+ end
319
+
320
+ def type_cast_for_object_attributes(value, klass)
321
+ if value.is_a?(klass)
322
+ value
323
+ elsif value.is_a?(Hash)
324
+ klass.from_hash(value)
325
+ else
326
+ nil
327
+ end
328
+ end
329
+
330
+ def type_cast_for_datetime_attributes(value)
331
+ case value
332
+ when Date, Time, DateTime
333
+ value
334
+ else
335
+ DateTime.parse(value.to_s) rescue nil
336
+ end
337
+ end
338
+ end
339
+ end
340
+ end