rest_framework 0.6.5 → 0.6.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68ebf30c14f9f70796c6963505e7b45421ee838d076784073505feb9b05c3ddb
4
- data.tar.gz: 71918c4b2b2eeaed10833d84cb54f6bbb860ea3205895bc75abf9a543d0bedc9
3
+ metadata.gz: 88e17bbb8855d22fd7f73d637059d35bd12cc3fc9d982fbde73e11097b66af17
4
+ data.tar.gz: 8fe97e8557a9c957b3a31b51b89c4a7623e97c2fbc75c9c8b80bb23b54dc8429
5
5
  SHA512:
6
- metadata.gz: d909e86567107d2565a65e9c99f67b771e462a2bd9dae04289c9846cc0ba68bfe3728b63d1b8a154c532e7a5cb1aeac1f593401a98bbc0b1f405f474aabb8527
7
- data.tar.gz: abe06a65bb999c4926e6a54295afc4c55e254fba1276b0257eb2af765bbc3f367fe5ebc5d9fe1d6181c45157369440de13a7f4aed331a49271426d2468110a99
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.5
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 %>
@@ -104,6 +104,11 @@ module RESTFramework::BaseControllerMixin
104
104
  return serializer_class
105
105
  end
106
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
+
107
112
  # Helper to get filtering backends, defaulting to no backends.
108
113
  def get_filter_backends
109
114
  return self.class.filter_backends || []
@@ -148,11 +153,6 @@ module RESTFramework::BaseControllerMixin
148
153
  json_kwargs = kwargs.delete(:json_kwargs) || {}
149
154
  xml_kwargs = kwargs.delete(:xml_kwargs) || {}
150
155
 
151
- # Do not use any adapters by default, if configured.
152
- if self.class.disable_adapters_by_default && !kwargs.key?(:adapter)
153
- kwargs[:adapter] = nil
154
- end
155
-
156
156
  # Raise helpful error if payload is nil. Usually this happens when a record is not found (e.g.,
157
157
  # when passing something like `User.find_by(id: some_id)` to `api_response`). The caller should
158
158
  # actually be calling `find_by!` to raise ActiveRecord::RecordNotFound and allowing the REST
@@ -161,14 +161,24 @@ module RESTFramework::BaseControllerMixin
161
161
  raise RESTFramework::NilPassedToAPIResponseError
162
162
  end
163
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
+
164
174
  # Flag to track if we had to rescue unknown format.
165
175
  already_rescued_unknown_format = false
166
176
 
167
177
  begin
168
178
  respond_to do |format|
169
179
  if payload == ""
170
- format.json { head(:no_content) } if self.class.serialize_to_json
171
- 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
172
182
  else
173
183
  format.json {
174
184
  jkwargs = kwargs.merge(json_kwargs)
@@ -191,11 +201,13 @@ module RESTFramework::BaseControllerMixin
191
201
  end
192
202
  @template_logo_text ||= "Rails REST Framework"
193
203
  @title ||= self.controller_name.camelize
194
- @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
+ )
195
207
  hkwargs = kwargs.merge(html_kwargs)
196
208
  begin
197
209
  render(**hkwargs)
198
- rescue ActionView::MissingTemplate # fallback to rest_framework layout
210
+ rescue ActionView::MissingTemplate # Fallback to `rest_framework` layout.
199
211
  hkwargs[:layout] = "rest_framework"
200
212
  hkwargs[:html] = ""
201
213
  render(**hkwargs)
@@ -114,11 +114,6 @@ module RESTFramework::BaseModelControllerMixin
114
114
  return super || RESTFramework::NativeSerializer
115
115
  end
116
116
 
117
- # Helper to serialize data using the `serializer_class`.
118
- def serialize(data, **kwargs)
119
- return self.get_serializer_class.new(data, controller: self, **kwargs).serialize
120
- end
121
-
122
117
  # Helper to get filtering backends, defaulting to using `ModelFilter` and `ModelOrderingFilter`.
123
118
  def get_filter_backends
124
119
  return self.class.filter_backends || [
@@ -128,35 +123,33 @@ module RESTFramework::BaseModelControllerMixin
128
123
 
129
124
  # Filter the request body for keys in current action's allowed_parameters/fields config.
130
125
  def get_body_params
131
- return @_get_body_params ||= begin
132
- # Filter the request body and map to strings. Return all params if we cannot resolve a list of
133
- # allowed parameters or fields.
134
- body_params = if allowed_params = self.get_allowed_parameters&.map(&:to_s)
135
- request.request_parameters.select { |p| allowed_params.include?(p) }
136
- else
137
- request.request_parameters
138
- 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
139
133
 
140
- # Add query params in place of missing body params, if configured. If fields are not defined,
141
- # fallback to using columns for this particular feature.
142
- if self.class.accept_generic_params_as_body_params
143
- (self.get_fields(fallback: true) - body_params.keys).each do |k|
144
- if (value = params[k])
145
- body_params[k] = value
146
- 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
147
140
  end
148
141
  end
142
+ end
149
143
 
150
- # Filter primary key if configured.
151
- if self.class.filter_pk_from_request_body
152
- body_params.delete(self.get_model&.primary_key)
153
- 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
154
148
 
155
- # Filter fields in exclude_body_fields.
156
- (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) }
157
151
 
158
- body_params
159
- end
152
+ return body_params
160
153
  end
161
154
  alias_method :get_create_params, :get_body_params
162
155
  alias_method :get_update_params, :get_body_params
@@ -182,9 +175,10 @@ module RESTFramework::BaseModelControllerMixin
182
175
  return nil
183
176
  end
184
177
 
185
- # 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.
186
180
  def get_recordset
187
- return @recordset if instance_variable_defined?(:@recordset) && @recordset
181
+ return @recordset if instance_variable_defined?(:@recordset)
188
182
  return (@recordset = self.class.recordset) if self.class.recordset
189
183
 
190
184
  # If there is a model, return that model's default scope (all records by default).
@@ -192,13 +186,21 @@ module RESTFramework::BaseModelControllerMixin
192
186
  return @recordset = model.all
193
187
  end
194
188
 
195
- return nil
189
+ return @recordset = nil
196
190
  end
197
191
 
198
- # 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.
199
201
  def get_record
200
202
  # Cache the result.
201
- return @_get_record if @_get_record
203
+ return @record if instance_variable_defined?(:@record)
202
204
 
203
205
  recordset = self.get_recordset
204
206
  find_by_key = self.get_model.primary_key
@@ -216,78 +218,74 @@ module RESTFramework::BaseModelControllerMixin
216
218
 
217
219
  # Filter recordset, if configured.
218
220
  if self.filter_recordset_before_find
219
- recordset = self.get_filtered_data(recordset)
221
+ recordset = self.get_records
220
222
  end
221
223
 
222
- # Return the record. Route key is always :id by Rails convention.
223
- 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])
224
226
  end
225
227
  end
226
228
 
227
229
  # Mixin for listing records.
228
230
  module RESTFramework::ListModelMixin
229
231
  def index
230
- api_response(self.index!)
232
+ return api_response(self.get_index_records)
231
233
  end
232
234
 
233
- def index!
234
- @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
235
238
 
236
239
  # Handle pagination, if enabled.
237
240
  if self.class.paginator_class
238
- paginator = self.class.paginator_class.new(data: @records, controller: self)
241
+ paginator = self.class.paginator_class.new(data: records, controller: self)
239
242
  page = paginator.get_page
240
243
  serialized_page = self.serialize(page)
241
244
  return paginator.get_paginated_response(serialized_page)
242
- else
243
- return self.serialize(@records)
244
245
  end
246
+
247
+ return records
245
248
  end
246
249
  end
247
250
 
248
251
  # Mixin for showing records.
249
252
  module RESTFramework::ShowModelMixin
250
253
  def show
251
- api_response(self.show!)
252
- end
253
-
254
- def show!
255
- @record ||= self.get_record
256
- return self.serialize(@record)
254
+ return api_response(self.get_record)
257
255
  end
258
256
  end
259
257
 
260
258
  # Mixin for creating records.
261
259
  module RESTFramework::CreateModelMixin
262
260
  def create
263
- api_response(self.create!)
261
+ return api_response(self.create!, status: :created)
264
262
  end
265
263
 
264
+ # Helper to perform the `create!` call and return the created record.
266
265
  def create!
267
266
  if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
268
267
  # Create with any properties inherited from the recordset. We exclude any `select` clauses in
269
268
  # case model callbacks need to call `count` on this collection, which typically raises a SQL
270
269
  # `SyntaxError`.
271
- @record ||= self.get_recordset.except(:select).create!(self.get_create_params)
270
+ return self.get_recordset.except(:select).create!(self.get_create_params)
272
271
  else
273
272
  # Otherwise, perform a "bare" create.
274
- @record ||= self.get_model.create!(self.get_create_params)
273
+ return self.get_model.create!(self.get_create_params)
275
274
  end
276
-
277
- return self.serialize(@record)
278
275
  end
279
276
  end
280
277
 
281
278
  # Mixin for updating records.
282
279
  module RESTFramework::UpdateModelMixin
283
280
  def update
284
- api_response(self.update!)
281
+ return api_response(self.update!)
285
282
  end
286
283
 
284
+ # Helper to perform the `update!` call and return the updated record.
287
285
  def update!
288
- @record ||= self.get_record
289
- @record.update!(self.get_update_params)
290
- return self.serialize(@record)
286
+ record = self.get_record
287
+ record.update!(self.get_update_params)
288
+ return record
291
289
  end
292
290
  end
293
291
 
@@ -295,12 +293,12 @@ end
295
293
  module RESTFramework::DestroyModelMixin
296
294
  def destroy
297
295
  self.destroy!
298
- api_response("")
296
+ return api_response("")
299
297
  end
300
298
 
299
+ # Helper to perform the `destroy!` call and return the destroyed (and frozen) record.
301
300
  def destroy!
302
- @record ||= self.get_record
303
- @record.destroy!
301
+ return self.get_record.destroy!
304
302
  end
305
303
  end
306
304
 
@@ -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.5
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-11-02 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`.