active_model_serializers 0.1.0

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,13 @@
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
+ task :default => :test
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ Gem::Specification.new do |gem|
3
+ gem.authors = ["José Valim", "Yehuda Katz"]
4
+ gem.email = ["jose.valim@gmail.com", "wycats@gmail.com"]
5
+ gem.description = %q{Making it easy to serialize models for client-side use}
6
+ gem.summary = %q{Bringing consistency and object orientation to model serialization. Works great for client-side MVC frameworks!}
7
+ gem.homepage = ""
8
+
9
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
10
+ gem.files = `git ls-files`.split("\n")
11
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
12
+ gem.name = "active_model_serializers"
13
+ gem.require_paths = ["lib"]
14
+ gem.version = "0.1.0"
15
+
16
+ gem.add_dependency 'activemodel', '~> 3.0'
17
+ gem.add_development_dependency "rails", "~> 3.0"
18
+ end
@@ -0,0 +1,52 @@
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+ in 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)
37
+ end
38
+
39
+ def _render_option_json(json, options)
40
+ if json.respond_to?(:active_model_serializer) && (serializer = json.active_model_serializer)
41
+ json = serializer.new(json, serialization_scope, options)
42
+ end
43
+ super
44
+ end
45
+
46
+ module ClassMethods
47
+ def serialization_scope(scope)
48
+ self._serialization_scope = scope
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,353 @@
1
+ require "active_support/core_ext/class/attribute"
2
+ require "active_support/core_ext/module/anonymous"
3
+
4
+ module ActiveModel
5
+ # Active Model Array Serializer
6
+ #
7
+ # It serializes an array checking if each element that implements
8
+ # the +active_model_serializer+ method passing down the current scope.
9
+ class ArraySerializer
10
+ attr_reader :object, :scope
11
+
12
+ def initialize(object, scope, options={})
13
+ @object, @scope, @options = object, scope, options
14
+ @hash = options[:hash]
15
+ end
16
+
17
+ def serializable_array
18
+ @object.map do |item|
19
+ if item.respond_to?(:active_model_serializer) && (serializer = item.active_model_serializer)
20
+ serializer.new(item, scope, :hash => @hash)
21
+ else
22
+ item
23
+ end
24
+ end
25
+ end
26
+
27
+ def as_json(*args)
28
+ @hash = {}
29
+ array = serializable_array.as_json(*args)
30
+
31
+ if root = @options[:root]
32
+ @hash.merge!(root => array)
33
+ else
34
+ array
35
+ end
36
+ end
37
+ end
38
+
39
+ # Active Model Serializer
40
+ #
41
+ # Provides a basic serializer implementation that allows you to easily
42
+ # control how a given object is going to be serialized. On initialization,
43
+ # it expects to object as arguments, a resource and a scope. For example,
44
+ # one may do in a controller:
45
+ #
46
+ # PostSerializer.new(@post, current_user).to_json
47
+ #
48
+ # The object to be serialized is the +@post+ and the scope is +current_user+.
49
+ #
50
+ # We use the scope to check if a given attribute should be serialized or not.
51
+ # For example, some attributes maybe only be returned if +current_user+ is the
52
+ # author of the post:
53
+ #
54
+ # class PostSerializer < ActiveModel::Serializer
55
+ # attributes :title, :body
56
+ # has_many :comments
57
+ #
58
+ # private
59
+ #
60
+ # def attributes
61
+ # hash = super
62
+ # hash.merge!(:email => post.email) if author?
63
+ # hash
64
+ # end
65
+ #
66
+ # def author?
67
+ # post.author == scope
68
+ # end
69
+ # end
70
+ #
71
+ class Serializer
72
+ module Associations #:nodoc:
73
+ class Config < Struct.new(:name, :options) #:nodoc:
74
+ def serializer
75
+ options[:serializer]
76
+ end
77
+
78
+ def key
79
+ options[:key] || name
80
+ end
81
+
82
+ protected
83
+
84
+ def find_serializable(object, scope, context, options)
85
+ if serializer
86
+ serializer.new(object, scope, options)
87
+ elsif object.respond_to?(:active_model_serializer) && (ams = object.active_model_serializer)
88
+ ams.new(object, scope, options)
89
+ else
90
+ object
91
+ end
92
+ end
93
+ end
94
+
95
+ class HasMany < Config #:nodoc:
96
+ def serialize(collection, scope, context, options)
97
+ array = collection.map do |item|
98
+ find_serializable(item, scope, context, options).as_json(:root => false)
99
+ end
100
+ { key => array }
101
+ end
102
+
103
+ def serialize_ids(collection, scope)
104
+ # Use pluck or select_columns if available
105
+ # return collection.ids if collection.respond_to?(:ids)
106
+
107
+ array = collection.map do |item|
108
+ item.read_attribute_for_serialization(:id)
109
+ end
110
+
111
+ { key => array }
112
+ end
113
+ end
114
+
115
+ class HasOne < Config #:nodoc:
116
+ def serialize(object, scope, context, options)
117
+ { key => object && find_serializable(object, scope, context, options).as_json(:root => false) }
118
+ end
119
+
120
+ def serialize_ids(object, scope)
121
+ { key => object && object.read_attribute_for_serialization(:id) }
122
+ end
123
+ end
124
+ end
125
+
126
+ class_attribute :_attributes
127
+ self._attributes = {}
128
+
129
+ class_attribute :_associations
130
+ self._associations = []
131
+
132
+ class_attribute :_root
133
+ class_attribute :_embed
134
+ self._embed = :objects
135
+ class_attribute :_root_embed
136
+
137
+ class << self
138
+ # Define attributes to be used in the serialization.
139
+ def attributes(*attrs)
140
+ self._attributes = _attributes.dup
141
+
142
+ attrs.each do |attr|
143
+ self._attributes[attr] = attr
144
+ end
145
+ end
146
+
147
+ def attribute(attr, options={})
148
+ self._attributes = _attributes.merge(attr => options[:key] || attr)
149
+ end
150
+
151
+ def associate(klass, attrs) #:nodoc:
152
+ options = attrs.extract_options!
153
+ self._associations += attrs.map do |attr|
154
+ unless method_defined?(attr)
155
+ class_eval "def #{attr}() object.#{attr} end", __FILE__, __LINE__
156
+ end
157
+ klass.new(attr, options)
158
+ end
159
+ end
160
+
161
+ # Defines an association in the object should be rendered.
162
+ #
163
+ # The serializer object should implement the association name
164
+ # as a method which should return an array when invoked. If a method
165
+ # with the association name does not exist, the association name is
166
+ # dispatched to the serialized object.
167
+ def has_many(*attrs)
168
+ associate(Associations::HasMany, attrs)
169
+ end
170
+
171
+ # Defines an association in the object should be rendered.
172
+ #
173
+ # The serializer object should implement the association name
174
+ # as a method which should return an object when invoked. If a method
175
+ # with the association name does not exist, the association name is
176
+ # dispatched to the serialized object.
177
+ def has_one(*attrs)
178
+ associate(Associations::HasOne, attrs)
179
+ end
180
+
181
+ # Return a schema hash for the current serializer. This information
182
+ # can be used to generate clients for the serialized output.
183
+ #
184
+ # The schema hash has two keys: +attributes+ and +associations+.
185
+ #
186
+ # The +attributes+ hash looks like this:
187
+ #
188
+ # { :name => :string, :age => :integer }
189
+ #
190
+ # The +associations+ hash looks like this:
191
+ #
192
+ # { :posts => { :has_many => :posts } }
193
+ #
194
+ # If :key is used:
195
+ #
196
+ # class PostsSerializer < ActiveModel::Serializer
197
+ # has_many :posts, :key => :my_posts
198
+ # end
199
+ #
200
+ # the hash looks like this:
201
+ #
202
+ # { :my_posts => { :has_many => :posts }
203
+ #
204
+ # This information is extracted from the serializer's model class,
205
+ # which is provided by +SerializerClass.model_class+.
206
+ #
207
+ # The schema method uses the +columns_hash+ and +reflect_on_association+
208
+ # methods, provided by default by ActiveRecord. You can implement these
209
+ # methods on your custom models if you want the serializer's schema method
210
+ # to work.
211
+ #
212
+ # TODO: This is currently coupled to Active Record. We need to
213
+ # figure out a way to decouple those two.
214
+ def schema
215
+ klass = model_class
216
+ columns = klass.columns_hash
217
+
218
+ attrs = _attributes.inject({}) do |hash, (name,key)|
219
+ column = columns[name.to_s]
220
+ hash.merge key => column.type
221
+ end
222
+
223
+ associations = _associations.inject({}) do |hash, association|
224
+ model_association = klass.reflect_on_association(association.name)
225
+ hash.merge association.key => { model_association.macro => model_association.name }
226
+ end
227
+
228
+ { :attributes => attrs, :associations => associations }
229
+ end
230
+
231
+ # The model class associated with this serializer.
232
+ def model_class
233
+ name.sub(/Serializer$/, '').constantize
234
+ end
235
+
236
+ # Define how associations should be embedded.
237
+ #
238
+ # embed :objects # Embed associations as full objects
239
+ # embed :ids # Embed only the association ids
240
+ # embed :ids, :include => true # Embed the association ids and include objects in the root
241
+ #
242
+ def embed(type, options={})
243
+ self._embed = type
244
+ self._root_embed = true if options[:include]
245
+ end
246
+
247
+ # Defines the root used on serialization. If false, disables the root.
248
+ def root(name)
249
+ self._root = name
250
+ end
251
+
252
+ def inherited(klass) #:nodoc:
253
+ return if klass.anonymous?
254
+ name = klass.name.demodulize.underscore.sub(/_serializer$/, '')
255
+
256
+ klass.class_eval do
257
+ alias_method name.to_sym, :object
258
+ root name.to_sym unless self._root == false
259
+ end
260
+ end
261
+ end
262
+
263
+ attr_reader :object, :scope
264
+
265
+ def initialize(object, scope, options={})
266
+ @object, @scope, @options = object, scope, options
267
+ @hash = options[:hash]
268
+ end
269
+
270
+ # Returns a json representation of the serializable
271
+ # object including the root.
272
+ def as_json(options=nil)
273
+ options ||= {}
274
+ if root = options.fetch(:root, @options.fetch(:root, _root))
275
+ @hash = hash = {}
276
+ hash.merge!(root => serializable_hash)
277
+ hash
278
+ else
279
+ @hash = serializable_hash
280
+ end
281
+ end
282
+
283
+ # Returns a hash representation of the serializable
284
+ # object without the root.
285
+ def serializable_hash
286
+ if _embed == :ids
287
+ merge_associations(@hash, associations) if _root_embed
288
+ attributes.merge(association_ids)
289
+ elsif _embed == :objects
290
+ attributes.merge(associations)
291
+ else
292
+ attributes
293
+ end
294
+ end
295
+
296
+ # Merge associations for embed case by always adding
297
+ # root associations to the given hash.
298
+ def merge_associations(hash, associations)
299
+ associations.each do |key, value|
300
+ if hash[key]
301
+ hash[key] |= value
302
+ elsif value
303
+ hash[key] = value
304
+ end
305
+ end
306
+ end
307
+
308
+ # Returns a hash representation of the serializable
309
+ # object associations.
310
+ def associations
311
+ hash = {}
312
+
313
+ _associations.each do |association|
314
+ associated_object = send(association.name)
315
+ hash.merge! association.serialize(associated_object, scope, self, :hash => @hash)
316
+ end
317
+
318
+ hash
319
+ end
320
+
321
+ # Returns a hash representation of the serializable
322
+ # object associations ids.
323
+ def association_ids
324
+ hash = {}
325
+
326
+ _associations.each do |association|
327
+ associated_object = send(association.name)
328
+ hash.merge! association.serialize_ids(associated_object, scope)
329
+ end
330
+
331
+ hash
332
+ end
333
+
334
+ # Returns a hash representation of the serializable
335
+ # object attributes.
336
+ def attributes
337
+ hash = {}
338
+
339
+ _attributes.each do |name,key|
340
+ hash[key] = @object.read_attribute_for_serialization(name)
341
+ end
342
+
343
+ hash
344
+ end
345
+ end
346
+ end
347
+
348
+ class Array
349
+ # Array uses ActiveModel::ArraySerializer.
350
+ def active_model_serializer
351
+ ActiveModel::ArraySerializer
352
+ end
353
+ end
@@ -0,0 +1,48 @@
1
+ require "active_support"
2
+ require "active_support/core_ext/string/inflections"
3
+ require "active_model"
4
+ require "active_model/serializer"
5
+
6
+ module ActiveModel::SerializerSupport
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods #:nodoc:
10
+ if "".respond_to?(:safe_constantize)
11
+ def active_model_serializer
12
+ @active_model_serializer ||= "#{self.name}Serializer".safe_constantize
13
+ end
14
+ else
15
+ def active_model_serializer
16
+ return @active_model_serializer if defined?(@active_model_serializer)
17
+
18
+ begin
19
+ @active_model_serializer = "#{self.name}Serializer".constantize
20
+ rescue NameError => e
21
+ raise unless e.message =~ /uninitialized constant/
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # Returns a model serializer for this object considering its namespace.
28
+ def active_model_serializer
29
+ self.class.active_model_serializer
30
+ end
31
+
32
+ alias :read_attribute_for_serialization :send
33
+ end
34
+
35
+ ActiveSupport.on_load(:active_record) do
36
+ include ActiveModel::SerializerSupport
37
+ end
38
+
39
+ begin
40
+ require 'action_controller'
41
+ require 'action_controller/serialization'
42
+
43
+ ActiveSupport.on_load(:action_controller) do
44
+ include ::ActionController::Serialization
45
+ end
46
+ rescue LoadError => ex
47
+ # rails on installed, continuing
48
+ end