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 +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
|
-
[![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`.
|