rest_framework 0.6.5 → 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 +4 -4
- data/README.md +1 -0
- data/VERSION +1 -1
- data/app/views/layouts/rest_framework.html.erb +89 -12
- data/app/views/rest_framework/_head.html.erb +125 -26
- data/app/views/rest_framework/_raw_form.html.erb +30 -0
- data/app/views/rest_framework/_route.html.erb +2 -2
- data/lib/rest_framework/controller_mixins/base.rb +21 -9
- data/lib/rest_framework/controller_mixins/models.rb +58 -60
- data/lib/rest_framework/utils.rb +28 -28
- metadata +4 -3
- data/README.md +0 -128
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88e17bbb8855d22fd7f73d637059d35bd12cc3fc9d982fbde73e11097b66af17
|
4
|
+
data.tar.gz: 8fe97e8557a9c957b3a31b51b89c4a7623e97c2fbc75c9c8b80bb23b54dc8429
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
<
|
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
|
-
|
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
|
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
|
-
<%
|
110
|
+
<% if @route_groups.present? %>
|
61
111
|
<div class="row">
|
62
|
-
<
|
63
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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()
|
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 &&
|
4
|
-
<%= link_to route[:relative_path],
|
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
|
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 #
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
156
|
-
|
149
|
+
# Filter fields in exclude_body_fields.
|
150
|
+
(self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
|
157
151
|
|
158
|
-
|
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)
|
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
|
-
#
|
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 @
|
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.
|
221
|
+
recordset = self.get_records
|
220
222
|
end
|
221
223
|
|
222
|
-
# Return the record. Route key is always
|
223
|
-
return @
|
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.
|
232
|
+
return api_response(self.get_index_records)
|
231
233
|
end
|
232
234
|
|
233
|
-
|
234
|
-
|
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:
|
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.
|
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
|
-
|
270
|
+
return self.get_recordset.except(:select).create!(self.get_create_params)
|
272
271
|
else
|
273
272
|
# Otherwise, perform a "bare" create.
|
274
|
-
|
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
|
-
|
289
|
-
|
290
|
-
return
|
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
|
-
|
303
|
-
@record.destroy!
|
301
|
+
return self.get_record.destroy!
|
304
302
|
end
|
305
303
|
end
|
306
304
|
|
data/lib/rest_framework/utils.rb
CHANGED
@@ -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.
|
66
|
-
|
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
|
-
|
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("/")
|
88
|
+
relative_path: path.split("/")[current_levels..]&.join("/"),
|
83
89
|
controller: r.defaults[:controller].presence,
|
84
90
|
action: r.defaults[:action].presence,
|
85
|
-
|
86
|
-
|
87
|
-
|
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[:
|
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
|
+
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-
|
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.
|
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
|
-
[](https://badge.fury.io/rb/rest_framework)
|
4
|
-
[](https://travis-ci.com/gregschmit/rails-rest-framework)
|
5
|
-
[](https://coveralls.io/github/gregschmit/rails-rest-framework?branch=master)
|
6
|
-
[](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`.
|