rest_framework 0.3.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dfa62b5fb49a4c30fabee7308e785d495f244b8aec21be6d5fc99bf0f2f8a3aa
4
- data.tar.gz: 65e70b6fd5eb3b1d06d54da6044035f1ca418f440b666e81bc7fee7d89ff0e7b
3
+ metadata.gz: 9791b0cbb0a806afa00955abf151c7d04bd1820bdbe189f237badc9782f87d6b
4
+ data.tar.gz: 69e29341f8804fac7894c0065b0f5535d88b0e8d60577bc77677fe13d633f2d5
5
5
  SHA512:
6
- metadata.gz: 2e92adef73b6fe78b9381480015c950b20e8a14e91a49f24b655e4f58a7ba1870e4d7d5b33a1983be514e50215a6c1a8e2197e1008faf4eae4b2da16072ddde0
7
- data.tar.gz: 27bc294410be0d235db8fccd20bc48b013a9814a0ac3b26fc01f5f7f46905738f6a65dd3fcdf4d54946b70cdeb9eb11c60b33139b169bd83ff8d4cbad76aaf60
6
+ metadata.gz: f04bf8059e765db9e13a9729db43c2b26a914035063fe728e9ad0bcb1ab968bb8939a4d278e239a39f64b671d183bfdd7a2487248495cd7e5bd26b93ac49a103
7
+ data.tar.gz: '0908d43c8b2188e4dde5920a84eb621c0dea2dc170891e314d77d95b823a32af6681ae9a6028277f32282b88be03f8e95da55ec310f75e3c89ec8c3a09934f09'
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021 Gregory N. Schmit
3
+ Copyright (c) 2022 Gregory N. Schmit
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.7
1
+ 0.5.0
@@ -29,14 +29,14 @@
29
29
  <ul class="nav nav-tabs">
30
30
  <% if @json_payload %>
31
31
  <li class="nav-item">
32
- <a class="nav-link active" href="#tab-json" data-toggle="tab" role="tab">
32
+ <a class="nav-link active" href="#tab-json" data-bs-toggle="tab" role="tab">
33
33
  .json
34
34
  </a>
35
35
  </li>
36
36
  <% end %>
37
37
  <% if @xml_payload %>
38
38
  <li class="nav-item">
39
- <a class="nav-link" href="#tab-xml" data-toggle="tab" role="tab">
39
+ <a class="nav-link" href="#tab-xml" data-bs-toggle="tab" role="tab">
40
40
  .xml
41
41
  </a>
42
42
  </li>
@@ -57,7 +57,7 @@
57
57
  </div>
58
58
  </div>
59
59
  <% end %>
60
- <% unless @routes.blank? %>
60
+ <% unless @route_groups.blank? %>
61
61
  <div class="row">
62
62
  <h2>Routes</h2>
63
63
  <%= render partial: 'rest_framework/routes' %>
@@ -1,11 +1,12 @@
1
1
  <meta charset="utf-8">
2
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
2
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3
3
  <%= csrf_meta_tags %>
4
4
  <%= csp_meta_tag rescue nil %>
5
5
 
6
- <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
7
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/styles/vs.min.css" integrity="sha512-aWjgJTbdG4imzxTxistV5TVNffcYGtIQQm2NBNahV6LmX14Xq9WwZTL1wPjaSglUuVzYgwrq+0EuI4+vKvQHHw==" crossorigin="anonymous" />
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous">
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/styles/vs.min.css" integrity="sha512-aWjgJTbdG4imzxTxistV5TVNffcYGtIQQm2NBNahV6LmX14Xq9WwZTL1wPjaSglUuVzYgwrq+0EuI4+vKvQHHw==" crossorigin="anonymous">
8
8
  <style>
9
+ /* Adjust headers to always take up their entire row, and tweak the sizing. */
9
10
  h1,h2,h3,h4,h5,h6 { width: 100%; font-weight: normal; }
10
11
  h1 { font-size: 2rem; }
11
12
  h2 { font-size: 1.7rem; }
@@ -13,11 +14,27 @@
13
14
  h4 { font-size: 1.3rem; }
14
15
  h5 { font-size: 1.1rem; }
15
16
  h6 { font-size: 1rem; }
17
+
18
+ /* Make route group expansion obvious to the user. */
19
+ .rrf-routes .rrf-route-group-header {
20
+ background-color: #f8f8f8;
21
+ }
22
+ .rrf-routes .rrf-route-group-header:hover {
23
+ background-color: #f0f0f0;
24
+ }
25
+ .rrf-routes .rrf-route-group-header td {
26
+ cursor: pointer;
27
+ }
28
+
29
+ /* Disable bootstrap's collapsing animation because in tables it causes delayed jerkiness. */
30
+ .rrf-routes .collapsing {
31
+ -webkit-transition: none;
32
+ transition: none;
33
+ display: none;
34
+ }
16
35
  </style>
17
36
 
18
- <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
19
- <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
20
- <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
37
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script>
21
38
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/highlight.min.js" integrity="sha512-TDKKr+IvoqZnPzc3l35hdjpHD0m+b2EC2SrLEgKDRWpxf2rFCxemkgvJ5kfU48ip+Y+m2XVKyOCD85ybtlZDmw==" crossorigin="anonymous"></script>
22
39
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/languages/json.min.js" integrity="sha512-FoN8JE+WWCdIGXAIT8KQXwpiavz0Mvjtfk7Rku3MDUNO0BDCiRMXAsSX+e+COFyZTcDb9HDgP+pM2RX12d4j+A==" crossorigin="anonymous"></script>
23
40
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/languages/xml.min.js" integrity="sha512-dICltIgnUP+QSJrnYGCV8943p3qSDgvcg2NU4W8IcOZP4tdrvxlXjbhIznhtVQEcXow0mOjLM0Q6/NvZsmUH4g==" crossorigin="anonymous"></script>
@@ -0,0 +1,9 @@
1
+ <tr>
2
+ <td><%= route[:path] %></td>
3
+ <td><%= route[:verb] %></td>
4
+ <% if route[:controller] && route[:action] %>
5
+ <td><%= route[:controller] %>#<%= route[:action] %></td>
6
+ <% else %>
7
+ <td><%= route[:route_app] %></td>
8
+ <% end %>
9
+ </tr>
@@ -1,18 +1,28 @@
1
- <table class="table">
2
- <thead>
3
- <tr>
4
- <th scope="col">Verb</th>
5
- <th scope="col">Path</th>
6
- <th scope="col">Action</th>
7
- </tr>
8
- </thead>
9
- <tbody>
10
- <% @routes.each do |r| %>
11
- <tr>
12
- <td><%= r[:verb] %></td>
13
- <td><%= r[:path] %></td>
14
- <td><%= r[:action] %></td>
15
- </tr>
1
+ <div class="table-responsive">
2
+ <table class="table table-responsive rrf-routes">
3
+ <thead>
4
+ <tr>
5
+ <th scope="col">Path</th>
6
+ <th scope="col">Verb</th>
7
+ <th scope="col">Controller#Action</th>
8
+ </tr>
9
+ </thead>
10
+ <%# Render first group of routes directly. %>
11
+ <tbody>
12
+ <% @route_groups.values[0].each do |route| %>
13
+ <%= render partial: "rest_framework/route", locals: {route: route} %>
14
+ <% end %>
15
+ </tbody>
16
+ <%# Render any other groups under dropdowns. %>
17
+ <% @route_groups.drop(1).each_with_index do |(name, route_group), index| %>
18
+ <tr data-bs-toggle="collapse" data-bs-target="#route-group-<%= index %>" class="rrf-route-group-header">
19
+ <td colspan="3" class="text-center user-select-none"><%= name %></td>
20
+ </tr>
21
+ <tbody id="route-group-<%= index %>" class="collapse">
22
+ <% route_group.each do |route| %>
23
+ <%= render partial: "rest_framework/route", locals: {route: route} %>
24
+ <% end %>
25
+ </tbody>
16
26
  <% end %>
17
- </tbody>
18
- </table>
27
+ </table>
28
+ </div>
@@ -1,6 +1,6 @@
1
- require_relative '../errors'
2
- require_relative '../serializers'
3
-
1
+ require_relative "../errors"
2
+ require_relative "../serializers"
3
+ require_relative "../utils"
4
4
 
5
5
  # This module provides the common functionality for any controller mixins, a `root` action, and
6
6
  # the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
@@ -29,8 +29,8 @@ module RESTFramework::BaseControllerMixin
29
29
  end
30
30
 
31
31
  def self.included(base)
32
- if base.is_a? Class
33
- base.extend ClassMethods
32
+ if base.is_a?(Class)
33
+ base.extend(ClassMethods)
34
34
 
35
35
  # Add class attributes (with defaults) unless they already exist.
36
36
  {
@@ -42,8 +42,8 @@ module RESTFramework::BaseControllerMixin
42
42
  filter_backends: nil,
43
43
  paginator_class: nil,
44
44
  page_size: 20,
45
- page_query_param: 'page',
46
- page_size_query_param: 'page_size',
45
+ page_query_param: "page",
46
+ page_size_query_param: "page_size",
47
47
  max_page_size: nil,
48
48
  serializer_class: nil,
49
49
  serialize_to_json: true,
@@ -51,13 +51,13 @@ module RESTFramework::BaseControllerMixin
51
51
  singleton_controller: nil,
52
52
  skip_actions: nil,
53
53
  }.each do |a, default|
54
- unless base.respond_to?(a)
55
- base.class_attribute(a)
54
+ next if base.respond_to?(a)
56
55
 
57
- # Set default manually so we can still support Rails 4. Maybe later we can use the default
58
- # parameter on `class_attribute`.
59
- base.send(:"#{a}=", default)
60
- end
56
+ base.class_attribute(a)
57
+
58
+ # Set default manually so we can still support Rails 4. Maybe later we can use the default
59
+ # parameter on `class_attribute`.
60
+ base.send(:"#{a}=", default)
61
61
  end
62
62
 
63
63
  # Alias `extra_actions` to `extra_collection_actions`.
@@ -67,7 +67,11 @@ module RESTFramework::BaseControllerMixin
67
67
  end
68
68
 
69
69
  # Skip csrf since this is an API.
70
- base.skip_before_action(:verify_authenticity_token) rescue nil
70
+ begin
71
+ base.skip_before_action(:verify_authenticity_token)
72
+ rescue
73
+ nil
74
+ end
71
75
 
72
76
  # Handle some common exceptions.
73
77
  base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
@@ -81,7 +85,14 @@ module RESTFramework::BaseControllerMixin
81
85
 
82
86
  # Helper to get the configured serializer class.
83
87
  def get_serializer_class
84
- return self.class.serializer_class
88
+ return nil unless serializer_class = self.class.serializer_class
89
+
90
+ # Wrap it with an adapter if it's an active_model_serializer.
91
+ if defined?(ActiveModel::Serializer) && (serializer_class < ActiveModel::Serializer)
92
+ serializer_class = RESTFramework::ActiveModelSerializerAdapterFactory.for(serializer_class)
93
+ end
94
+
95
+ return serializer_class
85
96
  end
86
97
 
87
98
  # Helper to get filtering backends, defaulting to no backends.
@@ -100,9 +111,9 @@ module RESTFramework::BaseControllerMixin
100
111
  end
101
112
 
102
113
  def record_invalid(e)
103
- return api_response({
104
- message: "Record invalid.", exception: e, errors: e.record&.errors
105
- }, status: 400)
114
+ return api_response(
115
+ {message: "Record invalid.", exception: e, errors: e.record&.errors}, status: 400
116
+ )
106
117
  end
107
118
 
108
119
  def record_not_found(e)
@@ -110,31 +121,15 @@ module RESTFramework::BaseControllerMixin
110
121
  end
111
122
 
112
123
  def record_not_saved(e)
113
- return api_response({
114
- message: "Record not saved.", exception: e, errors: e.record&.errors
115
- }, status: 406)
124
+ return api_response(
125
+ {message: "Record not saved.", exception: e, errors: e.record&.errors}, status: 406
126
+ )
116
127
  end
117
128
 
118
129
  def record_not_destroyed(e)
119
- return api_response({
120
- message: "Record not destroyed.", exception: e, errors: e.record&.errors
121
- }, status: 406)
122
- end
123
-
124
- # Helper for showing routes under a controller action, used for the browsable API.
125
- def _get_routes
126
- begin
127
- formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
128
- rescue NameError
129
- # :nocov:
130
- formatter = ActionDispatch::Routing::ConsoleFormatter
131
- # :nocov:
132
- end
133
- return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
134
- formatter.new
135
- ).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
136
- {verb: r[0], path: r[1], action: r[2]}
137
- }.select { |r| r[:path].start_with?(request.path) }
130
+ return api_response(
131
+ {message: "Record not destroyed.", exception: e, errors: e.record&.errors}, status: 406
132
+ )
138
133
  end
139
134
 
140
135
  # Helper to render a browsable API for `html` format, along with basic `json`/`xml` formats, and
@@ -153,38 +148,38 @@ module RESTFramework::BaseControllerMixin
153
148
  end
154
149
 
155
150
  respond_to do |format|
156
- if payload == ''
157
- format.json {head :no_content} if self.serialize_to_json
158
- format.xml {head :no_content} if self.serialize_to_xml
151
+ if payload == ""
152
+ format.json { head(:no_content) } if self.class.serialize_to_json
153
+ format.xml { head(:no_content) } if self.class.serialize_to_xml
159
154
  else
160
155
  format.json {
161
156
  jkwargs = kwargs.merge(json_kwargs)
162
157
  render(json: payload, layout: false, **jkwargs)
163
- } if self.serialize_to_json
158
+ } if self.class.serialize_to_json
164
159
  format.xml {
165
160
  xkwargs = kwargs.merge(xml_kwargs)
166
161
  render(xml: payload, layout: false, **xkwargs)
167
- } if self.serialize_to_xml
162
+ } if self.class.serialize_to_xml
168
163
  # TODO: possibly support more formats here if supported?
169
164
  end
170
165
  format.html {
171
166
  @payload = payload
172
- if payload == ''
173
- @json_payload = '' if self.serialize_to_json
174
- @xml_payload = '' if self.serialize_to_xml
167
+ if payload == ""
168
+ @json_payload = "" if self.class.serialize_to_json
169
+ @xml_payload = "" if self.class.serialize_to_xml
175
170
  else
176
- @json_payload = payload.to_json if self.serialize_to_json
177
- @xml_payload = payload.to_xml if self.serialize_to_xml
171
+ @json_payload = payload.to_json if self.class.serialize_to_json
172
+ @xml_payload = payload.to_xml if self.class.serialize_to_xml
178
173
  end
179
174
  @template_logo_text ||= "Rails REST Framework"
180
175
  @title ||= self.controller_name.camelize
181
- @routes ||= self._get_routes
176
+ @route_groups ||= RESTFramework::Utils.get_routes(Rails.application.routes, request)
182
177
  hkwargs = kwargs.merge(html_kwargs)
183
178
  begin
184
179
  render(**hkwargs)
185
180
  rescue ActionView::MissingTemplate # fallback to rest_framework layout
186
181
  hkwargs[:layout] = "rest_framework"
187
- hkwargs[:html] = ''
182
+ hkwargs[:html] = ""
188
183
  render(**hkwargs)
189
184
  end
190
185
  }
@@ -1,13 +1,12 @@
1
- require_relative 'base'
2
- require_relative '../filters'
3
-
1
+ require_relative "base"
2
+ require_relative "../filters"
4
3
 
5
4
  # This module provides the core functionality for controllers based on models.
6
5
  module RESTFramework::BaseModelControllerMixin
7
6
  include RESTFramework::BaseControllerMixin
8
7
 
9
8
  def self.included(base)
10
- if base.is_a? Class
9
+ if base.is_a?(Class)
11
10
  RESTFramework::BaseControllerMixin.included(base)
12
11
 
13
12
  # Add class attributes (with defaults) unless they already exist.
@@ -22,7 +21,7 @@ module RESTFramework::BaseModelControllerMixin
22
21
 
23
22
  # Attributes for finding records.
24
23
  find_by_fields: nil,
25
- find_by_query_param: 'find_by',
24
+ find_by_query_param: "find_by",
26
25
 
27
26
  # Attributes for create/update parameters.
28
27
  allowed_parameters: nil,
@@ -32,27 +31,28 @@ module RESTFramework::BaseModelControllerMixin
32
31
  native_serializer_config: nil,
33
32
  native_serializer_singular_config: nil,
34
33
  native_serializer_plural_config: nil,
34
+ native_serializer_except_query_param: "except",
35
35
 
36
36
  # Attributes for default model filtering (and ordering).
37
37
  filterset_fields: nil,
38
38
  ordering_fields: nil,
39
- ordering_query_param: 'ordering',
39
+ ordering_query_param: "ordering",
40
40
  ordering_no_reorder: false,
41
41
  search_fields: nil,
42
- search_query_param: 'search',
42
+ search_query_param: "search",
43
43
  search_ilike: false,
44
44
 
45
45
  # Other misc attributes.
46
46
  create_from_recordset: true, # Option for `recordset.create` vs `Model.create` behavior.
47
47
  filter_recordset_before_find: true, # Option to control if filtering is done before find.
48
48
  }.each do |a, default|
49
- unless base.respond_to?(a)
50
- base.class_attribute(a)
49
+ next if base.respond_to?(a)
51
50
 
52
- # Set default manually so we can still support Rails 4. Maybe later we can use the default
53
- # parameter on `class_attribute`.
54
- base.send(:"#{a}=", default)
55
- end
51
+ base.class_attribute(a)
52
+
53
+ # Set default manually so we can still support Rails 4. Maybe later we can use the default
54
+ # parameter on `class_attribute`.
55
+ base.send(:"#{a}=", default)
56
56
  end
57
57
  end
58
58
  end
@@ -105,7 +105,7 @@ module RESTFramework::BaseModelControllerMixin
105
105
 
106
106
  # Helper to get the configured serializer class, or `NativeSerializer` as a default.
107
107
  def get_serializer_class
108
- return self.class.serializer_class || RESTFramework::NativeSerializer
108
+ return super || RESTFramework::NativeSerializer
109
109
  end
110
110
 
111
111
  # Helper to get filtering backends, defaulting to using `ModelFilter` and `ModelOrderingFilter`.
@@ -143,8 +143,8 @@ module RESTFramework::BaseModelControllerMixin
143
143
  body_params
144
144
  end
145
145
  end
146
- alias :get_create_params :get_body_params
147
- alias :get_update_params :get_body_params
146
+ alias_method :get_create_params, :get_body_params
147
+ alias_method :get_update_params, :get_body_params
148
148
 
149
149
  # Get the model for this controller.
150
150
  def get_model(from_get_recordset: false)
@@ -160,7 +160,7 @@ module RESTFramework::BaseModelControllerMixin
160
160
 
161
161
  # Try to determine model from controller name.
162
162
  begin
163
- return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
163
+ return @model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize
164
164
  rescue NameError
165
165
  end
166
166
 
@@ -196,48 +196,51 @@ module RESTFramework::BaseModelControllerMixin
196
196
  recordset = self.get_filtered_data(recordset)
197
197
  end
198
198
 
199
- # Return the record.
200
- if find_by_value = params[:id] # Route key is always :id by Rails convention.
201
- return self.get_recordset.find_by!(find_by_key => find_by_value)
202
- end
203
- return nil
199
+ # Return the record. Route key is always :id by Rails convention.
200
+ return recordset.find_by!(find_by_key => params[:id])
204
201
  end
205
202
  end
206
203
 
207
-
208
204
  # Mixin for listing records.
209
205
  module RESTFramework::ListModelMixin
210
206
  def index
207
+ api_response(self._index)
208
+ end
209
+
210
+ def _index
211
211
  @records = self.get_filtered_data(self.get_recordset)
212
212
 
213
213
  # Handle pagination, if enabled.
214
214
  if self.class.paginator_class
215
215
  paginator = self.class.paginator_class.new(data: @records, controller: self)
216
216
  page = paginator.get_page
217
- serialized_page = self.get_serializer_class.new(object: page, controller: self).serialize
218
- data = paginator.get_paginated_response(serialized_page)
217
+ serialized_page = self.get_serializer_class.new(page, controller: self).serialize
218
+ return paginator.get_paginated_response(serialized_page)
219
219
  else
220
- data = self.get_serializer_class.new(object: @records, controller: self).serialize
220
+ return self.get_serializer_class.new(@records, controller: self).serialize
221
221
  end
222
-
223
- return api_response(data)
224
222
  end
225
223
  end
226
224
 
227
-
228
225
  # Mixin for showing records.
229
226
  module RESTFramework::ShowModelMixin
230
227
  def show
228
+ api_response(self._show)
229
+ end
230
+
231
+ def _show
231
232
  @record = self.get_record
232
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
233
- return api_response(serialized_record)
233
+ return self.get_serializer_class.new(@record, controller: self).serialize
234
234
  end
235
235
  end
236
236
 
237
-
238
237
  # Mixin for creating records.
239
238
  module RESTFramework::CreateModelMixin
240
239
  def create
240
+ api_response(self._create)
241
+ end
242
+
243
+ def _create
241
244
  if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
242
245
  # Create with any properties inherited from the recordset.
243
246
  @record = self.get_recordset.create!(self.get_create_params)
@@ -245,39 +248,43 @@ module RESTFramework::CreateModelMixin
245
248
  # Otherwise, perform a "bare" create.
246
249
  @record = self.get_model.create!(self.get_create_params)
247
250
  end
248
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
249
- return api_response(serialized_record)
251
+
252
+ return self.get_serializer_class.new(@record, controller: self).serialize
250
253
  end
251
254
  end
252
255
 
253
-
254
256
  # Mixin for updating records.
255
257
  module RESTFramework::UpdateModelMixin
256
258
  def update
259
+ api_response(self._update)
260
+ end
261
+
262
+ def _update
257
263
  @record = self.get_record
258
264
  @record.update!(self.get_update_params)
259
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
260
- return api_response(serialized_record)
265
+ return self.get_serializer_class.new(@record, controller: self).serialize
261
266
  end
262
267
  end
263
268
 
264
-
265
269
  # Mixin for destroying records.
266
270
  module RESTFramework::DestroyModelMixin
267
271
  def destroy
272
+ self._destroy
273
+ api_response("")
274
+ end
275
+
276
+ def _destroy
268
277
  @record = self.get_record
269
278
  @record.destroy!
270
- api_response('')
271
279
  end
272
280
  end
273
281
 
274
-
275
282
  # Mixin that includes show/list mixins.
276
283
  module RESTFramework::ReadOnlyModelControllerMixin
277
284
  include RESTFramework::BaseModelControllerMixin
278
285
 
279
286
  def self.included(base)
280
- if base.is_a? Class
287
+ if base.is_a?(Class)
281
288
  RESTFramework::BaseModelControllerMixin.included(base)
282
289
  end
283
290
  end
@@ -286,13 +293,12 @@ module RESTFramework::ReadOnlyModelControllerMixin
286
293
  include RESTFramework::ShowModelMixin
287
294
  end
288
295
 
289
-
290
296
  # Mixin that includes all the CRUD mixins.
291
297
  module RESTFramework::ModelControllerMixin
292
298
  include RESTFramework::BaseModelControllerMixin
293
299
 
294
300
  def self.included(base)
295
- if base.is_a? Class
301
+ if base.is_a?(Class)
296
302
  RESTFramework::BaseModelControllerMixin.included(base)
297
303
  end
298
304
  end
@@ -1,6 +1,5 @@
1
1
  module RESTFramework::ControllerMixins
2
2
  end
3
3
 
4
-
5
- require_relative 'controller_mixins/base'
6
- require_relative 'controller_mixins/models'
4
+ require_relative "controller_mixins/base"
5
+ require_relative "controller_mixins/models"
@@ -2,10 +2,9 @@
2
2
  class RESTFramework::Error < StandardError
3
3
  end
4
4
 
5
-
6
5
  class RESTFramework::NilPassedToAPIResponseError < RESTFramework::Error
7
6
  def message
8
- return <<~MSG.split("\n").join(' ')
7
+ return <<~MSG.split("\n").join(" ")
9
8
  Payload of `nil` was passed to `api_response`; this is unsupported. If you want a blank
10
9
  response, pass `''` (an empty string) as the payload. If this was the result of a `find_by`
11
10
  (or similar Active Record method) not finding a record, you should use the bang version (e.g.,
@@ -8,7 +8,6 @@ class RESTFramework::BaseFilter
8
8
  end
9
9
  end
10
10
 
11
-
12
11
  # A simple filtering backend that supports filtering a recordset based on fields defined on the
13
12
  # controller class.
14
13
  class RESTFramework::ModelFilter < RESTFramework::BaseFilter
@@ -31,19 +30,19 @@ class RESTFramework::ModelFilter < RESTFramework::BaseFilter
31
30
  end
32
31
  end
33
32
 
34
-
35
33
  # A filter backend which handles ordering of the recordset.
36
34
  class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
37
35
  # Convert ordering string to an ordering configuration.
38
36
  def _get_ordering
39
37
  return nil if @controller.class.ordering_query_param.blank?
38
+
40
39
  ordering_fields = @controller.send(:get_ordering_fields)
41
40
  order_string = @controller.params[@controller.class.ordering_query_param]
42
41
 
43
42
  unless order_string.blank?
44
43
  ordering = {}
45
- order_string.split(',').each do |field|
46
- if field[0] == '-'
44
+ order_string.split(",").each do |field|
45
+ if field[0] == "-"
47
46
  column = field[1..-1]
48
47
  direction = :desc
49
48
  else
@@ -63,7 +62,7 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
63
62
  # Order data according to the request query parameters.
64
63
  def get_filtered_data(data)
65
64
  ordering = self._get_ordering
66
- reorder = !@controller.send(:ordering_no_reorder)
65
+ reorder = !@controller.class.ordering_no_reorder
67
66
 
68
67
  if ordering && !ordering.empty?
69
68
  return data.send(reorder ? :reorder : :order, _get_ordering)
@@ -73,19 +72,21 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
73
72
  end
74
73
  end
75
74
 
76
-
77
75
  # Multi-field text searching on models.
78
76
  class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
79
77
  # Filter data according to the request query parameters.
80
78
  def get_filtered_data(data)
81
79
  fields = @controller.send(:get_search_fields)
82
- search = @controller.request.query_parameters[@controller.send(:search_query_param)]
80
+ search = @controller.request.query_parameters[@controller.class.search_query_param]
83
81
 
84
82
  # Ensure we use array conditions to prevent SQL injection.
85
83
  unless search.blank?
86
- return data.where(fields.map { |f|
87
- "CAST(#{f} AS CHAR) #{@controller.send(:search_ilike) ? "ILIKE" : "LIKE"} ?"
88
- }.join(' OR '), *(["%#{search}%"] * fields.length))
84
+ return data.where(
85
+ fields.map { |f|
86
+ "CAST(#{f} AS CHAR) #{@controller.class.search_ilike ? "ILIKE" : "LIKE"} ?"
87
+ }.join(" OR "),
88
+ *(["%#{search}%"] * fields.length),
89
+ )
89
90
  end
90
91
 
91
92
  return data
@@ -1,7 +1,6 @@
1
- require 'rails/generators'
1
+ require "rails/generators"
2
2
 
3
-
4
- # Some projects don't have the inflection "REST" as an acronym, so this is a helper class to prevent
3
+ # Most projects don't have the inflection "REST" as an acronym, so this is a helper class to prevent
5
4
  # this generator from being namespaced under `r_e_s_t_framework`.
6
5
  # :nocov:
7
6
  class RESTFrameworkCustomGeneratorControllerNamespace < String
@@ -11,18 +10,17 @@ class RESTFrameworkCustomGeneratorControllerNamespace < String
11
10
  end
12
11
  # :nocov:
13
12
 
14
-
15
13
  class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
16
- PATH_REGEX = /^\/*([a-z0-9_\/]*[a-z0-9_])(?:[\.a-z\/]*)$/
14
+ PATH_REGEX = %r{^[a-z0-9][a-z0-9_/]+$}
17
15
 
18
16
  desc <<~END
19
- Description:
17
+ Description:
20
18
  Generates a new REST Framework controller.
21
19
 
22
20
  Specify the controller as a path, including the module, if needed, like:
23
21
  'parent_module/controller_name'.
24
22
 
25
- Example:
23
+ Example:
26
24
  `rails generate rest_framework:controller user_api/groups`
27
25
 
28
26
  Generates a controller at `app/controllers/user_api/groups_controller.rb` named
@@ -31,10 +29,7 @@ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
31
29
 
32
30
  argument :path, type: :string
33
31
  class_option(
34
- :parent_class,
35
- type: :string,
36
- default: 'ApplicationController',
37
- desc: "Inheritance parent",
32
+ :parent_class, type: :string, default: "ApplicationController", desc: "Inheritance parent"
38
33
  )
39
34
  class_option(
40
35
  :include_base,
@@ -50,11 +45,13 @@ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
50
45
  end
51
46
 
52
47
  def create_rest_controller_file
53
- unless (path_match = PATH_REGEX.match(self.path))
54
- raise StandardError.new("Path isn't correct.")
48
+ unless PATH_REGEX.match?(self.path)
49
+ raise StandardError, "Path isn't valid."
55
50
  end
56
51
 
57
- cleaned_path = path_match[1]
52
+ # Remove '_controller' from end of path, if it exists.
53
+ cleaned_path = self.path.delete_suffix("_controller")
54
+
58
55
  content = <<~END
59
56
  class #{cleaned_path.camelize}Controller < #{options[:parent_class]}
60
57
  include RESTFramework::#{
@@ -62,6 +59,6 @@ class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
62
59
  }
63
60
  end
64
61
  END
65
- create_file("app/controllers/#{path}_controller.rb", content)
62
+ create_file("app/controllers/#{cleaned_path}_controller.rb", content)
66
63
  end
67
64
  end
@@ -1,5 +1,4 @@
1
1
  module RESTFramework::Generators
2
2
  end
3
3
 
4
-
5
- require_relative 'generators/controller_generator'
4
+ require_relative "generators/controller_generator"
@@ -15,7 +15,6 @@ class RESTFramework::BasePaginator
15
15
  end
16
16
  end
17
17
 
18
-
19
18
  # A simple paginator based on page numbers.
20
19
  #
21
20
  # Example: http://example.com/api/users/?page=3&page_size=50
@@ -26,7 +25,7 @@ class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
26
25
  @page_size = self._page_size
27
26
 
28
27
  @total_pages = @count / @page_size
29
- @total_pages += 1 if (@count % @page_size != 0)
28
+ @total_pages += 1 if @count % @page_size != 0
30
29
  end
31
30
 
32
31
  def _page_size
@@ -60,7 +59,7 @@ class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
60
59
  # Get the page and return it so the caller can serialize it.
61
60
  def get_page(page_number=nil)
62
61
  # If page number isn't provided, infer from the params or use 1 as a fallback value.
63
- if !page_number
62
+ unless page_number
64
63
  page_number = @controller&.params&.[](self._page_query_param)
65
64
  if page_number.blank?
66
65
  page_number = 1
@@ -90,7 +89,6 @@ class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
90
89
  end
91
90
  end
92
91
 
93
-
94
92
  # TODO: implement this
95
93
  # class RESTFramework::CountOffsetPaginator
96
94
  # end
@@ -1,46 +1,11 @@
1
- require 'action_dispatch/routing/mapper'
2
-
1
+ require "action_dispatch/routing/mapper"
2
+ require_relative "utils"
3
3
 
4
4
  module ActionDispatch::Routing
5
5
  class Mapper
6
- # Internal helper to take extra_actions hash and convert to a consistent format.
7
- protected def _parse_extra_actions(extra_actions)
8
- return (extra_actions || {}).map do |k,v|
9
- kwargs = {action: k}
10
- path = k
11
-
12
- # Convert structure to path/methods/kwargs.
13
- if v.is_a?(Hash) # allow kwargs
14
- v = v.symbolize_keys
15
-
16
- # Ensure methods is an array.
17
- if v[:methods].is_a?(String) || v[:methods].is_a?(Symbol)
18
- methods = [v.delete(:methods)]
19
- else
20
- methods = v.delete(:methods)
21
- end
22
-
23
- # Override path if it's provided.
24
- if v.key?(:path)
25
- path = v.delete(:path)
26
- end
27
-
28
- # Pass any further kwargs to the underlying Rails interface.
29
- kwargs = kwargs.merge(v)
30
- elsif v.is_a?(Symbol) || v.is_a?(String)
31
- methods = [v]
32
- else
33
- methods = v
34
- end
35
-
36
- # Return a hash with keys: :path, :methods, :kwargs.
37
- {path: path, methods: methods, kwargs: kwargs}
38
- end
39
- end
40
-
41
6
  # Internal interface to get the controller class from the name and current scope.
42
7
  protected def _get_controller_class(name, pluralize: true, fallback_reverse_pluralization: true)
43
- # get class name
8
+ # Get class name.
44
9
  name = name.to_s.camelize # camelize to leave plural names plural
45
10
  name = name.pluralize if pluralize
46
11
  if name == name.pluralize
@@ -51,14 +16,14 @@ module ActionDispatch::Routing
51
16
  name += "Controller"
52
17
  name_reverse += "Controller"
53
18
 
54
- # get scope for the class
19
+ # Get scope for the class.
55
20
  if @scope[:module]
56
21
  mod = @scope[:module].to_s.classify.constantize
57
22
  else
58
23
  mod = Object
59
24
  end
60
25
 
61
- # convert class name to class
26
+ # Convert class name to class.
62
27
  begin
63
28
  controller = mod.const_get(name)
64
29
  rescue NameError
@@ -92,13 +57,13 @@ module ActionDispatch::Routing
92
57
  if controller.is_a?(Class)
93
58
  controller_class = controller
94
59
  else
95
- controller_class = _get_controller_class(controller, pluralize: !default_singular)
60
+ controller_class = self._get_controller_class(controller, pluralize: !default_singular)
96
61
  end
97
62
 
98
63
  # Set controller if it's not explicitly set.
99
64
  kwargs[:controller] = name unless kwargs[:controller]
100
65
 
101
- # determine plural/singular resource
66
+ # Determine plural/singular resource.
102
67
  force_singular = kwargs.delete(:force_singular)
103
68
  force_plural = kwargs.delete(:force_plural)
104
69
  if force_singular
@@ -118,14 +83,16 @@ module ActionDispatch::Routing
118
83
  public_send(resource_method, name, except: skip, **kwargs) do
119
84
  if controller_class.respond_to?(:extra_member_actions)
120
85
  member do
121
- actions = self._parse_extra_actions(controller_class.extra_member_actions)
122
- _route_extra_actions(actions)
86
+ actions = RESTFramework::Utils.parse_extra_actions(
87
+ controller_class.extra_member_actions,
88
+ )
89
+ self._route_extra_actions(actions)
123
90
  end
124
91
  end
125
92
 
126
93
  collection do
127
- actions = self._parse_extra_actions(controller_class.extra_actions)
128
- _route_extra_actions(actions)
94
+ actions = RESTFramework::Utils.parse_extra_actions(controller_class.extra_actions)
95
+ self._route_extra_actions(actions)
129
96
  end
130
97
 
131
98
  yield if block_given?
@@ -160,14 +127,14 @@ module ActionDispatch::Routing
160
127
  kwargs[:controller] = name unless kwargs[:controller]
161
128
 
162
129
  # Route actions using the resourceful router, but skip all builtin actions.
163
- actions = self._parse_extra_actions(controller_class.extra_actions)
130
+ actions = RESTFramework::Utils.parse_extra_actions(controller_class.extra_actions)
164
131
  public_send(:resource, name, only: [], **kwargs) do
165
132
  # Route a root for this resource.
166
133
  if route_root_to
167
- get '', action: route_root_to
134
+ get("", action: route_root_to)
168
135
  end
169
136
 
170
- _route_extra_actions(actions, &block)
137
+ self._route_extra_actions(actions, &block)
171
138
  end
172
139
  end
173
140
 
@@ -179,13 +146,12 @@ module ActionDispatch::Routing
179
146
 
180
147
  # Remove path if name is nil (routing to the root of current namespace).
181
148
  unless name
182
- kwargs[:path] = ''
149
+ kwargs[:path] = ""
183
150
  end
184
151
 
185
152
  return rest_route(controller, route_root_to: root_action, **kwargs) do
186
153
  yield if block_given?
187
154
  end
188
155
  end
189
-
190
156
  end
191
157
  end
@@ -1,14 +1,23 @@
1
+ # The base serializer defines the interface for all REST Framework serializers.
1
2
  class RESTFramework::BaseSerializer
2
- def initialize(object: nil, controller: nil, **kwargs)
3
+ attr_accessor :object
4
+
5
+ def initialize(object=nil, controller: nil, **kwargs)
3
6
  @object = object
4
7
  @controller = controller
5
8
  end
6
9
 
7
- def serialize
10
+ # The primary interface for extracting a native Ruby types. This works both for records and
11
+ # collections.
12
+ def serialize(**kwargs)
8
13
  raise NotImplementedError
9
14
  end
10
- end
11
15
 
16
+ # Synonym for `serializable_hash` or compatibility with ActiveModelSerializers.
17
+ def serializable_hash(**kwargs)
18
+ return self.serialize(**kwargs)
19
+ end
20
+ end
12
21
 
13
22
  # This serializer uses `.serializable_hash` to convert objects to Ruby primitives (with the
14
23
  # top-level being either an array or a hash).
@@ -18,8 +27,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
18
27
  class_attribute :plural_config
19
28
  class_attribute :action_config
20
29
 
21
- def initialize(many: nil, model: nil, **kwargs)
22
- super(**kwargs)
30
+ def initialize(object=nil, many: nil, model: nil, **kwargs)
31
+ super(object, **kwargs)
23
32
 
24
33
  if many.nil?
25
34
  # Determine if we are dealing with many objects or just one.
@@ -31,9 +40,9 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
31
40
  # Determine model either explicitly, or by inspecting @object or @controller.
32
41
  @model = model
33
42
  @model ||= @object.class if @object.is_a?(ActiveRecord::Base)
34
- @model ||= @object[0].class if (
43
+ @model ||= @object[0].class if
35
44
  @many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
36
- )
45
+
37
46
  @model ||= @controller.send(:get_model) if @controller
38
47
  end
39
48
 
@@ -66,23 +75,66 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
66
75
  return nil unless @controller
67
76
 
68
77
  if @many == true
69
- controller_serializer = @controller.try(:native_serializer_plural_config)
78
+ controller_serializer = @controller.class.try(:native_serializer_plural_config)
70
79
  elsif @many == false
71
- controller_serializer = @controller.try(:native_serializer_singular_config)
80
+ controller_serializer = @controller.class.try(:native_serializer_singular_config)
72
81
  end
73
82
 
74
- return controller_serializer || @controller.try(:native_serializer_config)
83
+ return controller_serializer || @controller.class.try(:native_serializer_config)
75
84
  end
76
85
 
77
- # Get a configuration passable to `serializable_hash` for the object.
78
- def get_serializer_config
86
+ # Helper to filter (mutate) a single subconfig for specific keys.
87
+ def self.filter_subconfig(subconfig, except, additive: false)
88
+ return subconfig unless subconfig
89
+
90
+ if subconfig.is_a?(Array)
91
+ subconfig = subconfig.map(&:to_sym)
92
+ if additive
93
+ # Only add fields which are not already included.
94
+ subconfig += except - subconfig
95
+ else
96
+ subconfig -= except
97
+ end
98
+ elsif subconfig.is_a?(Hash)
99
+ subconfig.symbolize_keys!
100
+ subconfig.reject! { |k, _v| k.in?(except) }
101
+ end
102
+
103
+ return subconfig
104
+ end
105
+
106
+ # Helper to filter out configuration properties based on the :except query parameter.
107
+ def filter_except(config)
108
+ return config unless @controller
109
+
110
+ except_query_param = @controller.class.try(:native_serializer_except_query_param)
111
+ if except = @controller.request.query_parameters[except_query_param]
112
+ except = except.split(",").map(&:strip).map(&:to_sym)
113
+
114
+ unless except.empty?
115
+ # Duplicate the config to avoid mutating class state.
116
+ config = config.deep_dup
117
+
118
+ # Filter `only`, `except` (additive), `include`, and `methods`.
119
+ self.class.filter_subconfig(config[:only], except)
120
+ self.class.filter_subconfig(config[:except], except, additive: true)
121
+ self.class.filter_subconfig(config[:include], except)
122
+ self.class.filter_subconfig(config[:methods], except)
123
+ end
124
+ end
125
+
126
+ return config
127
+ end
128
+
129
+ # Get the raw serializer config.
130
+ def _get_raw_serializer_config
79
131
  # Return a locally defined serializer config if one is defined.
80
132
  if local_config = self.get_local_native_serializer_config
81
133
  return local_config
82
134
  end
83
135
 
84
136
  # Return a serializer config if one is defined on the controller.
85
- if serializer_config = get_controller_native_serializer_config
137
+ if serializer_config = self.get_controller_native_serializer_config
86
138
  return serializer_config
87
139
  end
88
140
 
@@ -103,13 +155,16 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
103
155
  return {}
104
156
  end
105
157
 
106
- # Convert the object (record or recordset) to Ruby primitives.
107
- def serialize
108
- raise "No object available to serialize!" unless @object
158
+ # Get a configuration passable to `serializable_hash` for the object, filtered if required.
159
+ def get_serializer_config
160
+ return filter_except(self._get_raw_serializer_config)
161
+ end
109
162
 
110
- if @object.is_a?(Enumerable)
163
+ def serialize(**kwargs)
164
+ if @object.respond_to?(:to_ary)
111
165
  return @object.map { |r| r.serializable_hash(self.get_serializer_config) }
112
166
  end
167
+
113
168
  return @object.serializable_hash(self.get_serializer_config)
114
169
  end
115
170
 
@@ -118,6 +173,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
118
173
  @_nested_config ||= self.get_serializer_config
119
174
  return @_nested_config[key]
120
175
  end
176
+
121
177
  def []=(key, value)
122
178
  @_nested_config ||= self.get_serializer_config
123
179
  return @_nested_config[key] = value
@@ -128,20 +184,20 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
128
184
  @_nested_config ||= self.new.get_serializer_config
129
185
  return @_nested_config[key]
130
186
  end
187
+
131
188
  def self.[]=(key, value)
132
189
  @_nested_config ||= self.new.get_serializer_config
133
190
  return @_nested_config[key] = value
134
191
  end
135
192
  end
136
193
 
137
-
138
194
  # :nocov:
139
195
  # Alias NativeModelSerializer -> NativeSerializer.
140
196
  class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
141
197
  def initialize(**kwargs)
142
198
  super
143
199
  ActiveSupport::Deprecation.warn(
144
- <<~MSG.split("\n").join(' ')
200
+ <<~MSG.split("\n").join(" "),
145
201
  RESTFramework::NativeModelSerializer is deprecated and will be removed in future versions of
146
202
  REST Framework; you should use RESTFramework::NativeSerializer instead.
147
203
  MSG
@@ -149,3 +205,19 @@ class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
149
205
  end
150
206
  end
151
207
  # :nocov:
208
+
209
+ # This is a helper factory to wrap an ActiveModelSerializer to provide a `serialize` method which
210
+ # accepts both collections and individual records. Use `.for` to build adapters.
211
+ class RESTFramework::ActiveModelSerializerAdapterFactory
212
+ def self.for(active_model_serializer)
213
+ return Class.new(active_model_serializer) do
214
+ def serialize
215
+ if self.object.respond_to?(:to_ary)
216
+ return self.object.map { |r| self.class.superclass.new(r).serializable_hash }
217
+ end
218
+
219
+ return self.serializable_hash
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,64 @@
1
+ module RESTFramework::Utils
2
+ # Helper to take extra_actions hash and convert to a consistent format:
3
+ # `{paths:, methods:, kwargs:}`.
4
+ def self.parse_extra_actions(extra_actions)
5
+ return (extra_actions || {}).map do |k, v|
6
+ kwargs = {action: k}
7
+ path = k
8
+
9
+ # Convert structure to path/methods/kwargs.
10
+ if v.is_a?(Hash) # allow kwargs
11
+ v = v.symbolize_keys
12
+
13
+ # Ensure methods is an array.
14
+ if v[:methods].is_a?(String) || v[:methods].is_a?(Symbol)
15
+ methods = [v.delete(:methods)]
16
+ else
17
+ methods = v.delete(:methods)
18
+ end
19
+
20
+ # Override path if it's provided.
21
+ if v.key?(:path)
22
+ path = v.delete(:path)
23
+ end
24
+
25
+ # Pass any further kwargs to the underlying Rails interface.
26
+ kwargs = kwargs.merge(v)
27
+ elsif v.is_a?(Symbol) || v.is_a?(String)
28
+ methods = [v]
29
+ else
30
+ methods = v
31
+ end
32
+
33
+ # Return a hash with keys: :path, :methods, :kwargs.
34
+ {path: path, methods: methods, kwargs: kwargs}
35
+ end
36
+ end
37
+
38
+ # Helper to get the current route pattern, stripped of the `(:format)` segment.
39
+ def self.get_route_pattern(application_routes, request)
40
+ application_routes.router.recognize(request) do |route, _, _|
41
+ return route.path.spec.to_s.gsub(/\(\.:format\)$/, "")
42
+ end
43
+ end
44
+
45
+ # Helper for showing routes under a controller action, used for the browsable API.
46
+ def self.get_routes(application_routes, request)
47
+ current_pattern = self.get_route_pattern(application_routes, request)
48
+ current_subdomain = request.subdomain.presence
49
+
50
+ # Return routes that match our current route subdomain/pattern, grouped by controller.
51
+ return application_routes.routes.map { |r|
52
+ {
53
+ verb: r.verb,
54
+ path: r.path.spec.to_s,
55
+ action: r.defaults[:action].presence,
56
+ controller: r.defaults[:controller].presence,
57
+ subdomain: r.defaults[:subdomain].presence,
58
+ route_app: r.app&.app&.inspect&.presence,
59
+ }
60
+ }.select { |r|
61
+ r[:subdomain] == current_subdomain && r[:path].start_with?(current_pattern)
62
+ }.group_by { |r| r[:controller] }
63
+ end
64
+ end
@@ -18,7 +18,7 @@ module RESTFramework
18
18
  end
19
19
 
20
20
  # No VERSION file, so version is unknown.
21
- return 'unknown'
21
+ return "unknown"
22
22
  end
23
23
 
24
24
  def self.stamp_version
@@ -30,5 +30,5 @@ module RESTFramework
30
30
  end
31
31
  end
32
32
 
33
- VERSION = Version.get_version()
33
+ VERSION = Version.get_version
34
34
  end
@@ -1,7 +1,6 @@
1
1
  module RESTFramework
2
2
  end
3
3
 
4
-
5
4
  require_relative "rest_framework/controller_mixins"
6
5
  require_relative "rest_framework/engine"
7
6
  require_relative "rest_framework/errors"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rest_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.7
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregory N. Schmit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-25 00:00:00.000000000 Z
11
+ date: 2022-03-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -36,6 +36,7 @@ files:
36
36
  - VERSION
37
37
  - app/views/layouts/rest_framework.html.erb
38
38
  - app/views/rest_framework/_head.html.erb
39
+ - app/views/rest_framework/_route.html.erb
39
40
  - app/views/rest_framework/_routes.html.erb
40
41
  - lib/rest_framework.rb
41
42
  - lib/rest_framework/controller_mixins.rb
@@ -49,6 +50,7 @@ files:
49
50
  - lib/rest_framework/paginators.rb
50
51
  - lib/rest_framework/routers.rb
51
52
  - lib/rest_framework/serializers.rb
53
+ - lib/rest_framework/utils.rb
52
54
  - lib/rest_framework/version.rb
53
55
  homepage: https://rails-rest-framework.com
54
56
  licenses:
@@ -72,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
74
  - !ruby/object:Gem::Version
73
75
  version: '0'
74
76
  requirements: []
75
- rubygems_version: 3.1.4
77
+ rubygems_version: 3.2.22
76
78
  signing_key:
77
79
  specification_version: 4
78
80
  summary: A framework for DRY RESTful APIs in Ruby on Rails.