restful_serializer 0.1.3 → 0.1.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.
- 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
|