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

gregschmit / rails-rest-framework / 12422775285

19 Dec 2024 11:55PM UTC coverage: 84.367% (-0.6%) from 84.952%
12422775285

push

github

gregschmit
Remove string/symbol serializer feature; cleanup.

25 of 35 new or added lines in 3 files covered. (71.43%)

3 existing lines in 2 files now uncovered.

966 of 1145 relevant lines covered (84.37%)

104.27 hits per line

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

79.17
/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:, kwargs:}`, and
5
  # additional metadata fields.
6
  #
7
  # If a controller is provided, labels will be added to any metadata fields.
8
  def self.parse_extra_actions(extra_actions, controller: nil)
2✔
9
    return (extra_actions || {}).map { |k, v|
86✔
10
      path = k
18✔
11
      metadata = {}
18✔
12

13
      # Convert structure to path/methods/kwargs.
14
      if v.is_a?(Hash)  # Allow kwargs to be used to define path differently from the key.
18✔
15
        # Symbolize keys (which also makes a copy so we don't mutate the original).
16
        v = v.symbolize_keys
×
17
        methods = v.delete(:methods)
×
18
        if v.key?(:method)
×
19
          methods = v.delete(:method)
×
20
        end
21

22
        # First, remove the route metadata.
23
        metadata = v.delete(:metadata) || {}
×
24

25
        # Add label to fields.
26
        if controller && metadata[:fields]
×
27
          metadata[:fields] = metadata[:fields].map { |f|
28
            [f, {}]
×
29
          }.to_h if metadata[:fields].is_a?(Array)
×
30
          metadata[:fields]&.each do |field, cfg|
×
NEW
31
            cfg[:label] = controller.label_for(field) unless cfg[:label]
×
32
          end
33
        end
34

35
        # Override path if it's provided.
36
        if v.key?(:path)
×
37
          path = v.delete(:path)
×
38
        end
39

40
        # Pass any further kwargs to the underlying Rails interface.
41
        kwargs = v.presence
×
42
      elsif v.is_a?(Array) && v.length == 1
18✔
43
        methods = v[0]
×
44
      else
45
        methods = v
18✔
46
      end
47

48
      # Insert action label if it's not provided.
49
      if controller
18✔
NEW
50
        metadata[:label] ||= controller.label_for(k)
×
51
      end
52

53
      next [
54
        k,
18✔
55
        {
56
          path: path,
57
          methods: methods,
58
          kwargs: kwargs,
59
          type: :extra,
60
          metadata: metadata.presence,
61
        }.compact,
62
      ]
63
    }.to_h
64
  end
65

66
  # Get actions which should be skipped for a given controller.
67
  def self.get_skipped_builtin_actions(controller_class)
2✔
68
    return (
69
      RESTFramework::BUILTIN_ACTIONS.keys + RESTFramework::BUILTIN_MEMBER_ACTIONS.keys
38✔
70
    ).reject do |action|
71
      controller_class.method_defined?(action)
266✔
72
    end
73
  end
74

75
  # Get the first route pattern which matches the given request.
76
  def self.get_request_route(application_routes, request)
2✔
77
    application_routes.router.recognize(request) { |route, _| return route }
120✔
78
  end
79

80
  # Normalize a path pattern by replacing URL params with generic placeholder, and removing the
81
  # `(.:format)` at the end.
82
  def self.comparable_path(path)
2✔
83
    return path.gsub("(.:format)", "").gsub(/:[0-9A-Za-z_-]+/, ":x")
11,640✔
84
  end
85

86
  # Show routes under a controller action; used for the browsable API.
87
  def self.get_routes(application_routes, request, current_route: nil)
2✔
88
    current_route ||= self.get_request_route(application_routes, request)
60✔
89
    current_path = current_route.path.spec.to_s.gsub("(.:format)", "")
60✔
90
    current_path = "" if current_path == "/"
60✔
91
    current_levels = current_path.count("/")
60✔
92
    current_comparable_path = %r{^#{Regexp.quote(self.comparable_path(current_path))}(/|$)}
60✔
93

94
    # Add helpful properties of the current route.
95
    path_args = current_route.required_parts.map { |n| request.path_parameters[n] }
88✔
96
    route_props = {
97
      with_path_args: ->(r) {
60✔
98
        r.format(r.required_parts.each_with_index.map { |p, i| [p, path_args[i]] }.to_h)
488✔
99
      },
100
    }
101

102
    # Return routes that match our current route subdomain/pattern, grouped by controller. We
103
    # precompute certain properties of the route for performance.
104
    return route_props, application_routes.routes.select { |r|
60✔
105
      # We `select` first to avoid unnecessarily calculating metadata for routes we don't even want
106
      # to show.
107
      (r.defaults[:subdomain].blank? || r.defaults[:subdomain] == request.subdomain) &&
11,580✔
108
        current_comparable_path.match?(self.comparable_path(r.path.spec.to_s)) &&
109
        r.defaults[:controller].present? &&
110
        r.defaults[:action].present?
111
    }.map { |r|
112
      path = r.path.spec.to_s.gsub("(.:format)", "")
844✔
113
      levels = path.count("/")
844✔
114
      matches_path = current_path == path
844✔
115
      matches_params = r.required_parts.length == current_route.required_parts.length
844✔
116

117
      {
118
        route: r,
844✔
119
        verb: r.verb,
120
        path: path,
121
        # Starts at the number of levels in current path, and removes the `(.:format)` at the end.
122
        relative_path: path.split("/")[current_levels..]&.join("/").presence || "/",
123
        controller: r.defaults[:controller].presence,
124
        action: r.defaults[:action].presence,
125
        matches_path: matches_path,
126
        matches_params: matches_params,
127
        # The following options are only used in subsequent processing in this method.
128
        _levels: levels,
129
      }
130
    }.sort_by { |r|
131
      [
132
        # Sort by levels first, so routes matching closely with current request show first.
133
        r[:_levels],
844✔
134
        # Then match by path, but manually sort ':' to the end using knowledge that Ruby sorts the
135
        # pipe character '|' after alphanumerics.
136
        r[:path].tr(":", "|"),
137
        # Finally, match by HTTP verb.
138
        HTTP_VERB_ORDERING.index(r[:verb]) || 99,
139
      ]
140
    }.group_by { |r| r[:controller] }.sort_by { |c, _r|
844✔
141
      # Sort the controller groups by current controller first, then alphanumerically.
142
      [request.params[:controller] == c ? 0 : 1, c]
130✔
143
    }.to_h
144
  end
145

146
  # Custom inflector for RESTful controllers.
147
  def self.inflect(s, acronyms=nil)
2✔
148
    acronyms&.each do |acronym|
556✔
149
      s = s.gsub(/\b#{acronym}\b/i, acronym)
2,780✔
150
    end
151

152
    return s
556✔
153
  end
154

155
  # Parse fields hashes.
156
  def self.parse_fields_hash(fields_hash, model, exclude_associations: nil)
2✔
157
    parsed_fields = fields_hash[:only] || (
38✔
158
      model ? self.fields_for(model, exclude_associations: exclude_associations) : []
12✔
159
    )
160
    parsed_fields += fields_hash[:include].map(&:to_s) if fields_hash[:include]
38✔
161
    parsed_fields -= fields_hash[:exclude].map(&:to_s) if fields_hash[:exclude]
38✔
162
    parsed_fields -= fields_hash[:except].map(&:to_s) if fields_hash[:except]
38✔
163

164
    # Warn for any unknown keys.
165
    (fields_hash.keys - [:only, :except, :include, :exclude]).each do |k|
38✔
166
      Rails.logger.warn("RRF: Unknown key in fields hash: #{k}")
×
167
    end
168

169
    # We should always return strings, not symbols.
170
    return parsed_fields.map(&:to_s)
38✔
171
  end
172

173
  # Get the fields for a given model, including not just columns (which includes
174
  # foreign keys), but also associations. Note that we always return an array of
175
  # strings, not symbols.
176
  def self.fields_for(model, exclude_associations: nil)
2✔
177
    foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
432✔
178
    base_fields = model.column_names.reject { |c| c.in?(foreign_keys) }
4,842✔
179

180
    return base_fields if exclude_associations
432✔
181

182
    # Add associations in addition to normal columns.
183
    return base_fields + model.reflections.map { |association, ref|
432✔
184
      # Ignore associations for which we have custom integrations.
185
      if ref.class_name.in?(%w(ActiveStorage::Attachment ActiveStorage::Blob ActionText::RichText))
3,052✔
186
        next nil
1,150✔
187
      end
188

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

195
      next association
1,902✔
196
    }.compact + model.reflect_on_all_associations(:has_one).collect(&:name).select { |n|
197
      n.to_s.start_with?("rich_text_")
892✔
198
    }.map { |n|
199
      n.to_s.delete_prefix("rich_text_")
230✔
200
    } + model.attachment_reflections.keys
201
  end
202

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

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

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

219
      return sub_fields
2✔
220
    end
221

222
    return ["id", "name"]
×
223
  end
224

225
  # Get a field's id/ids variation.
226
  def self.get_id_field(field, reflection)
2✔
227
    if reflection.collection?
204✔
228
      return "#{field.singularize}_ids"
116✔
229
    elsif reflection.belongs_to?
88✔
230
      # The id field for belongs_to is always the foreign key column name, even if the
231
      # association is named differently.
232
      return reflection.foreign_key
68✔
233
    end
234

235
    return nil
236
  end
237

238
  # Wrap a serializer with an adapter if it is an ActiveModel::Serializer.
239
  def self.wrap_ams(s)
2✔
240
    if defined?(ActiveModel::Serializer) && (s < ActiveModel::Serializer)
132✔
NEW
241
      return RESTFramework::ActiveModelSerializerAdapterFactory.for(s)
×
242
    end
243

244
    return s
132✔
245
  end
246
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