manageiq-api-common 0.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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.md +62 -0
  4. data/Rakefile +18 -0
  5. data/app/models/authentication.rb +19 -0
  6. data/app/models/concerns/encryption_concern.rb +52 -0
  7. data/app/models/encryption.rb +13 -0
  8. data/lib/generators/shared_utilities/migration_generator.rb +79 -0
  9. data/lib/generators/shared_utilities/orm_helper.rb +25 -0
  10. data/lib/generators/shared_utilities/templates/migration.rb +27 -0
  11. data/lib/generators/shared_utilities/templates/migration_existing.rb +28 -0
  12. data/lib/manageiq-api-common.rb +1 -0
  13. data/lib/manageiq/api/common.rb +13 -0
  14. data/lib/manageiq/api/common/api_error.rb +21 -0
  15. data/lib/manageiq/api/common/application_controller_mixins/api_doc.rb +39 -0
  16. data/lib/manageiq/api/common/application_controller_mixins/common.rb +27 -0
  17. data/lib/manageiq/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
  18. data/lib/manageiq/api/common/application_controller_mixins/parameters.rb +113 -0
  19. data/lib/manageiq/api/common/application_controller_mixins/request_body_validation.rb +61 -0
  20. data/lib/manageiq/api/common/application_controller_mixins/request_path.rb +75 -0
  21. data/lib/manageiq/api/common/engine.rb +20 -0
  22. data/lib/manageiq/api/common/entitlement.rb +35 -0
  23. data/lib/manageiq/api/common/error_document.rb +29 -0
  24. data/lib/manageiq/api/common/filter.rb +160 -0
  25. data/lib/manageiq/api/common/graphql.rb +117 -0
  26. data/lib/manageiq/api/common/graphql/associated_records.rb +44 -0
  27. data/lib/manageiq/api/common/graphql/association_loader.rb +35 -0
  28. data/lib/manageiq/api/common/graphql/generator.rb +149 -0
  29. data/lib/manageiq/api/common/graphql/templates/model_type.erb +35 -0
  30. data/lib/manageiq/api/common/graphql/templates/query_type.erb +47 -0
  31. data/lib/manageiq/api/common/graphql/templates/schema.erb +6 -0
  32. data/lib/manageiq/api/common/graphql/types/big_int.rb +23 -0
  33. data/lib/manageiq/api/common/graphql/types/date_time.rb +16 -0
  34. data/lib/manageiq/api/common/graphql/types/query_filter.rb +16 -0
  35. data/lib/manageiq/api/common/inflections.rb +28 -0
  36. data/lib/manageiq/api/common/logging.rb +17 -0
  37. data/lib/manageiq/api/common/metrics.rb +39 -0
  38. data/lib/manageiq/api/common/middleware/web_server_metrics.rb +62 -0
  39. data/lib/manageiq/api/common/open_api.rb +2 -0
  40. data/lib/manageiq/api/common/open_api/docs.rb +54 -0
  41. data/lib/manageiq/api/common/open_api/docs/component_collection.rb +67 -0
  42. data/lib/manageiq/api/common/open_api/docs/doc_v3.rb +92 -0
  43. data/lib/manageiq/api/common/open_api/docs/object_definition.rb +27 -0
  44. data/lib/manageiq/api/common/open_api/generator.rb +441 -0
  45. data/lib/manageiq/api/common/open_api/serializer.rb +31 -0
  46. data/lib/manageiq/api/common/option_redirect_enhancements.rb +23 -0
  47. data/lib/manageiq/api/common/paginated_response.rb +92 -0
  48. data/lib/manageiq/api/common/request.rb +107 -0
  49. data/lib/manageiq/api/common/routing.rb +26 -0
  50. data/lib/manageiq/api/common/user.rb +48 -0
  51. data/lib/manageiq/api/common/version.rb +7 -0
  52. data/lib/tasks/manageiq/api/common_tasks.rake +4 -0
  53. data/spec/support/default_as_json.rb +17 -0
  54. data/spec/support/requests_spec_helper.rb +7 -0
  55. data/spec/support/user_header_spec_helper.rb +62 -0
  56. metadata +375 -0
@@ -0,0 +1,2 @@
1
+ require "manageiq/api/common/open_api/docs"
2
+ require "manageiq/api/common/open_api/serializer"
@@ -0,0 +1,54 @@
1
+ require "manageiq/api/common/open_api/docs/component_collection"
2
+ require "manageiq/api/common/open_api/docs/object_definition"
3
+ require "manageiq/api/common/open_api/docs/doc_v3"
4
+
5
+ module ManageIQ
6
+ module API
7
+ module Common
8
+ module OpenApi
9
+ class Docs
10
+ def self.instance
11
+ @instance ||= new(Dir.glob(Rails.root.join("public", "doc", "openapi*.json")))
12
+ end
13
+
14
+ def initialize(glob)
15
+ @cache = {}
16
+ glob.each { |f| load_file(f) }
17
+ end
18
+
19
+ def load_file(file)
20
+ openapi_spec = JSON.parse(File.read(file))
21
+ store_doc(DocV3.new(openapi_spec))
22
+ end
23
+
24
+ def store_doc(doc)
25
+ update_doc_for_version(doc, doc.version.segments[0..1].join("."))
26
+ update_doc_for_version(doc, doc.version.segments.first.to_s)
27
+ end
28
+
29
+ def update_doc_for_version(doc, version)
30
+ if @cache[version].nil?
31
+ @cache[version] = doc
32
+ else
33
+ existing_version = @cache[version].version
34
+ @cache[version] = doc if doc.version > existing_version
35
+ end
36
+ end
37
+
38
+ def [](version)
39
+ @cache[version]
40
+ end
41
+
42
+ def routes
43
+ @routes ||= begin
44
+ @cache.each_with_object([]) do |(version, doc), routes|
45
+ next unless /\d+\.\d+/ =~ version # Skip unless major.minor
46
+ routes.concat(doc.routes)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,67 @@
1
+ module ManageIQ
2
+ module API
3
+ module Common
4
+ module OpenApi
5
+ class Docs
6
+ class ComponentCollection < Hash
7
+ attr_reader :doc
8
+
9
+ def initialize(doc, category)
10
+ @doc = doc
11
+ @category = category
12
+ end
13
+
14
+ def [](name)
15
+ super || load_definition(name)
16
+ end
17
+
18
+ def load_definition(name)
19
+ raw_definition = @doc.content.fetch_path(*@category.split("/"), name)
20
+ raise ArgumentError, "Failed to find definition for #{name}" unless raw_definition.kind_of?(Hash)
21
+
22
+ definition = substitute_regexes(raw_definition)
23
+ definition = substitute_references(definition)
24
+ self[name] = ::ManageIQ::API::Common::OpenApi::Docs::ObjectDefinition.new.replace(definition)
25
+ end
26
+
27
+ private
28
+
29
+ def substitute_references(object)
30
+ if object.kind_of?(Array)
31
+ object.collect { |i| substitute_references(i) }
32
+ elsif object.kind_of?(Hash)
33
+ return fetch_ref_value(object["$ref"]) if object.keys == ["$ref"]
34
+ object.each { |k, v| object[k] = substitute_references(v) }
35
+ else
36
+ object
37
+ end
38
+ end
39
+
40
+ def fetch_ref_value(ref_path)
41
+ ref_paths = ref_path.split("/")
42
+ property = ref_paths.last
43
+ section = ref_paths[1..-2]
44
+ public_send(:[], property)
45
+ end
46
+
47
+ def substitute_regexes(object)
48
+ if object.kind_of?(Array)
49
+ object.collect { |i| substitute_regexes(i) }
50
+ elsif object.kind_of?(Hash)
51
+ object.each_with_object({}) do |(k, v), o|
52
+ o[k] = k == "pattern" ? regexp_from_pattern(v) : substitute_regexes(v)
53
+ end
54
+ else
55
+ object
56
+ end
57
+ end
58
+
59
+ def regexp_from_pattern(pattern)
60
+ Regexp.new(pattern)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,92 @@
1
+ require "more_core_extensions/core_ext/hash"
2
+ require "more_core_extensions/core_ext/array"
3
+ require "openapi_parser"
4
+
5
+ module ManageIQ
6
+ module API
7
+ module Common
8
+ module OpenApi
9
+ class Docs
10
+ class DocV3
11
+ attr_reader :content
12
+
13
+ def initialize(content)
14
+ spec_version = content["openapi"]
15
+ raise "Unsupported OpenAPI Specification version #{spec_version}" unless spec_version =~ /\A3\..*\z/
16
+
17
+ @content = content
18
+ end
19
+
20
+ # Validates data types against OpenAPI schema
21
+ #
22
+ # @param http_method [String] POST/PATCH/...
23
+ # @param request_path [String] i.e. /api/sources/v1.0/sources
24
+ # @param api_version [String] i.e. "v1.0", has to be part of **request_path**
25
+ # @param payload [String] JSON if payload_content_type == 'application/json'
26
+ # @param payload_content_type [String]
27
+ #
28
+ # @raise OpenAPIParser::OpenAPIError
29
+ def validate!(http_method, request_path, api_version, payload, payload_content_type = 'application/json')
30
+ path = request_path.split(api_version)[1]
31
+ raise "API version not found in request_path" if path.nil?
32
+
33
+ request_operation = validator_doc.request_operation(http_method.to_s.downcase, path)
34
+ request_operation.validate_request_body(payload_content_type, payload)
35
+ end
36
+
37
+ def version
38
+ @version ||= Gem::Version.new(content.fetch_path("info", "version"))
39
+ end
40
+
41
+ def parameters
42
+ @parameters ||= ::ManageIQ::API::Common::OpenApi::Docs::ComponentCollection.new(self, "components/parameters")
43
+ end
44
+
45
+ def schemas
46
+ @schemas ||= ::ManageIQ::API::Common::OpenApi::Docs::ComponentCollection.new(self, "components/schemas")
47
+ end
48
+
49
+ def definitions
50
+ schemas
51
+ end
52
+
53
+ def example_attributes(key)
54
+ schemas[key]["properties"].each_with_object({}) do |(col, stuff), hash|
55
+ hash[col] = stuff["example"] if stuff.key?("example")
56
+ end
57
+ end
58
+
59
+ def base_path
60
+ @base_path ||= @content.fetch_path("servers", 0, "variables", "basePath", "default")
61
+ end
62
+
63
+ def paths
64
+ @content["paths"]
65
+ end
66
+
67
+ def to_json(options = nil)
68
+ content.to_json(options)
69
+ end
70
+
71
+ def routes
72
+ @routes ||= begin
73
+ paths.flat_map do |path, hash|
74
+ hash.collect do |verb, _details|
75
+ p = File.join(base_path, path).gsub(/{\w*}/, ":id")
76
+ {:path => p, :verb => verb.upcase}
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def validator_doc(opts = { :coerce_value => true, :datetime_coerce_class => DateTime })
85
+ @validator_doc ||= ::OpenAPIParser.parse(content, opts)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,27 @@
1
+ module ManageIQ
2
+ module API
3
+ module Common
4
+ module OpenApi
5
+ class Docs
6
+ class ObjectDefinition < Hash
7
+ def all_attributes
8
+ properties.keys
9
+ end
10
+
11
+ def read_only_attributes
12
+ properties.select { |k, v| v["readOnly"] == true }.keys
13
+ end
14
+
15
+ def required_attributes
16
+ self["required"]
17
+ end
18
+
19
+ def properties
20
+ self["properties"]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,441 @@
1
+ module ManageIQ
2
+ module API
3
+ module Common
4
+ module OpenApi
5
+ class Generator
6
+ require 'json'
7
+ require 'manageiq/api/common/graphql'
8
+
9
+ PARAMETERS_PATH = "/components/parameters".freeze
10
+ SCHEMAS_PATH = "/components/schemas".freeze
11
+
12
+ def path_parts(openapi_path)
13
+ openapi_path.split("/")[1..-1]
14
+ end
15
+
16
+ # Let's get the latest api version based on the openapi.json routes
17
+ def api_version
18
+ @api_version ||= Rails.application.routes.routes.each_with_object([]) do |route, array|
19
+ matches = ActionDispatch::Routing::RouteWrapper
20
+ .new(route)
21
+ .path.match(/\A.*\/v(\d+.\d+)\/openapi.json.*\z/)
22
+ array << matches[1] if matches
23
+ end.max
24
+ end
25
+
26
+ def rails_routes
27
+ Rails.application.routes.routes.each_with_object([]) do |route, array|
28
+ r = ActionDispatch::Routing::RouteWrapper.new(route)
29
+ next if r.internal? # Don't display rails routes
30
+ next if r.engine? # Don't care right now...
31
+
32
+ array << r
33
+ end
34
+ end
35
+
36
+ def openapi_file
37
+ @openapi_file ||= Rails.root.join("public", "doc", "openapi-3-v#{api_version}.0.json").to_s
38
+ end
39
+
40
+ def openapi_contents
41
+ @openapi_contents ||= begin
42
+ JSON.parse(File.read(openapi_file))
43
+ end
44
+ end
45
+
46
+ def initialize
47
+ app_prefix, app_name = base_path.match(/\A(.*)\/(.*)\/v\d+.\d+\z/).captures
48
+ ENV['APP_NAME'] = app_name
49
+ ENV['PATH_PREFIX'] = app_prefix
50
+ Rails.application.reload_routes!
51
+ end
52
+
53
+ def base_path
54
+ openapi_contents["servers"].first["variables"]["basePath"]["default"]
55
+ end
56
+
57
+ def applicable_rails_routes
58
+ rails_routes.select { |i| i.path.start_with?(base_path) }
59
+ end
60
+
61
+ def schemas
62
+ @schemas ||= {
63
+ "CollectionLinks" => {
64
+ "type" => "object",
65
+ "properties" => {
66
+ "first" => {
67
+ "type" => "string"
68
+ },
69
+ "last" => {
70
+ "type" => "string"
71
+ },
72
+ "next" => {
73
+ "type" => "string"
74
+ },
75
+ "prev" => {
76
+ "type" => "string"
77
+ },
78
+ }
79
+ },
80
+ "CollectionMetadata" => {
81
+ "type" => "object",
82
+ "properties" => {
83
+ "count" => {
84
+ "type" => "integer"
85
+ },
86
+ "limit" => {
87
+ "type" => "integer"
88
+ },
89
+ "offset" => {
90
+ "type" => "integer"
91
+ }
92
+ }
93
+ },
94
+ "ID" => {
95
+ "type" => "string",
96
+ "description" => "ID of the resource",
97
+ "pattern" => "^\\d+$",
98
+ "readOnly" => true,
99
+ },
100
+ }
101
+ end
102
+
103
+ def build_schema(klass_name)
104
+ schemas[klass_name] = openapi_schema(klass_name)
105
+ "##{SCHEMAS_PATH}/#{klass_name}"
106
+ end
107
+
108
+ def parameters
109
+ @parameters ||= {
110
+ "QueryFilter" => {
111
+ "in" => "query",
112
+ "name" => "filter",
113
+ "description" => "Filter for querying collections.",
114
+ "required" => false,
115
+ "style" => "deepObject",
116
+ "explode" => true,
117
+ "schema" => {
118
+ "type" => "object"
119
+ }
120
+ },
121
+ "QueryLimit" => {
122
+ "in" => "query",
123
+ "name" => "limit",
124
+ "description" => "The numbers of items to return per page.",
125
+ "required" => false,
126
+ "schema" => {
127
+ "type" => "integer",
128
+ "minimum" => 1,
129
+ "maximum" => 1000,
130
+ "default" => 100
131
+ }
132
+ },
133
+ "QueryOffset" => {
134
+ "in" => "query",
135
+ "name" => "offset",
136
+ "description" => "The number of items to skip before starting to collect the result set.",
137
+ "required" => false,
138
+ "schema" => {
139
+ "type" => "integer",
140
+ "minimum" => 0,
141
+ "default" => 0
142
+ }
143
+ },
144
+ }
145
+ end
146
+
147
+ def build_parameter(name, value = nil)
148
+ parameters[name] = value
149
+ "##{PARAMETERS_PATH}/#{name}"
150
+ end
151
+
152
+ def openapi_schema(klass_name)
153
+ {
154
+ "type" => "object",
155
+ "properties" => openapi_schema_properties(klass_name),
156
+ "additionalProperties" => false
157
+ }
158
+ end
159
+
160
+ def openapi_list_description(klass_name, primary_collection)
161
+ primary_collection = nil if primary_collection == klass_name
162
+ {
163
+ "summary" => "List #{klass_name.pluralize}#{" for #{primary_collection}" if primary_collection}",
164
+ "operationId" => "list#{primary_collection}#{klass_name.pluralize}",
165
+ "description" => "Returns an array of #{klass_name} objects",
166
+ "parameters" => [
167
+ { "$ref" => "##{PARAMETERS_PATH}/QueryLimit" },
168
+ { "$ref" => "##{PARAMETERS_PATH}/QueryOffset" },
169
+ { "$ref" => "##{PARAMETERS_PATH}/QueryFilter" }
170
+ ],
171
+ "responses" => {
172
+ "200" => {
173
+ "description" => "#{klass_name.pluralize} collection",
174
+ "content" => {
175
+ "application/json" => {
176
+ "schema" => { "$ref" => build_collection_schema(klass_name) }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }.tap do |h|
182
+ h["parameters"] << { "$ref" => build_parameter("ID") } if primary_collection
183
+ end
184
+ end
185
+
186
+ def build_collection_schema(klass_name)
187
+ collection_name = "#{klass_name.pluralize}Collection"
188
+ schemas[collection_name] = {
189
+ "type" => "object",
190
+ "properties" => {
191
+ "meta" => { "$ref" => "##{SCHEMAS_PATH}/CollectionMetadata" },
192
+ "links" => { "$ref" => "##{SCHEMAS_PATH}/CollectionLinks" },
193
+ "data" => {
194
+ "type" => "array",
195
+ "items" => { "$ref" => build_schema(klass_name) }
196
+ }
197
+ }
198
+ }
199
+
200
+ "##{SCHEMAS_PATH}/#{collection_name}"
201
+ end
202
+
203
+ def openapi_show_description(klass_name)
204
+ {
205
+ "summary" => "Show an existing #{klass_name}",
206
+ "operationId" => "show#{klass_name}",
207
+ "description" => "Returns a #{klass_name} object",
208
+ "parameters" => [{ "$ref" => build_parameter("ID") }],
209
+ "responses" => {
210
+ "200" => {
211
+ "description" => "#{klass_name} info",
212
+ "content" => {
213
+ "application/json" => {
214
+ "schema" => { "$ref" => build_schema(klass_name) }
215
+ }
216
+ }
217
+ },
218
+ "404" => {"description" => "Not found"}
219
+ }
220
+ }
221
+ end
222
+
223
+ def openapi_destroy_description(klass_name)
224
+ {
225
+ "summary" => "Delete an existing #{klass_name}",
226
+ "operationId" => "delete#{klass_name}",
227
+ "description" => "Deletes a #{klass_name} object",
228
+ "parameters" => [{ "$ref" => build_parameter("ID") }],
229
+ "responses" => {
230
+ "204" => { "description" => "#{klass_name} deleted" },
231
+ "404" => { "description" => "Not found" }
232
+ }
233
+ }
234
+ end
235
+
236
+ def openapi_create_description(klass_name)
237
+ {
238
+ "summary" => "Create a new #{klass_name}",
239
+ "operationId" => "create#{klass_name}",
240
+ "description" => "Creates a #{klass_name} object",
241
+ "requestBody" => {
242
+ "content" => {
243
+ "application/json" => {
244
+ "schema" => { "$ref" => build_schema(klass_name) }
245
+ }
246
+ },
247
+ "description" => "#{klass_name} attributes to create",
248
+ "required" => true
249
+ },
250
+ "responses" => {
251
+ "201" => {
252
+ "description" => "#{klass_name} creation successful",
253
+ "content" => {
254
+ "application/json" => {
255
+ "schema" => { "$ref" => build_schema(klass_name) }
256
+ }
257
+ }
258
+ }
259
+ }
260
+ }
261
+ end
262
+
263
+ def openapi_update_description(klass_name, verb)
264
+ action = verb == "patch" ? "Update" : "Replace"
265
+ {
266
+ "summary" => "#{action} an existing #{klass_name}",
267
+ "operationId" => "#{action.downcase}#{klass_name}",
268
+ "description" => "#{action}s a #{klass_name} object",
269
+ "parameters" => [
270
+ { "$ref" => build_parameter("ID") }
271
+ ],
272
+ "requestBody" => {
273
+ "content" => {
274
+ "application/json" => {
275
+ "schema" => { "$ref" => build_schema(klass_name) }
276
+ }
277
+ },
278
+ "description" => "#{klass_name} attributes to update",
279
+ "required" => true
280
+ },
281
+ "responses" => {
282
+ "204" => { "description" => "Updated, no content" },
283
+ "400" => { "description" => "Bad request" },
284
+ "404" => { "description" => "Not found" }
285
+ }
286
+ }
287
+ end
288
+
289
+ def openapi_schema_properties_value(klass_name, model, key, value)
290
+ if key == model.primary_key
291
+ {
292
+ "$ref" => "##{SCHEMAS_PATH}/ID"
293
+ }
294
+ elsif key.ends_with?("_id")
295
+ properties_value = {}
296
+ properties_value["$ref"] = if generator_read_only_definitions.include?(klass_name)
297
+ # Everything under providers data is read only for now
298
+ "##{SCHEMAS_PATH}/ID"
299
+ else
300
+ openapi_contents.dig(*path_parts(SCHEMAS_PATH), klass_name, "properties", key, "$ref") || "##{SCHEMAS_PATH}/ID"
301
+ end
302
+ properties_value
303
+ else
304
+ properties_value = {
305
+ "type" => "string"
306
+ }
307
+
308
+ case value.sql_type_metadata.type
309
+ when :datetime
310
+ properties_value["format"] = "date-time"
311
+ when :integer
312
+ properties_value["type"] = "integer"
313
+ when :float
314
+ properties_value["type"] = "number"
315
+ when :boolean
316
+ properties_value["type"] = "boolean"
317
+ when :jsonb
318
+ properties_value["type"] = "object"
319
+ ['type', 'items', 'properties', 'additionalProperties'].each do |property_key|
320
+ prop = openapi_contents.dig(*path_parts(SCHEMAS_PATH), klass_name, "properties", key, property_key)
321
+ properties_value[property_key] = prop unless prop.nil?
322
+ end
323
+ end
324
+
325
+ # Take existing attrs, that we won't generate
326
+ ['example', 'format', 'readOnly', 'title', 'description'].each do |property_key|
327
+ property_value = openapi_contents.dig(*path_parts(SCHEMAS_PATH), klass_name, "properties", key, property_key)
328
+ properties_value[property_key] = property_value if property_value
329
+ end
330
+
331
+ if generator_read_only_definitions.include?(klass_name) || generator_read_only_attributes.include?(key.to_sym)
332
+ # Everything under providers data is read only for now
333
+ properties_value['readOnly'] = true
334
+ end
335
+
336
+ properties_value.sort.to_h
337
+ end
338
+ end
339
+
340
+ def run(graphql = false)
341
+ new_content = openapi_contents.dup
342
+ new_content["paths"] = build_paths.sort.to_h
343
+ new_content["components"] ||= {}
344
+ new_content["components"]["schemas"] = schemas.sort.each_with_object({}) { |(name, val), h| h[name] = val || openapi_contents["components"]["schemas"][name] || {} }
345
+ new_content["components"]["parameters"] = parameters.sort.each_with_object({}) { |(name, val), h| h[name] = val || openapi_contents["components"]["parameters"][name] || {} }
346
+ File.write(openapi_file, JSON.pretty_generate(new_content) + "\n")
347
+ ManageIQ::API::Common::GraphQL::Generator.generate(api_version, new_content) if graphql
348
+ end
349
+
350
+ def openapi_schema_properties(klass_name)
351
+ model = klass_name.constantize
352
+ model.columns_hash.map do |key, value|
353
+ unless (generator_blacklist_allowed_attributes[key.to_sym] || []).include?(klass_name)
354
+ next if generator_blacklist_attributes.include?(key.to_sym)
355
+ end
356
+
357
+ if generator_blacklist_substitute_attributes.include?(key.to_sym)
358
+ generator_blacklist_substitute_attributes[key.to_sym]
359
+ else
360
+ [key, openapi_schema_properties_value(klass_name, model, key, value)]
361
+ end
362
+ end.compact.sort.to_h
363
+ end
364
+
365
+ def generator_blacklist_attributes
366
+ @generator_blacklist_attributes ||= [
367
+ :resource_timestamp,
368
+ :resource_timestamps,
369
+ :resource_timestamps_max,
370
+ :tenant_id,
371
+ ].to_set.freeze
372
+ end
373
+
374
+ def generator_blacklist_allowed_attributes
375
+ @generator_blacklist_allowed_attributes ||= {}
376
+ end
377
+
378
+ def generator_blacklist_substitute_attributes
379
+ @generator_blacklist_substitute_attributes ||= {}
380
+ end
381
+
382
+ def generator_read_only_attributes
383
+ @generator_read_only_attributes ||= [
384
+ :archived_at,
385
+ :created_at,
386
+ :last_seen_at,
387
+ :updated_at,
388
+ ].to_set.freeze
389
+ end
390
+
391
+ def generator_read_only_definitions
392
+ @generator_read_only_definitions ||= [].to_set.freeze
393
+ end
394
+
395
+ def build_paths
396
+ applicable_rails_routes.each_with_object({}) do |route, expected_paths|
397
+ without_format = route.path.split("(.:format)").first
398
+ sub_path = without_format.split(base_path).last.sub(/:[_a-z]*id/, "{id}")
399
+ klass_name = route.controller.split("/").last.camelize.singularize
400
+ verb = route.verb.downcase
401
+ primary_collection = sub_path.split("/")[1].camelize.singularize
402
+
403
+ expected_paths[sub_path] ||= {}
404
+ expected_paths[sub_path][verb] =
405
+ case route.action
406
+ when "index" then openapi_list_description(klass_name, primary_collection)
407
+ when "show" then openapi_show_description(klass_name)
408
+ when "destroy" then openapi_destroy_description(klass_name)
409
+ when "create" then openapi_create_description(klass_name)
410
+ when "update" then openapi_update_description(klass_name, verb)
411
+ else handle_custom_route_action(route.action.camelize, verb, primary_collection)
412
+ end
413
+
414
+ next if expected_paths[sub_path][verb]
415
+
416
+ # If it's not generic action but a custom method like e.g. `post "order", :to => "service_plans#order"`, we will
417
+ # try to take existing schema, because the description, summary, etc. are likely to be custom.
418
+ expected_paths[sub_path][verb] =
419
+ case verb
420
+ when "post"
421
+ if sub_path == "/graphql" && route.action == "query"
422
+ schemas["GraphQLResponse"] = ::ManageIQ::API::Common::GraphQL.openapi_graphql_response
423
+ ::ManageIQ::API::Common::GraphQL.openapi_graphql_description
424
+ else
425
+ openapi_contents.dig("paths", sub_path, verb) || openapi_create_description(klass_name)
426
+ end
427
+ when "get"
428
+ openapi_contents.dig("paths", sub_path, verb) || openapi_show_description(klass_name)
429
+ else
430
+ openapi_contents.dig("paths", sub_path, verb)
431
+ end
432
+ end
433
+ end
434
+
435
+ def handle_custom_route_action(_route_action, _verb, _primary_collection)
436
+ end
437
+ end
438
+ end
439
+ end
440
+ end
441
+ end