manageiq-api-common 0.1.0

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