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