rest_framework 1.0.1 → 1.1.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/README.md +31 -26
- data/VERSION +1 -1
- data/app/views/rest_framework/_head.html.erb +1 -6
- data/app/views/rest_framework/_routes_and_forms.html.erb +2 -5
- data/app/views/rest_framework/routes_and_forms/_raw_form.html.erb +1 -1
- data/lib/rest_framework/controller/bulk.rb +62 -0
- data/lib/rest_framework/controller/crud.rb +66 -0
- data/lib/rest_framework/controller/openapi.rb +249 -0
- data/lib/rest_framework/controller.rb +804 -0
- data/lib/rest_framework/engine.rb +12 -2
- data/lib/rest_framework/errors.rb +0 -1
- data/lib/rest_framework/filters/search_filter.rb +2 -2
- data/lib/rest_framework/mixins/base_controller_mixin.rb +3 -383
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +27 -68
- data/lib/rest_framework/mixins/model_controller_mixin.rb +60 -807
- data/lib/rest_framework/routers.rb +11 -6
- data/lib/rest_framework/serializers/native_serializer.rb +1 -1
- data/lib/rest_framework/utils.rb +17 -6
- data/lib/rest_framework.rb +12 -1
- metadata +6 -3
- data/lib/rest_framework/errors/unknown_model_error.rb +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebb2a69a51a24192200122bf40a9218e15e1f0d696bfdd63bb8f3aed9f43f82c
|
|
4
|
+
data.tar.gz: 3b630e5e5f8ec28096e245bc8ced22d5a63423d52799983543c1616b636999d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0eae7105c8fe37a710ac56c9ba0c4df9b0f0663883d117123af6e778735c63117ca58d82e60e260e353aefcc9536e996848341a6537ca963d764b498dfba979
|
|
7
|
+
data.tar.gz: 72e217a7d2c9bed976d6801f333be3a97ebdedf1998b07fa4a4594c48256919dcd1cd4b48030582fc75bdd9e2e6d002dc29df931e217ab78cd97b87872f65c78
|
data/README.md
CHANGED
|
@@ -38,10 +38,22 @@ bundle install
|
|
|
38
38
|
|
|
39
39
|
## Quick Usage Tutorial
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
To add REST framework features to a controller, include the `Controller` module:
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
```ruby
|
|
44
|
+
class ApiController < ApplicationController
|
|
45
|
+
include RESTFramework::Controller
|
|
46
|
+
|
|
47
|
+
# Here is where you can set configuration class attributes that will propagate to child
|
|
48
|
+
# controllers.
|
|
49
|
+
|
|
50
|
+
# Setting up a paginator class here makes more sense than defining it on every child controller.
|
|
51
|
+
self.paginator_class = RESTFramework::PageNumberPaginator
|
|
52
|
+
self.page_size = 30
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Here is what the directory structure might look like for resource controllers:
|
|
45
57
|
|
|
46
58
|
```text
|
|
47
59
|
controllers/
|
|
@@ -52,29 +64,18 @@ controllers/
|
|
|
52
64
|
└─ users_controller.rb
|
|
53
65
|
```
|
|
54
66
|
|
|
55
|
-
### Controller
|
|
56
|
-
|
|
57
|
-
The root `ApiController` can include any common behavior you want to share across all your API
|
|
58
|
-
controllers:
|
|
59
|
-
|
|
60
|
-
```ruby
|
|
61
|
-
class ApiController < ApplicationController
|
|
62
|
-
include RESTFramework::BaseControllerMixin
|
|
63
|
-
|
|
64
|
-
# Setting up a paginator class here makes more sense than defining it on every child controller.
|
|
65
|
-
self.paginator_class = RESTFramework::PageNumberPaginator
|
|
66
|
-
self.page_size = 30
|
|
67
|
-
end
|
|
68
|
-
```
|
|
67
|
+
### Root Controller
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
It is typically a good pattern for the root of your API to have a dedicated `Api::RootController`
|
|
70
|
+
outside the inheritance chain of your other API controllers, so that you can define actions on the
|
|
71
|
+
root without them propagating to child controllers, and so you can set global configuration on the
|
|
72
|
+
`ApiController`.
|
|
73
73
|
|
|
74
74
|
```ruby
|
|
75
75
|
class Api::RootController < ApiController
|
|
76
76
|
self.extra_actions = {test: :get}
|
|
77
77
|
|
|
78
|
+
# The root action is routed by `rest_root`.
|
|
78
79
|
def root
|
|
79
80
|
render(
|
|
80
81
|
api: {
|
|
@@ -94,17 +95,19 @@ class Api::RootController < ApiController
|
|
|
94
95
|
end
|
|
95
96
|
```
|
|
96
97
|
|
|
97
|
-
|
|
98
|
+
### Resource Controllers
|
|
99
|
+
|
|
100
|
+
Other API controllers can be associated to a resource/model by setting the `model` class attribute.
|
|
98
101
|
|
|
99
102
|
```ruby
|
|
100
103
|
class Api::MoviesController < ApiController
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
self.model = Movie # Automatically routes the standard CRUD actions for this controller.
|
|
105
|
+
self.bulk = true # Enables bulk create/update/destroy actions for this controller.
|
|
103
106
|
self.fields = [:id, :name, :release_date, :enabled]
|
|
104
107
|
self.extra_member_actions = {first: :get}
|
|
105
108
|
|
|
106
109
|
def first
|
|
107
|
-
# Always use
|
|
110
|
+
# Always use bang methods, since the framework will rescue `RecordNotFound` and return a
|
|
108
111
|
# sensible error response.
|
|
109
112
|
render(api: self.get_records.first!)
|
|
110
113
|
end
|
|
@@ -120,9 +123,11 @@ to include or exclude fields rather than defining them manually:
|
|
|
120
123
|
|
|
121
124
|
```ruby
|
|
122
125
|
class Api::UsersController < ApiController
|
|
123
|
-
include RESTFramework::ModelControllerMixin
|
|
124
|
-
|
|
125
126
|
self.fields = {include: [:calculated_popularity], exclude: [:impersonation_token]}
|
|
127
|
+
|
|
128
|
+
# You can even disable some of the builtin actions. For example, this effectively makes the
|
|
129
|
+
# resource read-only:
|
|
130
|
+
self.excluded_actions = [:create, :update, :destroy, :update_all, :destroy_all]
|
|
126
131
|
end
|
|
127
132
|
```
|
|
128
133
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0
|
|
1
|
+
1.1.0
|
|
@@ -291,14 +291,9 @@
|
|
|
291
291
|
|
|
292
292
|
// Replace the document when doing form submission (mainly to support PUT/PATCH/DELETE).
|
|
293
293
|
function rrfReplaceDocument(content) {
|
|
294
|
-
// Replace the document with provided content.
|
|
295
294
|
document.open()
|
|
296
295
|
document.write(content)
|
|
297
|
-
document.close()
|
|
298
|
-
|
|
299
|
-
// It seems that `DOMContentLoaded` is already triggered on `document.close()`.
|
|
300
|
-
// // Trigger `DOMContentLoaded` manually so our custom JavaScript works.
|
|
301
|
-
// // document.dispatchEvent(new Event("DOMContentLoaded", {bubbles: true, cancelable: true}))
|
|
296
|
+
document.close() // This also triggers `DOMContentLoaded`.
|
|
302
297
|
}
|
|
303
298
|
|
|
304
299
|
// Refresh the window as a `GET` request.
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
<%
|
|
2
|
-
@is_model_controller = controller.class.included_modules.include?(RESTFramework::ModelControllerMixin)
|
|
3
|
-
%>
|
|
4
1
|
<div class="row">
|
|
5
2
|
<div>
|
|
6
3
|
<ul class="nav nav-tabs">
|
|
@@ -25,7 +22,7 @@
|
|
|
25
22
|
</a>
|
|
26
23
|
</li>
|
|
27
24
|
<% end %>
|
|
28
|
-
<% if @_rrf_form_routes_html.present? &&
|
|
25
|
+
<% if @_rrf_form_routes_html.present? && controller.class.model %>
|
|
29
26
|
<li class="nav-item">
|
|
30
27
|
<a class="nav-link" href="#tabHtmlForm" data-bs-toggle="tab" role="tab">
|
|
31
28
|
HTML Form
|
|
@@ -43,7 +40,7 @@
|
|
|
43
40
|
<%= render partial: "rest_framework/routes_and_forms/raw_form" %>
|
|
44
41
|
</div>
|
|
45
42
|
<% end %>
|
|
46
|
-
<% if @_rrf_form_routes_html.present? &&
|
|
43
|
+
<% if @_rrf_form_routes_html.present? && controller.class.model %>
|
|
47
44
|
<div class="tab-pane fade" id="tabHtmlForm" role="tabpanel">
|
|
48
45
|
<%= render partial: "rest_framework/routes_and_forms/html_form" %>
|
|
49
46
|
</div>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
</label>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
32
|
-
<% if
|
|
32
|
+
<% if model = controller.class.model %>
|
|
33
33
|
<% if attachment_reflections = model.attachment_reflections.presence %>
|
|
34
34
|
<div class="mb-2" style="display: none" id="rawFilesFormWrapper">
|
|
35
35
|
<%= form_with(**{
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module RESTFramework::Controller
|
|
2
|
+
# Serialize the records, but also include any errors that might exist. This is used for bulk
|
|
3
|
+
# actions, however we include it here so the helper is available everywhere.
|
|
4
|
+
def bulk_serialize(records)
|
|
5
|
+
# This is kinda slow, so perhaps we should eventually integrate `errors` serialization into
|
|
6
|
+
# the serializer directly. This would fail for active model serializers, but maybe we don't
|
|
7
|
+
# care?
|
|
8
|
+
s = RESTFramework::Utils.wrap_ams(self.get_serializer_class)
|
|
9
|
+
records.map do |record|
|
|
10
|
+
s.new(record, controller: self).serialize.merge!({ errors: record.errors.presence }.compact)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Perform the `create` call, and return the collection of (possibly) created records.
|
|
15
|
+
def create_all!
|
|
16
|
+
create_data = self.get_create_params(bulk_mode: true)[:_json]
|
|
17
|
+
|
|
18
|
+
# Perform bulk create in a transaction.
|
|
19
|
+
ActiveRecord::Base.transaction { self.create_from.create(create_data) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def update_all
|
|
23
|
+
records = self.update_all!
|
|
24
|
+
serialized_records = self.bulk_serialize(records)
|
|
25
|
+
render(api: serialized_records)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Perform the `update` call and return the collection of (possibly) updated records.
|
|
29
|
+
def update_all!
|
|
30
|
+
pk = self.class.model.primary_key
|
|
31
|
+
data = if params[:_json].is_a?(Array)
|
|
32
|
+
self.get_create_params(bulk_mode: :update)[:_json].index_by { |r| r[pk] }
|
|
33
|
+
else
|
|
34
|
+
create_params = self.get_create_params
|
|
35
|
+
{ create_params[pk] => create_params }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Perform bulk update in a transaction.
|
|
39
|
+
ActiveRecord::Base.transaction { self.get_recordset.update(data.keys, data.values) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def destroy_all
|
|
43
|
+
if params[:_json].is_a?(Array)
|
|
44
|
+
records = self.destroy_all!
|
|
45
|
+
serialized_records = self.bulk_serialize(records)
|
|
46
|
+
return render(api: serialized_records)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
render(
|
|
50
|
+
api: { message: "Bulk destroy requires an array of primary keys as input." }, status: 400,
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Perform the `destroy!` call and return the destroyed (and frozen) record.
|
|
55
|
+
def destroy_all!
|
|
56
|
+
pk = self.class.model.primary_key
|
|
57
|
+
destroy_data = self.request.request_parameters[:_json]
|
|
58
|
+
|
|
59
|
+
# Perform bulk destroy in a transaction.
|
|
60
|
+
ActiveRecord::Base.transaction { self.get_recordset.where(pk => destroy_data).destroy_all }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module RESTFramework::Controller
|
|
2
|
+
def create
|
|
3
|
+
# Bulk create: if `bulk` is enabled and the request body is an array, delegate to `create_all!`.
|
|
4
|
+
if self.class.bulk && params[:_json].is_a?(Array)
|
|
5
|
+
records = self.create_all!
|
|
6
|
+
return render(api: self.bulk_serialize(records))
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
render(api: self.create!, status: :created)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Perform the `create!` call and return the created record.
|
|
13
|
+
def create!
|
|
14
|
+
self.create_from.create!(self.get_create_params)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def index
|
|
18
|
+
render(api: self.get_index_records)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get records with both filtering and pagination applied.
|
|
22
|
+
def get_index_records
|
|
23
|
+
records = self.get_records
|
|
24
|
+
|
|
25
|
+
# Handle pagination, if enabled.
|
|
26
|
+
if paginator_class = self.class.paginator_class
|
|
27
|
+
# Paginate if there is a `max_page_size`, or if there is no `page_size_query_param`, or if the
|
|
28
|
+
# page size is not set to "0".
|
|
29
|
+
max_page_size = self.class.max_page_size
|
|
30
|
+
page_size_query_param = self.class.page_size_query_param
|
|
31
|
+
if max_page_size || !page_size_query_param || params[page_size_query_param] != "0"
|
|
32
|
+
paginator = paginator_class.new(data: records, controller: self)
|
|
33
|
+
page = paginator.get_page
|
|
34
|
+
serialized_page = self.serialize(page)
|
|
35
|
+
return paginator.get_paginated_response(serialized_page)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
records
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def show
|
|
43
|
+
render(api: self.get_record)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def update
|
|
47
|
+
render(api: self.update!)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Perform the `update!` call and return the updated record.
|
|
51
|
+
def update!
|
|
52
|
+
record = self.get_record
|
|
53
|
+
record.update!(self.get_update_params)
|
|
54
|
+
record
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def destroy
|
|
58
|
+
self.destroy!
|
|
59
|
+
render(api: "")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Perform the `destroy!` call and return the destroyed (and frozen) record.
|
|
63
|
+
def destroy!
|
|
64
|
+
self.get_record.destroy!
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
module RESTFramework::Controller
|
|
2
|
+
module ClassMethods
|
|
3
|
+
def openapi_response_content_types
|
|
4
|
+
@openapi_response_content_types ||= [
|
|
5
|
+
"text/html",
|
|
6
|
+
self.serialize_to_json ? "application/json" : nil,
|
|
7
|
+
self.serialize_to_xml ? "application/xml" : nil,
|
|
8
|
+
].compact
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def openapi_request_content_types
|
|
12
|
+
@openapi_request_content_types ||= [
|
|
13
|
+
"application/json",
|
|
14
|
+
"application/x-www-form-urlencoded",
|
|
15
|
+
"multipart/form-data",
|
|
16
|
+
]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def openapi_paths(routes, tag)
|
|
20
|
+
resp_cts = self.openapi_response_content_types
|
|
21
|
+
req_cts = self.openapi_request_content_types
|
|
22
|
+
schema_name = self.openapi_schema_name if self.model
|
|
23
|
+
|
|
24
|
+
routes.group_by { |r| r[:concat_path] }.map { |concat_path, routes|
|
|
25
|
+
[
|
|
26
|
+
concat_path.gsub(/:([0-9A-Za-z_-]+)/, "{\\1}"),
|
|
27
|
+
routes.map { |route|
|
|
28
|
+
metadata = RESTFramework::ROUTE_METADATA[route[:path]] || {}
|
|
29
|
+
summary = metadata.delete(:label).presence || self.label_for(route[:action])
|
|
30
|
+
description = metadata.delete(:description).presence
|
|
31
|
+
extra_action = RESTFramework::EXTRA_ACTION_ROUTES.include?(route[:path])
|
|
32
|
+
error_response = { "$ref" => "#/components/responses/BadRequest" }
|
|
33
|
+
not_found_response = { "$ref" => "#/components/responses/NotFound" }
|
|
34
|
+
spec = { tags: [ tag ], summary: summary, description: description }.compact
|
|
35
|
+
|
|
36
|
+
# All routes should have a successful response.
|
|
37
|
+
success_code = if !extra_action
|
|
38
|
+
if route[:action] == "create"
|
|
39
|
+
201
|
|
40
|
+
elsif route[:action] == "destroy"
|
|
41
|
+
204
|
|
42
|
+
else
|
|
43
|
+
200
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
200
|
|
47
|
+
end
|
|
48
|
+
spec[:responses] = {
|
|
49
|
+
success_code => {
|
|
50
|
+
content: resp_cts.map { |ct|
|
|
51
|
+
[
|
|
52
|
+
ct,
|
|
53
|
+
(self.model && !extra_action && route[:verb] != "OPTIONS") ? {
|
|
54
|
+
schema: { "$ref" => "#/components/schemas/#{schema_name}" },
|
|
55
|
+
} : {},
|
|
56
|
+
]
|
|
57
|
+
}.to_h,
|
|
58
|
+
description: "Success",
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Builtin POST, PUT, PATCH, and DELETE should have a 400 and 404 response.
|
|
63
|
+
if route[:verb].in?([ "POST", "PUT", "PATCH", "DELETE" ]) && !extra_action
|
|
64
|
+
spec[:responses][400] = error_response
|
|
65
|
+
spec[:responses][404] = not_found_response
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# All POST, PUT, PATCH should have a request body.
|
|
69
|
+
if route[:verb].in?([ "POST", "PUT", "PATCH" ])
|
|
70
|
+
spec[:requestBody] ||= {
|
|
71
|
+
content: req_cts.map { |ct|
|
|
72
|
+
[
|
|
73
|
+
ct,
|
|
74
|
+
(self.model && !extra_action) ? {
|
|
75
|
+
schema: { "$ref" => "#/components/schemas/#{schema_name}" },
|
|
76
|
+
} : {},
|
|
77
|
+
]
|
|
78
|
+
}.to_h,
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Add remaining metadata as an extension.
|
|
83
|
+
spec["x-rrf-metadata"] = metadata if metadata.present?
|
|
84
|
+
|
|
85
|
+
next route[:verb].downcase, spec
|
|
86
|
+
}.to_h.merge(
|
|
87
|
+
{
|
|
88
|
+
parameters: routes.first[:route].required_parts.map { |p|
|
|
89
|
+
{
|
|
90
|
+
name: p,
|
|
91
|
+
in: "path",
|
|
92
|
+
required: true,
|
|
93
|
+
schema: { type: "integer" },
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
),
|
|
98
|
+
]
|
|
99
|
+
}.to_h
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def openapi_document(request, route_group_name, routes)
|
|
103
|
+
server = request.base_url + request.original_fullpath.gsub(/\?.*/, "")
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
openapi: "3.1.1",
|
|
107
|
+
info: {
|
|
108
|
+
title: self.get_title,
|
|
109
|
+
description: self.description,
|
|
110
|
+
version: self.version.to_s,
|
|
111
|
+
}.compact,
|
|
112
|
+
servers: [ { url: server } ],
|
|
113
|
+
paths: self.openapi_paths(routes, route_group_name),
|
|
114
|
+
tags: [ { name: route_group_name, description: self.description }.compact ],
|
|
115
|
+
components: {
|
|
116
|
+
schemas: {
|
|
117
|
+
"Error" => {
|
|
118
|
+
type: "object",
|
|
119
|
+
required: [ "message" ],
|
|
120
|
+
properties: {
|
|
121
|
+
message: { type: "string" },
|
|
122
|
+
errors: { type: "object" },
|
|
123
|
+
exception: { type: "string" },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}.merge(self.model ? { self.openapi_schema_name => self.openapi_schema } : {}),
|
|
127
|
+
responses: {
|
|
128
|
+
"BadRequest": {
|
|
129
|
+
description: "Bad Request",
|
|
130
|
+
content: self.openapi_response_content_types.map { |ct|
|
|
131
|
+
[
|
|
132
|
+
ct,
|
|
133
|
+
ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } },
|
|
134
|
+
]
|
|
135
|
+
}.to_h,
|
|
136
|
+
},
|
|
137
|
+
"NotFound": {
|
|
138
|
+
description: "Not Found",
|
|
139
|
+
content: self.openapi_response_content_types.map { |ct|
|
|
140
|
+
[
|
|
141
|
+
ct,
|
|
142
|
+
ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } },
|
|
143
|
+
]
|
|
144
|
+
}.to_h,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}.merge(self.model ? {
|
|
149
|
+
"x-rrf-primary_key" => self.model.primary_key,
|
|
150
|
+
"x-rrf-callbacks" => self._process_action_callbacks.as_json,
|
|
151
|
+
|
|
152
|
+
# While bulk update/destroy are obvious because they create new router endpoints, bulk
|
|
153
|
+
# create overloads the existing collection `POST` endpoint, so we add a special key to the
|
|
154
|
+
# OpenAPI metadata to indicate bulk create is supported.
|
|
155
|
+
"x-rrf-bulk-create": self.bulk,
|
|
156
|
+
} : {}).compact
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Only for model controllers.
|
|
160
|
+
def openapi_schema
|
|
161
|
+
return @openapi_schema if @openapi_schema
|
|
162
|
+
|
|
163
|
+
field_configuration = self.field_configuration
|
|
164
|
+
@openapi_schema = {
|
|
165
|
+
required: field_configuration.select { |_, cfg| cfg[:required] }.keys,
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: field_configuration.map { |f, cfg|
|
|
168
|
+
v = { title: cfg[:label] }
|
|
169
|
+
|
|
170
|
+
if cfg[:kind] == "association"
|
|
171
|
+
v[:type] = cfg[:reflection].collection? ? "array" : "object"
|
|
172
|
+
elsif cfg[:kind] == "rich_text"
|
|
173
|
+
v[:type] = "string"
|
|
174
|
+
v[:"x-rrf-rich_text"] = true
|
|
175
|
+
elsif cfg[:kind] == "attachment"
|
|
176
|
+
v[:type] = "string"
|
|
177
|
+
v[:"x-rrf-attachment"] = cfg[:attachment_type]
|
|
178
|
+
else
|
|
179
|
+
v[:type] = cfg[:type]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
v[:readOnly] = true if cfg[:read_only]
|
|
183
|
+
v[:default] = cfg[:default] if cfg.key?(:default)
|
|
184
|
+
|
|
185
|
+
if enum_variants = cfg[:enum_variants]
|
|
186
|
+
v[:enum] = enum_variants.keys
|
|
187
|
+
v[:"x-rrf-enum_variants"] = enum_variants
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
if validators = cfg[:validators]
|
|
191
|
+
v[:"x-rrf-validators"] = validators
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
v[:"x-rrf-kind"] = cfg[:kind] if cfg[:kind]
|
|
195
|
+
|
|
196
|
+
if cfg[:reflection]
|
|
197
|
+
v[:"x-rrf-reflection"] = {
|
|
198
|
+
class_name: cfg[:reflection].class_name,
|
|
199
|
+
foreign_key: cfg[:reflection].foreign_key,
|
|
200
|
+
association_foreign_key: cfg[:reflection].association_foreign_key,
|
|
201
|
+
association_primary_key: cfg[:reflection].association_primary_key,
|
|
202
|
+
inverse_of: cfg[:reflection].inverse_of&.name,
|
|
203
|
+
join_table: cfg[:reflection].join_table,
|
|
204
|
+
}.compact
|
|
205
|
+
v[:"x-rrf-association_pk"] = cfg[:association_pk]
|
|
206
|
+
v[:"x-rrf-sub_fields"] = cfg[:sub_fields]
|
|
207
|
+
v[:"x-rrf-sub_fields_metadata"] = cfg[:sub_fields_metadata]
|
|
208
|
+
v[:"x-rrf-id_field"] = cfg[:id_field]
|
|
209
|
+
v[:"x-rrf-nested_attributes_options"] = cfg[:nested_attributes_options]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
next [ f, v ]
|
|
213
|
+
}.to_h,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@openapi_schema
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Only for model controllers.
|
|
220
|
+
def openapi_schema_name
|
|
221
|
+
@openapi_schema_name ||= self.name.chomp("Controller").gsub("::", ".")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def openapi_document
|
|
226
|
+
first, *rest = self.route_groups.to_a
|
|
227
|
+
document = self.class.openapi_document(request, *first)
|
|
228
|
+
|
|
229
|
+
if self.class.openapi_include_children
|
|
230
|
+
rest.each do |route_group_name, routes|
|
|
231
|
+
controller = "#{routes.first[:route].defaults[:controller]}_controller".camelize.constantize
|
|
232
|
+
child_document = controller.openapi_document(request, route_group_name, routes)
|
|
233
|
+
|
|
234
|
+
# Merge child paths and tags into the parent document.
|
|
235
|
+
document[:paths].merge!(child_document[:paths])
|
|
236
|
+
document[:tags] += child_document[:tags]
|
|
237
|
+
|
|
238
|
+
# If the child document has schemas, merge them into the parent document.
|
|
239
|
+
if schemas = child_document.dig(:components, :schemas) # rubocop:disable Style/Next
|
|
240
|
+
document[:components] ||= {}
|
|
241
|
+
document[:components][:schemas] ||= {}
|
|
242
|
+
document[:components][:schemas].merge!(schemas)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
document
|
|
248
|
+
end
|
|
249
|
+
end
|