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

gregschmit / rails-rest-framework / 4662052844

pending completion
4662052844

push

github

Gregory N. Schmit
Fix typo in last commit (reversed logic).

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

934 of 1022 relevant lines covered (91.39%)

93.3 hits per line

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

87.21
/lib/rest_framework/utils.rb
1
module RESTFramework::Utils
1✔
2
  HTTP_VERB_ORDERING = %w(GET POST PUT PATCH DELETE OPTIONS HEAD)
1✔
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)
1✔
9
    return (extra_actions || {}).map { |k, v|
60✔
10
      path = k
21✔
11
      metadata = {}
21✔
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.
21✔
15
        # Symbolize keys (which also makes a copy so we don't mutate the original).
16
        v = v.symbolize_keys
3✔
17
        methods = v.delete(:methods)
3✔
18

19
        # First, remove the route metadata.
20
        metadata = v.delete(:metadata) || {}
3✔
21

22
        # Add label to fields.
23
        if controller && metadata[:fields]
3✔
24
          metadata[:fields] = metadata[:fields].map { |f| [f, {}] }.to_h if f.is_a?(Array)
×
25
          metadata[:fields]&.each do |field, cfg|
×
26
            cfg[:label] = controller.get_label(field) unless cfg[:label]
×
27
          end
28
        end
29

30
        # Override path if it's provided.
31
        if v.key?(:path)
3✔
32
          path = v.delete(:path)
1✔
33
        end
34

35
        # Pass any further kwargs to the underlying Rails interface.
36
        kwargs = v.presence
3✔
37
      elsif v.is_a?(Array) && v.length == 1
18✔
38
        methods = v[0]
×
39
      else
40
        methods = v
18✔
41
      end
42

43
      # Insert action label if it's not provided.
44
      if controller
21✔
45
        metadata[:label] ||= controller.get_label(k)
4✔
46
      end
47

48
      next [
49
        k,
21✔
50
        {
51
          path: path,
52
          methods: methods,
53
          kwargs: kwargs,
54
          type: :extra,
55
          metadata: metadata.presence,
56
        }.compact,
57
      ]
58
    }.to_h
59
  end
60

61
  # Get actions which should be skipped for a given controller.
62
  def self.get_skipped_builtin_actions(controller_class)
1✔
63
    return (
64
      RESTFramework::BUILTIN_ACTIONS.keys + RESTFramework::BUILTIN_MEMBER_ACTIONS.keys
26✔
65
    ).reject do |action|
66
      controller_class.method_defined?(action)
182✔
67
    end
68
  end
69

70
  # Get the first route pattern which matches the given request.
71
  def self.get_request_route(application_routes, request)
1✔
72
    application_routes.router.recognize(request) { |route, _| return route }
84✔
73
  end
74

75
  # Normalize a path pattern by replacing URL params with generic placeholder, and removing the
76
  # `(.:format)` at the end.
77
  def self.comparable_path(path)
1✔
78
    return path.gsub("(.:format)", "").gsub(/:[0-9A-Za-z_-]+/, ":x")
9,408✔
79
  end
80

81
  # Show routes under a controller action; used for the browsable API.
82
  def self.get_routes(application_routes, request, current_route: nil)
1✔
83
    current_route ||= self.get_request_route(application_routes, request)
42✔
84
    current_path = current_route.path.spec.to_s.gsub("(.:format)", "")
42✔
85
    current_path = "" if current_path == "/"
42✔
86
    current_levels = current_path.count("/")
42✔
87
    current_comparable_path = %r{^#{Regexp.quote(self.comparable_path(current_path))}(/|$)}
42✔
88

89
    # Add helpful properties of the current route.
90
    path_args = current_route.required_parts.map { |n| request.path_parameters[n] }
62✔
91
    route_props = {
92
      with_path_args: ->(r) {
42✔
93
        r.format(r.required_parts.each_with_index.map { |p, i| [p, path_args[i]] }.to_h)
374✔
94
      },
95
    }
96

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

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

143
  # Custom inflector for RESTful controllers.
144
  def self.inflect(s, acronyms=nil)
1✔
145
    acronyms&.each do |acronym|
395✔
146
      s = s.gsub(/\b#{acronym}\b/i, acronym)
1,975✔
147
    end
148

149
    return s
395✔
150
  end
151

152
  # Parse fields hashes.
153
  def self.parse_fields_hash(fields_hash, model, exclude_associations: nil)
1✔
154
    parsed_fields = fields_hash[:only] || (
16✔
155
      model ? self.fields_for(model, exclude_associations: exclude_associations) : []
×
156
    )
157
    parsed_fields += fields_hash[:include] if fields_hash[:include]
16✔
158
    parsed_fields -= fields_hash[:exclude] if fields_hash[:exclude]
16✔
159

160
    # Warn for any unknown keys.
161
    (fields_hash.keys - [:only, :include, :exclude]).each do |k|
16✔
162
      Rails.logger.warn("RRF: Unknown key in fields hash: #{k}")
×
163
    end
164

165
    # We should always return strings, not symbols.
166
    return parsed_fields.map(&:to_s)
16✔
167
  end
168

169
  # Get the fields for a given model, including not just columns (which includes
170
  # foreign keys), but also associations. Note that we always return an array of
171
  # strings, not symbols.
172
  def self.fields_for(model, exclude_associations: nil)
1✔
173
    foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
439✔
174

175
    if exclude_associations
439✔
176
      return model.column_names.reject { |c| c.in?(foreign_keys) }
×
177
    end
178

179
    # Add associations in addition to normal columns.
180
    return model.column_names.reject { |c|
439✔
181
      c.in?(foreign_keys)
3,512✔
182
    } + model.reflections.map { |association, ref|
183
      # Ignore associations for which we have custom integrations.
184
      if ref.class_name.in?(%w(ActiveStorage::Attachment ActiveStorage::Blob ActionText::RichText))
2,440✔
185
        next nil
1,203✔
186
      end
187

188
      # Exclude user-specified associations.
189
      if ref.class_name.in?(RESTFramework.config.exclude_association_classes)
1,237✔
190
        next nil
×
191
      end
192

193
      if ref.collection? && RESTFramework.config.large_reverse_association_tables&.include?(
1,237✔
194
        ref.table_name,
195
      )
196
        next nil
×
197
      end
198

199
      next association
1,237✔
200
    }.compact + model.reflect_on_all_associations(:has_one).collect(&:name).select { |n|
201
      n.to_s.start_with?("rich_text_")
1,089✔
202
    }.map { |n|
203
      n.to_s.delete_prefix("rich_text_")
325✔
204
    } + model.attachment_reflections.keys
205
  end
206

207
  # Get the sub-fields that may be serialized and filtered/ordered for a reflection.
208
  def self.sub_fields_for(ref)
1✔
209
    if !ref.polymorphic? && model = ref.klass
19✔
210
      sub_fields = [model.primary_key].flatten.compact
19✔
211
      label_fields = RESTFramework.config.label_fields
19✔
212

213
      # Preferrably find a database column to use as label.
214
      if match = label_fields.find { |f| f.in?(model.column_names) }
68✔
215
        return sub_fields + [match]
18✔
216
      end
217

218
      # Otherwise, find a method.
219
      if match = label_fields.find { |f| model.method_defined?(f) }
8✔
220
        return sub_fields + [match]
×
221
      end
222

223
      return sub_fields
1✔
224
    end
225

226
    return ["id", "name"]
×
227
  end
228
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