resto 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/lib/blankslate.rb +110 -0
  2. data/lib/resto.rb +264 -0
  3. data/lib/resto/attributes.rb +56 -0
  4. data/lib/resto/extra/copy.rb +34 -0
  5. data/lib/resto/extra/delegation.rb +26 -0
  6. data/lib/resto/extra/hash_args.rb +56 -0
  7. data/lib/resto/format.rb +20 -0
  8. data/lib/resto/format/default.rb +11 -0
  9. data/lib/resto/format/json.rb +30 -0
  10. data/lib/resto/format/plain.rb +14 -0
  11. data/lib/resto/format/xml.rb +66 -0
  12. data/lib/resto/property.rb +50 -0
  13. data/lib/resto/property/handler.rb +57 -0
  14. data/lib/resto/property/integer.rb +29 -0
  15. data/lib/resto/property/string.rb +19 -0
  16. data/lib/resto/property/time.rb +43 -0
  17. data/lib/resto/request/base.rb +88 -0
  18. data/lib/resto/request/factory.rb +66 -0
  19. data/lib/resto/request/header.rb +58 -0
  20. data/lib/resto/request/option.rb +126 -0
  21. data/lib/resto/request/uri.rb +50 -0
  22. data/lib/resto/response/base.rb +85 -0
  23. data/lib/resto/translator/request_factory.rb +44 -0
  24. data/lib/resto/translator/response_factory.rb +28 -0
  25. data/lib/resto/validate.rb +37 -0
  26. data/lib/resto/validate/inclusion.rb +39 -0
  27. data/lib/resto/validate/length.rb +36 -0
  28. data/lib/resto/validate/presence.rb +24 -0
  29. data/lib/resto/version.rb +5 -0
  30. data/resto.gemspec +43 -0
  31. data/spec/resto/extra/copy_spec.rb +58 -0
  32. data/spec/resto/extra/hash_args_spec.rb +71 -0
  33. data/spec/resto/format/default_spec.rb +24 -0
  34. data/spec/resto/format/json_spec.rb +29 -0
  35. data/spec/resto/format/plain_spec.rb +21 -0
  36. data/spec/resto/format/xml_spec.rb +105 -0
  37. data/spec/resto/property/handler_spec.rb +57 -0
  38. data/spec/resto/property/integer_spec.rb +67 -0
  39. data/spec/resto/property/time_spec.rb +124 -0
  40. data/spec/resto/property_spec.rb +60 -0
  41. data/spec/resto/request/base_spec.rb +253 -0
  42. data/spec/resto/request/factory_spec.rb +114 -0
  43. data/spec/resto/translator/response_factory_spec.rb +93 -0
  44. data/spec/resto/validate/presence_spec.rb +102 -0
  45. data/spec/resto_spec.rb +531 -0
  46. data/spec/spec_helper.rb +119 -0
  47. metadata +48 -3
data/lib/blankslate.rb ADDED
@@ -0,0 +1,110 @@
1
+ # encoding: utf-8
2
+ #!/usr/bin/env ruby
3
+ #--
4
+ # Copyright 2004, 2006 by Jim Weirich (jim@weirichhouse.org).
5
+ # All rights reserved.
6
+
7
+ # Permission is granted for use, copying, modification, distribution,
8
+ # and distribution of modified versions of this work as long as the
9
+ # above copyright notice is included.
10
+ #++
11
+
12
+ ######################################################################
13
+ # BlankSlate provides an abstract base class with no predefined
14
+ # methods (except for <tt>\_\_send__</tt> and <tt>\_\_id__</tt>).
15
+ # BlankSlate is useful as a base class when writing classes that
16
+ # depend upon <tt>method_missing</tt> (e.g. dynamic proxies).
17
+ #
18
+ class BlankSlate
19
+ class << self
20
+
21
+ # Hide the method named +name+ in the BlankSlate class. Don't
22
+ # hide +instance_eval+ or any method beginning with "__".
23
+ def hide(name)
24
+ if instance_methods.include?(name.to_s) and
25
+ name !~ /^(__|instance_eval)/
26
+ @hidden_methods ||= {}
27
+ @hidden_methods[name.to_sym] = instance_method(name)
28
+ undef_method name
29
+ end
30
+ end
31
+
32
+ def find_hidden_method(name)
33
+ @hidden_methods ||= {}
34
+ @hidden_methods[name] || superclass.find_hidden_method(name)
35
+ end
36
+
37
+ # Redefine a previously hidden method so that it may be called on a blank
38
+ # slate object.
39
+ def reveal(name)
40
+ hidden_method = find_hidden_method(name)
41
+ fail "Don't know how to reveal method '#{name}'" unless hidden_method
42
+ define_method(name, hidden_method)
43
+ end
44
+ end
45
+
46
+ instance_methods.each { |m| hide(m) }
47
+ end
48
+
49
+ ######################################################################
50
+ # Since Ruby is very dynamic, methods added to the ancestors of
51
+ # BlankSlate <em>after BlankSlate is defined</em> will show up in the
52
+ # list of available BlankSlate methods. We handle this by defining a
53
+ # hook in the Object and Kernel classes that will hide any method
54
+ # defined after BlankSlate has been loaded.
55
+ #
56
+ module Kernel
57
+ class << self
58
+ alias_method :blank_slate_method_added, :method_added
59
+
60
+ # Detect method additions to Kernel and remove them in the
61
+ # BlankSlate class.
62
+ def method_added(name)
63
+ result = blank_slate_method_added(name)
64
+ return result if self != Kernel
65
+ BlankSlate.hide(name)
66
+ result
67
+ end
68
+ end
69
+ end
70
+
71
+ ######################################################################
72
+ # Same as above, except in Object.
73
+ #
74
+ class Object
75
+ class << self
76
+ alias_method :blank_slate_method_added, :method_added
77
+
78
+ # Detect method additions to Object and remove them in the
79
+ # BlankSlate class.
80
+ def method_added(name)
81
+ result = blank_slate_method_added(name)
82
+ return result if self != Object
83
+ BlankSlate.hide(name)
84
+ result
85
+ end
86
+
87
+ def find_hidden_method(name)
88
+ nil
89
+ end
90
+ end
91
+ end
92
+
93
+ ######################################################################
94
+ # Also, modules included into Object need to be scanned and have their
95
+ # instance methods removed from blank slate. In theory, modules
96
+ # included into Kernel would have to be removed as well, but a
97
+ # "feature" of Ruby prevents late includes into modules from being
98
+ # exposed in the first place.
99
+ #
100
+ class Module
101
+ alias blankslate_original_append_features append_features
102
+ def append_features(mod)
103
+ result = blankslate_original_append_features(mod)
104
+ return result if mod != Object
105
+ instance_methods.each do |name|
106
+ BlankSlate.hide(name)
107
+ end
108
+ result
109
+ end
110
+ end
data/lib/resto.rb ADDED
@@ -0,0 +1,264 @@
1
+ # encoding: utf-8
2
+
3
+ require 'resto/request/base'
4
+ require 'resto/response/base'
5
+ require 'resto/property'
6
+ require 'resto/attributes'
7
+ require 'resto/extra/copy'
8
+
9
+ module Resto
10
+ def self.included(klass)
11
+ klass.extend ClassMethods
12
+ klass.class_eval do
13
+ @request = Request::Base.new
14
+ @response = Response::Base.new.klass(klass)
15
+ end
16
+ end
17
+
18
+ def self.url(url)
19
+ Request::Base.new.url(url)
20
+ end
21
+ end
22
+
23
+ module Resto
24
+ module ClassMethods
25
+
26
+ def inherited(sub_class)
27
+ sub_class.class_exec(self) do |parent_class|
28
+ @request = parent_class.request
29
+ @response = parent_class.base_response.klass(sub_class)
30
+ end
31
+ end
32
+
33
+ def resto_request(&block)
34
+ @request.instance_exec(&block)
35
+ end
36
+
37
+
38
+ def resto_response(&block)
39
+ @response.instance_exec(&block)
40
+ end
41
+
42
+ def resource_id
43
+ @resource_identifier
44
+ end
45
+
46
+ def resource_identifier(id)
47
+ @resource_identifier = id
48
+ end
49
+
50
+ def has_many(name, options)
51
+ class_name = options.fetch(:class_name)
52
+
53
+ params = options.fetch(:params, {})
54
+ params = params.map { |key, value| ":#{key} => #{value}" }.join(', ')
55
+
56
+ relation = options.fetch(:relation, {})
57
+ relation = relation.map { |key, value| ":#{key} => #{value}" }.join(', ')
58
+
59
+ method_definition = %Q{
60
+ def #{name}(params = {})
61
+ params ||= {}
62
+ raise ArgumentError unless params.is_a?(Hash)
63
+
64
+ @#{name} ||= {}
65
+
66
+ @#{name}[params] ||= #{class_name.capitalize}.
67
+ all({#{params}}.update(params), {#{relation}})
68
+ end
69
+ }
70
+
71
+ class_eval(method_definition, __FILE__, __LINE__)
72
+ end
73
+
74
+ def belongs_to(name)
75
+
76
+ method_definition = %Q{
77
+ def #{name}(reload = false)
78
+ if reload
79
+ @#{name} = #{name.capitalize}.get(#{name}_id)
80
+ else
81
+ @#{name} ||= #{name.capitalize}.get(#{name}_id)
82
+ end
83
+ end
84
+ }
85
+
86
+ class_eval(method_definition, __FILE__, __LINE__)
87
+ end
88
+
89
+ def property(name, property, options={}, &block)
90
+ property = Resto::Property.const_get(property.to_s).new(name, options)
91
+ property.instance_exec(&block) if block_given?
92
+
93
+ property_handler.add(property)
94
+
95
+ attribute_methods = %Q{
96
+ def #{name}
97
+ @attributes.get(:#{name})
98
+ end
99
+
100
+ def #{name}_without_cast
101
+ @attributes.get_without_cast(:#{name})
102
+ end
103
+
104
+ def #{name}?
105
+ @attributes.present?(:#{name})
106
+ end
107
+
108
+ def #{name}=(value)
109
+ @attributes.set(:#{name}, value)
110
+ end
111
+ }
112
+
113
+ class_eval(attribute_methods, __FILE__, __LINE__)
114
+ end
115
+
116
+
117
+ def all(params = {}, request_path_options = {}, &block)
118
+ req = @request.construct_path(request_path_options)
119
+ block.call(req) if block_given?
120
+
121
+ res =
122
+ if params.keys.empty?
123
+ req.get
124
+ else
125
+ req.params(params).get
126
+ end
127
+
128
+ response(res).to_collection
129
+ end
130
+
131
+ def head(request_path_options = {})
132
+ req = @request.construct_path(request_path_options)
133
+ response(req.head)
134
+ end
135
+
136
+ def get(id, request_path_options = {}, &block)
137
+ req = @request.construct_path(request_path_options)
138
+ block.call(req) if block_given?
139
+
140
+ response(req.append_path(id).get).to_object
141
+ end
142
+
143
+ def fetch(params = {}, request_path_options = {}, &block)
144
+ req = @request.construct_path(request_path_options)
145
+ block.call(req) if block_given?
146
+
147
+ res =
148
+ if params.keys.empty?
149
+ req.get
150
+ else
151
+ req.params(params).get
152
+ end
153
+
154
+ response(res).to_object
155
+ end
156
+
157
+ def post(attributes, request_path_options = {}, &block)
158
+ req = @request.construct_path(request_path_options)
159
+ block.call(req) if block_given?
160
+
161
+ attributes.delete(resource_id)
162
+ remote_attributes = property_handler.remote_attributes(attributes)
163
+ response(req.body(remote_attributes).post).to_object
164
+ end
165
+
166
+ def put(attributes, request_path_options = {}, &block)
167
+ req = @request.construct_path(request_path_options)
168
+ block.call(req) if block_given?
169
+
170
+ id = attributes.delete(resource_id)
171
+ remote_attributes = property_handler.remote_attributes(attributes)
172
+ response(req.append_path(id).body(remote_attributes).put).to_object
173
+ end
174
+
175
+ def delete(id, request_path_options = {}, &block)
176
+ req = @request.construct_path(request_path_options)
177
+ block.call(req) if block_given?
178
+
179
+ response(req.append_path(id).delete).to_object
180
+ end
181
+
182
+ def request
183
+ Extra::Copy.request_base(@request)
184
+ end
185
+
186
+ def response(response)
187
+ base_response.http_response(response)
188
+ end
189
+
190
+ def base_response
191
+ Extra::Copy.response_base(@response)
192
+ end
193
+
194
+ def property_handler
195
+ @property_handler ||= Property::Handler.new
196
+ end
197
+
198
+ end
199
+ end
200
+
201
+ module Resto
202
+ def initialize(attributes)
203
+ raise "Must be a hash" unless attributes.is_a?(Hash)
204
+
205
+ @attributes = Attributes.new(attributes, self)
206
+ end
207
+
208
+ attr_accessor :response
209
+
210
+ def get
211
+ id = attributes.fetch(self.class.resource_id)
212
+ self.class.get(id, request_path_options) { add_to_request }
213
+ end
214
+
215
+ alias reload get
216
+
217
+ def put
218
+ self.class.put(attributes, request_path_options) { add_to_request }
219
+ end
220
+
221
+ def delete
222
+ id = attributes.fetch(self.class.resource_id)
223
+ self.class.delete(id, request_path_options) { add_to_request }
224
+ end
225
+
226
+ def add_to_request
227
+ lambda { |request| request }
228
+ end
229
+
230
+ def request_path_options
231
+ {}
232
+ end
233
+
234
+ def valid?
235
+ @attributes.valid? and valid_response?(false)
236
+ end
237
+
238
+ def valid_response?(must_have_a_response_variable=true)
239
+ if must_have_a_response_variable
240
+ response ? response.valid? : false
241
+ else
242
+ response ? response.valid? : true
243
+ end
244
+ end
245
+
246
+ def add_error(key, value)
247
+ @attributes.add_error(key, value)
248
+ end
249
+
250
+ def errors
251
+ @attributes.errors
252
+ end
253
+
254
+ def update_attributes(attributes)
255
+ tap { @attributes.update_attributes(attributes) }
256
+ end
257
+
258
+ alias body update_attributes
259
+
260
+ def attributes
261
+ @attributes.to_hash
262
+ end
263
+
264
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+
3
+ module Resto
4
+ class Attributes
5
+ def initialize(attributes, resource, property_handler = nil)
6
+ @resource = resource
7
+ @property_handler = property_handler || resource.class.property_handler
8
+ @attributes = {} # TODO must handle indifferent access :name and 'name'
9
+ @attributes_before_cast = {}
10
+ @errors = {}
11
+
12
+ update_attributes(attributes)
13
+ end
14
+
15
+ def update_attributes(attributes)
16
+ attributes.each do |key, value|
17
+ key = @property_handler.attribute_key(key)
18
+ set(key, value) if key
19
+ end
20
+ end
21
+
22
+ def set(key, value)
23
+ @attributes_before_cast.store(key, value)
24
+ @attributes.store(key, @property_handler.cast(key, value, @errors))
25
+ end
26
+
27
+ def get(key)
28
+ @attributes.fetch(key, nil)
29
+ end
30
+
31
+ def get_without_cast(key)
32
+ @attributes_before_cast.fetch(key, nil)
33
+ end
34
+
35
+ def present?(key)
36
+ @attributes.fetch(key, false) ? true : false
37
+ end
38
+
39
+ def valid?
40
+ @property_handler.validate(@resource)
41
+ errors.empty?
42
+ end
43
+
44
+ def add_error(key, value)
45
+ @errors.store(key, value)
46
+ end
47
+
48
+ def errors
49
+ @errors.map {|key, value| value }.compact
50
+ end
51
+
52
+ def to_hash
53
+ @attributes.merge({})
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+
3
+ require 'resto/request/base'
4
+ require 'resto/response/base'
5
+
6
+ module Resto
7
+ module Extra
8
+ module Copy
9
+
10
+ def self.request_base(request_base)
11
+ Resto::Request::Base.new.tap do |copy|
12
+ copy_instance_variables(request_base, copy, ["@request"])
13
+
14
+ request_klass = request_base.instance_variable_get("@request_klass")
15
+ copy.instance_variable_set("@request", request_klass.new(copy))
16
+ end
17
+ end
18
+
19
+ def self.response_base(response_base)
20
+ Resto::Response::Base.new.tap do |copy|
21
+ copy_instance_variables(response_base, copy, ["@response"])
22
+ end
23
+ end
24
+
25
+ def self.copy_instance_variables(from, to, exclude = [])
26
+ (from.instance_variables.map(&:to_s) - exclude).each do |name|
27
+ instance_variable = from.instance_variable_get(name)
28
+
29
+ to.instance_variable_set(name, instance_variable)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end