insights-api-common 3.0.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 +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +102 -0
- data/Rakefile +18 -0
- data/app/models/authentication.rb +19 -0
- data/app/models/concerns/encryption_concern.rb +52 -0
- data/app/models/encryption.rb +13 -0
- data/lib/generators/shared_utilities/migration_generator.rb +79 -0
- data/lib/generators/shared_utilities/orm_helper.rb +25 -0
- data/lib/generators/shared_utilities/templates/migration.rb +27 -0
- data/lib/generators/shared_utilities/templates/migration_existing.rb +28 -0
- data/lib/insights.rb +1 -0
- data/lib/insights/api/common.rb +12 -0
- data/lib/insights/api/common/application_controller_mixins/api_doc.rb +39 -0
- data/lib/insights/api/common/application_controller_mixins/common.rb +27 -0
- data/lib/insights/api/common/application_controller_mixins/exception_handling.rb +41 -0
- data/lib/insights/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
- data/lib/insights/api/common/application_controller_mixins/parameters.rb +134 -0
- data/lib/insights/api/common/application_controller_mixins/request_body_validation.rb +48 -0
- data/lib/insights/api/common/application_controller_mixins/request_parameter_validation.rb +29 -0
- data/lib/insights/api/common/application_controller_mixins/request_path.rb +70 -0
- data/lib/insights/api/common/engine.rb +20 -0
- data/lib/insights/api/common/entitlement.rb +37 -0
- data/lib/insights/api/common/error_document.rb +29 -0
- data/lib/insights/api/common/filter.rb +175 -0
- data/lib/insights/api/common/graphql.rb +127 -0
- data/lib/insights/api/common/graphql/associated_records.rb +44 -0
- data/lib/insights/api/common/graphql/association_loader.rb +35 -0
- data/lib/insights/api/common/graphql/generator.rb +148 -0
- data/lib/insights/api/common/graphql/templates/model_type.erb +35 -0
- data/lib/insights/api/common/graphql/templates/query_type.erb +49 -0
- data/lib/insights/api/common/graphql/templates/schema.erb +6 -0
- data/lib/insights/api/common/graphql/types/big_int.rb +23 -0
- data/lib/insights/api/common/graphql/types/date_time.rb +16 -0
- data/lib/insights/api/common/graphql/types/query_filter.rb +16 -0
- data/lib/insights/api/common/graphql/types/query_sort_by.rb +16 -0
- data/lib/insights/api/common/inflections.rb +28 -0
- data/lib/insights/api/common/logging.rb +17 -0
- data/lib/insights/api/common/metrics.rb +39 -0
- data/lib/insights/api/common/middleware/web_server_metrics.rb +62 -0
- data/lib/insights/api/common/open_api.rb +2 -0
- data/lib/insights/api/common/open_api/docs.rb +54 -0
- data/lib/insights/api/common/open_api/docs/component_collection.rb +67 -0
- data/lib/insights/api/common/open_api/docs/doc_v3.rb +102 -0
- data/lib/insights/api/common/open_api/docs/object_definition.rb +39 -0
- data/lib/insights/api/common/open_api/generator.rb +520 -0
- data/lib/insights/api/common/open_api/serializer.rb +31 -0
- data/lib/insights/api/common/option_redirect_enhancements.rb +23 -0
- data/lib/insights/api/common/paginated_response.rb +108 -0
- data/lib/insights/api/common/rbac/access.rb +66 -0
- data/lib/insights/api/common/rbac/acl.rb +74 -0
- data/lib/insights/api/common/rbac/policies.rb +33 -0
- data/lib/insights/api/common/rbac/query_shared_resource.rb +45 -0
- data/lib/insights/api/common/rbac/roles.rb +77 -0
- data/lib/insights/api/common/rbac/seed.rb +140 -0
- data/lib/insights/api/common/rbac/service.rb +67 -0
- data/lib/insights/api/common/rbac/share_resource.rb +60 -0
- data/lib/insights/api/common/rbac/unshare_resource.rb +32 -0
- data/lib/insights/api/common/rbac/utilities.rb +30 -0
- data/lib/insights/api/common/request.rb +111 -0
- data/lib/insights/api/common/routing.rb +26 -0
- data/lib/insights/api/common/user.rb +48 -0
- data/lib/insights/api/common/version.rb +7 -0
- data/lib/tasks/insights/api/common_tasks.rake +4 -0
- data/spec/support/default_as_json.rb +17 -0
- data/spec/support/rbac_shared_contexts.rb +44 -0
- data/spec/support/requests_spec_helper.rb +7 -0
- data/spec/support/service_spec_helper.rb +26 -0
- data/spec/support/user_header_spec_helper.rb +68 -0
- metadata +403 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
require "insights/api/common/open_api/docs/component_collection"
|
2
|
+
require "insights/api/common/open_api/docs/object_definition"
|
3
|
+
require "insights/api/common/open_api/docs/doc_v3"
|
4
|
+
|
5
|
+
module Insights
|
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 Insights
|
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] = ::Insights::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,102 @@
|
|
1
|
+
require "more_core_extensions/core_ext/hash"
|
2
|
+
require "more_core_extensions/core_ext/array"
|
3
|
+
require "openapi_parser"
|
4
|
+
|
5
|
+
module Insights
|
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 validate_parameters!(http_method, request_path, api_version, params)
|
38
|
+
path = request_path.split(api_version)[1]
|
39
|
+
raise "API version not found in request_path" if path.nil?
|
40
|
+
|
41
|
+
request_operation = validator_doc.request_operation(http_method.to_s.downcase, path)
|
42
|
+
return unless request_operation
|
43
|
+
|
44
|
+
request_operation.validate_request_parameter(params, {})
|
45
|
+
end
|
46
|
+
|
47
|
+
def version
|
48
|
+
@version ||= Gem::Version.new(content.fetch_path("info", "version"))
|
49
|
+
end
|
50
|
+
|
51
|
+
def parameters
|
52
|
+
@parameters ||= ::Insights::API::Common::OpenApi::Docs::ComponentCollection.new(self, "components/parameters")
|
53
|
+
end
|
54
|
+
|
55
|
+
def schemas
|
56
|
+
@schemas ||= ::Insights::API::Common::OpenApi::Docs::ComponentCollection.new(self, "components/schemas")
|
57
|
+
end
|
58
|
+
|
59
|
+
def definitions
|
60
|
+
schemas
|
61
|
+
end
|
62
|
+
|
63
|
+
def example_attributes(key)
|
64
|
+
schemas[key]["properties"].each_with_object({}) do |(col, stuff), hash|
|
65
|
+
hash[col] = stuff["example"] if stuff.key?("example")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def base_path
|
70
|
+
@base_path ||= @content.fetch_path("servers", 0, "variables", "basePath", "default")
|
71
|
+
end
|
72
|
+
|
73
|
+
def paths
|
74
|
+
@content["paths"]
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_json(options = nil)
|
78
|
+
content.to_json(options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def routes
|
82
|
+
@routes ||= begin
|
83
|
+
paths.flat_map do |path, hash|
|
84
|
+
hash.collect do |verb, _details|
|
85
|
+
p = File.join(base_path, path).gsub(/{\w*}/, ":id")
|
86
|
+
{:path => p, :verb => verb.upcase}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def validator_doc(opts = { :coerce_value => true, :datetime_coerce_class => DateTime })
|
95
|
+
@validator_doc ||= ::OpenAPIParser.parse(content, opts)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Insights
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module OpenApi
|
5
|
+
class Docs
|
6
|
+
class ObjectDefinition < Hash
|
7
|
+
def all_attributes
|
8
|
+
properties.map { |key, val| all_attributes_recursive(key, val) }
|
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
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def all_attributes_recursive(key, value)
|
26
|
+
if value["properties"]
|
27
|
+
{
|
28
|
+
key => value["properties"].map { |k, v| all_attributes_recursive(k, v) }
|
29
|
+
}
|
30
|
+
else
|
31
|
+
key
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,520 @@
|
|
1
|
+
module Insights
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module OpenApi
|
5
|
+
class Generator
|
6
|
+
require 'json'
|
7
|
+
require 'insights/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
|
+
"SortByAttribute" => {
|
101
|
+
"type" => "string",
|
102
|
+
"description" => "Attribute with optional order to sort the result set by.",
|
103
|
+
"pattern" => "^[a-z\\-_]+(:asc|:desc)?$"
|
104
|
+
}
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_schema(klass_name)
|
109
|
+
schemas[klass_name] = openapi_schema(klass_name)
|
110
|
+
"##{SCHEMAS_PATH}/#{klass_name}"
|
111
|
+
end
|
112
|
+
|
113
|
+
def build_schema_error_not_found
|
114
|
+
klass_name = "ErrorNotFound"
|
115
|
+
|
116
|
+
schemas[klass_name] = {
|
117
|
+
"type" => "object",
|
118
|
+
"properties" => {
|
119
|
+
"errors" => {
|
120
|
+
"type" => "array",
|
121
|
+
"items" => {
|
122
|
+
"type" => "object",
|
123
|
+
"properties" => {
|
124
|
+
"status" => {
|
125
|
+
"type" => "integer",
|
126
|
+
"example" => 404
|
127
|
+
},
|
128
|
+
"detail" => {
|
129
|
+
"type" => "string",
|
130
|
+
"example" => "Record not found"
|
131
|
+
}
|
132
|
+
}
|
133
|
+
}
|
134
|
+
}
|
135
|
+
}
|
136
|
+
}
|
137
|
+
|
138
|
+
"##{SCHEMAS_PATH}/#{klass_name}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def parameters
|
142
|
+
@parameters ||= {
|
143
|
+
"QueryFilter" => {
|
144
|
+
"in" => "query",
|
145
|
+
"name" => "filter",
|
146
|
+
"description" => "Filter for querying collections.",
|
147
|
+
"required" => false,
|
148
|
+
"style" => "deepObject",
|
149
|
+
"explode" => true,
|
150
|
+
"schema" => {
|
151
|
+
"type" => "object"
|
152
|
+
}
|
153
|
+
},
|
154
|
+
"QueryLimit" => {
|
155
|
+
"in" => "query",
|
156
|
+
"name" => "limit",
|
157
|
+
"description" => "The numbers of items to return per page.",
|
158
|
+
"required" => false,
|
159
|
+
"schema" => {
|
160
|
+
"type" => "integer",
|
161
|
+
"minimum" => 1,
|
162
|
+
"maximum" => 1000,
|
163
|
+
"default" => 100
|
164
|
+
}
|
165
|
+
},
|
166
|
+
"QueryOffset" => {
|
167
|
+
"in" => "query",
|
168
|
+
"name" => "offset",
|
169
|
+
"description" => "The number of items to skip before starting to collect the result set.",
|
170
|
+
"required" => false,
|
171
|
+
"schema" => {
|
172
|
+
"type" => "integer",
|
173
|
+
"minimum" => 0,
|
174
|
+
"default" => 0
|
175
|
+
}
|
176
|
+
},
|
177
|
+
"QuerySortBy" => {
|
178
|
+
"in" => "query",
|
179
|
+
"name" => "sort_by",
|
180
|
+
"description" => "The list of attribute and order to sort the result set by.",
|
181
|
+
"required" => false,
|
182
|
+
"schema" => {
|
183
|
+
"oneOf" => [
|
184
|
+
{ "$ref" => "##{SCHEMAS_PATH}/SortByAttribute" },
|
185
|
+
{ "type" => "array", "items" => { "$ref" => "##{SCHEMAS_PATH}/SortByAttribute" } }
|
186
|
+
]
|
187
|
+
}
|
188
|
+
}
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
def build_parameter(name, value = nil)
|
193
|
+
parameters[name] = value
|
194
|
+
"##{PARAMETERS_PATH}/#{name}"
|
195
|
+
end
|
196
|
+
|
197
|
+
def openapi_schema(klass_name)
|
198
|
+
{
|
199
|
+
"type" => "object",
|
200
|
+
"properties" => openapi_schema_properties(klass_name),
|
201
|
+
"additionalProperties" => false
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
def openapi_list_description(klass_name, primary_collection)
|
206
|
+
sub_collection = (primary_collection != klass_name)
|
207
|
+
{
|
208
|
+
"summary" => "List #{klass_name.pluralize}#{" for #{primary_collection}" if sub_collection}",
|
209
|
+
"operationId" => "list#{primary_collection if sub_collection}#{klass_name.pluralize}",
|
210
|
+
"description" => "Returns an array of #{klass_name} objects",
|
211
|
+
"parameters" => [
|
212
|
+
{ "$ref" => "##{PARAMETERS_PATH}/QueryLimit" },
|
213
|
+
{ "$ref" => "##{PARAMETERS_PATH}/QueryOffset" },
|
214
|
+
{ "$ref" => "##{PARAMETERS_PATH}/QueryFilter" },
|
215
|
+
{ "$ref" => "##{PARAMETERS_PATH}/QuerySortBy" }
|
216
|
+
],
|
217
|
+
"responses" => {
|
218
|
+
"200" => {
|
219
|
+
"description" => "#{klass_name.pluralize} collection",
|
220
|
+
"content" => {
|
221
|
+
"application/json" => {
|
222
|
+
"schema" => { "$ref" => build_collection_schema(klass_name) }
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
}
|
227
|
+
}.tap do |h|
|
228
|
+
h["parameters"] << { "$ref" => build_parameter("ID") } if sub_collection
|
229
|
+
|
230
|
+
next unless sub_collection
|
231
|
+
|
232
|
+
h["responses"]["404"] = {
|
233
|
+
"description" => "Not found",
|
234
|
+
"content" => {
|
235
|
+
"application/json" => {
|
236
|
+
"schema" => { "$ref" => build_schema_error_not_found }
|
237
|
+
}
|
238
|
+
}
|
239
|
+
}
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def build_collection_schema(klass_name)
|
244
|
+
collection_name = "#{klass_name.pluralize}Collection"
|
245
|
+
schemas[collection_name] = {
|
246
|
+
"type" => "object",
|
247
|
+
"properties" => {
|
248
|
+
"meta" => { "$ref" => "##{SCHEMAS_PATH}/CollectionMetadata" },
|
249
|
+
"links" => { "$ref" => "##{SCHEMAS_PATH}/CollectionLinks" },
|
250
|
+
"data" => {
|
251
|
+
"type" => "array",
|
252
|
+
"items" => { "$ref" => build_schema(klass_name) }
|
253
|
+
}
|
254
|
+
}
|
255
|
+
}
|
256
|
+
|
257
|
+
"##{SCHEMAS_PATH}/#{collection_name}"
|
258
|
+
end
|
259
|
+
|
260
|
+
def openapi_show_description(klass_name)
|
261
|
+
{
|
262
|
+
"summary" => "Show an existing #{klass_name}",
|
263
|
+
"operationId" => "show#{klass_name}",
|
264
|
+
"description" => "Returns a #{klass_name} object",
|
265
|
+
"parameters" => [{ "$ref" => build_parameter("ID") }],
|
266
|
+
"responses" => {
|
267
|
+
"200" => {
|
268
|
+
"description" => "#{klass_name} info",
|
269
|
+
"content" => {
|
270
|
+
"application/json" => {
|
271
|
+
"schema" => { "$ref" => build_schema(klass_name) }
|
272
|
+
}
|
273
|
+
}
|
274
|
+
},
|
275
|
+
"404" => {
|
276
|
+
"description" => "Not found",
|
277
|
+
"content" => {
|
278
|
+
"application/json" => {
|
279
|
+
"schema" => { "$ref" => build_schema_error_not_found }
|
280
|
+
}
|
281
|
+
}
|
282
|
+
}
|
283
|
+
}
|
284
|
+
}
|
285
|
+
end
|
286
|
+
|
287
|
+
def openapi_destroy_description(klass_name)
|
288
|
+
{
|
289
|
+
"summary" => "Delete an existing #{klass_name}",
|
290
|
+
"operationId" => "delete#{klass_name}",
|
291
|
+
"description" => "Deletes a #{klass_name} object",
|
292
|
+
"parameters" => [{ "$ref" => build_parameter("ID") }],
|
293
|
+
"responses" => {
|
294
|
+
"204" => { "description" => "#{klass_name} deleted" },
|
295
|
+
"404" => {
|
296
|
+
"description" => "Not found",
|
297
|
+
"content" => {
|
298
|
+
"application/json" => {
|
299
|
+
"schema" => { "$ref" => build_schema_error_not_found }
|
300
|
+
}
|
301
|
+
}
|
302
|
+
}
|
303
|
+
}
|
304
|
+
}
|
305
|
+
end
|
306
|
+
|
307
|
+
def openapi_create_description(klass_name)
|
308
|
+
{
|
309
|
+
"summary" => "Create a new #{klass_name}",
|
310
|
+
"operationId" => "create#{klass_name}",
|
311
|
+
"description" => "Creates a #{klass_name} object",
|
312
|
+
"requestBody" => {
|
313
|
+
"content" => {
|
314
|
+
"application/json" => {
|
315
|
+
"schema" => { "$ref" => build_schema(klass_name) }
|
316
|
+
}
|
317
|
+
},
|
318
|
+
"description" => "#{klass_name} attributes to create",
|
319
|
+
"required" => true
|
320
|
+
},
|
321
|
+
"responses" => {
|
322
|
+
"201" => {
|
323
|
+
"description" => "#{klass_name} creation successful",
|
324
|
+
"content" => {
|
325
|
+
"application/json" => {
|
326
|
+
"schema" => { "$ref" => build_schema(klass_name) }
|
327
|
+
}
|
328
|
+
}
|
329
|
+
}
|
330
|
+
}
|
331
|
+
}
|
332
|
+
end
|
333
|
+
|
334
|
+
def openapi_update_description(klass_name, verb)
|
335
|
+
action = verb == "patch" ? "Update" : "Replace"
|
336
|
+
{
|
337
|
+
"summary" => "#{action} an existing #{klass_name}",
|
338
|
+
"operationId" => "#{action.downcase}#{klass_name}",
|
339
|
+
"description" => "#{action}s a #{klass_name} object",
|
340
|
+
"parameters" => [
|
341
|
+
{ "$ref" => build_parameter("ID") }
|
342
|
+
],
|
343
|
+
"requestBody" => {
|
344
|
+
"content" => {
|
345
|
+
"application/json" => {
|
346
|
+
"schema" => { "$ref" => build_schema(klass_name) }
|
347
|
+
}
|
348
|
+
},
|
349
|
+
"description" => "#{klass_name} attributes to update",
|
350
|
+
"required" => true
|
351
|
+
},
|
352
|
+
"responses" => {
|
353
|
+
"204" => { "description" => "Updated, no content" },
|
354
|
+
"400" => { "description" => "Bad request" },
|
355
|
+
"404" => {
|
356
|
+
"description" => "Not found",
|
357
|
+
"content" => {
|
358
|
+
"application/json" => {
|
359
|
+
"schema" => { "$ref" => build_schema_error_not_found }
|
360
|
+
}
|
361
|
+
}
|
362
|
+
}
|
363
|
+
}
|
364
|
+
}
|
365
|
+
end
|
366
|
+
|
367
|
+
def openapi_schema_properties_value(klass_name, model, key, value)
|
368
|
+
if key == model.primary_key
|
369
|
+
{
|
370
|
+
"$ref" => "##{SCHEMAS_PATH}/ID"
|
371
|
+
}
|
372
|
+
elsif key.ends_with?("_id")
|
373
|
+
properties_value = {}
|
374
|
+
properties_value["$ref"] = if generator_read_only_definitions.include?(klass_name)
|
375
|
+
# Everything under providers data is read only for now
|
376
|
+
"##{SCHEMAS_PATH}/ID"
|
377
|
+
else
|
378
|
+
openapi_contents.dig(*path_parts(SCHEMAS_PATH), klass_name, "properties", key, "$ref") || "##{SCHEMAS_PATH}/ID"
|
379
|
+
end
|
380
|
+
properties_value
|
381
|
+
else
|
382
|
+
properties_value = {
|
383
|
+
"type" => "string"
|
384
|
+
}
|
385
|
+
|
386
|
+
case value.sql_type_metadata.type
|
387
|
+
when :datetime
|
388
|
+
properties_value["format"] = "date-time"
|
389
|
+
when :integer
|
390
|
+
properties_value["type"] = "integer"
|
391
|
+
when :float
|
392
|
+
properties_value["type"] = "number"
|
393
|
+
when :boolean
|
394
|
+
properties_value["type"] = "boolean"
|
395
|
+
when :jsonb
|
396
|
+
properties_value["type"] = "object"
|
397
|
+
['type', 'items', 'properties', 'additionalProperties'].each do |property_key|
|
398
|
+
prop = openapi_contents.dig(*path_parts(SCHEMAS_PATH), klass_name, "properties", key, property_key)
|
399
|
+
properties_value[property_key] = prop unless prop.nil?
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# Take existing attrs, that we won't generate
|
404
|
+
['example', 'format', 'readOnly', 'title', 'description'].each do |property_key|
|
405
|
+
property_value = openapi_contents.dig(*path_parts(SCHEMAS_PATH), klass_name, "properties", key, property_key)
|
406
|
+
properties_value[property_key] = property_value if property_value
|
407
|
+
end
|
408
|
+
|
409
|
+
if generator_read_only_definitions.include?(klass_name) || generator_read_only_attributes.include?(key.to_sym)
|
410
|
+
# Everything under providers data is read only for now
|
411
|
+
properties_value['readOnly'] = true
|
412
|
+
end
|
413
|
+
|
414
|
+
properties_value.sort.to_h
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
def run(graphql = false)
|
419
|
+
new_content = openapi_contents.dup
|
420
|
+
new_content["paths"] = build_paths.sort.to_h
|
421
|
+
new_content["components"] ||= {}
|
422
|
+
new_content["components"]["schemas"] = schemas.sort.each_with_object({}) { |(name, val), h| h[name] = val || openapi_contents["components"]["schemas"][name] || {} }
|
423
|
+
new_content["components"]["parameters"] = parameters.sort.each_with_object({}) { |(name, val), h| h[name] = val || openapi_contents["components"]["parameters"][name] || {} }
|
424
|
+
File.write(openapi_file, JSON.pretty_generate(new_content) + "\n")
|
425
|
+
Insights::API::Common::GraphQL::Generator.generate(api_version, new_content) if graphql
|
426
|
+
end
|
427
|
+
|
428
|
+
def openapi_schema_properties(klass_name)
|
429
|
+
model = klass_name.constantize
|
430
|
+
model.columns_hash.map do |key, value|
|
431
|
+
unless (generator_blacklist_allowed_attributes[key.to_sym] || []).include?(klass_name)
|
432
|
+
next if generator_blacklist_attributes.include?(key.to_sym)
|
433
|
+
end
|
434
|
+
|
435
|
+
if generator_blacklist_substitute_attributes.include?(key.to_sym)
|
436
|
+
generator_blacklist_substitute_attributes[key.to_sym]
|
437
|
+
else
|
438
|
+
[key, openapi_schema_properties_value(klass_name, model, key, value)]
|
439
|
+
end
|
440
|
+
end.compact.sort.to_h
|
441
|
+
end
|
442
|
+
|
443
|
+
def generator_blacklist_attributes
|
444
|
+
@generator_blacklist_attributes ||= [
|
445
|
+
:resource_timestamp,
|
446
|
+
:resource_timestamps,
|
447
|
+
:resource_timestamps_max,
|
448
|
+
:tenant_id,
|
449
|
+
].to_set.freeze
|
450
|
+
end
|
451
|
+
|
452
|
+
def generator_blacklist_allowed_attributes
|
453
|
+
@generator_blacklist_allowed_attributes ||= {}
|
454
|
+
end
|
455
|
+
|
456
|
+
def generator_blacklist_substitute_attributes
|
457
|
+
@generator_blacklist_substitute_attributes ||= {}
|
458
|
+
end
|
459
|
+
|
460
|
+
def generator_read_only_attributes
|
461
|
+
@generator_read_only_attributes ||= [
|
462
|
+
:archived_at,
|
463
|
+
:created_at,
|
464
|
+
:last_seen_at,
|
465
|
+
:updated_at,
|
466
|
+
].to_set.freeze
|
467
|
+
end
|
468
|
+
|
469
|
+
def generator_read_only_definitions
|
470
|
+
@generator_read_only_definitions ||= [].to_set.freeze
|
471
|
+
end
|
472
|
+
|
473
|
+
def build_paths
|
474
|
+
applicable_rails_routes.each_with_object({}) do |route, expected_paths|
|
475
|
+
without_format = route.path.split("(.:format)").first
|
476
|
+
sub_path = without_format.split(base_path).last.sub(/:[_a-z]*id/, "{id}")
|
477
|
+
klass_name = route.controller.split("/").last.camelize.singularize
|
478
|
+
verb = route.verb.downcase
|
479
|
+
primary_collection = sub_path.split("/")[1].camelize.singularize
|
480
|
+
|
481
|
+
expected_paths[sub_path] ||= {}
|
482
|
+
expected_paths[sub_path][verb] =
|
483
|
+
case route.action
|
484
|
+
when "index" then openapi_list_description(klass_name, primary_collection)
|
485
|
+
when "show" then openapi_show_description(klass_name)
|
486
|
+
when "destroy" then openapi_destroy_description(klass_name)
|
487
|
+
when "create" then openapi_create_description(klass_name)
|
488
|
+
when "update" then openapi_update_description(klass_name, verb)
|
489
|
+
else handle_custom_route_action(route.action.camelize, verb, primary_collection)
|
490
|
+
end
|
491
|
+
|
492
|
+
next if expected_paths[sub_path][verb]
|
493
|
+
|
494
|
+
# If it's not generic action but a custom method like e.g. `post "order", :to => "service_plans#order"`, we will
|
495
|
+
# try to take existing schema, because the description, summary, etc. are likely to be custom.
|
496
|
+
expected_paths[sub_path][verb] =
|
497
|
+
case verb
|
498
|
+
when "post"
|
499
|
+
if sub_path == "/graphql" && route.action == "query"
|
500
|
+
schemas["GraphQLRequest"] = ::Insights::API::Common::GraphQL.openapi_graphql_request
|
501
|
+
schemas["GraphQLResponse"] = ::Insights::API::Common::GraphQL.openapi_graphql_response
|
502
|
+
::Insights::API::Common::GraphQL.openapi_graphql_description
|
503
|
+
else
|
504
|
+
openapi_contents.dig("paths", sub_path, verb) || openapi_create_description(klass_name)
|
505
|
+
end
|
506
|
+
when "get"
|
507
|
+
openapi_contents.dig("paths", sub_path, verb) || openapi_show_description(klass_name)
|
508
|
+
else
|
509
|
+
openapi_contents.dig("paths", sub_path, verb)
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
def handle_custom_route_action(_route_action, _verb, _primary_collection)
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|