rest_framework 0.6.9 → 0.6.10
Sign up to get free protection for your applications and to get access to all the features.
- 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
|