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