active_model_serializers 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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