restful_serializer 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +60 -12
- data/lib/restful/configuration.rb +414 -0
- data/lib/restful/serializer.rb +121 -44
- data/lib/restful/version.rb +1 -1
- data/lib/restful.rb +102 -79
- data/spec/configuration_spec.rb +494 -0
- data/spec/restful_spec.rb +130 -46
- data/spec/serializer_spec.rb +179 -102
- data/spec/web_service_spec.rb +66 -0
- metadata +9 -4
data/README.rdoc
CHANGED
@@ -25,44 +25,81 @@ Models:
|
|
25
25
|
# bar1
|
26
26
|
# bar2
|
27
27
|
# system
|
28
|
+
# subbar1
|
29
|
+
# subbar2
|
30
|
+
# type
|
28
31
|
class Bar < ActiveRecord::Base
|
29
32
|
has_one :foo
|
30
33
|
end
|
31
34
|
|
35
|
+
class SubBar1 < Bar
|
36
|
+
def interesting_state; ... end
|
37
|
+
end
|
38
|
+
|
39
|
+
class SubBar2 < Bar
|
40
|
+
end
|
41
|
+
|
32
42
|
Example configuration (config/initializers/restful.rb:
|
33
43
|
|
34
|
-
#
|
35
|
-
Restful.
|
36
|
-
|
44
|
+
# Required for href route generation. (This can also be set per web_service)
|
45
|
+
Restful.default_url_options(:host => 'foo.com')
|
46
|
+
|
47
|
+
Restful.register_web_service('Foo Web Service') do |config|
|
48
|
+
|
49
|
+
# This is the url prefix used for calls to the web service. It defaults to
|
50
|
+
# the web service name, but can be set to be something different, or to
|
51
|
+
# nil, if your web service has no base uri and named_routes
|
52
|
+
# rely solely on the model class.
|
53
|
+
config.api_prefix = 'foo_api'
|
54
|
+
|
37
55
|
# This configuration provides information which anyone authorized to see a
|
38
56
|
# given object at the most basic level should be able to see.
|
39
|
-
|
57
|
+
#
|
40
58
|
# Note: it is advisable to set :serialization => :only for all models, so that
|
41
59
|
# new attributes do not automatically become available through the api.
|
42
|
-
|
43
|
-
:foo => {
|
60
|
+
config.register_resource(:foo,
|
44
61
|
:serialization => {
|
45
62
|
:only => [:id, :column1, :column2],
|
46
63
|
},
|
47
64
|
:associations => {:bars => nil}
|
48
|
-
|
49
|
-
|
65
|
+
)
|
66
|
+
|
67
|
+
config.register_resource(:bar,
|
50
68
|
:serialization => {
|
51
69
|
:only => [:id, :bar1, :bar2],
|
52
70
|
:include => {
|
53
71
|
:foo => { :only => [:column1] }
|
54
72
|
},
|
55
|
-
}
|
73
|
+
},
|
56
74
|
:associations => {:foo => nil}
|
57
|
-
|
58
|
-
|
75
|
+
)
|
76
|
+
|
77
|
+
# Alternatively you may manipulate the resource configuration directly.
|
78
|
+
# This can be helpful if you are inheriting properties from a base class and
|
79
|
+
# want fine-grained control over how the options are merged and finalized.
|
80
|
+
config.register_resource(:sub_bar1) do |resource|
|
81
|
+
resource.serialization do |serial|
|
82
|
+
serial.only << :subbar1
|
83
|
+
serial.methods << :interesting_state
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
config.register_resource(:sub_bar2) do |resource|
|
88
|
+
resource.serialization do |serial|
|
89
|
+
serial.only << :subbar1
|
90
|
+
end
|
91
|
+
# If shallow is set false, then associations will be presented when the
|
92
|
+
# resource is generated from lists (as in foo.bars.restful)
|
93
|
+
resource.shallow = false
|
94
|
+
end
|
95
|
+
end
|
59
96
|
|
60
97
|
Note that the serialization configuration is the same which you would normally pass to ActiveRecord::Serialization (in a model.to_json call, for instance).
|
61
98
|
|
62
99
|
Example output:
|
63
100
|
|
64
101
|
foo = Foo.create(:name => 'A foo', :column1 => 1, :column2 => 2, :secret => "very secret")
|
65
|
-
pp foo.restful
|
102
|
+
pp foo.restful('Foo Web Service')
|
66
103
|
# =>
|
67
104
|
# {"href"=>"http://test.app/client_api/foos/1",
|
68
105
|
# "name"=>"A foo",
|
@@ -73,6 +110,17 @@ Example output:
|
|
73
110
|
# "column1"=>1,
|
74
111
|
# "column2"=>2,}}
|
75
112
|
|
113
|
+
pp foo.restful('Foo Web Service') do |configure|
|
114
|
+
configure.serialization.only = [:id, :name]
|
115
|
+
end
|
116
|
+
# =>
|
117
|
+
# {"href"=>"http://test.app/client_api/foos/1",
|
118
|
+
# "name"=>"A foo",
|
119
|
+
# "bars_href"=>"http://test.app/client_api/foos/1/bars",
|
120
|
+
# "foo"=>
|
121
|
+
# {"id"=>1,
|
122
|
+
# "name"=>"A foo",}}
|
123
|
+
|
76
124
|
= More Docs
|
77
125
|
|
78
126
|
Please see the Restful rdoc and the specs for more details.
|
@@ -0,0 +1,414 @@
|
|
1
|
+
# This file is part of restful_serializer. Copyright 2011 Joshua Partlow. This is free software, see the LICENSE file for details.
|
2
|
+
|
3
|
+
module Restful
|
4
|
+
module Configuration
|
5
|
+
|
6
|
+
# Provides a register constructor which takes a block,
|
7
|
+
# exposing the created instance for fine grained configuration.
|
8
|
+
#
|
9
|
+
# A class including configurable should declare one or more
|
10
|
+
# options using the +option+ class macro.
|
11
|
+
#
|
12
|
+
# It expects a single option Hash for initialization.
|
13
|
+
#
|
14
|
+
# If you override initialize, you must call super(option_hash).
|
15
|
+
#
|
16
|
+
# A Configurable should allow an initialize with no arguments if
|
17
|
+
# it is going to be an element of an another Configurable's Hash
|
18
|
+
# or Array option. See (Option#generate_from)
|
19
|
+
module Configurable
|
20
|
+
|
21
|
+
def self.included(base)
|
22
|
+
base.class_eval do
|
23
|
+
extend ClassMethods
|
24
|
+
class_inheritable_array :options
|
25
|
+
self.options = []
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
# Constructor which takes a block, exposing the new instance
|
31
|
+
# for fine grained configuration
|
32
|
+
def register(*args, &block)
|
33
|
+
instance = new(*args)
|
34
|
+
yield(instance) if block_given?
|
35
|
+
return instance
|
36
|
+
end
|
37
|
+
|
38
|
+
def option(name, *args)
|
39
|
+
option = Restful::Configuration::Option.new(name, *args)
|
40
|
+
self.options << option
|
41
|
+
class_eval do
|
42
|
+
define_method(option.name) do
|
43
|
+
config[option.name]
|
44
|
+
end
|
45
|
+
|
46
|
+
define_method(option.mutator_method) do |value|
|
47
|
+
_was_explicitly_set(option.name)
|
48
|
+
config[option.name] = option.generate_from(value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(options = {})
|
55
|
+
options = (options || {}).symbolize_keys
|
56
|
+
set(options)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Clears the current configuration.
|
60
|
+
def reset
|
61
|
+
@config = nil
|
62
|
+
@explicitly_set = nil
|
63
|
+
options.each do |opt|
|
64
|
+
self.config[opt.name] = opt.initialized
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Clears the current configuration and resets with the passed options.
|
69
|
+
def set(options)
|
70
|
+
reset
|
71
|
+
_set(options)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns a plain deeply duplicated Hash with all options as key/values.
|
75
|
+
# Deeply converts all Configurable nodes to hash as well.
|
76
|
+
#
|
77
|
+
# * to_hash_options
|
78
|
+
# * :ignore_empty => if +ignore_empty+ is set to true, will not include
|
79
|
+
# options that point to empty containers. (Default false)
|
80
|
+
# * :skip_defaults => if +skip_defaults+ is set to true, will not include
|
81
|
+
# options that are passively set to their default value. Options actively
|
82
|
+
# set to their default value during initialization should remain. (Default false)
|
83
|
+
def to_hash(to_hash_options = {})
|
84
|
+
to_hash_options = (to_hash_options || {}).symbolize_keys
|
85
|
+
ignore_empty = to_hash_options[:ignore_empty] || false
|
86
|
+
skip_defaults = to_hash_options[:skip_defaults] || false
|
87
|
+
|
88
|
+
deeply_dup = lambda do |element|
|
89
|
+
return case element
|
90
|
+
when Configurable then element.to_hash(to_hash_options)
|
91
|
+
when Array then element.map { |e| deeply_dup.call(e) }
|
92
|
+
when Hash then
|
93
|
+
duped = element.dup
|
94
|
+
duped.each { |k,v| duped[k] = deeply_dup.call(v) }
|
95
|
+
duped
|
96
|
+
else element
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
config_hash = {}
|
101
|
+
config.each do |key,value|
|
102
|
+
next if skip_defaults && !value.nil? && !explicitly_set?(key) && find_option(key).default == value
|
103
|
+
|
104
|
+
duplicated = deeply_dup.call(value)
|
105
|
+
unless ignore_empty && duplicated.respond_to?(:empty?) && duplicated.empty?
|
106
|
+
config_hash[key] = duplicated
|
107
|
+
end
|
108
|
+
end
|
109
|
+
return config_hash
|
110
|
+
end
|
111
|
+
|
112
|
+
# A configurable equals another configurable if they are of the same class and
|
113
|
+
# their configurations are equal.
|
114
|
+
def ==(other)
|
115
|
+
return false unless other.class <= self.class
|
116
|
+
return self.config == other.config
|
117
|
+
end
|
118
|
+
|
119
|
+
# Equal Configurables should have equal hashes.
|
120
|
+
def hash
|
121
|
+
return config.hash
|
122
|
+
end
|
123
|
+
|
124
|
+
def deep_clone
|
125
|
+
Marshal.load(Marshal.dump(self))
|
126
|
+
end
|
127
|
+
|
128
|
+
# Produces a new Configurable deeply merged with the passed Configurable or Hash
|
129
|
+
# According to the semantics of the +deep_merge+ gem's deep_merge! call.
|
130
|
+
# If the Configurable or Hash has options unknown to this Configurable, ArgumentErrors
|
131
|
+
# will be raised.
|
132
|
+
#
|
133
|
+
# Empty container options and defaulted options are skipped.
|
134
|
+
def deep_merge!(other)
|
135
|
+
other_hash = case other
|
136
|
+
when Configurable then other.to_hash(:ignore_empty => true, :skip_defaults => true)
|
137
|
+
else other
|
138
|
+
end
|
139
|
+
|
140
|
+
hash = to_hash(:ignore_empty => true, :skip_defaults => true)
|
141
|
+
new_configurable = self.class.new
|
142
|
+
hash._rs_deep_merge!(other_hash)
|
143
|
+
return new_configurable.set(hash)
|
144
|
+
end
|
145
|
+
|
146
|
+
def option_names
|
147
|
+
options.map { |o| o.name }
|
148
|
+
end
|
149
|
+
|
150
|
+
# Lookup option by name.
|
151
|
+
def find_option(name)
|
152
|
+
return options.find { |o| o.name == name }
|
153
|
+
end
|
154
|
+
|
155
|
+
# True if the option was explicitly set by passing in a value (as opposed
|
156
|
+
# to passively set by default).
|
157
|
+
def explicitly_set?(option_name)
|
158
|
+
explicitly_set.include?(option_name.to_sym)
|
159
|
+
end
|
160
|
+
|
161
|
+
protected
|
162
|
+
|
163
|
+
def config
|
164
|
+
@config ||= {}
|
165
|
+
end
|
166
|
+
|
167
|
+
# Array of all option names which have been set by initialization options or through mutators.
|
168
|
+
# (not by default)
|
169
|
+
def explicitly_set
|
170
|
+
@explicitly_set ||= []
|
171
|
+
end
|
172
|
+
|
173
|
+
def _set(new_options)
|
174
|
+
new_options.each do |k,v|
|
175
|
+
option = find_option(k)
|
176
|
+
raise(ArgumentError, "#{self}#_set: Unknown option: #{k}. (known options: #{option_names.inspect})") unless option
|
177
|
+
_was_explicitly_set(k)
|
178
|
+
send(option.mutator_method, v)
|
179
|
+
end
|
180
|
+
return self
|
181
|
+
end
|
182
|
+
|
183
|
+
def _was_explicitly_set(option_name)
|
184
|
+
explicitly_set << option_name.to_sym
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
# Named option
|
190
|
+
#
|
191
|
+
# * :name => the name of the option, for use when configuring
|
192
|
+
# * :type => a default type for a complex option (like an Array, Hash or Resource)
|
193
|
+
# * :element_type => specifies the type for elements of a container like an
|
194
|
+
# Array or Hash
|
195
|
+
# * :default => if no type is given, may provide a default (such as +false+
|
196
|
+
# or +:value+)
|
197
|
+
class Option
|
198
|
+
attr_accessor :name, :default
|
199
|
+
attr_writer :type, :element_type
|
200
|
+
|
201
|
+
def initialize(name, options = {})
|
202
|
+
options = (options || {}).symbolize_keys
|
203
|
+
|
204
|
+
self.name = name.to_sym
|
205
|
+
self.type = options[:type]
|
206
|
+
self.element_type = options[:element_type]
|
207
|
+
self.default = options[:default]
|
208
|
+
end
|
209
|
+
|
210
|
+
def mutator_method
|
211
|
+
"#{name}="
|
212
|
+
end
|
213
|
+
|
214
|
+
def type
|
215
|
+
# lazy initialization
|
216
|
+
@type_class ||= _to_class(@type)
|
217
|
+
end
|
218
|
+
|
219
|
+
def element_type
|
220
|
+
@element_type_class ||= _to_class(@element_type)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Constructs a new option instance of the given type (or nil)
|
224
|
+
def initialized
|
225
|
+
case
|
226
|
+
when type then type.new
|
227
|
+
else default
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Generates a new option value from the given configuration. If
|
232
|
+
# type/element_type are configured this will generate new options of the
|
233
|
+
# given type from passed configuration Hashes.
|
234
|
+
def generate_from(value)
|
235
|
+
if type.nil?
|
236
|
+
value
|
237
|
+
elsif type <= Configurator
|
238
|
+
case value
|
239
|
+
when Hash then type.new(value)
|
240
|
+
when Configurator then value
|
241
|
+
else raise(ArgumentError, "Expected option '#{name}' to be of type '#{type}', but received '#{value}'")
|
242
|
+
end
|
243
|
+
elsif type <= Array
|
244
|
+
case value
|
245
|
+
when Array
|
246
|
+
element_type ? value.map { |e| _element_of_type(e) } : value
|
247
|
+
else Array(value)
|
248
|
+
end
|
249
|
+
elsif type <= Hash
|
250
|
+
case value
|
251
|
+
when Hash
|
252
|
+
if element_type
|
253
|
+
value.inject({}) do |hash,row|
|
254
|
+
hash[row[0].to_sym] = _element_of_type(row[1])
|
255
|
+
hash
|
256
|
+
end
|
257
|
+
else
|
258
|
+
value.symbolize_keys
|
259
|
+
end
|
260
|
+
when Array then value.inject({}) { |hash,v| hash[v] = nil; hash }
|
261
|
+
else { value => nil }
|
262
|
+
end
|
263
|
+
else
|
264
|
+
# A type we don't handle
|
265
|
+
value
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
private
|
270
|
+
|
271
|
+
# Generates a new element of element_type class
|
272
|
+
def _element_of_type(option_hash)
|
273
|
+
return element_type <= Restful::Configuration::Configurable ?
|
274
|
+
# Bypass any custom initialization and use Configurable#set(options)
|
275
|
+
element_type.new.set(option_hash) :
|
276
|
+
element_type.new(option_hash)
|
277
|
+
end
|
278
|
+
|
279
|
+
def _to_class(type)
|
280
|
+
case type
|
281
|
+
when String, Symbol then type.to_s.classify.constantize
|
282
|
+
else type
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
class Configurator
|
288
|
+
include Configurable
|
289
|
+
end
|
290
|
+
|
291
|
+
# Configuration object for one web service.
|
292
|
+
#
|
293
|
+
# = Options
|
294
|
+
#
|
295
|
+
# * :name => the name of the web service
|
296
|
+
#
|
297
|
+
# * :api_prefix => used to supply a prefix to generated name_route methods if method
|
298
|
+
# names cannot be inferred purely by resource class names.
|
299
|
+
#
|
300
|
+
# A web service for Reservation might prefix with :guest_api, so the named routes would
|
301
|
+
# be +guest_api_reservations_url+ rather than reservations_url, which might instead
|
302
|
+
# return a path for accessing a Reservation through an HTML interface (perhaps through
|
303
|
+
# a separate controller...)
|
304
|
+
#
|
305
|
+
# * :default_url_options => ActionController::UrlWriter requires a hash of default
|
306
|
+
# url options (notable { :host => 'foo.com' }). This can be set per WebService
|
307
|
+
# and globally via Restful.default_url_options.
|
308
|
+
#
|
309
|
+
# = Resources
|
310
|
+
#
|
311
|
+
# Resources configurations are set with a call to +register_resource+.
|
312
|
+
class WebService < Configurator
|
313
|
+
option :name
|
314
|
+
option :api_prefix
|
315
|
+
option :default_url_options, :type => :hash
|
316
|
+
option :resources, :type => :hash, :element_type => 'Restful::Configuration::Resource'
|
317
|
+
|
318
|
+
def initialize(name, options = {})
|
319
|
+
super(options.merge(:name => name))
|
320
|
+
end
|
321
|
+
|
322
|
+
# Adds a Restful::Configuration::Resource.
|
323
|
+
def register_resource(name, options = {}, &block)
|
324
|
+
resources[name] = Resource.register(options, &block)
|
325
|
+
end
|
326
|
+
|
327
|
+
# Returns a deep clone of the requested resource looked up by the passed key.
|
328
|
+
# Key may be a symbol, string, Class or instance of Class of the resource.
|
329
|
+
def resource_configuration_for(resource_key)
|
330
|
+
resource = case resource_key
|
331
|
+
when Symbol
|
332
|
+
resources[resource_key]
|
333
|
+
when String
|
334
|
+
resources[resource_key.to_sym]
|
335
|
+
when Class
|
336
|
+
resources[resource_key.name.underscore.to_sym]
|
337
|
+
else
|
338
|
+
resources[resource_key.class.name.underscore.to_sym]
|
339
|
+
end
|
340
|
+
return resource.nil? ? nil : resource.deep_clone
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Configuration object for one resource.
|
345
|
+
#
|
346
|
+
# = Options
|
347
|
+
#
|
348
|
+
# The following options may be set for each resource configured on a web service:
|
349
|
+
#
|
350
|
+
# * :name_method => method to call on an instance to produce a human meaningful
|
351
|
+
# reference for the instance. Defaults to :name.
|
352
|
+
# * :serialization => options to be passed to a
|
353
|
+
# Restful::Configuration::ARSerialization to configure serialization of the
|
354
|
+
# ActiveRecord instance itself.
|
355
|
+
# * :url_for => if the named_route helper method cannot be guessed from normal
|
356
|
+
# Rails restful syntax, it may be overriden here.
|
357
|
+
# * :associations => you may include href references to the instance's
|
358
|
+
# associations. This can be a single association, a simple array of
|
359
|
+
# assocations, or a hash of association href keys to assocation names. If
|
360
|
+
# this is set as a hash, then the key is the name of the href (without
|
361
|
+
# '_href'), and the value is the model association name. If value is nil,
|
362
|
+
# then key is assumed to be both.
|
363
|
+
# * :shallow => if you are serializing an association, by default member
|
364
|
+
# includes and association references are stripped. Set this to false to
|
365
|
+
# traverse deeply.
|
366
|
+
# * :no_inherited_options => normally a subclass inherits and merges into its
|
367
|
+
# base class settings. Setting this to true prevents this so that only the
|
368
|
+
# options specifically set for the class will be used. Default false.
|
369
|
+
#
|
370
|
+
class Resource < Configurator
|
371
|
+
option :name_method, :default => :name
|
372
|
+
option :url_for
|
373
|
+
option :associations, :type => :hash
|
374
|
+
option :serialization, :type => 'Restful::Configuration::ARSerialization'
|
375
|
+
option :no_inherited_options, :default => false
|
376
|
+
option :shallow, :default => false
|
377
|
+
|
378
|
+
alias_method :serialization_without_block, :serialization
|
379
|
+
def serialization(&block)
|
380
|
+
if block_given?
|
381
|
+
yield(serialization_without_block)
|
382
|
+
end
|
383
|
+
return serialization_without_block
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# Configuration for the ActiveRecord::Serialization::Serializer
|
388
|
+
# of one activerecord class.
|
389
|
+
#
|
390
|
+
# = Options
|
391
|
+
#
|
392
|
+
# * :only => an attribute name or an array of attribute names. Defines which
|
393
|
+
# attributes will be serialized.
|
394
|
+
# * :except => an attribute name or an array of attribute names. All attributes
|
395
|
+
# except these will be serialized. Inverse of :only. :only takes precedence.
|
396
|
+
# * :include => nested ARSerialization definitions for associations.
|
397
|
+
# * :methods => an method name or an array of method names to be included in the
|
398
|
+
# serialization.
|
399
|
+
#
|
400
|
+
# See ActiveRecord::Serialization.to_json for more details
|
401
|
+
class ARSerialization < Configurator
|
402
|
+
option :only, :type => :array
|
403
|
+
option :except, :type => :array
|
404
|
+
option :include, :type => :hash, :element_type => 'Restful::Configuration::ARSerialization'
|
405
|
+
option :methods, :type => :array
|
406
|
+
|
407
|
+
# Allows you to configure an included ARSerialization instance.
|
408
|
+
# Expects a block.
|
409
|
+
def includes(name, options = {}, &block)
|
410
|
+
self.include[name] = ARSerialization.register(options, &block)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|