rest_framework 1.0.2 → 1.2.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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -26
  3. data/VERSION +1 -1
  4. data/app/views/rest_framework/_routes_and_forms.html.erb +2 -5
  5. data/app/views/rest_framework/routes_and_forms/_html_form.html.erb +1 -1
  6. data/app/views/rest_framework/routes_and_forms/_raw_form.html.erb +1 -1
  7. data/lib/rest_framework/controller/bulk.rb +272 -0
  8. data/lib/rest_framework/controller/crud.rb +65 -0
  9. data/lib/rest_framework/controller/openapi.rb +252 -0
  10. data/lib/rest_framework/controller.rb +839 -0
  11. data/lib/rest_framework/engine.rb +12 -2
  12. data/lib/rest_framework/errors.rb +53 -4
  13. data/lib/rest_framework/filters/ordering_filter.rb +0 -1
  14. data/lib/rest_framework/filters/query_filter.rb +7 -2
  15. data/lib/rest_framework/filters/search_filter.rb +5 -5
  16. data/lib/rest_framework/mixins/base_controller_mixin.rb +3 -383
  17. data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +27 -68
  18. data/lib/rest_framework/mixins/model_controller_mixin.rb +60 -807
  19. data/lib/rest_framework/paginators/page_number_paginator.rb +10 -11
  20. data/lib/rest_framework/routers.rb +20 -9
  21. data/lib/rest_framework/serializers/native_serializer.rb +5 -3
  22. data/lib/rest_framework/utils.rb +24 -6
  23. data/lib/rest_framework.rb +13 -5
  24. metadata +6 -7
  25. data/lib/rest_framework/errors/base_error.rb +0 -5
  26. data/lib/rest_framework/errors/nil_passed_to_render_api_error.rb +0 -14
  27. data/lib/rest_framework/errors/unknown_model_error.rb +0 -18
  28. data/lib/rest_framework/generators/controller_generator.rb +0 -64
  29. data/lib/rest_framework/generators.rb +0 -4
@@ -0,0 +1,252 @@
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
+ ref = cfg[:reflection]
198
+ v[:"x-rrf-reflection"] = {
199
+ class_name: ref.respond_to?(:class_name) ? ref.class_name : nil,
200
+ foreign_key: ref.respond_to?(:foreign_key) ? ref.foreign_key : nil,
201
+ association_foreign_key: ref.respond_to?(:association_foreign_key) ?
202
+ ref.association_foreign_key : nil,
203
+ association_primary_key: ref.respond_to?(:association_primary_key) ?
204
+ ref.association_primary_key : nil,
205
+ inverse_of: ref.respond_to?(:inverse_of) ? ref.inverse_of&.name : nil,
206
+ join_table: ref.respond_to?(:join_table) ? ref.join_table : nil,
207
+ }.compact
208
+ v[:"x-rrf-association_pk"] = cfg[:association_pk]
209
+ v[:"x-rrf-sub_fields"] = cfg[:sub_fields]
210
+ v[:"x-rrf-sub_fields_metadata"] = cfg[:sub_fields_metadata]
211
+ v[:"x-rrf-id_field"] = cfg[:id_field]
212
+ v[:"x-rrf-nested_attributes_options"] = cfg[:nested_attributes_options]
213
+ end
214
+
215
+ next [ f, v ]
216
+ }.to_h,
217
+ }
218
+
219
+ @openapi_schema
220
+ end
221
+
222
+ # Only for model controllers.
223
+ def openapi_schema_name
224
+ @openapi_schema_name ||= self.name.chomp("Controller").gsub("::", ".")
225
+ end
226
+ end
227
+
228
+ def openapi_document
229
+ first, *rest = self.route_groups.to_a
230
+ document = self.class.openapi_document(request, *first)
231
+
232
+ if self.class.openapi_include_children
233
+ rest.each do |route_group_name, routes|
234
+ controller = "#{routes.first[:route].defaults[:controller]}_controller".camelize.constantize
235
+ child_document = controller.openapi_document(request, route_group_name, routes)
236
+
237
+ # Merge child paths and tags into the parent document.
238
+ document[:paths].merge!(child_document[:paths])
239
+ document[:tags] += child_document[:tags]
240
+
241
+ # If the child document has schemas, merge them into the parent document.
242
+ if schemas = child_document.dig(:components, :schemas) # rubocop:disable Style/Next
243
+ document[:components] ||= {}
244
+ document[:components][:schemas] ||= {}
245
+ document[:components][:schemas].merge!(schemas)
246
+ end
247
+ end
248
+ end
249
+
250
+ document
251
+ end
252
+ end