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

gregschmit / rails-rest-framework / 3974050292

pending completion
3974050292

push

github

Gregory N. Schmit
CI: Replace Travis with GitHub Actions.

805 of 887 relevant lines covered (90.76%)

74.33 hits per line

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

87.5
/lib/rest_framework/utils.rb
1
module RESTFramework::Utils
1✔
2
  HTTP_METHOD_ORDERING = %w(GET POST PUT PATCH DELETE OPTIONS HEAD)
1✔
3
  LABEL_FIELDS = %w(name label login title email username url)
1✔
4

5
  # Convert `extra_actions` hash to a consistent format: `{path:, methods:, kwargs:}`, and
6
  # additional metadata fields.
7
  #
8
  # If a controller is provided, labels will be added to any metadata fields.
9
  def self.parse_extra_actions(extra_actions, controller: nil)
1✔
10
    return (extra_actions || {}).map { |k, v|
48✔
11
      path = k
22✔
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.
22✔
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 any metadata keys.
20
        delegate = v.delete(:delegate)
3✔
21
        fields = v.delete(:fields)
3✔
22

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

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

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

44
      next [
45
        k,
22✔
46
        {
47
          path: path,
48
          methods: methods,
49
          kwargs: kwargs,
50
          delegate: delegate,
51
          fields: fields,
52
          type: :extra,
53
        }.compact,
54
      ]
55
    }.to_h
56
  end
57

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

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

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

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

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

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

111
      {
112
        route: r,
475✔
113
        verb: r.verb,
114
        path: path,
115
        # Starts at the number of levels in current path, and removes the `(.:format)` at the end.
116
        relative_path: path.split("/")[current_levels..]&.join("/").presence || "/",
117
        controller: r.defaults[:controller].presence,
118
        action: r.defaults[:action].presence,
119
        matches_path: matches_path,
120
        matches_params: matches_params,
121
        # The following options are only used in subsequent processing in this method.
122
        _levels: levels,
123
      }
124
    }.sort_by { |r|
125
      # Sort by levels first, so the routes matching closely with current request show first, then
126
      # by the path, and finally by the HTTP verb.
127
      [r[:_levels], r[:_path], HTTP_METHOD_ORDERING.index(r[:verb]) || 99]
475✔
128
    }.group_by { |r| r[:controller] }.sort_by { |c, _r|
475✔
129
      # Sort the controller groups by current controller first, then alphanumerically.
130
      [request.params[:controller] == c ? 0 : 1, c]
84✔
131
    }.to_h
132
  end
133

134
  # Custom inflector for RESTful controllers.
135
  def self.inflect(s, acronyms=nil)
1✔
136
    acronyms&.each do |acronym|
113✔
137
      s = s.gsub(/\b#{acronym}\b/i, acronym)
565✔
138
    end
139

140
    return s
113✔
141
  end
142

143
  # Parse fields hashes.
144
  def self.parse_fields_hash(fields_hash, model, exclude_associations: nil)
1✔
145
    parsed_fields = fields_hash[:only] || (
13✔
146
      model ? self.fields_for(model, exclude_associations: exclude_associations) : []
×
147
    )
148
    parsed_fields += fields_hash[:include] if fields_hash[:include]
13✔
149
    parsed_fields -= fields_hash[:exclude] if fields_hash[:exclude]
13✔
150

151
    # Warn for any unknown keys.
152
    (fields_hash.keys - [:only, :include, :exclude]).each do |k|
13✔
153
      Rails.logger.warn("RRF: Unknown key in fields hash: #{k}")
×
154
    end
155

156
    return parsed_fields
13✔
157
  end
158

159
  # Get the fields for a given model, including not just columns (which includes
160
  # foreign keys), but also associations.
161
  def self.fields_for(model, exclude_associations: nil)
1✔
162
    foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
272✔
163

164
    if exclude_associations
272✔
165
      return model.column_names.reject { |c| c.in?(foreign_keys) }
×
166
    end
167

168
    # Add associations in addition to normal columns.
169
    return model.column_names.reject { |c|
272✔
170
      c.in?(foreign_keys)
2,176✔
171
    } + model.reflections.map { |association, ref|
172
      # Exclude certain associations (by default, active storage and action text associations).
173
      if ref.class_name.in?(RESTFramework.config.exclude_association_classes)
1,304✔
174
        next nil
528✔
175
      end
176

177
      if ref.collection? && RESTFramework.config.large_reverse_association_tables&.include?(
776✔
178
        ref.table_name,
179
      )
180
        next nil
×
181
      end
182

183
      next association
776✔
184
    }.compact
185
  end
186

187
  # Get the sub-fields that may be serialized and filtered/ordered for a reflection.
188
  def self.sub_fields_for(ref)
1✔
189
    if !ref.polymorphic? && model = ref.klass
242✔
190
      sub_fields = [model.primary_key].flatten.compact
242✔
191

192
      # Preferrably find a database column to use as label.
193
      if match = LABEL_FIELDS.find { |f| f.in?(model.column_names) }
872✔
194
        return sub_fields + [match]
230✔
195
      end
196

197
      # Otherwise, find a method.
198
      if match = LABEL_FIELDS.find { |f| model.method_defined?(f) }
96✔
199
        return sub_fields + [match]
×
200
      end
201

202
      return sub_fields
12✔
203
    end
204

205
    return ["id", "name"]
×
206
  end
207
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