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

gregschmit / rails-rest-framework / 4002900794

pending completion
4002900794

push

github

GitHub
Bump commonmarker from 0.23.6 to 0.23.7 in /docs

807 of 890 relevant lines covered (90.67%)

74.1 hits per line

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

96.67
/lib/rest_framework/controller_mixins/base.rb
1
require_relative "../errors"
1✔
2
require_relative "../serializers"
1✔
3
require_relative "../utils"
1✔
4

5
# This module provides the common functionality for any controller mixins, a `root` action, and
6
# the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
7
# is defined.
8
module RESTFramework::BaseControllerMixin
1✔
9
  RRF_BASE_CONTROLLER_CONFIG = {
10
    filter_pk_from_request_body: true,
1✔
11
    exclude_body_fields: [
12
      :created_at, :created_by, :created_by_id, :updated_at, :updated_by, :updated_by_id
13
    ].freeze,
14
    extra_actions: nil,
15
    extra_member_actions: nil,
16
    filter_backends: nil,
17
    singleton_controller: nil,
18

19
    # Options related to metadata and display.
20
    title: nil,
21
    description: nil,
22
    inflect_acronyms: ["ID", "IDs", "REST", "API", "APIs"].freeze,
23

24
    # Options related to serialization.
25
    rescue_unknown_format_with: :json,
26
    serializer_class: nil,
27
    serialize_to_json: true,
28
    serialize_to_xml: true,
29

30
    # Options related to pagination.
31
    paginator_class: nil,
32
    page_size: 20,
33
    page_query_param: "page",
34
    page_size_query_param: "page_size",
35
    max_page_size: nil,
36

37
    # Options related to bulk actions and batch processing.
38
    bulk_guard_query_param: nil,
39
    enable_batch_processing: nil,
40

41
    # Option to disable serializer adapters by default, mainly introduced because Active Model
42
    # Serializers will do things like serialize `[]` into `{"":[]}`.
43
    disable_adapters_by_default: true,
44
  }
45

46
  # Default action for API root.
47
  def root
1✔
48
    api_response({message: "This is the API root."})
5✔
49
  end
50

51
  module ClassMethods
1✔
52
    # Get the title of this controller. By default, this is the name of the controller class,
53
    # titleized and with any custom inflection acronyms applied.
54
    def get_title
1✔
55
      return self.title || RESTFramework::Utils.inflect(
49✔
56
        self.name.demodulize.chomp("Controller").titleize(keep_id_suffix: true),
57
        self.inflect_acronyms,
58
      )
59
    end
60

61
    # Get a label from a field/column name, titleized and inflected.
62
    def get_label(s)
1✔
63
      return RESTFramework::Utils.inflect(s.titleize(keep_id_suffix: true), self.inflect_acronyms)
64✔
64
    end
65

66
    # Collect actions (including extra actions) metadata for this controller.
67
    def get_actions_metadata
1✔
68
      actions = {}
7✔
69

70
      # Start with builtin actions.
71
      RESTFramework::BUILTIN_ACTIONS.merge(
7✔
72
        RESTFramework::RRF_BUILTIN_ACTIONS,
73
      ).each do |action, methods|
74
        if self.method_defined?(action)
28✔
75
          actions[action] = {path: "", methods: methods, type: :builtin}
21✔
76
        end
77
      end
78

79
      # Add builtin bulk actions.
80
      RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
7✔
81
        if self.method_defined?(action)
14✔
82
          actions[action] = {path: "", methods: methods, type: :builtin}
×
83
        end
84
      end
85

86
      # Add extra actions.
87
      if extra_actions = self.try(:extra_actions)
7✔
88
        actions.merge!(RESTFramework::Utils.parse_extra_actions(extra_actions, controller: self))
1✔
89
      end
90

91
      return actions
7✔
92
    end
93

94
    # Collect member actions (including extra member actions) metadata for this controller.
95
    def get_member_actions_metadata
1✔
96
      actions = {}
7✔
97

98
      # Start with builtin actions.
99
      RESTFramework::BUILTIN_MEMBER_ACTIONS.each do |action, methods|
7✔
100
        if self.method_defined?(action)
28✔
101
          actions[action] = {path: "", methods: methods, type: :builtin}
21✔
102
        end
103
      end
104

105
      # Add extra actions.
106
      if extra_actions = self.try(:extra_member_actions)
7✔
107
        actions.merge!(RESTFramework::Utils.parse_extra_actions(extra_actions, controller: self))
2✔
108
      end
109

110
      return actions
7✔
111
    end
112

113
    # Get a hash of metadata to be rendered in the `OPTIONS` response. Cache the result.
114
    def get_options_metadata
1✔
115
      return @_base_options_metadata ||= {
7✔
116
        title: self.get_title,
117
        description: self.description,
118
        renders: [
119
          "text/html",
120
          self.serialize_to_json ? "application/json" : nil,
7✔
121
          self.serialize_to_xml ? "application/xml" : nil,
7✔
122
        ].compact,
123
        actions: self.get_actions_metadata,
124
        member_actions: self.get_member_actions_metadata,
125
      }.compact
126
    end
127

128
    # Define any behavior to execute at the end of controller definition.
129
    # :nocov:
130
    def rrf_finalize
131
      if RESTFramework.config.freeze_config
132
        self::RRF_BASE_CONTROLLER_CONFIG.keys.each { |k|
133
          v = self.send(k)
134
          v.freeze if v.is_a?(Hash) || v.is_a?(Array)
135
        }
136
      end
137
    end
138
    # :nocov:
139
  end
140

141
  def self.included(base)
1✔
142
    return unless base.is_a?(Class)
29✔
143

144
    base.extend(ClassMethods)
25✔
145

146
    # Add class attributes (with defaults) unless they already exist.
147
    RRF_BASE_CONTROLLER_CONFIG.each do |a, default|
25✔
148
      next if base.respond_to?(a)
525✔
149

150
      base.class_attribute(a)
105✔
151

152
      # Set default manually so we can still support Rails 4. Maybe later we can use the default
153
      # parameter on `class_attribute`.
154
      base.send(:"#{a}=", default)
105✔
155
    end
156

157
    # Alias `extra_actions` to `extra_collection_actions`.
158
    unless base.respond_to?(:extra_collection_actions)
25✔
159
      base.alias_method(:extra_collection_actions, :extra_actions)
25✔
160
      base.alias_method(:extra_collection_actions=, :extra_actions=)
25✔
161
    end
162

163
    # Skip CSRF since this is an API.
164
    begin
165
      base.skip_before_action(:verify_authenticity_token)
25✔
166
    rescue
167
      nil
25✔
168
    end
169

170
    # Handle some common exceptions.
171
    unless RESTFramework.config.disable_rescue_from
25✔
172
      base.rescue_from(
25✔
173
        ActionController::ParameterMissing,
174
        ActionController::UnpermittedParameters,
175
        ActiveRecord::AssociationTypeMismatch,
176
        ActiveRecord::NotNullViolation,
177
        ActiveRecord::RecordNotFound,
178
        ActiveRecord::RecordInvalid,
179
        ActiveRecord::RecordNotSaved,
180
        ActiveRecord::RecordNotDestroyed,
181
        ActiveRecord::RecordNotUnique,
182
        ActiveModel::UnknownAttributeError,
183
        with: :rrf_error_handler,
184
      )
185
    end
186

187
    # Use `TracePoint` hook to automatically call `rrf_finalize`.
188
    unless RESTFramework.config.disable_auto_finalize
25✔
189
      # :nocov:
190
      TracePoint.trace(:end) do |t|
191
        next if base != t.self
192

193
        base.rrf_finalize
194

195
        # It's important to disable the trace once we've found the end of the base class definition,
196
        # for performance.
197
        t.disable
198
      end
199
      # :nocov:
200
    end
201
  end
202

203
  # Get the configured serializer class.
204
  def get_serializer_class
1✔
205
    return nil unless serializer_class = self.class.serializer_class
105✔
206

207
    # Support dynamically resolving serializer given a symbol or string.
208
    serializer_class = serializer_class.to_s if serializer_class.is_a?(Symbol)
17✔
209
    if serializer_class.is_a?(String)
17✔
210
      serializer_class = self.class.const_get(serializer_class)
×
211
    end
212

213
    # Wrap it with an adapter if it's an active_model_serializer.
214
    if defined?(ActiveModel::Serializer) && (serializer_class < ActiveModel::Serializer)
17✔
215
      serializer_class = RESTFramework::ActiveModelSerializerAdapterFactory.for(serializer_class)
2✔
216
    end
217

218
    return serializer_class
17✔
219
  end
220

221
  # Serialize the given data using the `serializer_class`.
222
  def serialize(data, **kwargs)
1✔
223
    return self.get_serializer_class.new(data, controller: self, **kwargs).serialize
105✔
224
  end
225

226
  # Get filtering backends, defaulting to no backends.
227
  def get_filter_backends
1✔
228
    return self.class.filter_backends || []
×
229
  end
230

231
  # Filter an arbitrary data set over all configured filter backends.
232
  def get_filtered_data(data)
1✔
233
    # Apply each filter sequentially.
234
    self.get_filter_backends.each do |filter_class|
111✔
235
      filter = filter_class.new(controller: self)
235✔
236
      data = filter.get_filtered_data(data)
235✔
237
    end
238

239
    return data
111✔
240
  end
241

242
  def get_options_metadata
1✔
243
    return self.class.get_options_metadata
×
244
  end
245

246
  def rrf_error_handler(e)
1✔
247
    status = case e
22✔
248
    when ActiveRecord::RecordNotFound
249
      404
19✔
250
    else
251
      400
3✔
252
    end
253

254
    return api_response(
22✔
255
      {
256
        message: e.message,
257
        errors: e.try(:record).try(:errors),
258
        exception: RESTFramework.config.show_backtrace ? e.full_message : nil,
22✔
259
      }.compact,
260
      status: status,
261
    )
262
  end
263

264
  # Render a browsable API for `html` format, along with basic `json`/`xml` formats, and with
265
  # support or passing custom `kwargs` to the underlying `render` calls.
266
  def api_response(payload, html_kwargs: nil, **kwargs)
1✔
267
    html_kwargs ||= {}
166✔
268
    json_kwargs = kwargs.delete(:json_kwargs) || {}
166✔
269
    xml_kwargs = kwargs.delete(:xml_kwargs) || {}
166✔
270

271
    # Raise helpful error if payload is nil. Usually this happens when a record is not found (e.g.,
272
    # when passing something like `User.find_by(id: some_id)` to `api_response`). The caller should
273
    # actually be calling `find_by!` to raise ActiveRecord::RecordNotFound and allowing the REST
274
    # framework to catch this error and return an appropriate error response.
275
    if payload.nil?
166✔
276
      raise RESTFramework::NilPassedToAPIResponseError
3✔
277
    end
278

279
    # If `payload` is an `ActiveRecord::Relation` or `ActiveRecord::Base`, then serialize it.
280
    if payload.is_a?(ActiveRecord::Base) || payload.is_a?(ActiveRecord::Relation)
163✔
281
      payload = self.serialize(payload)
80✔
282
    end
283

284
    # Do not use any adapters by default, if configured.
285
    if self.class.disable_adapters_by_default && !kwargs.key?(:adapter)
163✔
286
      kwargs[:adapter] = nil
163✔
287
    end
288

289
    # Flag to track if we had to rescue unknown format.
290
    already_rescued_unknown_format = false
163✔
291

292
    begin
293
      respond_to do |format|
164✔
294
        if payload == ""
164✔
295
          format.json { head(kwargs[:status] || :no_content) } if self.class.serialize_to_json
10✔
296
          format.xml { head(kwargs[:status] || :no_content) } if self.class.serialize_to_xml
10✔
297
        else
298
          format.json {
299
            jkwargs = kwargs.merge(json_kwargs)
98✔
300
            render(json: payload, layout: false, **jkwargs)
98✔
301
          } if self.class.serialize_to_json
155✔
302
          format.xml {
303
            xkwargs = kwargs.merge(xml_kwargs)
20✔
304
            render(xml: payload, layout: false, **xkwargs)
20✔
305
          } if self.class.serialize_to_xml
155✔
306
          # TODO: possibly support more formats here if supported?
307
        end
308
        format.html {
164✔
309
          @payload = payload
42✔
310
          if payload == ""
42✔
311
            @json_payload = "" if self.class.serialize_to_json
7✔
312
            @xml_payload = "" if self.class.serialize_to_xml
7✔
313
          else
314
            @json_payload = payload.to_json if self.class.serialize_to_json
35✔
315
            @xml_payload = payload.to_xml if self.class.serialize_to_xml
35✔
316
          end
317
          @template_logo_text ||= "Rails REST Framework"
42✔
318
          @title ||= self.class.get_title
42✔
319
          @description ||= self.class.description
42✔
320
          @route_props, @route_groups = RESTFramework::Utils.get_routes(
42✔
321
            Rails.application.routes, request
322
          )
323
          hkwargs = kwargs.merge(html_kwargs)
42✔
324
          begin
325
            render(**hkwargs)
42✔
326
          rescue ActionView::MissingTemplate  # Fallback to `rest_framework` layout.
327
            hkwargs[:layout] = "rest_framework"
42✔
328
            hkwargs[:html] = ""
42✔
329
            render(**hkwargs)
42✔
330
          end
331
        }
332
      end
333
    rescue ActionController::UnknownFormat
2✔
334
      if !already_rescued_unknown_format && rescue_format = self.class.rescue_unknown_format_with
2✔
335
        request.format = rescue_format
1✔
336
        already_rescued_unknown_format = true
1✔
337
        retry
1✔
338
      else
339
        raise
1✔
340
      end
341
    end
342
  end
343

344
  # Provide a generic `OPTIONS` response with metadata such as available actions.
345
  def options
1✔
346
    return api_response(self.get_options_metadata)
7✔
347
  end
348
end
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

© 2025 Coveralls, Inc