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

gregschmit / rails-rest-framework / 24615084524

18 Apr 2026 10:14PM UTC coverage: 87.679% (-0.5%) from 88.164%
24615084524

push

github

gregschmit
Fix rubocop and tests.

2 of 2 new or added lines in 1 file covered. (100.0%)

74 existing lines in 7 files now uncovered.

1103 of 1258 relevant lines covered (87.68%)

205.64 hits per line

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

90.12
/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
  EXTRACT_FROM_QUERY = ->(p, controller) {
2✔
5
    return Set[] if p.blank?
632✔
6
    (
7
      controller.request&.query_parameters&.[](p).presence&.split(",")&.map { |x|
632✔
8
        x.strip.presence
24✔
9
      }&.compact || []
10
    ).to_set
11
  }
12
  class_attribute :config
2✔
13
  class_attribute :singular_config
2✔
14
  class_attribute :plural_config
2✔
15
  class_attribute :action_config
2✔
16

17
  # Accept/ignore `*args` to be compatible with the `ActiveModel::Serializer#initialize` signature.
18
  def initialize(object = nil, *args, many: nil, model: nil, **kwargs)
2✔
19
    super(object, *args, **kwargs)
174✔
20

21
    if many.nil?
174✔
22
      # Determine if we are dealing with many objects or just one.
23
      @many = @object.is_a?(Enumerable)
172✔
24
    else
25
      @many = many
2✔
26
    end
27

28
    # Determine model either explicitly, or by inspecting @object or @controller.
29
    @model = model
174✔
30
    @model ||= @object.class if @object.is_a?(ActiveRecord::Base)
174✔
31
    @model ||= @object.klass if @many && @object.is_a?(ActiveRecord::Relation)
174✔
32
    @model ||= @object.first.class if @many &&
174✔
33
      @object.is_a?(Enumerable) &&
34
      @object.first.is_a?(ActiveRecord::Base)
35

36
    @model ||= @controller.class.model if @controller
174✔
37
  end
38

39
  def action
2✔
40
    @action ||= @controller&.action_name&.to_sym
172✔
41
  end
42

43
  def fields
2✔
44
    return @fields if defined?(@fields)
316✔
45
    return nil unless base_fields = @controller&.get_fields
158✔
46

47
    only_param = @controller.class.native_serializer_only_query_param
158✔
48
    except_param = @controller.class.native_serializer_except_query_param
158✔
49
    include_param = @controller.class.native_serializer_include_query_param
158✔
50
    exclude_param = @controller.class.native_serializer_exclude_query_param
158✔
51

52
    only = EXTRACT_FROM_QUERY.call(only_param, @controller)
158✔
53
    except = EXTRACT_FROM_QUERY.call(except_param, @controller)
158✔
54
    include = EXTRACT_FROM_QUERY.call(include_param, @controller)
158✔
55
    exclude = EXTRACT_FROM_QUERY.call(exclude_param, @controller)
158✔
56

57
    field_configuration = @controller.class.field_configuration
158✔
58
    @fields = base_fields.select do |f|
158✔
59
      cfg = field_configuration[f]
2,096✔
60

61
      # We never serialize write-only fields.
62
      next false if cfg[:write_only]
2,096✔
63

64
      # We never serialize `hidden_from_index` fields for collections as this is a performance
65
      # option.
66
      next false if cfg[:hidden_from_index] && @many
2,096✔
67

68
      # Explicitly excluded fields should never be serialized.
69
      next false if f.in?(except) || f.in?(exclude)
2,090✔
70

71
      # Hidden fields must be in `only` or `include` to be serialized; for non-hidden fields, either
72
      # `only` must be empty, or the field must be in `only` or `include`.
73
      if cfg[:hidden]
2,084✔
74
        next true if f.in?(only) || f.in?(include)
12✔
75
      elsif only.empty? || f.in?(only) || f.in?(include)
2,072✔
76
        next true
1,954✔
77
      end
78

79
      next false
122✔
80
    end
81

82
    @fields
158✔
83
  end
84

85
  def get_local_native_serializer_config
2✔
86
    if (action = self.action) && (cfg = action_config)
172✔
87
      # Index action should use :list serializer config if :index is not provided.
88
      action = :list if action == :index && !cfg.key?(:index)
8✔
89

90
      return cfg[action] if cfg[action]
8✔
91
    end
92

93
    # No action_config, so try singular/plural config if explicitly instructed to via @many.
94
    return self.plural_config if @many == true && self.plural_config
164✔
95
    return self.singular_config if @many == false && self.singular_config
164✔
96

97
    # Lastly, try returning the default config.
98
    self.config
164✔
99
  end
100

101
  # Get a native serializer configuration from the controller.
102
  def get_controller_native_serializer_config
2✔
103
    return nil unless @controller
162✔
104

105
    if @many == true
162✔
106
      controller_serializer = @controller.class.native_serializer_plural_config
92✔
107
    elsif @many == false
70✔
108
      controller_serializer = @controller.class.native_serializer_singular_config
70✔
109
    end
110

111
    controller_serializer || @controller.class.native_serializer_config
162✔
112
  end
113

114
  # Get the associations limit from the controller.
115
  def _associations_limit
2✔
116
    return @_associations_limit if defined?(@_associations_limit)
334✔
117

118
    limit = @controller&.class&.native_serializer_associations_limit
138✔
119

120
    # Extract the limit from the query parameters if it's set.
121
    if query_param = @controller&.class&.native_serializer_associations_limit_query_param
138✔
122
      if @controller.request.query_parameters.key?(query_param)
138✔
123
        query_limit = @controller.request.query_parameters[query_param].to_i
×
UNCOV
124
        if query_limit > 0
×
125
          limit = query_limit
×
126
        else
UNCOV
127
          limit = nil
×
128
        end
129
      end
130
    end
131

132
    @_associations_limit = limit
138✔
133
  end
134

135
  # Get a serializer configuration from the controller. `@controller` and `@model` must be set.
136
  def _get_controller_serializer_config
2✔
137
    columns = []
158✔
138
    includes = {}
158✔
139
    methods = []
158✔
140
    serializer_methods = {}
158✔
141

142
    # We try to construct performant queries using Active Record's `includes` method. This is
143
    # sometimes impossible, for example when limiting the number of associated records returned, so
144
    # we should only add associations here when it's useful, and using the `Bullet` gem is helpful
145
    # in determining when that is the case.
146
    includes_map = {}
158✔
147

148
    column_names = @model.column_names
158✔
149
    reflections = @model.reflections
158✔
150
    attachment_reflections = @model.attachment_reflections
158✔
151

152
    self.fields.each do |f|
158✔
153
      field_config = @controller.class.field_configuration[f]
1,962✔
154
      next if field_config[:write_only]
1,962✔
155

156
      if f.in?(column_names)
1,962✔
157
        columns << f
1,184✔
158
      elsif ref = reflections[f]
778✔
159
        sub_columns = []
588✔
160
        sub_methods = []
588✔
161
        field_config[:sub_fields].each do |sf|
588✔
162
          if !ref.polymorphic? && sf.in?(ref.klass.column_names)
1,144✔
163
            sub_columns << sf
1,144✔
164
          else
UNCOV
165
            sub_methods << sf
×
166
          end
167
        end
168
        sub_config = { only: sub_columns, methods: sub_methods }
588✔
169

170
        # Apply certain rules regarding collection associations.
171
        if ref.collection?
588✔
172
          # If we need to limit the number of serialized association records, then dynamically add a
173
          # serializer method to do so.
174
          if limit = self._associations_limit
334✔
175
            serializer_methods[f] = f
190✔
176
            self.define_singleton_method(f) do |record|
190✔
177
              next record.send(f).limit(limit).as_json(**sub_config)
2,608✔
178
            end
179

180
            # Disable this for now, as it's not clear if this improves performance of count.
181
            #
182
            # # Even though we use a serializer method, if the count will later be added, then put
183
            # # this field into the includes_map.
184
            # if @controller.class.native_serializer_include_associations_count
185
            #   includes_map[f] = f.to_sym
186
            # end
187
          else
188
            includes[f] = sub_config
144✔
189
            includes_map[f] = f.to_sym
144✔
190
          end
191

192
          # If we need to include the association count, then add it here.
193
          if @controller.class.native_serializer_include_associations_count
334✔
194
            method_name = "#{f}.count"
190✔
195
            serializer_methods[method_name] = method_name
190✔
196
            self.define_singleton_method(method_name) do |record|
190✔
197
              next record.send(f).count
2,608✔
198
            end
199
          end
200
        else
201
          includes[f] = sub_config
254✔
202
          includes_map[f] = f.to_sym
254✔
203
        end
204
      elsif @controller.class.enable_action_text && ref = reflections["rich_text_#{f}"]
190✔
205
        # ActionText Integration: Define rich text serializer method.
206
        includes_map[f] = :"rich_text_#{f}"
58✔
207
        serializer_methods[f] = f
58✔
208
        self.define_singleton_method(f) do |record|
58✔
209
          next record.send(f).to_s
918✔
210
        end
211
      elsif @controller.class.enable_active_storage && ref = attachment_reflections[f]
132✔
212
        # ActiveStorage Integration: Define attachment serializer method.
213
        if ref.macro == :has_one_attached
116✔
214
          serializer_methods[f] = f
58✔
215
          includes_map[f] = { "#{f}_attachment": :blob }
58✔
216
          self.define_singleton_method(f) do |record|
58✔
217
            attached = record.send(f)
918✔
218
            next attached.attachment ? {
918✔
219
              filename: attached.filename,
220
              signed_id: attached.signed_id,
221
              url: attached.url,
222
            } : nil
223
          end
224
        elsif ref.macro == :has_many_attached
58✔
225
          serializer_methods[f] = f
58✔
226
          includes_map[f] = { "#{f}_attachments": :blob }
58✔
227
          self.define_singleton_method(f) do |record|
58✔
228
            # Iterating the collection yields attachment objects.
229
            next record.send(f).map { |a|
918✔
230
              {
UNCOV
231
                filename: a.filename,
×
232
                signed_id: a.signed_id,
233
                url: a.url,
234
              }
235
            }
236
          end
237
        end
238
      elsif @model.method_defined?(f)
16✔
239
        methods << f
12✔
240
      else
241
        # Assume anything else is a virtual column.
242
        columns << f
4✔
243
      end
244
    end
245

246
    {
247
      only: columns,
158✔
248
      include: includes,
249
      methods: methods,
250
      serializer_methods: serializer_methods,
251
      includes_map: includes_map,
252
    }
253
  end
254

255
  # Get the raw serializer config, prior to any adjustments from the request.
256
  #
257
  # Use `deep_dup` on any class mutables (array, hash, etc) to avoid mutating class state.
258
  def get_raw_serializer_config
2✔
259
    # Return a locally defined serializer config if one is defined.
260
    if local_config = self.get_local_native_serializer_config
172✔
261
      return local_config.deep_dup
10✔
262
    end
263

264
    # Return a serializer config if one is defined on the controller.
265
    if serializer_config = self.get_controller_native_serializer_config
162✔
266
      return serializer_config.deep_dup
4✔
267
    end
268

269
    # If the config wasn't determined, build a serializer config from controller fields.
270
    if @model && self.fields
158✔
271
      return self._get_controller_serializer_config
158✔
272
    end
273

274
    # By default, pass an empty configuration, using the default Rails serializer.
UNCOV
275
    {}
×
276
  end
277

278
  # Get a configuration passable to `serializable_hash` for the object.
279
  def get_serializer_config
2✔
280
    self.get_raw_serializer_config
172✔
281
  end
282

283
  # Serialize a single record and merge results of `serializer_methods`.
284
  def _serialize(record, config, serializer_methods)
2✔
285
    # Ensure serializer_methods is either falsy, or a hash.
286
    if serializer_methods && !serializer_methods.is_a?(Hash)
23,754✔
UNCOV
287
      serializer_methods = [ serializer_methods ].flatten.map { |m| [ m, m ] }.to_h
×
288
    end
289

290
    # Merge serialized record with any serializer method results.
291
    record.serializable_hash(config).merge(
23,754✔
292
      serializer_methods&.map { |m, k| [ k.to_sym, self.send(m, record) ] }.to_h,
7,970✔
293
    )
294
  end
295

296
  def serialize(*args)
2✔
297
    config = self.get_serializer_config
172✔
298
    serializer_methods = config.delete(:serializer_methods)
172✔
299
    includes_map = config.delete(:includes_map)
172✔
300

301
    if @object.respond_to?(:to_ary)
172✔
302
      # Preload associations using `includes` to avoid N+1 queries. For now this also allows filter
303
      # backends to use associated data; perhaps it may be wise to have a system in place for
304
      # filters to preload their own associations?
305
      @object = @object.includes(*includes_map.values) if includes_map.present?
100✔
306

307
      return @object.map { |r| self._serialize(r, config, serializer_methods) }
23,782✔
308
    end
309

310
    self._serialize(@object, config, serializer_methods)
72✔
311
  end
312

313
  # Allow a serializer instance to be used as a hash directly in a nested serializer config.
314
  def [](key)
2✔
UNCOV
315
    @_nested_config ||= self.get_serializer_config
×
UNCOV
316
    @_nested_config[key]
×
317
  end
318

319
  def []=(key, value)
2✔
UNCOV
320
    @_nested_config ||= self.get_serializer_config
×
UNCOV
321
    @_nested_config[key] = value
×
322
  end
323

324
  # Allow a serializer class to be used as a hash directly in a nested serializer config.
325
  def self.[](key)
2✔
UNCOV
326
    @_nested_config ||= self.new.get_serializer_config
×
UNCOV
327
    @_nested_config[key]
×
328
  end
329

330
  def self.[]=(key, value)
2✔
UNCOV
331
    @_nested_config ||= self.new.get_serializer_config
×
UNCOV
332
    @_nested_config[key] = value
×
333
  end
334
end
335

336
# Alias for convenience.
337
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