• 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

93.26
/lib/rest_framework/utils.rb
1
module RESTFramework::Utils
2✔
2
  HTTP_VERB_ORDERING = %w[GET POST PUT PATCH DELETE OPTIONS HEAD]
2✔
3

4
  # Convert `extra_actions` hash to a consistent format: `{path:, methods:, metadata:, kwargs:}`.
5
  def self.parse_extra_actions(extra_actions)
2✔
6
    (extra_actions || {}).map { |k, v|
90✔
7
      path = k
22✔
8
      kwargs = {}
22✔
9

10
      # Convert structure to path/methods/kwargs.
11
      if v.is_a?(Hash)
22✔
12
        # Symbolize keys (which also makes a copy so we don't mutate the original).
13
        v = v.symbolize_keys
6✔
14

15
        # Cast method/methods to an array.
16
        methods = [ v.delete(:methods), v.delete(:method) ].flatten.compact
6✔
17

18
        # Override path if it's provided.
19
        if v.key?(:path)
6✔
20
          path = v.delete(:path)
×
21
        end
22

23
        # Extract metadata, if provided.
24
        metadata = v.delete(:metadata).presence
6✔
25

26
        # Pass any further kwargs to the underlying Rails interface.
27
        kwargs = v
6✔
28
      else
29
        methods = [ v ].flatten
16✔
30
      end
31

32
      next [
33
        k,
22✔
34
        {
35
          path: path,
36
          methods: methods,
37
          metadata: metadata,
38
          kwargs: kwargs,
39
        }.compact,
40
      ]
41
    }.to_h
42
  end
43

44
  # Get actions which should be skipped for a given controller.
45
  def self.get_skipped_builtin_actions(controller_class)
2✔
46
    (
47
      RESTFramework::BUILTIN_ACTIONS.keys + RESTFramework::BUILTIN_MEMBER_ACTIONS.keys
40✔
48
    ).reject do |action|
49
      controller_class.method_defined?(action)
280✔
50
    end
51
  end
52

53
  # Get the first route pattern which matches the given request.
54
  def self.get_request_route(application_routes, request)
2✔
55
    application_routes.router.recognize(request) { |route, _| return route }
120✔
56
  end
57

58
  # Normalize a path pattern by replacing URL params with generic placeholder, and removing the
59
  # `(.:format)` at the end.
60
  def self.comparable_path(path)
2✔
61
    path.gsub("(.:format)", "").gsub(/:[0-9A-Za-z_-]+/, ":x")
12,240✔
62
  end
63

64
  # Show routes under a controller action; used for the browsable API.
65
  def self.get_routes(application_routes, request, current_route: nil)
2✔
66
    current_route ||= self.get_request_route(application_routes, request)
60✔
67
    current_path = current_route.path.spec.to_s.gsub("(.:format)", "")
60✔
68
    current_path = "" if current_path == "/"
60✔
69
    current_levels = current_path.count("/")
60✔
70
    current_comparable_path = %r{^#{Regexp.quote(self.comparable_path(current_path))}(/|$)}
60✔
71

72
    # Get current route path parameters.
73
    path_params = current_route.required_parts.map { |n| request.path_parameters[n] }
88✔
74

75
    # Return routes that match our current route subdomain/pattern, grouped by controller. We
76
    # precompute certain properties of the route for performance.
77
    application_routes.routes.select { |r|
60✔
78
      # We `select` first to avoid unnecessarily calculating metadata for routes we don't even want
79
      # to show.
80
      (r.defaults[:subdomain].blank? || r.defaults[:subdomain] == request.subdomain) &&
12,180✔
81
          current_comparable_path.match?(self.comparable_path(r.path.spec.to_s)) &&
82
          r.defaults[:controller].present? &&
83
          r.defaults[:action].present?
84
    }.map { |r|
85
      path = r.path.spec.to_s.gsub("(.:format)", "")
890✔
86

87
      # Starts at the number of levels in current path, and removes the `(.:format)` at the end.
88
      relative_path = path.split("/")[current_levels..]&.join("/").presence || "/"
890✔
89

90
      # This path is what would need to be concatenated onto the current path to get to the
91
      # destination path.
92
      concat_path = relative_path.gsub(/^[^\/]*/, "").presence || "/"
890✔
93

94
      levels = path.count("/")
890✔
95
      matches_path = current_path == path
890✔
96
      matches_params = r.required_parts.length == current_route.required_parts.length
890✔
97

98
      {
99
        route: r,
890✔
100
        verb: r.verb,
101
        path: path,
102
        path_with_params: r.format(
103
          r.required_parts.each_with_index.map { |p, i| [ p, path_params[i] ] }.to_h,
602✔
104
        ),
105
        relative_path: relative_path,
106
        concat_path: concat_path,
107
        controller: r.defaults[:controller].presence,
108
        action: r.defaults[:action].presence,
109
        matches_path: matches_path,
110
        matches_params: matches_params,
111
        # The following options are only used in subsequent processing in this method.
112
        _levels: levels,
113
      }
114
    }.sort_by { |r|
115
      [
116
        # Sort by levels first, so routes matching closely with current request show first.
117
        r[:_levels],
890✔
118
        # Then match by path, but manually sort ':' to the end using knowledge that Ruby sorts the
119
        # pipe character '|' after alphanumerics.
120
        r[:path].tr(":", "|"),
121
        # Finally, match by HTTP verb.
122
        HTTP_VERB_ORDERING.index(r[:verb]) || 99,
123
      ]
124
    }.group_by { |r| r[:controller] }.sort_by { |c, _r|
890✔
125
      # Sort the controller groups by current controller first, then alphanumerically.
126
      # Note: Use `controller_path` instead of `params[:controller]` to avoid re-raising a
127
      # `ActionDispatch::Http::Parameters::ParseError` exception.
128
      [ request.controller_class.controller_path == c ? 0 : 1, c ]
134✔
129
    }.to_h
130
  end
131

132
  # Custom inflector for RESTful controllers.
133
  def self.inflect(s, acronyms = nil)
2✔
134
    acronyms&.each do |acronym|
636✔
135
      s = s.gsub(/\b#{acronym}\b/i, acronym)
3,180✔
136
    end
137

138
    s
636✔
139
  end
140

141
  # Parse fields hashes.
142
  def self.parse_fields_hash(h, model, exclude_associations:, action_text:, active_storage:)
2✔
143
    parsed_fields = h[:only] || (
40✔
144
      model ? self.fields_for(
14✔
145
        model,
146
        exclude_associations: exclude_associations,
147
        action_text: action_text,
148
        active_storage: active_storage,
149
      ) : []
150
    )
151
    parsed_fields += h[:include].map(&:to_s) if h[:include]
40✔
152
    parsed_fields -= h[:exclude].map(&:to_s) if h[:exclude]
40✔
153
    parsed_fields -= h[:except].map(&:to_s) if h[:except]
40✔
154

155
    # Warn for any unknown keys.
156
    (h.keys - [ :only, :except, :include, :exclude ]).each do |k|
40✔
157
      Rails.logger.warn("RRF: Unknown key in fields hash: #{k}")
×
158
    end
159

160
    # We should always return strings, not symbols.
161
    parsed_fields.map(&:to_s)
40✔
162
  end
163

164
  # Get the fields for a given model, including not just columns (which includes
165
  # foreign keys), but also associations. Note that we always return an array of
166
  # strings, not symbols.
167
  def self.fields_for(model, exclude_associations:, action_text:, active_storage:)
2✔
168
    foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
520✔
169
    base_fields = model.column_names.reject { |c| c.in?(foreign_keys) }
5,458✔
170

171
    return base_fields if exclude_associations
520✔
172

173
    # ActionText Integration: Determine the normalized field names for action text attributes.
174
    atf = action_text ? model.reflect_on_all_associations(:has_one).collect(&:name).select { |n|
520✔
175
      n.to_s.start_with?("rich_text_")
796✔
176
    }.map { |n| n.to_s.delete_prefix("rich_text_") } : []
228✔
177

178
    # ActiveStorage Integration: Determine the normalized field names for active storage attributes.
179
    asf = active_storage ? model.attachment_reflections.keys : []
520✔
180

181
    # Associations:
182
    associations = model.reflections.map { |association, ref|
520✔
183
      # Ignore associations for which we have custom integrations.
184
      if ref.class_name.in?(%w[ActionText::RichText ActiveStorage::Attachment ActiveStorage::Blob])
3,756✔
185
        next nil
1,590✔
186
      end
187

188
      if ref.collection? && RESTFramework.config.large_reverse_association_tables&.include?(
2,166✔
189
        ref.table_name,
190
      )
191
        next nil
×
192
      end
193

194
      next association
2,166✔
195
    }.compact
196

197
    base_fields + associations + atf + asf
520✔
198
  end
199

200
  # Get the sub-fields that may be serialized and filtered/ordered for a reflection.
201
  def self.sub_fields_for(ref)
2✔
202
    if !ref.polymorphic? && model = ref.klass
40✔
203
      sub_fields = [ model.primary_key ].flatten.compact
40✔
204
      label_fields = RESTFramework.config.label_fields
40✔
205

206
      # Preferrably find a database column to use as label.
207
      if match = label_fields.find { |f| f.in?(model.column_names) }
152✔
208
        return sub_fields + [ match ]
38✔
209
      end
210

211
      # Otherwise, find a method.
212
      if match = label_fields.find { |f| model.method_defined?(f) }
16✔
213
        return sub_fields + [ match ]
×
214
      end
215

216
      return sub_fields
2✔
217
    end
218

219
    [ "id", "name" ]
×
220
  end
221

222
  # Get a field's id/ids variation.
223
  def self.id_field_for(field, reflection)
2✔
224
    if reflection.collection?
42✔
225
      return "#{field.singularize}_ids"
24✔
226
    elsif reflection.belongs_to?
18✔
227
      # The id field for belongs_to is always the foreign key column name, even if the
228
      # association is named differently.
229
      return reflection.foreign_key
14✔
230
    end
231

232
    nil
233
  end
234

235
  # Wrap a serializer with an adapter if it is an ActiveModel::Serializer.
236
  def self.wrap_ams(s)
2✔
237
    if defined?(ActiveModel::Serializer) && (s < ActiveModel::Serializer)
160✔
238
      return RESTFramework::ActiveModelSerializerAdapterFactory.for(s)
×
239
    end
240

241
    s
160✔
242
  end
243
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

© 2026 Coveralls, Inc