rest_framework 0.6.9 → 0.6.10
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/VERSION +1 -1
- data/app/views/layouts/rest_framework.html.erb +7 -1
- data/app/views/rest_framework/_head.html.erb +35 -17
- data/lib/rest_framework/controller_mixins/base.rb +117 -72
- data/lib/rest_framework/controller_mixins/models.rb +224 -97
- data/lib/rest_framework/filters.rb +24 -6
- data/lib/rest_framework/routers.rb +56 -15
- data/lib/rest_framework/serializers.rb +15 -5
- data/lib/rest_framework/utils.rb +18 -12
- data/lib/rest_framework.rb +16 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2111609983018e956218533467140d0d8fb268cf3830cc722daeb502d1a0fda
|
4
|
+
data.tar.gz: a1c4b5a0255d37afff40e8f4c94f69891cfd50658e9cbd9abee06f915a35e4fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a86179680989af8aa7e6d09886ac8f6d6a8cc295949678d0346411cf73ec22547266b3870c6d489d06bcca641f519edc47ff3b507e37212d8f1a700d18b9126
|
7
|
+
data.tar.gz: 33ef69ba052e03c5da388bbbb2f8ee401d1e925542f4b7e7e87253df466e0150f76d7eadceb64508ffe2cd1361311208e113f6153957dcb79de940831c1cfcd2
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.6.
|
1
|
+
0.6.10
|
@@ -51,6 +51,9 @@
|
|
51
51
|
<% if @route_groups.values[0].any? { |r| r[:matches_path] && r[:verb] == "DELETE" } %>
|
52
52
|
<button type="button" class="btn btn-danger" onclick="rrfDelete(this)">DELETE</button>
|
53
53
|
<% end %>
|
54
|
+
<% if @route_groups.values[0].any? { |r| r[:matches_path] && r[:verb] == "OPTIONS" } %>
|
55
|
+
<button type="button" class="btn btn-primary" onclick="rrfOptions(this)">OPTIONS</button>
|
56
|
+
<% end %>
|
54
57
|
<button type="button" class="btn btn-primary" onclick="rrfRefresh(this)">GET</button>
|
55
58
|
</div>
|
56
59
|
</div>
|
@@ -95,7 +98,10 @@
|
|
95
98
|
<div class="tab-pane fade show active" id="tab-json" role="tab">
|
96
99
|
<% if @json_payload.present? %>
|
97
100
|
<div>
|
98
|
-
<pre class="rrf-copy"><code class="language-json"><%=
|
101
|
+
<pre class="rrf-copy"><code class="language-json"><%=
|
102
|
+
JSON.pretty_generate(
|
103
|
+
JSON.parse(@json_payload)
|
104
|
+
) unless @json_payload == '' %></code></pre>
|
99
105
|
</div>
|
100
106
|
<% end %>
|
101
107
|
</div>
|
@@ -57,12 +57,34 @@ code {
|
|
57
57
|
</style>
|
58
58
|
|
59
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>
|
60
|
+
<script src="https://cdn.jsdelivr.net/npm/neatjson@0.10.5/javascript/neatjson.min.js"></script>
|
60
61
|
<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>
|
61
62
|
<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>
|
62
63
|
<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>
|
63
|
-
<script>hljs.initHighlightingOnLoad()</script>
|
64
64
|
<script>
|
65
|
-
|
65
|
+
hljs.initHighlightingOnLoad()
|
66
|
+
|
67
|
+
// What to do when document loads.
|
68
|
+
document.addEventListener("DOMContentLoaded", (event) => {
|
69
|
+
// Pretty-print JSON.
|
70
|
+
[...document.getElementsByClassName("language-json")].forEach((element, index) => {
|
71
|
+
element.innerHTML = neatJSON(JSON.parse(element.innerHTML), {
|
72
|
+
wrap: 80,
|
73
|
+
afterComma: 1,
|
74
|
+
afterColon: 1,
|
75
|
+
})
|
76
|
+
});
|
77
|
+
|
78
|
+
// Insert copy link and callback to copy contents of `<code>` element.
|
79
|
+
[...document.getElementsByClassName("rrf-copy")].forEach((element, index) => {
|
80
|
+
element.insertAdjacentHTML(
|
81
|
+
"afterbegin",
|
82
|
+
"<a class=\"rrf-copy-link\" onclick=\"return rrfCopyToClipboard(this)\" href=\"#\">Copy to Clipboard</a>",
|
83
|
+
)
|
84
|
+
})
|
85
|
+
})
|
86
|
+
|
87
|
+
// Replace the document when doing form submission (mainly to support PUT/PATCH/DELETE).
|
66
88
|
function rrfReplaceDocument(content) {
|
67
89
|
// Replace the document with provided content.
|
68
90
|
document.open()
|
@@ -73,7 +95,7 @@ function rrfReplaceDocument(content) {
|
|
73
95
|
document.dispatchEvent(new Event("DOMContentLoaded", {bubbles: true, cancelable: true}))
|
74
96
|
}
|
75
97
|
|
76
|
-
//
|
98
|
+
// Copy the element's next `<code>` sibling's content to the clipboard.
|
77
99
|
function rrfCopyToClipboard(element) {
|
78
100
|
let range = document.createRange()
|
79
101
|
range.selectNode(element.nextSibling)
|
@@ -93,29 +115,25 @@ function rrfCopyToClipboard(element) {
|
|
93
115
|
return false
|
94
116
|
}
|
95
117
|
|
96
|
-
//
|
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.
|
118
|
+
// Refresh the window.
|
107
119
|
function rrfRefresh(button) {
|
108
120
|
button.disabled = true
|
109
121
|
window.location.reload()
|
110
122
|
}
|
111
123
|
|
112
|
-
//
|
124
|
+
// Call `DELETE` on the current path.
|
113
125
|
function rrfDelete(button) {
|
114
126
|
button.disabled = true
|
115
127
|
rrfAPICall(window.location.pathname, "DELETE")
|
116
128
|
}
|
117
129
|
|
118
|
-
//
|
130
|
+
// Call `OPTIONS` on the current path.
|
131
|
+
function rrfOptions(button) {
|
132
|
+
button.disabled = true
|
133
|
+
rrfAPICall(window.location.pathname, "OPTIONS")
|
134
|
+
}
|
135
|
+
|
136
|
+
// Submit the raw form.
|
119
137
|
function rrfSubmitRawForm(button) {
|
120
138
|
button.disabled = true
|
121
139
|
|
@@ -128,7 +146,7 @@ function rrfSubmitRawForm(button) {
|
|
128
146
|
rrfAPICall(path, method, {body, headers: {"Content-Type": media_type}})
|
129
147
|
}
|
130
148
|
|
131
|
-
//
|
149
|
+
// Make an HTML API call and replace the document with the response.
|
132
150
|
function rrfAPICall(path, method, kwargs={}) {
|
133
151
|
const headers = kwargs.headers || {}
|
134
152
|
delete kwargs.headers
|
@@ -12,87 +12,123 @@ module RESTFramework::BaseControllerMixin
|
|
12
12
|
end
|
13
13
|
|
14
14
|
module ClassMethods
|
15
|
-
#
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
15
|
+
# Collect actions (including extra actions) metadata for this controller.
|
16
|
+
def get_actions_metadata
|
17
|
+
actions = {}
|
18
|
+
|
19
|
+
# Start with builtin actions.
|
20
|
+
RESTFramework::BUILTIN_ACTIONS.merge(
|
21
|
+
RESTFramework::RRF_BUILTIN_ACTIONS,
|
22
|
+
).each do |action, methods|
|
23
|
+
actions[action] = {path: "", methods: methods}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Add extra actions.
|
27
|
+
if extra_actions = self.try(:extra_actions)
|
28
|
+
actions.merge!(RESTFramework::Utils.parse_extra_actions(extra_actions))
|
25
29
|
end
|
26
30
|
|
27
|
-
return
|
31
|
+
return actions
|
28
32
|
end
|
29
|
-
end
|
30
33
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
+
# Collect member actions (including extra member actions) metadata for this controller.
|
35
|
+
def get_member_actions_metadata
|
36
|
+
actions = {}
|
34
37
|
|
35
|
-
#
|
36
|
-
|
37
|
-
|
38
|
-
exclude_body_fields: [:created_at, :created_by, :updated_at, :updated_by],
|
39
|
-
accept_generic_params_as_body_params: false,
|
40
|
-
show_backtrace: false,
|
41
|
-
extra_actions: nil,
|
42
|
-
extra_member_actions: nil,
|
43
|
-
filter_backends: nil,
|
44
|
-
singleton_controller: nil,
|
45
|
-
skip_actions: nil,
|
46
|
-
|
47
|
-
# Options related to serialization.
|
48
|
-
rescue_unknown_format_with: :json,
|
49
|
-
serializer_class: nil,
|
50
|
-
serialize_to_json: true,
|
51
|
-
serialize_to_xml: true,
|
52
|
-
|
53
|
-
# Options related to pagination.
|
54
|
-
paginator_class: nil,
|
55
|
-
page_size: 20,
|
56
|
-
page_query_param: "page",
|
57
|
-
page_size_query_param: "page_size",
|
58
|
-
max_page_size: nil,
|
59
|
-
|
60
|
-
# Option to disable serializer adapters by default, mainly introduced because Active Model
|
61
|
-
# Serializers will do things like serialize `[]` into `{"":[]}`.
|
62
|
-
disable_adapters_by_default: true,
|
63
|
-
}.each do |a, default|
|
64
|
-
next if base.respond_to?(a)
|
65
|
-
|
66
|
-
base.class_attribute(a)
|
67
|
-
|
68
|
-
# Set default manually so we can still support Rails 4. Maybe later we can use the default
|
69
|
-
# parameter on `class_attribute`.
|
70
|
-
base.send(:"#{a}=", default)
|
38
|
+
# Start with builtin actions.
|
39
|
+
RESTFramework::BUILTIN_MEMBER_ACTIONS.each do |action, methods|
|
40
|
+
actions[action] = {path: "", methods: methods}
|
71
41
|
end
|
72
42
|
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
base.alias_method(:extra_collection_actions=, :extra_actions=)
|
43
|
+
# Add extra actions.
|
44
|
+
if extra_actions = self.try(:extra_member_actions)
|
45
|
+
actions.merge!(RESTFramework::Utils.parse_extra_actions(extra_actions))
|
77
46
|
end
|
78
47
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
48
|
+
return actions
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get a hash of metadata to be rendered in the `OPTIONS` response. Cache the result.
|
52
|
+
def get_options_metadata
|
53
|
+
return @_base_options_metadata ||= {
|
54
|
+
name: self.metadata&.name || self.controller_name.titleize,
|
55
|
+
description: self.metadata&.description,
|
56
|
+
renders: [
|
57
|
+
"text/html",
|
58
|
+
self.serialize_to_json ? "application/json" : nil,
|
59
|
+
self.serialize_to_xml ? "application/xml" : nil,
|
60
|
+
].compact,
|
61
|
+
actions: self.get_actions_metadata,
|
62
|
+
member_actions: self.get_member_actions_metadata,
|
63
|
+
}.compact
|
64
|
+
end
|
65
|
+
end
|
85
66
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
67
|
+
def self.included(base)
|
68
|
+
return unless base.is_a?(Class)
|
69
|
+
|
70
|
+
base.extend(ClassMethods)
|
71
|
+
|
72
|
+
# Add class attributes (with defaults) unless they already exist.
|
73
|
+
{
|
74
|
+
filter_pk_from_request_body: true,
|
75
|
+
exclude_body_fields: [:created_at, :created_by, :updated_at, :updated_by],
|
76
|
+
accept_generic_params_as_body_params: false,
|
77
|
+
show_backtrace: false,
|
78
|
+
extra_actions: nil,
|
79
|
+
extra_member_actions: nil,
|
80
|
+
filter_backends: nil,
|
81
|
+
singleton_controller: nil,
|
82
|
+
metadata: nil,
|
83
|
+
|
84
|
+
# Options related to serialization.
|
85
|
+
rescue_unknown_format_with: :json,
|
86
|
+
serializer_class: nil,
|
87
|
+
serialize_to_json: true,
|
88
|
+
serialize_to_xml: true,
|
89
|
+
|
90
|
+
# Options related to pagination.
|
91
|
+
paginator_class: nil,
|
92
|
+
page_size: 20,
|
93
|
+
page_query_param: "page",
|
94
|
+
page_size_query_param: "page_size",
|
95
|
+
max_page_size: nil,
|
96
|
+
|
97
|
+
# Option to disable serializer adapters by default, mainly introduced because Active Model
|
98
|
+
# Serializers will do things like serialize `[]` into `{"":[]}`.
|
99
|
+
disable_adapters_by_default: true,
|
100
|
+
}.each do |a, default|
|
101
|
+
next if base.respond_to?(a)
|
102
|
+
|
103
|
+
base.class_attribute(a)
|
104
|
+
|
105
|
+
# Set default manually so we can still support Rails 4. Maybe later we can use the default
|
106
|
+
# parameter on `class_attribute`.
|
107
|
+
base.send(:"#{a}=", default)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Alias `extra_actions` to `extra_collection_actions`.
|
111
|
+
unless base.respond_to?(:extra_collection_actions)
|
112
|
+
base.alias_method(:extra_collection_actions, :extra_actions)
|
113
|
+
base.alias_method(:extra_collection_actions=, :extra_actions=)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Skip CSRF since this is an API.
|
117
|
+
begin
|
118
|
+
base.skip_before_action(:verify_authenticity_token)
|
119
|
+
rescue
|
120
|
+
nil
|
92
121
|
end
|
122
|
+
|
123
|
+
# Handle some common exceptions.
|
124
|
+
base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
|
125
|
+
base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
|
126
|
+
base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
|
127
|
+
base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
|
128
|
+
base.rescue_from(ActiveModel::UnknownAttributeError, with: :unknown_attribute_error)
|
93
129
|
end
|
94
130
|
|
95
|
-
#
|
131
|
+
# Get the configured serializer class.
|
96
132
|
def get_serializer_class
|
97
133
|
return nil unless serializer_class = self.class.serializer_class
|
98
134
|
|
@@ -110,17 +146,17 @@ module RESTFramework::BaseControllerMixin
|
|
110
146
|
return serializer_class
|
111
147
|
end
|
112
148
|
|
113
|
-
#
|
149
|
+
# Serialize the given data using the `serializer_class`.
|
114
150
|
def serialize(data, **kwargs)
|
115
151
|
return self.get_serializer_class.new(data, controller: self, **kwargs).serialize
|
116
152
|
end
|
117
153
|
|
118
|
-
#
|
154
|
+
# Get filtering backends, defaulting to no backends.
|
119
155
|
def get_filter_backends
|
120
156
|
return self.class.filter_backends || []
|
121
157
|
end
|
122
158
|
|
123
|
-
#
|
159
|
+
# Filter an arbitrary data set over all configured filter backends.
|
124
160
|
def get_filtered_data(data)
|
125
161
|
self.get_filter_backends.each do |filter_class|
|
126
162
|
filter = filter_class.new(controller: self)
|
@@ -130,6 +166,10 @@ module RESTFramework::BaseControllerMixin
|
|
130
166
|
return data
|
131
167
|
end
|
132
168
|
|
169
|
+
def get_options_metadata
|
170
|
+
return self.class.get_options_metadata
|
171
|
+
end
|
172
|
+
|
133
173
|
def record_invalid(e)
|
134
174
|
return api_response(
|
135
175
|
{
|
@@ -229,7 +269,7 @@ module RESTFramework::BaseControllerMixin
|
|
229
269
|
@xml_payload = payload.to_xml if self.class.serialize_to_xml
|
230
270
|
end
|
231
271
|
@template_logo_text ||= "Rails REST Framework"
|
232
|
-
@title ||= self.controller_name.
|
272
|
+
@title ||= self.controller_name.titleize
|
233
273
|
@route_props, @route_groups = RESTFramework::Utils.get_routes(
|
234
274
|
Rails.application.routes, request
|
235
275
|
)
|
@@ -253,4 +293,9 @@ module RESTFramework::BaseControllerMixin
|
|
253
293
|
end
|
254
294
|
end
|
255
295
|
end
|
296
|
+
|
297
|
+
# Provide a generic `OPTIONS` response with metadata such as available actions.
|
298
|
+
def options
|
299
|
+
return api_response(self.get_options_metadata)
|
300
|
+
end
|
256
301
|
end
|
@@ -5,56 +5,212 @@ require_relative "../filters"
|
|
5
5
|
module RESTFramework::BaseModelControllerMixin
|
6
6
|
include RESTFramework::BaseControllerMixin
|
7
7
|
|
8
|
+
module ClassMethods
|
9
|
+
IGNORE_VALIDATORS_WITH_KEYS = [:if, :unless]
|
10
|
+
|
11
|
+
# Get the model for this controller.
|
12
|
+
def get_model(from_get_recordset: false)
|
13
|
+
return @model if @model
|
14
|
+
return (@model = self.model) if self.model
|
15
|
+
|
16
|
+
# Delegate to the recordset's model, if it's defined.
|
17
|
+
unless from_get_recordset # Prevent infinite recursion.
|
18
|
+
if (recordset = self.new.get_recordset)
|
19
|
+
return @model = recordset.klass
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Try to determine model from controller name.
|
24
|
+
begin
|
25
|
+
return @model = self.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize
|
26
|
+
rescue NameError
|
27
|
+
end
|
28
|
+
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get metadata about the resource's fields.
|
33
|
+
def get_fields_metadata(fields: nil)
|
34
|
+
# Get metadata sources.
|
35
|
+
model = self.get_model
|
36
|
+
fields ||= self.fields || model&.column_names || []
|
37
|
+
fields = fields.map(&:to_s)
|
38
|
+
columns = model&.columns_hash
|
39
|
+
column_defaults = model&.column_defaults
|
40
|
+
attributes = model&._default_attributes
|
41
|
+
|
42
|
+
return fields.map { |f|
|
43
|
+
# Initialize metadata to make the order consistent.
|
44
|
+
metadata = {
|
45
|
+
type: nil, kind: nil, label: nil, primary_key: nil, required: nil, read_only: nil
|
46
|
+
}
|
47
|
+
|
48
|
+
# Determine `primary_key` based on model.
|
49
|
+
if model&.primary_key == f
|
50
|
+
metadata[:primary_key] = true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Determine `type`, `required`, `label`, and `kind` based on schema.
|
54
|
+
if column = columns[f]
|
55
|
+
metadata[:type] = column.type
|
56
|
+
metadata[:required] = true unless column.null
|
57
|
+
metadata[:label] = column.human_name.instance_eval { |n| n == "Id" ? "ID" : n }
|
58
|
+
metadata[:kind] = "column"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Determine `default` based on schema; we use `column_defaults` rather than `columns_hash`
|
62
|
+
# because these are casted to the proper type.
|
63
|
+
column_default = column_defaults[f]
|
64
|
+
unless column_default.nil?
|
65
|
+
metadata[:default] = column_default
|
66
|
+
end
|
67
|
+
|
68
|
+
# Determine `default` and `kind` based on attribute only if not determined by the DB.
|
69
|
+
if attributes.key?(f) && attribute = attributes[f]
|
70
|
+
unless metadata.key?(:default)
|
71
|
+
default = attribute.value_before_type_cast
|
72
|
+
metadata[:default] = default unless default.nil?
|
73
|
+
end
|
74
|
+
|
75
|
+
unless metadata[:kind]
|
76
|
+
metadata[:kind] = "attribute"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Determine if `kind` is a association or method if not determined already.
|
81
|
+
unless metadata[:kind]
|
82
|
+
if association = model.reflections[f]
|
83
|
+
metadata[:kind] = "association.#{association.macro}"
|
84
|
+
elsif model.method_defined?(f)
|
85
|
+
metadata[:kind] = "method"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Collect validator options into a hash on their type, while also updating `required` based
|
90
|
+
# on any presence validators.
|
91
|
+
model.validators_on(f).each do |validator|
|
92
|
+
kind = validator.kind
|
93
|
+
options = validator.options
|
94
|
+
|
95
|
+
# Reject validator if it includes keys like `:if` and `:unless` because those are
|
96
|
+
# conditionally applied in a way that is not feasible to communicate via the API.
|
97
|
+
next if IGNORE_VALIDATORS_WITH_KEYS.any? { |k| options.key?(k) }
|
98
|
+
|
99
|
+
# Update `required` if we find a presence validator.
|
100
|
+
metadata[:required] = true if kind == :presence
|
101
|
+
|
102
|
+
metadata[:validators] ||= {}
|
103
|
+
metadata[:validators][kind] ||= []
|
104
|
+
metadata[:validators][kind] << options
|
105
|
+
end
|
106
|
+
|
107
|
+
next [f, metadata.compact]
|
108
|
+
}.to_h
|
109
|
+
end
|
110
|
+
|
111
|
+
# Get a hash of metadata to be rendered in the `OPTIONS` response. Cache the result.
|
112
|
+
def get_options_metadata(fields: nil)
|
113
|
+
return super().merge(
|
114
|
+
{
|
115
|
+
fields: self.get_fields_metadata(fields: fields),
|
116
|
+
},
|
117
|
+
)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
8
121
|
def self.included(base)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
122
|
+
return unless base.is_a?(Class)
|
123
|
+
|
124
|
+
RESTFramework::BaseControllerMixin.included(base)
|
125
|
+
base.extend(ClassMethods)
|
126
|
+
|
127
|
+
# Add class attributes (with defaults) unless they already exist.
|
128
|
+
{
|
129
|
+
# Core attributes related to models.
|
130
|
+
model: nil,
|
131
|
+
recordset: nil,
|
132
|
+
|
133
|
+
# Attributes for configuring record fields.
|
134
|
+
fields: nil,
|
135
|
+
action_fields: nil,
|
136
|
+
metadata_fields: nil,
|
137
|
+
|
138
|
+
# Attributes for finding records.
|
139
|
+
find_by_fields: nil,
|
140
|
+
find_by_query_param: "find_by",
|
141
|
+
|
142
|
+
# Attributes for create/update parameters.
|
143
|
+
allowed_parameters: nil,
|
144
|
+
allowed_action_parameters: nil,
|
145
|
+
|
146
|
+
# Attributes for the default native serializer.
|
147
|
+
native_serializer_config: nil,
|
148
|
+
native_serializer_singular_config: nil,
|
149
|
+
native_serializer_plural_config: nil,
|
150
|
+
native_serializer_only_query_param: "only",
|
151
|
+
native_serializer_except_query_param: "except",
|
152
|
+
|
153
|
+
# Attributes for default model filtering, ordering, and searching.
|
154
|
+
filterset_fields: nil,
|
155
|
+
ordering_fields: nil,
|
156
|
+
ordering_query_param: "ordering",
|
157
|
+
ordering_no_reorder: false,
|
158
|
+
search_fields: nil,
|
159
|
+
search_query_param: "search",
|
160
|
+
search_ilike: false,
|
161
|
+
|
162
|
+
# Other misc attributes.
|
163
|
+
create_from_recordset: true, # Option for `recordset.create` vs `Model.create` behavior.
|
164
|
+
filter_recordset_before_find: true, # Control if filtering is done before find.
|
165
|
+
}.each do |a, default|
|
166
|
+
next if base.respond_to?(a)
|
167
|
+
|
168
|
+
base.class_attribute(a)
|
169
|
+
|
170
|
+
# Set default manually so we can still support Rails 4. Maybe later we can use the default
|
171
|
+
# parameter on `class_attribute`.
|
172
|
+
base.send(:"#{a}=", default)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Actions to run at the end of the class definition.
|
176
|
+
TracePoint.trace(:end) do |t|
|
177
|
+
next if base != t.self
|
178
|
+
|
179
|
+
# Delegate extra actions.
|
180
|
+
base.extra_actions&.each do |action, config|
|
181
|
+
next unless config.is_a?(Hash) && config[:delegate]
|
182
|
+
|
183
|
+
base.define_method(action) do
|
184
|
+
model = self.class.get_model
|
185
|
+
next unless model.respond_to?(action)
|
186
|
+
|
187
|
+
if model.method(action).parameters.last&.first == :keyrest
|
188
|
+
return api_response(model.send(action, **params))
|
189
|
+
else
|
190
|
+
return api_response(model.send(action))
|
191
|
+
end
|
192
|
+
end
|
57
193
|
end
|
194
|
+
|
195
|
+
# Delegate extra member actions.
|
196
|
+
base.extra_member_actions&.each do |action, config|
|
197
|
+
next unless config.is_a?(Hash) && config[:delegate]
|
198
|
+
|
199
|
+
base.define_method(action) do
|
200
|
+
record = self.get_record
|
201
|
+
next unless record.respond_to?(action)
|
202
|
+
|
203
|
+
if record.method(action).parameters.last&.first == :keyrest
|
204
|
+
return api_response(record.send(action, **params))
|
205
|
+
else
|
206
|
+
return api_response(record.send(action))
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# It's important to disable the trace once we've found the end of the base class definition,
|
212
|
+
# for performance.
|
213
|
+
t.disable
|
58
214
|
end
|
59
215
|
end
|
60
216
|
|
@@ -75,33 +231,25 @@ module RESTFramework::BaseModelControllerMixin
|
|
75
231
|
fields = _get_specific_action_config(:action_fields, :fields)
|
76
232
|
|
77
233
|
if fallback
|
78
|
-
fields ||= self.get_model&.column_names || []
|
234
|
+
fields ||= self.class.get_model&.column_names || []
|
79
235
|
end
|
80
236
|
|
81
237
|
return fields
|
82
238
|
end
|
83
239
|
|
84
|
-
#
|
85
|
-
def
|
86
|
-
return self.class.
|
87
|
-
end
|
88
|
-
|
89
|
-
# Get a list of find_by fields for the current action. Default to the model column names.
|
90
|
-
def get_filterset_fields
|
91
|
-
return self.class.filterset_fields || self.get_fields(fallback: true)
|
92
|
-
end
|
93
|
-
|
94
|
-
# Get a list of ordering fields for the current action.
|
95
|
-
def get_ordering_fields
|
96
|
-
return self.class.ordering_fields || self.get_fields
|
240
|
+
# Pass fields to get dynamic metadata based on which fields are available.
|
241
|
+
def get_options_metadata
|
242
|
+
return self.class.get_options_metadata(fields: self.get_fields(fallback: true))
|
97
243
|
end
|
98
244
|
|
99
|
-
# Get a list of
|
100
|
-
|
101
|
-
|
245
|
+
# Get a list of find_by fields for the current action. Do not fallback to columns in case the user
|
246
|
+
# wants to find by virtual columns.
|
247
|
+
def get_find_by_fields
|
248
|
+
return self.class.find_by_fields || self.get_fields
|
102
249
|
end
|
103
250
|
|
104
|
-
# Get a list of parameters allowed for the current action.
|
251
|
+
# Get a list of parameters allowed for the current action. By default we do not fallback to
|
252
|
+
# columns so arbitrary fields can be submitted if no fields are defined.
|
105
253
|
def get_allowed_parameters
|
106
254
|
return _get_specific_action_config(
|
107
255
|
:allowed_action_parameters,
|
@@ -143,7 +291,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
143
291
|
|
144
292
|
# Filter primary key if configured.
|
145
293
|
if self.class.filter_pk_from_request_body
|
146
|
-
body_params.delete(self.get_model&.primary_key)
|
294
|
+
body_params.delete(self.class.get_model&.primary_key)
|
147
295
|
end
|
148
296
|
|
149
297
|
# Filter fields in exclude_body_fields.
|
@@ -154,27 +302,6 @@ module RESTFramework::BaseModelControllerMixin
|
|
154
302
|
alias_method :get_create_params, :get_body_params
|
155
303
|
alias_method :get_update_params, :get_body_params
|
156
304
|
|
157
|
-
# Get the model for this controller.
|
158
|
-
def get_model(from_get_recordset: false)
|
159
|
-
return @model if instance_variable_defined?(:@model) && @model
|
160
|
-
return (@model = self.class.model) if self.class.model
|
161
|
-
|
162
|
-
# Delegate to the recordset's model, if it's defined.
|
163
|
-
unless from_get_recordset # prevent infinite recursion
|
164
|
-
if (recordset = self.get_recordset)
|
165
|
-
return @model = recordset.klass
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
# Try to determine model from controller name.
|
170
|
-
begin
|
171
|
-
return @model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize
|
172
|
-
rescue NameError
|
173
|
-
end
|
174
|
-
|
175
|
-
return nil
|
176
|
-
end
|
177
|
-
|
178
305
|
# Get the set of records this controller has access to. The return value is cached and exposed to
|
179
306
|
# the view as the `@recordset` instance variable.
|
180
307
|
def get_recordset
|
@@ -182,7 +309,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
182
309
|
return (@recordset = self.class.recordset) if self.class.recordset
|
183
310
|
|
184
311
|
# If there is a model, return that model's default scope (all records by default).
|
185
|
-
if (model = self.get_model(from_get_recordset: true))
|
312
|
+
if (model = self.class.get_model(from_get_recordset: true))
|
186
313
|
return @recordset = model.all
|
187
314
|
end
|
188
315
|
|
@@ -203,7 +330,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
203
330
|
return @record if instance_variable_defined?(:@record)
|
204
331
|
|
205
332
|
recordset = self.get_recordset
|
206
|
-
find_by_key = self.get_model.primary_key
|
333
|
+
find_by_key = self.class.get_model.primary_key
|
207
334
|
|
208
335
|
# Find by another column if it's permitted.
|
209
336
|
if find_by_param = self.class.find_by_query_param.presence
|
@@ -276,7 +403,7 @@ module RESTFramework::CreateModelMixin
|
|
276
403
|
return self.get_recordset.except(:select).create!(self.get_create_params)
|
277
404
|
else
|
278
405
|
# Otherwise, perform a "bare" create.
|
279
|
-
return self.get_model.create!(self.get_create_params)
|
406
|
+
return self.class.get_model.create!(self.get_create_params)
|
280
407
|
end
|
281
408
|
end
|
282
409
|
end
|
@@ -313,9 +440,9 @@ module RESTFramework::ReadOnlyModelControllerMixin
|
|
313
440
|
include RESTFramework::BaseModelControllerMixin
|
314
441
|
|
315
442
|
def self.included(base)
|
316
|
-
|
317
|
-
|
318
|
-
|
443
|
+
return unless base.is_a?(Class)
|
444
|
+
|
445
|
+
RESTFramework::BaseModelControllerMixin.included(base)
|
319
446
|
end
|
320
447
|
|
321
448
|
include RESTFramework::ListModelMixin
|
@@ -327,9 +454,9 @@ module RESTFramework::ModelControllerMixin
|
|
327
454
|
include RESTFramework::BaseModelControllerMixin
|
328
455
|
|
329
456
|
def self.included(base)
|
330
|
-
|
331
|
-
|
332
|
-
|
457
|
+
return unless base.is_a?(Class)
|
458
|
+
|
459
|
+
RESTFramework::BaseModelControllerMixin.included(base)
|
333
460
|
end
|
334
461
|
|
335
462
|
include RESTFramework::ListModelMixin
|
@@ -11,10 +11,16 @@ end
|
|
11
11
|
# A simple filtering backend that supports filtering a recordset based on fields defined on the
|
12
12
|
# controller class.
|
13
13
|
class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
14
|
+
# Get a list of filterset fields for the current action. Fallback to columns because we don't want
|
15
|
+
# to try filtering by any query parameter because that could clash with other query parameters.
|
16
|
+
def _get_fields
|
17
|
+
return @controller.class.filterset_fields || @controller.get_fields(fallback: true)
|
18
|
+
end
|
19
|
+
|
14
20
|
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
15
21
|
def _get_filter_params
|
16
22
|
# Map filterset fields to strings because query parameter keys are strings.
|
17
|
-
if fields =
|
23
|
+
if fields = self._get_fields.map(&:to_s)
|
18
24
|
return @controller.request.query_parameters.select { |p, _| fields.include?(p) }
|
19
25
|
end
|
20
26
|
|
@@ -34,15 +40,21 @@ end
|
|
34
40
|
|
35
41
|
# A filter backend which handles ordering of the recordset.
|
36
42
|
class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
43
|
+
# Get a list of ordering fields for the current action. Do not fallback to columns in case the
|
44
|
+
# user wants to order by a virtual column.
|
45
|
+
def _get_fields
|
46
|
+
return @controller.class.ordering_fields || @controller.get_fields
|
47
|
+
end
|
48
|
+
|
37
49
|
# Convert ordering string to an ordering configuration.
|
38
50
|
def _get_ordering
|
39
51
|
return nil if @controller.class.ordering_query_param.blank?
|
40
52
|
|
41
53
|
# Ensure ordering_fields are strings since the split param will be strings.
|
42
|
-
|
54
|
+
fields = self._get_fields&.map(&:to_s)
|
43
55
|
order_string = @controller.params[@controller.class.ordering_query_param]
|
44
56
|
|
45
|
-
|
57
|
+
if order_string.present? && fields
|
46
58
|
ordering = {}.with_indifferent_access
|
47
59
|
order_string.split(",").each do |field|
|
48
60
|
if field[0] == "-"
|
@@ -52,7 +64,7 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
52
64
|
column = field
|
53
65
|
direction = :asc
|
54
66
|
end
|
55
|
-
if !
|
67
|
+
if !fields || column.in?(fields)
|
56
68
|
ordering[column] = direction
|
57
69
|
end
|
58
70
|
end
|
@@ -77,13 +89,19 @@ end
|
|
77
89
|
|
78
90
|
# Multi-field text searching on models.
|
79
91
|
class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
|
92
|
+
# Get a list of search fields for the current action. Fallback to columns because we need an
|
93
|
+
# explicit list of columns to search on, so `nil` is useless in this context.
|
94
|
+
def _get_fields
|
95
|
+
return @controller.class.search_fields || @controller.get_fields(fallback: true)
|
96
|
+
end
|
97
|
+
|
80
98
|
# Filter data according to the request query parameters.
|
81
99
|
def get_filtered_data(data)
|
82
|
-
fields =
|
100
|
+
fields = self._get_fields
|
83
101
|
search = @controller.request.query_parameters[@controller.class.search_query_param]
|
84
102
|
|
85
103
|
# Ensure we use array conditions to prevent SQL injection.
|
86
|
-
|
104
|
+
if search.present? && !fields.empty?
|
87
105
|
return data.where(
|
88
106
|
fields.map { |f|
|
89
107
|
"CAST(#{f} AS VARCHAR) #{@controller.class.search_ilike ? "ILIKE" : "LIKE"} ?"
|
@@ -39,9 +39,9 @@ module ActionDispatch::Routing
|
|
39
39
|
|
40
40
|
# Interal interface for routing extra actions.
|
41
41
|
def _route_extra_actions(actions, &block)
|
42
|
-
actions.each do |
|
43
|
-
|
44
|
-
public_send(m,
|
42
|
+
actions.each do |action, config|
|
43
|
+
config[:methods].each do |m|
|
44
|
+
public_send(m, config[:path], action: action, **(config[:kwargs] || {}))
|
45
45
|
end
|
46
46
|
yield if block_given?
|
47
47
|
end
|
@@ -51,8 +51,7 @@ module ActionDispatch::Routing
|
|
51
51
|
# @param default_singular [Boolean] the default plurality of the resource if the plurality is
|
52
52
|
# not otherwise defined by the controller
|
53
53
|
# @param name [Symbol] the resource name, from which path and controller are deduced by default
|
54
|
-
|
55
|
-
def _rest_resources(default_singular, name, skip_undefined: true, **kwargs, &block)
|
54
|
+
def _rest_resources(default_singular, name, **kwargs, &block)
|
56
55
|
controller = kwargs.delete(:controller) || name
|
57
56
|
if controller.is_a?(Class)
|
58
57
|
controller_class = controller
|
@@ -63,6 +62,9 @@ module ActionDispatch::Routing
|
|
63
62
|
# Set controller if it's not explicitly set.
|
64
63
|
kwargs[:controller] = name unless kwargs[:controller]
|
65
64
|
|
65
|
+
# Passing `unscoped: true` will prevent a nested resource from being scoped.
|
66
|
+
unscoped = kwargs.delete(:unscoped)
|
67
|
+
|
66
68
|
# Determine plural/singular resource.
|
67
69
|
force_singular = kwargs.delete(:force_singular)
|
68
70
|
force_plural = kwargs.delete(:force_plural)
|
@@ -78,24 +80,39 @@ module ActionDispatch::Routing
|
|
78
80
|
resource_method = singular ? :resource : :resources
|
79
81
|
|
80
82
|
# Call either `resource` or `resources`, passing appropriate modifiers.
|
81
|
-
|
82
|
-
skip = controller_class.get_skip_actions(skip_undefined: skip_undefined)
|
83
|
+
skip = RESTFramework::Utils.get_skipped_builtin_actions(controller_class)
|
83
84
|
public_send(resource_method, name, except: skip, **kwargs) do
|
84
85
|
if controller_class.respond_to?(:extra_member_actions)
|
85
86
|
member do
|
86
|
-
|
87
|
-
controller_class.extra_member_actions,
|
87
|
+
self._route_extra_actions(
|
88
|
+
RESTFramework::Utils.parse_extra_actions(controller_class.extra_member_actions),
|
88
89
|
)
|
89
|
-
self._route_extra_actions(actions)
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
93
|
collection do
|
94
|
-
|
95
|
-
self._route_extra_actions(
|
94
|
+
# Route extra controller-defined actions.
|
95
|
+
self._route_extra_actions(
|
96
|
+
RESTFramework::Utils.parse_extra_actions(controller_class.extra_actions),
|
97
|
+
)
|
98
|
+
|
99
|
+
# Route extra RRF-defined actions.
|
100
|
+
RESTFramework::RRF_BUILTIN_ACTIONS.each do |action, methods|
|
101
|
+
next unless controller_class.method_defined?(action)
|
102
|
+
|
103
|
+
[methods].flatten.each do |m|
|
104
|
+
public_send(m, "", action: action) if self.respond_to?(m)
|
105
|
+
end
|
106
|
+
end
|
96
107
|
end
|
97
108
|
|
98
|
-
|
109
|
+
if unscoped
|
110
|
+
yield if block_given?
|
111
|
+
else
|
112
|
+
scope(module: name, as: name) do
|
113
|
+
yield if block_given?
|
114
|
+
end
|
115
|
+
end
|
99
116
|
end
|
100
117
|
end
|
101
118
|
|
@@ -126,15 +143,39 @@ module ActionDispatch::Routing
|
|
126
143
|
# Set controller if it's not explicitly set.
|
127
144
|
kwargs[:controller] = name unless kwargs[:controller]
|
128
145
|
|
146
|
+
# Passing `unscoped: true` will prevent a nested resource from being scoped.
|
147
|
+
unscoped = kwargs.delete(:unscoped)
|
148
|
+
|
129
149
|
# Route actions using the resourceful router, but skip all builtin actions.
|
130
|
-
actions = RESTFramework::Utils.parse_extra_actions(controller_class.extra_actions)
|
131
150
|
public_send(:resource, name, only: [], **kwargs) do
|
132
151
|
# Route a root for this resource.
|
133
152
|
if route_root_to
|
134
153
|
get("", action: route_root_to)
|
135
154
|
end
|
136
155
|
|
137
|
-
|
156
|
+
collection do
|
157
|
+
# Route extra controller-defined actions.
|
158
|
+
self._route_extra_actions(
|
159
|
+
RESTFramework::Utils.parse_extra_actions(controller_class.extra_actions),
|
160
|
+
)
|
161
|
+
|
162
|
+
# Route extra RRF-defined actions.
|
163
|
+
RESTFramework::RRF_BUILTIN_ACTIONS.each do |action, methods|
|
164
|
+
next unless controller_class.method_defined?(action)
|
165
|
+
|
166
|
+
[methods].flatten.each do |m|
|
167
|
+
public_send(m, "", action: action) if self.respond_to?(m)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
if unscoped
|
173
|
+
yield if block_given?
|
174
|
+
else
|
175
|
+
scope(module: name, as: name) do
|
176
|
+
yield if block_given?
|
177
|
+
end
|
178
|
+
end
|
138
179
|
end
|
139
180
|
end
|
140
181
|
|
@@ -61,7 +61,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
61
61
|
@model ||= @object[0].class if
|
62
62
|
@many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
|
63
63
|
|
64
|
-
@model ||= @controller.get_model if @controller
|
64
|
+
@model ||= @controller.class.get_model if @controller
|
65
65
|
end
|
66
66
|
|
67
67
|
# Get controller action, if possible.
|
@@ -182,7 +182,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
182
182
|
cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: only, only: true)
|
183
183
|
elsif cfg[:except]
|
184
184
|
# For the `except` part of the serializer, we need to append any columns not in `only`.
|
185
|
-
model = @controller.get_model
|
185
|
+
model = @controller.class.get_model
|
186
186
|
except_cols = model&.column_names&.map(&:to_sym)&.reject { |c| c.in?(only) }
|
187
187
|
cfg[:except] = self.class.filter_subcfg(cfg[:except], fields: except_cols, add: true)
|
188
188
|
else
|
@@ -216,14 +216,24 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
216
216
|
if fields = @controller&.get_fields
|
217
217
|
fields = fields.deep_dup
|
218
218
|
|
219
|
+
columns = []
|
220
|
+
includes = []
|
221
|
+
methods = []
|
219
222
|
if @model
|
220
|
-
|
223
|
+
fields.each do |f|
|
224
|
+
if f.in?(@model.column_names)
|
225
|
+
columns << f
|
226
|
+
elsif @model.reflections.key?(f)
|
227
|
+
includes << f
|
228
|
+
elsif @model.method_defined?(f)
|
229
|
+
methods << f
|
230
|
+
end
|
231
|
+
end
|
221
232
|
else
|
222
233
|
columns = fields
|
223
|
-
methods = []
|
224
234
|
end
|
225
235
|
|
226
|
-
return {only: columns, methods: methods}
|
236
|
+
return {only: columns, include: includes, methods: methods}
|
227
237
|
end
|
228
238
|
|
229
239
|
# By default, pass an empty configuration, allowing the serialization of all columns.
|
data/lib/rest_framework/utils.rb
CHANGED
@@ -1,11 +1,9 @@
|
|
1
1
|
module RESTFramework::Utils
|
2
|
-
HTTP_METHOD_ORDERING = %w(GET POST PUT PATCH DELETE)
|
2
|
+
HTTP_METHOD_ORDERING = %w(GET POST PUT PATCH DELETE OPTIONS HEAD)
|
3
3
|
|
4
|
-
#
|
5
|
-
# `{paths:, methods:, kwargs:}`.
|
4
|
+
# Convert `extra_actions` hash to a consistent format: `{path:, methods:, kwargs:}`.
|
6
5
|
def self.parse_extra_actions(extra_actions)
|
7
|
-
return (extra_actions || {}).map
|
8
|
-
kwargs = {action: k}
|
6
|
+
return (extra_actions || {}).map { |k, v|
|
9
7
|
path = k
|
10
8
|
|
11
9
|
# Convert structure to path/methods/kwargs.
|
@@ -25,30 +23,38 @@ module RESTFramework::Utils
|
|
25
23
|
end
|
26
24
|
|
27
25
|
# Pass any further kwargs to the underlying Rails interface.
|
28
|
-
kwargs =
|
26
|
+
kwargs = v.presence&.except(:delegate)
|
29
27
|
elsif v.is_a?(Symbol) || v.is_a?(String)
|
30
28
|
methods = [v]
|
31
29
|
else
|
32
30
|
methods = v
|
33
31
|
end
|
34
32
|
|
35
|
-
|
36
|
-
|
33
|
+
[k, {path: path, methods: methods, kwargs: kwargs}.compact]
|
34
|
+
}.to_h
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get actions which should be skipped for a given controller.
|
38
|
+
def self.get_skipped_builtin_actions(controller_class)
|
39
|
+
return (
|
40
|
+
RESTFramework::BUILTIN_ACTIONS.keys + RESTFramework::BUILTIN_MEMBER_ACTIONS.keys
|
41
|
+
).reject do |action|
|
42
|
+
controller_class.method_defined?(action)
|
37
43
|
end
|
38
44
|
end
|
39
45
|
|
40
|
-
#
|
46
|
+
# Get the first route pattern which matches the given request.
|
41
47
|
def self.get_request_route(application_routes, request)
|
42
48
|
application_routes.router.recognize(request) { |route, _| return route }
|
43
49
|
end
|
44
50
|
|
45
|
-
#
|
46
|
-
#
|
51
|
+
# Normalize a path pattern by replacing URL params with generic placeholder, and removing the
|
52
|
+
# `(.:format)` at the end.
|
47
53
|
def self.comparable_path(path)
|
48
54
|
return path.gsub("(.:format)", "").gsub(/:[0-9A-Za-z_-]+/, ":x")
|
49
55
|
end
|
50
56
|
|
51
|
-
#
|
57
|
+
# Show routes under a controller action; used for the browsable API.
|
52
58
|
def self.get_routes(application_routes, request, current_route: nil)
|
53
59
|
current_route ||= self.get_request_route(application_routes, request)
|
54
60
|
current_path = current_route.path.spec.to_s.gsub("(.:format)", "")
|
data/lib/rest_framework.rb
CHANGED
@@ -1,12 +1,27 @@
|
|
1
1
|
module RESTFramework
|
2
|
+
BUILTIN_ACTIONS = {
|
3
|
+
index: :get,
|
4
|
+
new: :get,
|
5
|
+
create: :post,
|
6
|
+
}.freeze
|
7
|
+
BUILTIN_MEMBER_ACTIONS = {
|
8
|
+
show: :get,
|
9
|
+
edit: :get,
|
10
|
+
update: [:put, :patch],
|
11
|
+
destroy: :delete,
|
12
|
+
}.freeze
|
13
|
+
RRF_BUILTIN_ACTIONS = {
|
14
|
+
options: :options,
|
15
|
+
}.freeze
|
2
16
|
end
|
3
17
|
|
4
18
|
require_relative "rest_framework/controller_mixins"
|
5
19
|
require_relative "rest_framework/engine"
|
6
20
|
require_relative "rest_framework/errors"
|
7
21
|
require_relative "rest_framework/filters"
|
22
|
+
require_relative "rest_framework/generators"
|
8
23
|
require_relative "rest_framework/paginators"
|
9
24
|
require_relative "rest_framework/routers"
|
10
25
|
require_relative "rest_framework/serializers"
|
26
|
+
require_relative "rest_framework/utils"
|
11
27
|
require_relative "rest_framework/version"
|
12
|
-
require_relative "rest_framework/generators"
|
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.10
|
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-12-
|
11
|
+
date: 2022-12-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|