gpi-active_model_serializers 0.8.2

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.
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rake/testtask"
4
+
5
+ desc 'Run tests'
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'lib'
8
+ t.libs << 'test'
9
+ t.pattern = 'test/**/*_test.rb'
10
+ t.verbose = true
11
+ end
12
+
13
+ desc 'Benchmark'
14
+ task :bench do
15
+ load 'bench/perf.rb'
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ $:.unshift File.expand_path("../lib", __FILE__)
4
+ require "active_model/serializer/version"
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.authors = ["José Valim", "Yehuda Katz"]
8
+ gem.email = ["jose.valim@gmail.com", "wycats@gmail.com"]
9
+ gem.description = %q{Making it easy to serialize models for client-side use}
10
+ gem.summary = %q{Bringing consistency and object orientation to model serialization. Works great for client-side MVC frameworks!}
11
+ gem.homepage = "https://github.com/goplaceit/active_model_serializers"
12
+
13
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ gem.name = "gpi-active_model_serializers"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = ActiveModel::Serializer::VERSION
19
+
20
+ gem.required_ruby_version = ">= 1.9.3"
21
+
22
+ gem.add_dependency "activemodel", ">= 3.2"
23
+
24
+ gem.add_development_dependency "rails", ">= 3.2"
25
+ gem.add_development_dependency "pry"
26
+ gem.add_development_dependency "simplecov"
27
+ gem.add_development_dependency "coveralls"
28
+ end
@@ -0,0 +1,43 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ require "active_model_serializers"
4
+ require "active_support/json"
5
+ require "benchmark"
6
+
7
+ class User < Struct.new(:id,:name,:age,:about)
8
+ include ActiveModel::SerializerSupport
9
+
10
+ def fast_hash
11
+ h = {
12
+ id: read_attribute_for_serialization(:id),
13
+ name: read_attribute_for_serialization(:name),
14
+ about: read_attribute_for_serialization(:about)
15
+ }
16
+ h[:age] = read_attribute_for_serialization(:age) if age > 18
17
+ h
18
+ end
19
+ end
20
+
21
+ class UserSerializer < ActiveModel::Serializer
22
+ attributes :id, :name, :age, :about
23
+
24
+ def include_age?
25
+ object.age > 18
26
+ end
27
+ end
28
+
29
+
30
+
31
+ u = User.new(1, "sam", 10, "about")
32
+ s = UserSerializer.new(u)
33
+
34
+ n = 100000
35
+
36
+ Benchmark.bmbm {|x|
37
+ x.report("init") { n.times { UserSerializer.new(u) } }
38
+ x.report("fast_hash") { n.times { u.fast_hash } }
39
+ x.report("attributes") { n.times { UserSerializer.new(u).attributes } }
40
+ x.report("serializable_hash") { n.times { UserSerializer.new(u).serializable_hash } }
41
+ }
42
+
43
+
@@ -0,0 +1,19 @@
1
+ As of Ruby 1.9.3, it is impossible to dynamically generate a Symbol
2
+ through interpolation without generating garbage. Theoretically, Ruby
3
+ should be able to take care of this by building up the String in C and
4
+ interning the C String.
5
+
6
+ Because of this, we avoid generating dynamic Symbols at runtime. For
7
+ example, instead of generating the instrumentation event dynamically, we
8
+ have a constant with a Hash of events:
9
+
10
+ ```ruby
11
+ INSTRUMENT = {
12
+ serialize: :"serialize.serializer",
13
+ associations: :"associations.serializer"
14
+ }
15
+ ```
16
+
17
+ If Ruby ever fixes this issue and avoids generating garbage with dynamic
18
+ symbols, this code can be removed.
19
+
@@ -0,0 +1,58 @@
1
+ module ActionController
2
+ # Action Controller Serialization
3
+ #
4
+ # Overrides render :json to check if the given object implements +active_model_serializer+
5
+ # as a method. If so, use the returned serializer instead of calling +to_json+ on the object.
6
+ #
7
+ # This module also provides a serialization_scope method that allows you to configure the
8
+ # +serialization_scope+ of the serializer. Most apps will likely set the +serialization_scope+
9
+ # to the current user:
10
+ #
11
+ # class ApplicationController < ActionController::Base
12
+ # serialization_scope :current_user
13
+ # end
14
+ #
15
+ # If you need more complex scope rules, you can simply override the serialization_scope:
16
+ #
17
+ # class ApplicationController < ActionController::Base
18
+ # private
19
+ #
20
+ # def serialization_scope
21
+ # current_user
22
+ # end
23
+ # end
24
+ #
25
+ module Serialization
26
+ extend ActiveSupport::Concern
27
+
28
+ include ActionController::Renderers
29
+
30
+ included do
31
+ class_attribute :_serialization_scope
32
+ self._serialization_scope = :current_user
33
+ end
34
+
35
+ def serialization_scope
36
+ send(_serialization_scope) if _serialization_scope && respond_to?(_serialization_scope, true)
37
+ end
38
+
39
+ def default_serializer_options
40
+ end
41
+
42
+ def _render_option_json(resource, options)
43
+ json = ActiveModel::Serializer.build_json(self, resource, options)
44
+
45
+ if json
46
+ super(json, options)
47
+ else
48
+ super
49
+ end
50
+ end
51
+
52
+ module ClassMethods
53
+ def serialization_scope(scope)
54
+ self._serialization_scope = scope
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,65 @@
1
+ require 'active_model/serializable'
2
+ require 'active_model/serializer/caching'
3
+ require "active_support/core_ext/class/attribute"
4
+ require 'active_support/dependencies'
5
+ require 'active_support/descendants_tracker'
6
+
7
+ module ActiveModel
8
+ # Active Model Array Serializer
9
+ #
10
+ # Serializes an Array, checking if each element implements
11
+ # the +active_model_serializer+ method.
12
+ #
13
+ # To disable serialization of root elements:
14
+ #
15
+ # ActiveModel::ArraySerializer.root = false
16
+ #
17
+ class ArraySerializer
18
+ extend ActiveSupport::DescendantsTracker
19
+
20
+ include ActiveModel::Serializable
21
+ include ActiveModel::Serializer::Caching
22
+
23
+ attr_reader :object, :options
24
+
25
+ class_attribute :root
26
+
27
+ class_attribute :cache
28
+ class_attribute :perform_caching
29
+
30
+ class << self
31
+ # set perform caching like root
32
+ def cached(value = true)
33
+ self.perform_caching = value
34
+ end
35
+ end
36
+
37
+ def initialize(object, options={})
38
+ @object = object
39
+ @options = options
40
+ end
41
+
42
+ def serialize_object
43
+ serializable_array
44
+ end
45
+
46
+ def serializable_array
47
+ object.map do |item|
48
+ if options.has_key? :each_serializer
49
+ serializer = options[:each_serializer]
50
+ elsif item.respond_to?(:active_model_serializer)
51
+ serializer = item.active_model_serializer
52
+ end
53
+ serializer ||= DefaultSerializer
54
+
55
+ serializable = serializer.new(item, options.merge(root: nil))
56
+
57
+ if serializable.respond_to?(:serializable_hash)
58
+ serializable.serializable_hash
59
+ else
60
+ serializable.as_json
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_support/core_ext/object/json'
2
+
3
+ module ActiveModel
4
+ # Enable classes to Classes including this module to serialize themselves by implementing a serialize method and an options method.
5
+ #
6
+ # Example:
7
+ #
8
+ # require 'active_model_serializers'
9
+ #
10
+ # class MySerializer
11
+ # include ActiveModel::Serializable
12
+ #
13
+ # def initialize
14
+ # @options = {}
15
+ # end
16
+ #
17
+ # attr_reader :options
18
+ #
19
+ # def serialize
20
+ # { a: 1 }
21
+ # end
22
+ # end
23
+ #
24
+ # puts MySerializer.new.to_json
25
+ module Serializable
26
+ def as_json(args={})
27
+ if root = args[:root] || options[:root]
28
+ options[:hash] = hash = {}
29
+ options[:unique_values] = {}
30
+
31
+ hash.merge!(root => serialize)
32
+ include_meta hash
33
+ hash
34
+ else
35
+ serialize
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def include_meta(hash)
42
+ hash[meta_key] = options[:meta] if options.has_key?(:meta)
43
+ end
44
+
45
+ def meta_key
46
+ options[:meta_key].try(:to_sym) || :meta
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,475 @@
1
+ require 'active_model/serializable'
2
+ require 'active_model/serializer/caching'
3
+ require "active_support/core_ext/class/attribute"
4
+ require "active_support/core_ext/module/anonymous"
5
+ require 'active_support/dependencies'
6
+ require 'active_support/descendants_tracker'
7
+
8
+ module ActiveModel
9
+ # Active Model Serializer
10
+ #
11
+ # Provides a basic serializer implementation that allows you to easily
12
+ # control how a given object is going to be serialized. On initialization,
13
+ # it expects two objects as arguments, a resource and options. For example,
14
+ # one may do in a controller:
15
+ #
16
+ # PostSerializer.new(@post, scope: current_user).to_json
17
+ #
18
+ # The object to be serialized is the +@post+ and the current user is passed
19
+ # in for authorization purposes.
20
+ #
21
+ # We use the scope to check if a given attribute should be serialized or not.
22
+ # For example, some attributes may only be returned if +current_user+ is the
23
+ # author of the post:
24
+ #
25
+ # class PostSerializer < ActiveModel::Serializer
26
+ # attributes :title, :body
27
+ # has_many :comments
28
+ #
29
+ # private
30
+ #
31
+ # def attributes
32
+ # hash = super
33
+ # hash.merge!(email: post.email) if author?
34
+ # hash
35
+ # end
36
+ #
37
+ # def author?
38
+ # post.author == scope
39
+ # end
40
+ # end
41
+ #
42
+ class Serializer
43
+ extend ActiveSupport::DescendantsTracker
44
+
45
+ include ActiveModel::Serializable
46
+ include ActiveModel::Serializer::Caching
47
+
48
+ INCLUDE_METHODS = {}
49
+ INSTRUMENT = { serialize: :"serialize.serializer", associations: :"associations.serializer" }
50
+
51
+ class IncludeError < StandardError
52
+ attr_reader :source, :association
53
+
54
+ def initialize(source, association)
55
+ @source, @association = source, association
56
+ end
57
+
58
+ def to_s
59
+ "Cannot serialize #{association} when #{source} does not have a root!"
60
+ end
61
+ end
62
+
63
+ class_attribute :_attributes
64
+ self._attributes = {}
65
+
66
+ class_attribute :_associations
67
+ self._associations = {}
68
+
69
+ class_attribute :_root
70
+ class_attribute :_embed
71
+ self._embed = :objects
72
+ class_attribute :_root_embed
73
+
74
+ class_attribute :cache
75
+ class_attribute :perform_caching
76
+
77
+ class << self
78
+ def cached(value = true)
79
+ self.perform_caching = value
80
+ end
81
+
82
+ # Define attributes to be used in the serialization.
83
+ def attributes(*attrs)
84
+
85
+ self._attributes = _attributes.dup
86
+
87
+ attrs.each do |attr|
88
+ if Hash === attr
89
+ attr.each {|attr_real, key| attribute(attr_real, key: key) }
90
+ else
91
+ attribute attr
92
+ end
93
+ end
94
+ end
95
+
96
+ def attribute(attr, options={})
97
+ self._attributes = _attributes.merge(attr.is_a?(Hash) ? attr : {attr => options[:key] || attr.to_s.gsub(/\?$/, '').to_sym})
98
+
99
+ attr = attr.keys[0] if attr.is_a? Hash
100
+
101
+ unless method_defined?(attr)
102
+ define_method attr do
103
+ object.read_attribute_for_serialization(attr.to_sym)
104
+ end
105
+ end
106
+
107
+ define_include_method attr
108
+
109
+ # protect inheritance chains and open classes
110
+ # if a serializer inherits from another OR
111
+ # attributes are added later in a classes lifecycle
112
+ # poison the cache
113
+ define_method :_fast_attributes do
114
+ raise NameError
115
+ end
116
+
117
+ end
118
+
119
+ def associate(klass, attrs) #:nodoc:
120
+ options = attrs.extract_options!
121
+ self._associations = _associations.dup
122
+
123
+ attrs.each do |attr|
124
+ unless method_defined?(attr)
125
+ define_method attr do
126
+ object.send attr
127
+ end
128
+ end
129
+
130
+ define_include_method attr
131
+
132
+ self._associations[attr] = [klass, options]
133
+ end
134
+ end
135
+
136
+ def define_include_method(name)
137
+ method = "include_#{name}?".to_sym
138
+
139
+ INCLUDE_METHODS[name] = method
140
+
141
+ unless method_defined?(method)
142
+ define_method method do
143
+ true
144
+ end
145
+ end
146
+ end
147
+
148
+ # Defines an association in the object should be rendered.
149
+ #
150
+ # The serializer object should implement the association name
151
+ # as a method which should return an array when invoked. If a method
152
+ # with the association name does not exist, the association name is
153
+ # dispatched to the serialized object.
154
+ def has_many(*attrs)
155
+ associate(Association::HasMany, attrs)
156
+ end
157
+
158
+ # Defines an association in the object should be rendered.
159
+ #
160
+ # The serializer object should implement the association name
161
+ # as a method which should return an object when invoked. If a method
162
+ # with the association name does not exist, the association name is
163
+ # dispatched to the serialized object.
164
+ def has_one(*attrs)
165
+ associate(Association::HasOne, attrs)
166
+ end
167
+
168
+ # Return a schema hash for the current serializer. This information
169
+ # can be used to generate clients for the serialized output.
170
+ #
171
+ # The schema hash has two keys: +attributes+ and +associations+.
172
+ #
173
+ # The +attributes+ hash looks like this:
174
+ #
175
+ # { name: :string, age: :integer }
176
+ #
177
+ # The +associations+ hash looks like this:
178
+ # { posts: { has_many: :posts } }
179
+ #
180
+ # If :key is used:
181
+ #
182
+ # class PostsSerializer < ActiveModel::Serializer
183
+ # has_many :posts, key: :my_posts
184
+ # end
185
+ #
186
+ # the hash looks like this:
187
+ #
188
+ # { my_posts: { has_many: :posts }
189
+ #
190
+ # This information is extracted from the serializer's model class,
191
+ # which is provided by +SerializerClass.model_class+.
192
+ #
193
+ # The schema method uses the +columns_hash+ and +reflect_on_association+
194
+ # methods, provided by default by ActiveRecord. You can implement these
195
+ # methods on your custom models if you want the serializer's schema method
196
+ # to work.
197
+ #
198
+ # TODO: This is currently coupled to Active Record. We need to
199
+ # figure out a way to decouple those two.
200
+ def schema
201
+ klass = model_class
202
+ columns = klass.columns_hash
203
+
204
+ attrs = {}
205
+ _attributes.each do |name, key|
206
+ if column = columns[name.to_s]
207
+ attrs[key] = column.type
208
+ else
209
+ # Computed attribute (method on serializer or model). We cannot
210
+ # infer the type, so we put nil, unless specified in the attribute declaration
211
+ if name != key
212
+ attrs[name] = key
213
+ else
214
+ attrs[key] = nil
215
+ end
216
+ end
217
+ end
218
+
219
+ associations = {}
220
+ _associations.each do |attr, (association_class, options)|
221
+ association = association_class.new(attr, options)
222
+
223
+ if model_association = klass.reflect_on_association(association.name)
224
+ # Real association.
225
+ associations[association.key] = { model_association.macro => model_association.name }
226
+ else
227
+ # Computed association. We could infer has_many vs. has_one from
228
+ # the association class, but that would make it different from
229
+ # real associations, which read has_one vs. belongs_to from the
230
+ # model.
231
+ associations[association.key] = nil
232
+ end
233
+ end
234
+
235
+ { attributes: attrs, associations: associations }
236
+ end
237
+
238
+ # The model class associated with this serializer.
239
+ def model_class
240
+ name.sub(/Serializer$/, '').constantize
241
+ end
242
+
243
+ # Define how associations should be embedded.
244
+ #
245
+ # embed :objects # Embed associations as full objects
246
+ # embed :ids # Embed only the association ids
247
+ # embed :ids, include: true # Embed the association ids and include objects in the root
248
+ #
249
+ def embed(type, options={})
250
+ self._embed = type
251
+ self._root_embed = true if options[:include]
252
+ end
253
+
254
+ # Defines the root used on serialization. If false, disables the root.
255
+ def root(name)
256
+ self._root = name
257
+ end
258
+ alias_method :root=, :root
259
+
260
+ # Used internally to create a new serializer object based on controller
261
+ # settings and options for a given resource. These settings are typically
262
+ # set during the request lifecycle or by the controller class, and should
263
+ # not be manually defined for this method.
264
+ def build_json(controller, resource, options)
265
+ default_options = controller.send(:default_serializer_options) || {}
266
+ options = default_options.merge(options || {})
267
+
268
+ serializer = options.delete(:serializer) ||
269
+ (resource.respond_to?(:active_model_serializer) &&
270
+ resource.active_model_serializer)
271
+
272
+ return serializer unless serializer
273
+
274
+ if resource.respond_to?(:to_ary)
275
+ unless serializer <= ActiveModel::ArraySerializer
276
+ raise ArgumentError.new("#{serializer.name} is not an ArraySerializer. " +
277
+ "You may want to use the :each_serializer option instead.")
278
+ end
279
+
280
+ if options[:root] != false && serializer.root != false
281
+ # the serializer for an Array is ActiveModel::ArraySerializer
282
+ options[:root] ||= serializer.root || controller.controller_name
283
+ end
284
+ end
285
+
286
+ options[:scope] = controller.serialization_scope unless options.has_key?(:scope)
287
+ options[:scope_name] = controller._serialization_scope unless options.has_key?(:scope_name)
288
+ options[:url_options] = controller.url_options
289
+
290
+ serializer.new(resource, options)
291
+ end
292
+ end
293
+
294
+ attr_reader :object, :options
295
+
296
+ def initialize(object, options={})
297
+ @object, @options = object, options
298
+
299
+ scope_name = @options[:scope_name]
300
+ if scope_name && !respond_to?(scope_name)
301
+ self.class.class_eval do
302
+ define_method scope_name, lambda { scope }
303
+ end
304
+ end
305
+ end
306
+
307
+ def root_name
308
+ return false if self._root == false
309
+
310
+ class_name = self.class.name.demodulize.underscore.sub(/_serializer$/, '').to_sym unless self.class.name.blank?
311
+
312
+ if self._root == true
313
+ class_name
314
+ else
315
+ self._root || class_name
316
+ end
317
+ end
318
+
319
+ def url_options
320
+ @options[:url_options] || {}
321
+ end
322
+
323
+ # Returns a json representation of the serializable
324
+ # object including the root.
325
+ def as_json(args={})
326
+ super(root: args.fetch(:root, options.fetch(:root, root_name)))
327
+ end
328
+
329
+ def serialize_object
330
+ serializable_hash
331
+ end
332
+
333
+ # Returns a hash representation of the serializable
334
+ # object without the root.
335
+ def serializable_hash
336
+ return nil if @object.nil?
337
+ @node = attributes
338
+ include_associations! if _embed
339
+ @node
340
+ end
341
+
342
+ def include_associations!
343
+ _associations.each_key do |name|
344
+ include!(name) if include?(name)
345
+ end
346
+ end
347
+
348
+ def include?(name)
349
+ return false if @options.key?(:only) && !Array(@options[:only]).include?(name)
350
+ return false if @options.key?(:except) && Array(@options[:except]).include?(name)
351
+ send INCLUDE_METHODS[name]
352
+ end
353
+
354
+ def include!(name, options={})
355
+ hash = @options[:hash]
356
+ unique_values = @options[:unique_values] ||= {}
357
+
358
+ node = options[:node] ||= @node
359
+ value = options[:value]
360
+
361
+ if options[:include] == nil
362
+ if @options.key?(:include)
363
+ options[:include] = @options[:include].include?(name)
364
+ elsif @options.include?(:exclude)
365
+ options[:include] = !@options[:exclude].include?(name)
366
+ end
367
+ end
368
+
369
+ klass, klass_options = _associations[name]
370
+ association_class =
371
+ if klass
372
+ options = klass_options.merge options
373
+ klass
374
+ elsif value.respond_to?(:to_ary)
375
+ Association::HasMany
376
+ else
377
+ Association::HasOne
378
+ end
379
+
380
+ options = default_embed_options.merge!(options)
381
+ options[:value] ||= send(name)
382
+ association = association_class.new(name, options, self.options)
383
+
384
+ if association.embed_ids?
385
+ node[association.key] = association.serialize_ids
386
+
387
+ if association.embed_in_root? && hash.nil?
388
+ raise IncludeError.new(self.class, association.name)
389
+ elsif association.embed_in_root? && association.embeddable?
390
+ merge_association hash, association.root, association.serializables, unique_values
391
+ end
392
+ elsif association.embed_objects?
393
+ node[association.key] = association.serialize
394
+ end
395
+ end
396
+
397
+ # In some cases, an Array of associations is built by merging the associated
398
+ # content for all of the children. For instance, if a Post has_many comments,
399
+ # which has_many tags, the top-level :tags key will contain the merged list
400
+ # of all tags for all comments of the post.
401
+ #
402
+ # In order to make this efficient, we store a :unique_values hash containing
403
+ # a unique list of all of the objects that are already in the Array. This
404
+ # avoids the need to scan through the Array looking for entries every time
405
+ # we want to merge a new list of values.
406
+ def merge_association(hash, key, serializables, unique_values)
407
+ already_serialized = (unique_values[key] ||= {})
408
+ serializable_hashes = (hash[key] ||= [])
409
+
410
+ serializables.each do |serializable|
411
+ unless already_serialized.include? serializable.object
412
+ already_serialized[serializable.object] = true
413
+ serializable_hashes << serializable.serializable_hash
414
+ end
415
+ end
416
+ end
417
+
418
+ # Returns a hash representation of the serializable
419
+ # object attributes.
420
+ def attributes
421
+ _fast_attributes
422
+ rescue NameError
423
+ method = "def _fast_attributes\n"
424
+
425
+ method << " h = {}\n"
426
+
427
+ _attributes.each do |name,key|
428
+ method << " h[:\"#{key}\"] = read_attribute_for_serialization(:\"#{name}\") if include?(:\"#{name}\")\n"
429
+ end
430
+ method << " h\nend"
431
+
432
+ self.class.class_eval method
433
+ _fast_attributes
434
+ end
435
+
436
+ # Returns options[:scope]
437
+ def scope
438
+ @options[:scope]
439
+ end
440
+
441
+ alias :read_attribute_for_serialization :send
442
+
443
+ # Use ActiveSupport::Notifications to send events to external systems.
444
+ # The event name is: name.class_name.serializer
445
+ def instrument(name, payload = {}, &block)
446
+ event_name = INSTRUMENT[name]
447
+ ActiveSupport::Notifications.instrument(event_name, payload, &block)
448
+ end
449
+
450
+ private
451
+
452
+ def default_embed_options
453
+ {
454
+ embed: _embed,
455
+ include: _root_embed
456
+ }
457
+ end
458
+ end
459
+
460
+ # DefaultSerializer
461
+ #
462
+ # Provides a constant interface for all items, particularly
463
+ # for ArraySerializer.
464
+ class DefaultSerializer
465
+ attr_reader :object, :options
466
+
467
+ def initialize(object, options={})
468
+ @object, @options = object, options
469
+ end
470
+
471
+ def serializable_hash
472
+ @object.as_json(@options)
473
+ end
474
+ end
475
+ end