rest_framework 0.6.4 → 0.6.6

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: 58fa867a9eed508be6ef78687d445cb5f3c0dc8cf0c004d8a8eab7f003f622cb
4
- data.tar.gz: da285af9bf75273dcd864362d57ada399cc876e0e28767e546a89d1a8f280d28
3
+ metadata.gz: 88e17bbb8855d22fd7f73d637059d35bd12cc3fc9d982fbde73e11097b66af17
4
+ data.tar.gz: 8fe97e8557a9c957b3a31b51b89c4a7623e97c2fbc75c9c8b80bb23b54dc8429
5
5
  SHA512:
6
- metadata.gz: 480e49357afb571538b0f5ec730eff04a0dc326ca4d9e93e9bbba7a60571c3dc2b09ddd3cbfa4ca2dec734e4de5de6faec542e5d06d5e9674e2c492575d2b450
7
- data.tar.gz: 138e532ede2fdd207a557edf0cc8fd89f24eceb782b251adf107c3e445ad2e0a32d69fa66475d1215321f6085a953cce98f586284e39d207d453727e66f8a714
6
+ metadata.gz: 5362740a21ef0ef6248c36783da74ff5ad0b142ea9d2de7e228d5052d9badc7af3f78e259dba1015112c04379fd17c16265b0843c26cc2654e89a1742f53e051
7
+ data.tar.gz: 61d6ac5dca5113be086b9e3232e1e7c2e3fa26d45634764993c474cffcc62403f5285b9da0ebdcfd69b4dbe0b9fedaaa5f9d30d2366286a71c458fdbc9fe4ef7
data/README.md ADDED
@@ -0,0 +1 @@
1
+ docs/index.md
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.4
1
+ 0.6.6
@@ -19,22 +19,70 @@
19
19
  <div class="container py-3">
20
20
  <div class="container">
21
21
  <div class="row">
22
- <h1><%= (@header_text if defined? @header_text) || @title %></h1>
22
+ <nav>
23
+ <ol class="breadcrumb">
24
+ <%
25
+ breadcrumbs = request.path.split("/").inject([["/", "(root)"]]) { |breadcrumbs, part|
26
+ # Ignore blank parts of the path (leading slash or double-slashes).
27
+ next breadcrumbs if part.blank?
28
+
29
+ last_path = breadcrumbs[-1][0]
30
+ breadcrumbs << [
31
+ [last_path, part].join(last_path[-1] == "/" ? "" : "/"),
32
+ part,
33
+ ]
34
+ breadcrumbs
35
+ }
36
+ %>
37
+ <% breadcrumbs.each_with_index do |(path, label), i| %>
38
+ <% if i != breadcrumbs.size - 1 %>
39
+ <li class="breadcrumb-item"><a href="<%= path %>"><%= label %></a></li>
40
+ <% else %>
41
+ <li class="breadcrumb-item active"><%= label %></li>
42
+ <% end %>
43
+ <% end %>
44
+ </ol>
45
+ </nav>
46
+ </div>
47
+ <div class="row">
48
+ <div>
49
+ <h1><%= (@header_text if defined? @header_text) || @title %></h1>
50
+ <div style="float: right">
51
+ <% if @route_groups.values[0].any? { |r| r[:matches_path] && r[:verb] == "DELETE" } %>
52
+ <button type="button" class="btn btn-danger" onclick="rrfDelete(this)">DELETE</button>
53
+ <% end %>
54
+ <button type="button" class="btn btn-primary" onclick="rrfRefresh(this)">GET</button>
55
+ </div>
56
+ </div>
23
57
  </div>
24
58
  <hr/>
25
- <% if @json_payload || @xml_payload %>
59
+ <div class="row">
60
+ <div>
61
+ <pre style="white-space: normal">
62
+ <code class="language-plaintext">
63
+ <strong><%= request.method %></strong> <%= request.path %><br>
64
+ </code>
65
+ </pre>
66
+ <pre style="white-space: normal">
67
+ <code class="language-plaintext">
68
+ <strong>HTTP <%= response.status %> <%= response.message %></strong><br>
69
+ <strong>Content-Type:</strong> <%= response.content_type %>
70
+ </code>
71
+ </pre>
72
+ </div>
73
+ </div>
74
+ <% if @json_payload.present? || @xml_payload.present? %>
26
75
  <div class="row">
27
- <h2>Payload</h2>
28
76
  <div class="w-100">
29
77
  <ul class="nav nav-tabs">
30
- <% if @json_payload %>
78
+ <% if @json_payload.present? %>
31
79
  <li class="nav-item">
32
80
  <a class="nav-link active" href="#tab-json" data-bs-toggle="tab" role="tab">
33
81
  .json
34
82
  </a>
35
83
  </li>
36
84
  <% end %>
37
- <% if @xml_payload %>
85
+ <% if @xml_payload.present? %>
38
86
  <li class="nav-item">
39
87
  <a class="nav-link" href="#tab-xml" data-bs-toggle="tab" role="tab">
40
88
  .xml
@@ -45,22 +93,51 @@
45
93
  </div>
46
94
  <div class="tab-content w-100 pt-3">
47
95
  <div class="tab-pane fade show active" id="tab-json" role="tab">
48
- <% if @json_payload %>
49
- <div><pre><code class="language-json"><%= JSON.pretty_generate(JSON.parse(@json_payload)) unless @json_payload == '' %></code></pre></div>
96
+ <% if @json_payload.present? %>
97
+ <div>
98
+ <pre class="rrf-copy"><code class="language-json"><%= JSON.pretty_generate(JSON.parse(@json_payload)) unless @json_payload == '' %></code></pre>
99
+ </div>
50
100
  <% end %>
51
101
  </div>
52
102
  <div class="tab-pane fade" id="tab-xml" role="tab">
53
- <% if @xml_payload %>
54
- <div><pre><code class="language-xml"><%= @xml_payload %></code></pre></div>
103
+ <% if @xml_payload.present? %>
104
+ <div><pre class="rrf-copy"><code class="language-xml"><%= @xml_payload %></code></pre></div>
55
105
  <% end %>
56
106
  </div>
57
107
  </div>
58
108
  </div>
59
109
  <% end %>
60
- <% unless @route_groups.blank? %>
110
+ <% if @route_groups.present? %>
61
111
  <div class="row">
62
- <h2>Routes</h2>
63
- <%= render partial: 'rest_framework/routes' %>
112
+ <div class="w-100">
113
+ <ul class="nav nav-tabs">
114
+ <li class="nav-item">
115
+ <a class="nav-link active" href="#tab-routes" data-bs-toggle="tab" role="tab">
116
+ Routes
117
+ </a>
118
+ </li>
119
+ <% raw_form_routes = @route_groups.values[0].select { |r|
120
+ r[:matches_params] && r[:verb].in?(["POST", "PUT", "PATCH"])
121
+ } %>
122
+ <% unless raw_form_routes.empty? %>
123
+ <li class="nav-item">
124
+ <a class="nav-link" href="#tab-raw-form" data-bs-toggle="tab" role="tab">
125
+ Raw Form
126
+ </a>
127
+ </li>
128
+ <% end %>
129
+ </ul>
130
+ </div>
131
+ <div class="tab-content w-100 pt-3">
132
+ <div class="tab-pane fade show active" id="tab-routes" role="tab">
133
+ <%= render partial: 'rest_framework/routes' %>
134
+ </div>
135
+ <% unless raw_form_routes.empty? %>
136
+ <div class="tab-pane fade" id="tab-raw-form" role="tab">
137
+ <%= render partial: 'rest_framework/raw_form', locals: {raw_form_routes: raw_form_routes} %>
138
+ </div>
139
+ <% end %>
140
+ </div>
64
141
  </div>
65
142
  <% end %>
66
143
  </div>
@@ -6,36 +6,135 @@
6
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
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. */
10
- h1,h2,h3,h4,h5,h6 { width: 100%; font-weight: normal; }
11
- h1 { font-size: 2rem; }
12
- h2 { font-size: 1.7rem; }
13
- h3 { font-size: 1.5rem; }
14
- h4 { font-size: 1.3rem; }
15
- h5 { font-size: 1.1rem; }
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
- }
9
+ /* Adjust headers to always take up their entire row, and tweak the sizing. */
10
+ h1,h2,h3,h4,h5,h6 { display: inline-block; font-weight: normal; margin-bottom: 0; }
11
+ h1 { font-size: 2rem; }
12
+ h2 { font-size: 1.7rem; }
13
+ h3 { font-size: 1.5rem; }
14
+ h4 { font-size: 1.3rem; }
15
+ h5 { font-size: 1.1rem; }
16
+ h6 { font-size: 1rem; }
28
17
 
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
- }
18
+ /* Make code and code blocks a little nicer looking. */
19
+ code {
20
+ padding: 0 .35em;
21
+ background-color: #eee !important;
22
+ border: 1px solid #aaa;
23
+ border-radius: 3px;
24
+ }
25
+
26
+ /* Make route group expansion obvious to the user. */
27
+ .rrf-routes .rrf-route-group-header {
28
+ background-color: #f8f8f8;
29
+ }
30
+ .rrf-routes .rrf-route-group-header:hover {
31
+ background-color: #f0f0f0;
32
+ }
33
+ .rrf-routes .rrf-route-group-header td {
34
+ cursor: pointer;
35
+ }
36
+
37
+ /* Disable bootstrap's collapsing animation because in tables it causes delayed jerkiness. */
38
+ .rrf-routes .collapsing {
39
+ -webkit-transition: none;
40
+ transition: none;
41
+ display: none;
42
+ }
43
+
44
+ /* Copy-to-clipboard styles. */
45
+ .rrf-copy {
46
+ position: relative;
47
+ }
48
+ .rrf-copy .rrf-copy-link {
49
+ position: absolute;
50
+ top: .5em;
51
+ right: .5em;
52
+ transition: 0.3s ease;
53
+ }
54
+ .rrf-copy .rrf-copy-link.rrf-clicked{
55
+ color: green;
56
+ }
35
57
  </style>
36
58
 
37
59
  <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>
38
60
  <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>
39
61
  <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>
40
62
  <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>
41
- <script>hljs.initHighlightingOnLoad();</script>
63
+ <script>hljs.initHighlightingOnLoad()</script>
64
+ <script>
65
+ // Helper to replace the document when doing form submission (mainly to support PUT/PATCH/DELETE).
66
+ function rrfReplaceDocument(content) {
67
+ // Replace the document with provided content.
68
+ document.open()
69
+ document.write(content)
70
+ document.close()
71
+
72
+ // Trigger `DOMContentLoaded` manually so our custom JavaScript works.
73
+ document.dispatchEvent(new Event("DOMContentLoaded", {bubbles: true, cancelable: true}))
74
+ }
75
+
76
+ // Helper to copy the element's next `<code>` sibling's content to the clipboard.
77
+ function rrfCopyToClipboard(element) {
78
+ let range = document.createRange()
79
+ range.selectNode(element.nextSibling)
80
+ window.getSelection().removeAllRanges()
81
+ window.getSelection().addRange(range)
82
+ if (document.execCommand("copy")) {
83
+ // Trigger clicked animation.
84
+ element.classList.add("rrf-clicked")
85
+ element.innerText = "Copied!"
86
+ setTimeout(() => {
87
+ element.classList.remove("rrf-clicked")
88
+ element.innerText = "Copy to Clipboard"
89
+ }, 700)
90
+ }
91
+
92
+ // Return false to prevent normal link behavior.
93
+ return false
94
+ }
95
+
96
+ // Insert copy link and callback to copy contents of `<code>` element.
97
+ document.addEventListener("DOMContentLoaded", (event) => {
98
+ [...document.getElementsByClassName("rrf-copy")].forEach((element, index) => {
99
+ element.insertAdjacentHTML(
100
+ "afterbegin",
101
+ "<a class=\"rrf-copy-link\" onclick=\"return rrfCopyToClipboard(this)\" href=\"#\">Copy to Clipboard</a>",
102
+ )
103
+ })
104
+ })
105
+
106
+ // Helper to refresh the window.
107
+ function rrfRefresh(button) {
108
+ button.disabled = true
109
+ window.location.reload()
110
+ }
111
+
112
+ // Helper to call `DELETE` on the current path.
113
+ function rrfDelete(button) {
114
+ button.disabled = true
115
+ rrfAPICall(window.location.pathname, "DELETE")
116
+ }
117
+
118
+ // Helper to submit the raw form.
119
+ function rrfSubmitRawForm(button) {
120
+ button.disabled = true
121
+
122
+ // Grab the selected route/method, media type, and the body.
123
+ const [method, path] = document.getElementById("rawFormRoute").value.split(":")
124
+ const media_type = document.getElementById("rawFormMediaType").value
125
+ const body = document.getElementById("rawFormContent").value
126
+
127
+ // Perform the API call.
128
+ rrfAPICall(path, method, {body, headers: {"Content-Type": media_type}})
129
+ }
130
+
131
+ // Helper to make an HTML API call and replace the document with the response.
132
+ function rrfAPICall(path, method, kwargs={}) {
133
+ const headers = kwargs.headers || {}
134
+ delete kwargs.headers
135
+
136
+ fetch(path, {method, headers: {"Accept": "text/html", ...headers}, ...kwargs})
137
+ .then((response) => response.text())
138
+ .then((body) => { rrfReplaceDocument(body) })
139
+ }
140
+ </script>
@@ -0,0 +1,30 @@
1
+ <div style="max-width: 60em; margin: auto">
2
+ <div class="mb-2">
3
+ <label class="form-label w-100">Route
4
+ <select class="form-control" id="rawFormRoute">
5
+ <% raw_form_routes.each do |route| %>
6
+ <% path = @route_props[:with_path_args].call(route[:route]) %>
7
+ <option value="<%= route[:verb] %>:<%= path %>"><%= route[:verb] %> <%= route[:relative_path] %></option>
8
+ <% end %>
9
+ </select>
10
+ </label>
11
+ </div>
12
+
13
+ <div class="mb-2">
14
+ <label class="form-label w-100">Media Type
15
+ <select class="form-control" id="rawFormMediaType">
16
+ <% ["application/json", "application/x-www-form-urlencoded", "multipart/form-data"].each do |t| %>
17
+ <option value="<%= t %>"><%= t %></option>
18
+ <% end %>
19
+ </select>
20
+ </label>
21
+ </div>
22
+
23
+ <div class="mb-2">
24
+ <label class="form-label w-100">Content
25
+ <textarea class="form-control" style="font-family: monospace" id="rawFormContent" rows="8" cols="60"></textarea>
26
+ </label>
27
+ </div>
28
+
29
+ <button type="button" class="btn btn-primary" style="float: right" onclick="rrfSubmitRawForm(this)">Submit</button>
30
+ </div>
@@ -1,7 +1,7 @@
1
1
  <tr>
2
2
  <td>
3
- <% if route[:route].name && link_args = route[:show_link_args] %>
4
- <%= link_to route[:relative_path], self.send("#{route[:route].name}_path", *link_args) %>
3
+ <% if route[:route].name && route[:verb] == "GET" && route[:matches_params] %>
4
+ <%= link_to route[:relative_path], @route_props[:with_path_args].call(route[:route]) %>
5
5
  <% else %>
6
6
  <%= route[:relative_path] %>
7
7
  <% end %>
@@ -90,6 +90,12 @@ module RESTFramework::BaseControllerMixin
90
90
  def get_serializer_class
91
91
  return nil unless serializer_class = self.class.serializer_class
92
92
 
93
+ # Support dynamically resolving serializer given a symbol or string.
94
+ serializer_class = serializer_class.to_s if serializer_class.is_a?(Symbol)
95
+ if serializer_class.is_a?(String)
96
+ serializer_class = self.class.const_get(serializer_class)
97
+ end
98
+
93
99
  # Wrap it with an adapter if it's an active_model_serializer.
94
100
  if defined?(ActiveModel::Serializer) && (serializer_class < ActiveModel::Serializer)
95
101
  serializer_class = RESTFramework::ActiveModelSerializerAdapterFactory.for(serializer_class)
@@ -98,6 +104,11 @@ module RESTFramework::BaseControllerMixin
98
104
  return serializer_class
99
105
  end
100
106
 
107
+ # Helper to serialize data using the `serializer_class`.
108
+ def serialize(data, **kwargs)
109
+ return self.get_serializer_class.new(data, controller: self, **kwargs).serialize
110
+ end
111
+
101
112
  # Helper to get filtering backends, defaulting to no backends.
102
113
  def get_filter_backends
103
114
  return self.class.filter_backends || []
@@ -142,11 +153,6 @@ module RESTFramework::BaseControllerMixin
142
153
  json_kwargs = kwargs.delete(:json_kwargs) || {}
143
154
  xml_kwargs = kwargs.delete(:xml_kwargs) || {}
144
155
 
145
- # Do not use any adapters by default, if configured.
146
- if self.class.disable_adapters_by_default && !kwargs.key?(:adapter)
147
- kwargs[:adapter] = nil
148
- end
149
-
150
156
  # Raise helpful error if payload is nil. Usually this happens when a record is not found (e.g.,
151
157
  # when passing something like `User.find_by(id: some_id)` to `api_response`). The caller should
152
158
  # actually be calling `find_by!` to raise ActiveRecord::RecordNotFound and allowing the REST
@@ -155,14 +161,24 @@ module RESTFramework::BaseControllerMixin
155
161
  raise RESTFramework::NilPassedToAPIResponseError
156
162
  end
157
163
 
164
+ # If `payload` is an `ActiveRecord::Relation` or `ActiveRecord::Base`, then serialize it.
165
+ if payload.is_a?(ActiveRecord::Base) || payload.is_a?(ActiveRecord::Relation)
166
+ payload = self.serialize(payload)
167
+ end
168
+
169
+ # Do not use any adapters by default, if configured.
170
+ if self.class.disable_adapters_by_default && !kwargs.key?(:adapter)
171
+ kwargs[:adapter] = nil
172
+ end
173
+
158
174
  # Flag to track if we had to rescue unknown format.
159
175
  already_rescued_unknown_format = false
160
176
 
161
177
  begin
162
178
  respond_to do |format|
163
179
  if payload == ""
164
- format.json { head(:no_content) } if self.class.serialize_to_json
165
- format.xml { head(:no_content) } if self.class.serialize_to_xml
180
+ format.json { head(kwargs[:status] || :no_content) } if self.class.serialize_to_json
181
+ format.xml { head(kwargs[:status] || :no_content) } if self.class.serialize_to_xml
166
182
  else
167
183
  format.json {
168
184
  jkwargs = kwargs.merge(json_kwargs)
@@ -185,11 +201,13 @@ module RESTFramework::BaseControllerMixin
185
201
  end
186
202
  @template_logo_text ||= "Rails REST Framework"
187
203
  @title ||= self.controller_name.camelize
188
- @route_groups ||= RESTFramework::Utils.get_routes(Rails.application.routes, request)
204
+ @route_props, @route_groups = RESTFramework::Utils.get_routes(
205
+ Rails.application.routes, request
206
+ )
189
207
  hkwargs = kwargs.merge(html_kwargs)
190
208
  begin
191
209
  render(**hkwargs)
192
- rescue ActionView::MissingTemplate # fallback to rest_framework layout
210
+ rescue ActionView::MissingTemplate # Fallback to `rest_framework` layout.
193
211
  hkwargs[:layout] = "rest_framework"
194
212
  hkwargs[:html] = ""
195
213
  render(**hkwargs)
@@ -123,35 +123,33 @@ module RESTFramework::BaseModelControllerMixin
123
123
 
124
124
  # Filter the request body for keys in current action's allowed_parameters/fields config.
125
125
  def get_body_params
126
- return @_get_body_params ||= begin
127
- # Filter the request body and map to strings. Return all params if we cannot resolve a list of
128
- # allowed parameters or fields.
129
- body_params = if allowed_params = self.get_allowed_parameters&.map(&:to_s)
130
- request.request_parameters.select { |p| allowed_params.include?(p) }
131
- else
132
- request.request_parameters
133
- end
126
+ # Filter the request body and map to strings. Return all params if we cannot resolve a list of
127
+ # allowed parameters or fields.
128
+ body_params = if allowed_params = self.get_allowed_parameters&.map(&:to_s)
129
+ request.request_parameters.select { |p| allowed_params.include?(p) }
130
+ else
131
+ request.request_parameters
132
+ end
134
133
 
135
- # Add query params in place of missing body params, if configured. If fields are not defined,
136
- # fallback to using columns for this particular feature.
137
- if self.class.accept_generic_params_as_body_params
138
- (self.get_fields(fallback: true) - body_params.keys).each do |k|
139
- if (value = params[k])
140
- body_params[k] = value
141
- end
134
+ # Add query params in place of missing body params, if configured. If fields are not defined,
135
+ # fallback to using columns for this particular feature.
136
+ if self.class.accept_generic_params_as_body_params
137
+ (self.get_fields(fallback: true) - body_params.keys).each do |k|
138
+ if (value = params[k])
139
+ body_params[k] = value
142
140
  end
143
141
  end
142
+ end
144
143
 
145
- # Filter primary key if configured.
146
- if self.class.filter_pk_from_request_body
147
- body_params.delete(self.get_model&.primary_key)
148
- end
144
+ # Filter primary key if configured.
145
+ if self.class.filter_pk_from_request_body
146
+ body_params.delete(self.get_model&.primary_key)
147
+ end
149
148
 
150
- # Filter fields in exclude_body_fields.
151
- (self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
149
+ # Filter fields in exclude_body_fields.
150
+ (self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
152
151
 
153
- body_params
154
- end
152
+ return body_params
155
153
  end
156
154
  alias_method :get_create_params, :get_body_params
157
155
  alias_method :get_update_params, :get_body_params
@@ -177,9 +175,10 @@ module RESTFramework::BaseModelControllerMixin
177
175
  return nil
178
176
  end
179
177
 
180
- # Get the set of records this controller has access to.
178
+ # Get the set of records this controller has access to. The return value is cached and exposed to
179
+ # the view as the `@recordset` instance variable.
181
180
  def get_recordset
182
- return @recordset if instance_variable_defined?(:@recordset) && @recordset
181
+ return @recordset if instance_variable_defined?(:@recordset)
183
182
  return (@recordset = self.class.recordset) if self.class.recordset
184
183
 
185
184
  # If there is a model, return that model's default scope (all records by default).
@@ -187,13 +186,21 @@ module RESTFramework::BaseModelControllerMixin
187
186
  return @recordset = model.all
188
187
  end
189
188
 
190
- return nil
189
+ return @recordset = nil
191
190
  end
192
191
 
193
- # Get a single record by primary key or another column, if allowed.
192
+ # Helper to get the records this controller has access to *after* any filtering is applied.
193
+ def get_records
194
+ return @records if instance_variable_defined?(:@records)
195
+
196
+ return @records = self.get_filtered_data(self.get_recordset)
197
+ end
198
+
199
+ # Get a single record by primary key or another column, if allowed. The return value is cached and
200
+ # exposed to the view as the `@record` instance variable.
194
201
  def get_record
195
202
  # Cache the result.
196
- return @_get_record if @_get_record
203
+ return @record if instance_variable_defined?(:@record)
197
204
 
198
205
  recordset = self.get_recordset
199
206
  find_by_key = self.get_model.primary_key
@@ -211,78 +218,74 @@ module RESTFramework::BaseModelControllerMixin
211
218
 
212
219
  # Filter recordset, if configured.
213
220
  if self.filter_recordset_before_find
214
- recordset = self.get_filtered_data(recordset)
221
+ recordset = self.get_records
215
222
  end
216
223
 
217
- # Return the record. Route key is always :id by Rails convention.
218
- return @_get_record = recordset.find_by!(find_by_key => params[:id])
224
+ # Return the record. Route key is always `:id` by Rails convention.
225
+ return @record = recordset.find_by!(find_by_key => params[:id])
219
226
  end
220
227
  end
221
228
 
222
229
  # Mixin for listing records.
223
230
  module RESTFramework::ListModelMixin
224
231
  def index
225
- api_response(self.index!)
232
+ return api_response(self.get_index_records)
226
233
  end
227
234
 
228
- def index!
229
- @records ||= self.get_filtered_data(self.get_recordset)
235
+ # Helper to get records with both filtering and pagination applied.
236
+ def get_index_records
237
+ records = self.get_records
230
238
 
231
239
  # Handle pagination, if enabled.
232
240
  if self.class.paginator_class
233
- paginator = self.class.paginator_class.new(data: @records, controller: self)
241
+ paginator = self.class.paginator_class.new(data: records, controller: self)
234
242
  page = paginator.get_page
235
- serialized_page = self.get_serializer_class.new(page, controller: self).serialize
243
+ serialized_page = self.serialize(page)
236
244
  return paginator.get_paginated_response(serialized_page)
237
- else
238
- return self.get_serializer_class.new(@records, controller: self).serialize
239
245
  end
246
+
247
+ return records
240
248
  end
241
249
  end
242
250
 
243
251
  # Mixin for showing records.
244
252
  module RESTFramework::ShowModelMixin
245
253
  def show
246
- api_response(self.show!)
247
- end
248
-
249
- def show!
250
- @record ||= self.get_record
251
- return self.get_serializer_class.new(@record, controller: self).serialize
254
+ return api_response(self.get_record)
252
255
  end
253
256
  end
254
257
 
255
258
  # Mixin for creating records.
256
259
  module RESTFramework::CreateModelMixin
257
260
  def create
258
- api_response(self.create!)
261
+ return api_response(self.create!, status: :created)
259
262
  end
260
263
 
264
+ # Helper to perform the `create!` call and return the created record.
261
265
  def create!
262
266
  if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
263
267
  # Create with any properties inherited from the recordset. We exclude any `select` clauses in
264
268
  # case model callbacks need to call `count` on this collection, which typically raises a SQL
265
269
  # `SyntaxError`.
266
- @record ||= self.get_recordset.except(:select).create!(self.get_create_params)
270
+ return self.get_recordset.except(:select).create!(self.get_create_params)
267
271
  else
268
272
  # Otherwise, perform a "bare" create.
269
- @record ||= self.get_model.create!(self.get_create_params)
273
+ return self.get_model.create!(self.get_create_params)
270
274
  end
271
-
272
- return self.get_serializer_class.new(@record, controller: self).serialize
273
275
  end
274
276
  end
275
277
 
276
278
  # Mixin for updating records.
277
279
  module RESTFramework::UpdateModelMixin
278
280
  def update
279
- api_response(self.update!)
281
+ return api_response(self.update!)
280
282
  end
281
283
 
284
+ # Helper to perform the `update!` call and return the updated record.
282
285
  def update!
283
- @record ||= self.get_record
284
- @record.update!(self.get_update_params)
285
- return self.get_serializer_class.new(@record, controller: self).serialize
286
+ record = self.get_record
287
+ record.update!(self.get_update_params)
288
+ return record
286
289
  end
287
290
  end
288
291
 
@@ -290,12 +293,12 @@ end
290
293
  module RESTFramework::DestroyModelMixin
291
294
  def destroy
292
295
  self.destroy!
293
- api_response("")
296
+ return api_response("")
294
297
  end
295
298
 
299
+ # Helper to perform the `destroy!` call and return the destroyed (and frozen) record.
296
300
  def destroy!
297
- @record ||= self.get_record
298
- @record.destroy!
301
+ return self.get_record.destroy!
299
302
  end
300
303
  end
301
304
 
@@ -282,21 +282,6 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
282
282
  end
283
283
  end
284
284
 
285
- # :nocov:
286
- # Alias NativeModelSerializer -> NativeSerializer.
287
- class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
288
- def initialize(**kwargs)
289
- super
290
- ActiveSupport::Deprecation.warn(
291
- <<~MSG.split("\n").join(" "),
292
- RESTFramework::NativeModelSerializer is deprecated and will be removed in future versions of
293
- REST Framework; you should use RESTFramework::NativeSerializer instead.
294
- MSG
295
- )
296
- end
297
- end
298
- # :nocov:
299
-
300
285
  # This is a helper factory to wrap an ActiveModelSerializer to provide a `serialize` method which
301
286
  # accepts both collections and individual records. Use `.for` to build adapters.
302
287
  class RESTFramework::ActiveModelSerializerAdapterFactory
@@ -42,11 +42,6 @@ module RESTFramework::Utils
42
42
  application_routes.router.recognize(request) { |route, _| return route }
43
43
  end
44
44
 
45
- # Helper to get the route pattern for a route, stripped of the `(:format)` segment.
46
- def self.get_route_pattern(route)
47
- return route.path.spec.to_s.gsub(/\(\.:format\)$/, "")
48
- end
49
-
50
45
  # Helper to normalize a path pattern by replacing URL params with generic placeholder, and
51
46
  # removing the `(.:format)` at the end.
52
47
  def self.comparable_path(path)
@@ -56,47 +51,52 @@ module RESTFramework::Utils
56
51
  # Helper for showing routes under a controller action; used for the browsable API.
57
52
  def self.get_routes(application_routes, request, current_route: nil)
58
53
  current_route ||= self.get_request_route(application_routes, request)
59
- current_path = current_route.path.spec.to_s
54
+ current_path = current_route.path.spec.to_s.gsub("(.:format)", "")
60
55
  current_levels = current_path.count("/")
61
56
  current_comparable_path = self.comparable_path(current_path)
62
57
 
58
+ # Add helpful properties of the current route.
59
+ path_args = current_route.required_parts.map { |n| request.path_parameters[n] }
60
+ route_props = {
61
+ with_path_args: ->(r) {
62
+ r.format(r.required_parts.each_with_index.map { |p, i| [p, path_args[i]] }.to_h)
63
+ },
64
+ }
65
+
63
66
  # Return routes that match our current route subdomain/pattern, grouped by controller. We
64
67
  # precompute certain properties of the route for performance.
65
- return application_routes.routes.map { |r|
66
- path = r.path.spec.to_s
68
+ return route_props, application_routes.routes.select { |r|
69
+ # We `select` first to avoid unnecessarily calculating metadata for routes we don't even want
70
+ # to show.
71
+ (
72
+ (r.defaults[:subdomain].blank? || r.defaults[:subdomain] == request.subdomain) &&
73
+ self.comparable_path(r.path.spec.to_s).start_with?(current_comparable_path) &&
74
+ r.defaults[:controller].present? &&
75
+ r.defaults[:action].present?
76
+ )
77
+ }.map { |r|
78
+ path = r.path.spec.to_s.gsub("(.:format)", "")
67
79
  levels = path.count("/")
68
-
69
- # Show link if the route is GET and our current route has all required URL params.
70
- if r.verb == "GET" && r.path.required_names.length == current_route.path.required_names.length
71
- show_link_args = current_route.path.required_names.map { |n| request.params[n] }.compact
72
- else
73
- show_link_args = nil
74
- end
80
+ matches_path = current_path == path
81
+ matches_params = r.required_parts.length == current_route.required_parts.length
75
82
 
76
83
  {
77
84
  route: r,
78
85
  verb: r.verb,
79
86
  path: path,
80
- comparable_path: self.comparable_path(path),
81
87
  # Starts at the number of levels in current path, and removes the `(.:format)` at the end.
82
- relative_path: path.split("/")[current_levels..]&.join("/")&.gsub("(.:format)", ""),
88
+ relative_path: path.split("/")[current_levels..]&.join("/"),
83
89
  controller: r.defaults[:controller].presence,
84
90
  action: r.defaults[:action].presence,
85
- subdomain: r.defaults[:subdomain].presence,
86
- levels: levels,
87
- show_link_args: show_link_args,
91
+ matches_path: matches_path,
92
+ matches_params: matches_params,
93
+ # The following options are only used in subsequent processing in this method.
94
+ _levels: levels,
88
95
  }
89
- }.select { |r|
90
- (
91
- (!r[:subdomain] || r[:subdomain] == request.subdomain.presence) &&
92
- r[:comparable_path].start_with?(current_comparable_path) &&
93
- r[:controller] &&
94
- r[:action]
95
- )
96
96
  }.sort_by { |r|
97
97
  # Sort by levels first, so the routes matching closely with current request show first, then
98
98
  # by the path, and finally by the HTTP verb.
99
- [r[:levels], r[:path], HTTP_METHOD_ORDERING.index(r[:verb]) || 99]
99
+ [r[:_levels], r[:_path], HTTP_METHOD_ORDERING.index(r[:verb]) || 99]
100
100
  }.group_by { |r| r[:controller] }.sort_by { |c, _r|
101
101
  # Sort the controller groups by current controller first, then depth, then alphanumerically.
102
102
  [request.params[:controller] == c ? 0 : 1, c.count("/"), c]
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.6.4
4
+ version: 0.6.6
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: 2022-10-10 00:00:00.000000000 Z
11
+ date: 2022-11-07 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/_raw_form.html.erb
39
40
  - app/views/rest_framework/_route.html.erb
40
41
  - app/views/rest_framework/_routes.html.erb
41
42
  - lib/rest_framework.rb
@@ -74,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
75
  - !ruby/object:Gem::Version
75
76
  version: '0'
76
77
  requirements: []
77
- rubygems_version: 3.2.22
78
+ rubygems_version: 3.2.33
78
79
  signing_key:
79
80
  specification_version: 4
80
81
  summary: A framework for DRY RESTful APIs in Ruby on Rails.
data/README.md DELETED
@@ -1,128 +0,0 @@
1
- # Rails REST Framework
2
-
3
- [![Gem Version](https://badge.fury.io/rb/rest_framework.svg)](https://badge.fury.io/rb/rest_framework)
4
- [![Build Status](https://travis-ci.com/gregschmit/rails-rest-framework.svg?branch=master)](https://travis-ci.com/gregschmit/rails-rest-framework)
5
- [![Coverage Status](https://coveralls.io/repos/github/gregschmit/rails-rest-framework/badge.svg?branch=master)](https://coveralls.io/github/gregschmit/rails-rest-framework?branch=master)
6
- [![Maintainability](https://api.codeclimate.com/v1/badges/ba5df7706cb544d78555/maintainability)](https://codeclimate.com/github/gregschmit/rails-rest-framework/maintainability)
7
-
8
- A framework for DRY RESTful APIs in Ruby on Rails.
9
-
10
- **The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
11
- logic, and routing them can be obnoxious. Building and maintaining features like ordering,
12
- filtering, and pagination can be tedious.
13
-
14
- **The Solution**: This framework implements browsable API responses, CRUD actions for your models,
15
- and features like ordering/filtering/pagination, so you can focus on building awesome APIs.
16
-
17
- Website/Guide: [https://rails-rest-framework.com](https://rails-rest-framework.com)
18
-
19
- Source: [https://github.com/gregschmit/rails-rest-framework](https://github.com/gregschmit/rails-rest-framework)
20
-
21
- YARD Docs: [https://rubydoc.info/gems/rest_framework](https://rubydoc.info/gems/rest_framework)
22
-
23
- ## Installation
24
-
25
- Add this line to your application's Gemfile:
26
-
27
- ```ruby
28
- gem 'rest_framework'
29
- ```
30
-
31
- And then execute:
32
-
33
- ```shell
34
- $ bundle install
35
- ```
36
-
37
- Or install it yourself with:
38
-
39
- ```shell
40
- $ gem install rest_framework
41
- ```
42
-
43
- ## Quick Usage Tutorial
44
-
45
- ### Controller Mixins
46
-
47
- To transform a controller into a RESTful controller, you can either include `BaseControllerMixin`,
48
- `ReadOnlyModelControllerMixin`, or `ModelControllerMixin`. `BaseControllerMixin` provides a `root`
49
- action and a simple interface for routing arbitrary additional actions:
50
-
51
- ```ruby
52
- class ApiController < ApplicationController
53
- include RESTFramework::BaseControllerMixin
54
- self.extra_actions = {test: [:get]}
55
-
56
- def test
57
- render inline: "Test successful!"
58
- end
59
- end
60
- ```
61
-
62
- `ModelControllerMixin` assists with providing the standard model CRUD for your controller.
63
-
64
- ```ruby
65
- class Api::MoviesController < ApiController
66
- include RESTFramework::ModelControllerMixin
67
-
68
- self.recordset = Movie.where(enabled: true)
69
- end
70
- ```
71
-
72
- `ReadOnlyModelControllerMixin` only enables list/show actions, but since we're naming this
73
- controller in a way that doesn't make the model obvious, we can set that explicitly:
74
-
75
- ```ruby
76
- class Api::ReadOnlyMoviesController < ApiController
77
- include RESTFramework::ReadOnlyModelControllerMixin
78
-
79
- self.model = Movie
80
- end
81
- ```
82
-
83
- Note that you can also override the `get_recordset` instance method to override the API behavior
84
- dynamically per-request.
85
-
86
- ### Routing
87
-
88
- You can use Rails' `resource`/`resources` routers to route your API, however if you want
89
- `extra_actions` / `extra_member_actions` to be routed automatically, then you can use `rest_route`
90
- for non-resourceful controllers, or `rest_resource` / `rest_resources` resourceful routers. You can
91
- also use `rest_root` to route the root of your API:
92
-
93
- ```ruby
94
- Rails.application.routes.draw do
95
- rest_root :api # will find `api_controller` and route the `root` action to '/api'
96
- namespace :api do
97
- rest_resources :movies
98
- rest_resources :users
99
- end
100
- end
101
- ```
102
-
103
- Or if you want the API root to be routed to `Api::RootController#root`:
104
-
105
- ```ruby
106
- Rails.application.routes.draw do
107
- namespace :api do
108
- rest_root # will route `Api::RootController#root` to '/' in this namespace ('/api')
109
- rest_resources :movies
110
- rest_resources :users
111
- end
112
- end
113
- ```
114
-
115
- ## Development/Testing
116
-
117
- After you clone the repository, cd'ing into the directory should create a new gemset if you are
118
- using RVM. Then run `bundle install` to install the appropriate gems.
119
-
120
- To run the test suite:
121
-
122
- ```shell
123
- $ rails test
124
- ```
125
-
126
- The top-level `bin/rails` proxies all Rails commands to the test project, so you can operate it via
127
- the usual commands. Ensure you run `rails db:setup` before running `rails server` or
128
- `rails console`.