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 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
- # url prefix for calls to the web service
35
- Restful.api_prefix = 'client_api'
36
- Restful.model_configuration = {
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
- :bar => {
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