• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gregschmit / rails-rest-framework / 15565579744

10 Jun 2025 04:57PM UTC coverage: 90.008%. Remained the same
15565579744

push

github

gregschmit
Remove Ruby 2.x from testing matrix.

1117 of 1241 relevant lines covered (90.01%)

153.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.7
/lib/rest_framework/serializers/native_serializer.rb
1
# This serializer uses `.serializable_hash` to convert objects to Ruby primitives (with the
2
# top-level being either an array or a hash).
3
class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers::BaseSerializer
2✔
4
  class_attribute :config
2✔
5
  class_attribute :singular_config
2✔
6
  class_attribute :plural_config
2✔
7
  class_attribute :action_config
2✔
8

9
  # Accept/ignore `*args` to be compatible with the `ActiveModel::Serializer#initialize` signature.
10
  def initialize(object = nil, *args, many: nil, model: nil, **kwargs)
2✔
11
    super(object, *args, **kwargs)
176✔
12

13
    if many.nil?
176✔
14
      # Determine if we are dealing with many objects or just one.
15
      @many = @object.is_a?(Enumerable)
174✔
16
    else
17
      @many = many
2✔
18
    end
19

20
    # Determine model either explicitly, or by inspecting @object or @controller.
21
    @model = model
176✔
22
    @model ||= @object.class if @object.is_a?(ActiveRecord::Base)
176✔
23
    @model ||= @object[0].class if
24
      @many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
176✔
25

26
    @model ||= @controller.class.get_model if @controller
176✔
27
  end
28

29
  # Get controller action, if possible.
30
  def get_action
2✔
31
    @controller&.action_name&.to_sym
174✔
32
  end
33

34
  # Get a locally defined native serializer configuration, if one is defined.
35
  def get_local_native_serializer_config
2✔
36
    action = self.get_action
174✔
37

38
    if action && self.action_config
174✔
39
      # Index action should use :list serializer config if :index is not provided.
40
      action = :list if action == :index && !self.action_config.key?(:index)
8✔
41

42
      return self.action_config[action] if self.action_config[action]
8✔
43
    end
44

45
    # No action_config, so try singular/plural config if explicitly instructed to via @many.
46
    return self.plural_config if @many == true && self.plural_config
166✔
47
    return self.singular_config if @many == false && self.singular_config
166✔
48

49
    # Lastly, try returning the default config, or singular/plural config in that order.
50
    self.config || self.singular_config || self.plural_config
166✔
51
  end
52

53
  # Get a native serializer configuration from the controller.
54
  def get_controller_native_serializer_config
2✔
55
    return nil unless @controller
164✔
56

57
    if @many == true
164✔
58
      controller_serializer = @controller.class.native_serializer_plural_config
76✔
59
    elsif @many == false
88✔
60
      controller_serializer = @controller.class.native_serializer_singular_config
88✔
61
    end
62

63
    controller_serializer || @controller.class.native_serializer_config
164✔
64
  end
65

66
  # Filter a single subconfig for specific keys. By default, keys from `fields` are removed from the
67
  # provided `subcfg`. There are two (mutually exclusive) options to adjust the behavior:
68
  #
69
  #  `add`: Add any `fields` to the `subcfg` which aren't already in the `subcfg`.
70
  #  `only`: Remove any values found in the `subcfg` not in `fields`.
71
  def self.filter_subcfg(subcfg, fields:, add: false, only: false)
2✔
72
    raise "`add` and `only` conflict with one another" if add && only
20✔
73

74
    # Don't process nil `subcfg`s.
75
    return subcfg unless subcfg
20✔
76

77
    if subcfg.is_a?(Array)
20✔
78
      subcfg = subcfg.map(&:to_sym)
8✔
79

80
      if add
8✔
81
        # Only add fields which are not already included.
82
        subcfg += fields - subcfg
×
83
      elsif only
8✔
84
        subcfg.select! { |c| c.in?(fields) }
16✔
85
      else
86
        subcfg -= fields
4✔
87
      end
88
    elsif subcfg.is_a?(Hash)
12✔
89
      subcfg = subcfg.symbolize_keys
12✔
90

91
      if add
12✔
92
        # Add doesn't make sense in a hash context since we wouldn't know the values.
93
      elsif only
12✔
94
        subcfg.select! { |k, _v| k.in?(fields) }
30✔
95
      else
96
        subcfg.reject! { |k, _v| k.in?(fields) }
30✔
97
      end
98
    else  # Subcfg is a single element (assume string/symbol).
99
      subcfg = subcfg.to_sym
×
100

101
      if add
×
102
        subcfg = subcfg.in?(fields) ? fields : [ subcfg, *fields ]
×
103
      elsif only
×
104
        subcfg = subcfg.in?(fields) ? subcfg : []
×
105
      else
106
        subcfg = subcfg.in?(fields) ? [] : subcfg
×
107
      end
108
    end
109

110
    subcfg
20✔
111
  end
112

113
  # Filter out configuration properties based on the :except/:only query parameters.
114
  def filter_from_request(cfg)
2✔
115
    return cfg unless @controller
174✔
116

117
    except_param = @controller.class.native_serializer_except_query_param
172✔
118
    only_param = @controller.class.native_serializer_only_query_param
172✔
119
    if except_param && except = @controller.request&.query_parameters&.[](except_param).presence
172✔
120
      if except = except.split(",").map(&:strip).map(&:to_sym).presence
2✔
121
        # Filter `only`, `except` (additive), `include`, `methods`, and `serializer_methods`.
122
        if cfg[:only]
2✔
123
          cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: except)
2✔
124
        elsif cfg[:except]
×
125
          cfg[:except] = self.class.filter_subcfg(cfg[:except], fields: except, add: true)
×
126
        else
127
          cfg[:except] = except
×
128
        end
129

130
        cfg[:include] = self.class.filter_subcfg(cfg[:include], fields: except)
2✔
131
        cfg[:methods] = self.class.filter_subcfg(cfg[:methods], fields: except)
2✔
132
        cfg[:serializer_methods] = self.class.filter_subcfg(
2✔
133
          cfg[:serializer_methods], fields: except
134
        )
135
        cfg[:includes_map] = self.class.filter_subcfg(cfg[:includes_map], fields: except)
2✔
136
      end
137
    elsif only_param && only = @controller.request&.query_parameters&.[](only_param).presence
170✔
138
      if only = only.split(",").map(&:strip).map(&:to_sym).presence
2✔
139
        # Filter `only`, `include`, and `methods`. Adding anything to `except` is not needed,
140
        # because any configuration there takes precedence over `only`.
141
        if cfg[:only]
2✔
142
          cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: only, only: true)
2✔
143
        else
144
          cfg[:only] = only
×
145
        end
146

147
        cfg[:include] = self.class.filter_subcfg(cfg[:include], fields: only, only: true)
2✔
148
        cfg[:methods] = self.class.filter_subcfg(cfg[:methods], fields: only, only: true)
2✔
149
        cfg[:serializer_methods] = self.class.filter_subcfg(
2✔
150
          cfg[:serializer_methods], fields: only, only: true
151
        )
152
        cfg[:includes_map] = self.class.filter_subcfg(cfg[:includes_map], fields: only, only: true)
2✔
153
      end
154
    end
155

156
    cfg
172✔
157
  end
158

159
  # Get the associations limit from the controller.
160
  def _get_associations_limit
2✔
161
    return @_get_associations_limit if defined?(@_get_associations_limit)
348✔
162

163
    limit = @controller&.class&.native_serializer_associations_limit
150✔
164

165
    # Extract the limit from the query parameters if it's set.
166
    if query_param = @controller&.class&.native_serializer_associations_limit_query_param
150✔
167
      if @controller.request.query_parameters.key?(query_param)
150✔
168
        query_limit = @controller.request.query_parameters[query_param].to_i
×
169
        if query_limit > 0
×
170
          limit = query_limit
×
171
        else
172
          limit = nil
×
173
        end
174
      end
175
    end
176

177
    @_get_associations_limit = limit
150✔
178
  end
179

180
  # Get a serializer configuration from the controller. `@controller` and `@model` must be set.
181
  def _get_controller_serializer_config(fields)
2✔
182
    columns = []
160✔
183
    includes = {}
160✔
184
    methods = []
160✔
185
    serializer_methods = {}
160✔
186

187
    # We try to construct performant queries using Active Record's `includes` method. This is
188
    # sometimes impossible, for example when limiting the number of associated records returned, so
189
    # we should only add associations here when it's useful, and using the `Bullet` gem is helpful
190
    # in determining when that is the case.
191
    includes_map = {}
160✔
192

193
    column_names = @model.column_names
160✔
194
    reflections = @model.reflections
160✔
195
    attachment_reflections = @model.attachment_reflections
160✔
196

197
    fields.each do |f|
160✔
198
      field_config = @controller.class.field_configuration[f]
1,998✔
199
      next if field_config[:write_only]
1,998✔
200

201
      if f.in?(column_names)
1,998✔
202
        columns << f
1,160✔
203
      elsif ref = reflections[f]
838✔
204
        sub_columns = []
594✔
205
        sub_methods = []
594✔
206
        field_config[:sub_fields].each do |sf|
594✔
207
          if !ref.polymorphic? && sf.in?(ref.klass.column_names)
1,166✔
208
            sub_columns << sf
1,166✔
209
          else
210
            sub_methods << sf
×
211
          end
212
        end
213
        sub_config = { only: sub_columns, methods: sub_methods }
594✔
214

215
        # Apply certain rules regarding collection associations.
216
        if ref.collection?
594✔
217
          # If we need to limit the number of serialized association records, then dynamically add a
218
          # serializer method to do so.
219
          if limit = self._get_associations_limit
348✔
220
            serializer_methods[f] = f
230✔
221
            self.define_singleton_method(f) do |record|
230✔
222
              next record.send(f).limit(limit).as_json(**sub_config)
2,416✔
223
            end
224

225
            # Disable this for now, as it's not clear if this improves performance of count.
226
            #
227
            # # Even though we use a serializer method, if the count will later be added, then put
228
            # # this field into the includes_map.
229
            # if @controller.class.native_serializer_include_associations_count
230
            #   includes_map[f] = f.to_sym
231
            # end
232
          else
233
            includes[f] = sub_config
118✔
234
            includes_map[f] = f.to_sym
118✔
235
          end
236

237
          # If we need to include the association count, then add it here.
238
          if @controller.class.native_serializer_include_associations_count
348✔
239
            method_name = "#{f}.count"
230✔
240
            serializer_methods[method_name] = method_name
230✔
241
            self.define_singleton_method(method_name) do |record|
230✔
242
              next record.send(f).count
2,416✔
243
            end
244
          end
245
        else
246
          includes[f] = sub_config
246✔
247
          includes_map[f] = f.to_sym
246✔
248
        end
249
      elsif @controller.class.enable_action_text && ref = reflections["rich_text_#{f}"]
244✔
250
        # ActionText Integration: Define rich text serializer method.
251
        includes_map[f] = :"rich_text_#{f}"
80✔
252
        serializer_methods[f] = f
80✔
253
        self.define_singleton_method(f) do |record|
80✔
254
          next record.send(f).to_s
822✔
255
        end
256
      elsif @controller.class.enable_active_storage && ref = attachment_reflections[f]
164✔
257
        # ActiveStorage Integration: Define attachment serializer method.
258
        if ref.macro == :has_one_attached
160✔
259
          serializer_methods[f] = f
80✔
260
          includes_map[f] = { "#{f}_attachment": :blob }
80✔
261
          self.define_singleton_method(f) do |record|
80✔
262
            attached = record.send(f)
822✔
263
            next attached.attachment ? {
822✔
264
              filename: attached.filename,
265
              signed_id: attached.signed_id,
266
              url: attached.url,
267
            } : nil
268
          end
269
        elsif ref.macro == :has_many_attached
80✔
270
          serializer_methods[f] = f
80✔
271
          includes_map[f] = { "#{f}_attachments": :blob }
80✔
272
          self.define_singleton_method(f) do |record|
80✔
273
            # Iterating the collection yields attachment objects.
274
            next record.send(f).map { |a|
822✔
275
              {
276
                filename: a.filename,
×
277
                signed_id: a.signed_id,
278
                url: a.url,
279
              }
280
            }
281
          end
282
        end
283
      elsif @model.method_defined?(f)
4✔
284
        methods << f
×
285
      else
286
        # Assume anything else is a virtual column.
287
        columns << f
4✔
288
      end
289
    end
290

291
    {
292
      only: columns,
160✔
293
      include: includes,
294
      methods: methods,
295
      serializer_methods: serializer_methods,
296
      includes_map: includes_map,
297
    }
298
  end
299

300
  # Get the raw serializer config, prior to any adjustments from the request.
301
  #
302
  # Use `deep_dup` on any class mutables (array, hash, etc) to avoid mutating class state.
303
  def get_raw_serializer_config
2✔
304
    # Return a locally defined serializer config if one is defined.
305
    if local_config = self.get_local_native_serializer_config
174✔
306
      return local_config.deep_dup
10✔
307
    end
308

309
    # Return a serializer config if one is defined on the controller.
310
    if serializer_config = self.get_controller_native_serializer_config
164✔
311
      return serializer_config.deep_dup
4✔
312
    end
313

314
    # If the config wasn't determined, build a serializer config from controller fields.
315
    if @model && fields = @controller&.get_fields
160✔
316
      return self._get_controller_serializer_config(fields.deep_dup)
160✔
317
    end
318

319
    # By default, pass an empty configuration, using the default Rails serializer.
320
    {}
×
321
  end
322

323
  # Get a configuration passable to `serializable_hash` for the object, filtered if required.
324
  def get_serializer_config
2✔
325
    self.filter_from_request(self.get_raw_serializer_config)
174✔
326
  end
327

328
  # Serialize a single record and merge results of `serializer_methods`.
329
  def _serialize(record, config, serializer_methods)
2✔
330
    # Ensure serializer_methods is either falsy, or a hash.
331
    if serializer_methods && !serializer_methods.is_a?(Hash)
13,572✔
332
      serializer_methods = [ serializer_methods ].flatten.map { |m| [ m, m ] }.to_h
×
333
    end
334

335
    # Merge serialized record with any serializer method results.
336
    record.serializable_hash(config).merge(
13,572✔
337
      serializer_methods&.map { |m, k| [ k.to_sym, self.send(m, record) ] }.to_h,
7,298✔
338
    )
339
  end
340

341
  def serialize(*args)
2✔
342
    config = self.get_serializer_config
174✔
343
    serializer_methods = config.delete(:serializer_methods)
174✔
344
    includes_map = config.delete(:includes_map)
174✔
345

346
    if @object.respond_to?(:to_ary)
174✔
347
      # Preload associations using `includes` to avoid N+1 queries. For now this also allows filter
348
      # backends to use associated data; perhaps it may be wise to have a system in place for
349
      # filters to preload their own associations?
350
      @object = @object.includes(*includes_map.values) if includes_map.present?
84✔
351

352
      return @object.map { |r| self._serialize(r, config, serializer_methods) }
13,566✔
353
    end
354

355
    self._serialize(@object, config, serializer_methods)
90✔
356
  end
357

358
  # Allow a serializer instance to be used as a hash directly in a nested serializer config.
359
  def [](key)
2✔
360
    @_nested_config ||= self.get_serializer_config
×
361
    @_nested_config[key]
×
362
  end
363

364
  def []=(key, value)
2✔
365
    @_nested_config ||= self.get_serializer_config
×
366
    @_nested_config[key] = value
×
367
  end
368

369
  # Allow a serializer class to be used as a hash directly in a nested serializer config.
370
  def self.[](key)
2✔
371
    @_nested_config ||= self.new.get_serializer_config
×
372
    @_nested_config[key]
×
373
  end
374

375
  def self.[]=(key, value)
2✔
376
    @_nested_config ||= self.new.get_serializer_config
×
377
    @_nested_config[key] = value
×
378
  end
379
end
380

381
# Alias for convenience.
382
RESTFramework::NativeSerializer = RESTFramework::Serializers::NativeSerializer
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc