resto 0.0.3 → 0.0.4

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