apia-open_api 0.1.8 → 0.1.10
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/lib/apia/open_api/helpers.rb +14 -0
- data/lib/apia/open_api/objects/parameters.rb +56 -4
- data/lib/apia/open_api/objects/path.rb +96 -3
- data/lib/apia/open_api/rack.rb +19 -2
- data/lib/apia/open_api/specification.rb +97 -9
- data/lib/apia/open_api/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2018fe92f478024edd0b15086e2844c6f540cecccdc508d246c2ee9f705dde8d
|
|
4
|
+
data.tar.gz: 0dda2bc96afb05c930a42e89d50c66bfdc38630345be9772cd06244e94cb3574
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19ff009aa99228b72250b56c354a191a2407fe5f4abde636de07d637e7ed4bc4ddd6ae419d38234f8e1fdf58c073895379144d97ce2030aab440ccc8a2f0c21e
|
|
7
|
+
data.tar.gz: 9b739acb6d8ced837e49a5e25e46956ed2847143e3a60c0cd8fb46acbe79d6b5c2b25f64e5c9e2ba5db656f076f7add0c7e54c114e51b4b0a6d41dfcf270fec3
|
|
@@ -52,6 +52,20 @@ module Apia
|
|
|
52
52
|
schema
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
def generate_array_schema(definition)
|
|
56
|
+
type = definition.type
|
|
57
|
+
schema = {
|
|
58
|
+
type: "array",
|
|
59
|
+
items: {
|
|
60
|
+
type: convert_type_to_open_api_data_type(type)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
schema[:description] = definition.description if definition.description.present?
|
|
64
|
+
schema[:items][:format] = "float" if type.klass == Apia::Scalars::Decimal
|
|
65
|
+
schema[:items][:format] = "date" if type.klass == Apia::Scalars::Date
|
|
66
|
+
schema
|
|
67
|
+
end
|
|
68
|
+
|
|
55
69
|
def generate_schema_ref(definition, id: nil, sibling_props: false, **schema_opts)
|
|
56
70
|
id ||= generate_id_from_definition(definition.type.klass.definition)
|
|
57
71
|
success = add_to_components_schemas(definition, id, **schema_opts)
|
|
@@ -69,6 +69,9 @@ module Apia
|
|
|
69
69
|
schema: generate_scalar_schema(@argument)
|
|
70
70
|
}
|
|
71
71
|
param[:description] = @argument.description if @argument.description.present?
|
|
72
|
+
|
|
73
|
+
add_pagination_params(param)
|
|
74
|
+
|
|
72
75
|
param[:required] = true if @argument.required?
|
|
73
76
|
add_to_parameters(param)
|
|
74
77
|
end
|
|
@@ -76,6 +79,19 @@ module Apia
|
|
|
76
79
|
|
|
77
80
|
private
|
|
78
81
|
|
|
82
|
+
def add_pagination_params(param)
|
|
83
|
+
if param[:name] == "page"
|
|
84
|
+
param[:description] = "The page number to request. If not provided, the first page will be returned."
|
|
85
|
+
param[:schema][:default] = 1
|
|
86
|
+
param[:schema][:minimum] = 1
|
|
87
|
+
elsif param[:name] == "per_page"
|
|
88
|
+
param[:description] =
|
|
89
|
+
"The number of items to return per page. If not provided, the default value will be used."
|
|
90
|
+
param[:schema][:default] = @argument.default
|
|
91
|
+
param[:schema][:minimum] = 1
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
79
95
|
# Complex argument sets are not supported in query params (e.g. nested objects)
|
|
80
96
|
# For any LookupArgumentSet only one argument is expected to be provided.
|
|
81
97
|
# However, OpenAPI does not currently support describing mutually exclusive query params.
|
|
@@ -87,19 +103,55 @@ module Apia
|
|
|
87
103
|
next if child_arg.type.argument_set?
|
|
88
104
|
|
|
89
105
|
param = {
|
|
90
|
-
|
|
91
|
-
in: "query",
|
|
92
|
-
schema: generate_scalar_schema(child_arg)
|
|
106
|
+
in: "query"
|
|
93
107
|
}
|
|
108
|
+
|
|
94
109
|
description = []
|
|
95
110
|
description << formatted_description(@argument.description) if @argument.description.present?
|
|
96
111
|
description << formatted_description(child_arg.description) if child_arg.description.present?
|
|
97
|
-
|
|
112
|
+
|
|
113
|
+
add_lookup_description(description)
|
|
114
|
+
|
|
115
|
+
if @argument.array
|
|
116
|
+
param[:name] = "#{@argument.name}[][#{child_arg.name}]"
|
|
117
|
+
param[:schema] = generate_array_schema(child_arg)
|
|
118
|
+
|
|
119
|
+
add_description_section(
|
|
120
|
+
description,
|
|
121
|
+
"All `#{@argument.name}[]` params should have the same amount of elements."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
else
|
|
125
|
+
param[:name] = "#{@argument.name}[#{child_arg.name}]"
|
|
126
|
+
param[:schema] = generate_scalar_schema(child_arg)
|
|
127
|
+
end
|
|
128
|
+
|
|
98
129
|
param[:description] = description.join(" ")
|
|
99
130
|
add_to_parameters(param)
|
|
100
131
|
end
|
|
101
132
|
end
|
|
102
133
|
|
|
134
|
+
# Adds a section to the description of a parameter.
|
|
135
|
+
# If the description is not empty, a blank line is added before the section.
|
|
136
|
+
#
|
|
137
|
+
# @param description [String] The current description of the parameter.
|
|
138
|
+
# @param addition [String] The section to be added to the description.
|
|
139
|
+
# @return [String] The updated description with the added section.
|
|
140
|
+
def add_description_section(description, addition)
|
|
141
|
+
unless description.empty?
|
|
142
|
+
description << "\n\n"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
description << addition
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def add_lookup_description(description)
|
|
149
|
+
add_description_section(
|
|
150
|
+
description,
|
|
151
|
+
"All '#{@argument.name}[]' params are mutually exclusive, only one can be provided."
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
103
155
|
def add_to_parameters(param)
|
|
104
156
|
@route_spec[:parameters] << param
|
|
105
157
|
end
|
|
@@ -36,20 +36,31 @@ module Apia
|
|
|
36
36
|
@api_authenticator = api_authenticator
|
|
37
37
|
@route_spec = {
|
|
38
38
|
operationId: convert_route_to_id,
|
|
39
|
-
|
|
39
|
+
summary: @route.endpoint.definition.name,
|
|
40
|
+
description: @route.endpoint.definition.description,
|
|
41
|
+
tags: route.group ? get_group_tags(route.group) : [name],
|
|
42
|
+
security: []
|
|
40
43
|
}
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
def add_to_spec
|
|
47
|
+
add_scopes_description
|
|
48
|
+
add_scopes_security
|
|
44
49
|
path = @route.path
|
|
50
|
+
|
|
45
51
|
if @route.request_method == :get
|
|
46
52
|
add_parameters
|
|
47
53
|
else
|
|
48
54
|
add_request_body
|
|
49
55
|
end
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
path = "/#{path}"
|
|
58
|
+
# Remove the `:` from the url parameters in the path
|
|
59
|
+
# This is because some tools based on the OpenAPI spec don't like the `:` in the path
|
|
60
|
+
path = path.gsub(/:([^\/]+)/, '\1')
|
|
61
|
+
|
|
62
|
+
@spec[:paths][path] ||= {}
|
|
63
|
+
@spec[:paths][path][@route.request_method.to_s] = @route_spec
|
|
53
64
|
|
|
54
65
|
add_responses
|
|
55
66
|
end
|
|
@@ -79,6 +90,63 @@ module Apia
|
|
|
79
90
|
).add_to_spec
|
|
80
91
|
end
|
|
81
92
|
|
|
93
|
+
# Adds a description of the scopes to the route specification.
|
|
94
|
+
#
|
|
95
|
+
# This method checks if the route's endpoint definition has any scopes.
|
|
96
|
+
# If there are scopes, it appends a description of the scopes to the existing route specification description.
|
|
97
|
+
# The description of the scopes is formatted as a markdown list, with each scope represented as a bullet point.
|
|
98
|
+
def add_scopes_description
|
|
99
|
+
return unless @route.endpoint.definition.scopes.any?
|
|
100
|
+
|
|
101
|
+
@route_spec[:description] =
|
|
102
|
+
<<~DESCRIPTION
|
|
103
|
+
#{@route_spec[:description]}
|
|
104
|
+
## Scopes
|
|
105
|
+
#{@route.endpoint.definition.scopes.map do |scope|
|
|
106
|
+
"- `#{scope}`"
|
|
107
|
+
end.join("\n")}
|
|
108
|
+
DESCRIPTION
|
|
109
|
+
|
|
110
|
+
@spec[:security].each do |auth|
|
|
111
|
+
auth.each_key do |key|
|
|
112
|
+
scope_prefix = @spec[:components][:securitySchemes][key][:"x-scope-prefix"]
|
|
113
|
+
next unless scope_prefix.present?
|
|
114
|
+
|
|
115
|
+
@route_spec[:description] =
|
|
116
|
+
<<~DESCRIPTION
|
|
117
|
+
#{@route_spec[:description]}
|
|
118
|
+
### #{key} Scopes
|
|
119
|
+
When using #{key} authentication, scopes are prefixed with `#{scope_prefix}`.
|
|
120
|
+
DESCRIPTION
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Adds scopes security to the OpenAPI path specification.
|
|
126
|
+
#
|
|
127
|
+
# This method checks if the route's endpoint definition has any scopes defined.
|
|
128
|
+
# If scopes are present, it iterates over the security schemes in the OpenAPI
|
|
129
|
+
# specification and adds the corresponding scopes to the route's security section.
|
|
130
|
+
#
|
|
131
|
+
# @return [void]
|
|
132
|
+
def add_scopes_security
|
|
133
|
+
unless @route.endpoint.definition.scopes.any?
|
|
134
|
+
@route_spec.delete(:security)
|
|
135
|
+
return
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
@spec[:security].each do |auth|
|
|
139
|
+
auth.each_key do |key|
|
|
140
|
+
scopes = @route.endpoint.definition.scopes
|
|
141
|
+
if scope_prefix = @spec[:components][:securitySchemes][key][:"x-scope-prefix"]
|
|
142
|
+
scopes = scopes.map { |v| "#{scope_prefix}/#{v}" }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@route_spec[:security] << { key => scopes }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
82
150
|
# It's worth creating a 'nice' operationId for each route, as this is used as the
|
|
83
151
|
# basis for the method name when calling the endpoint using a generated client.
|
|
84
152
|
def convert_route_to_id
|
|
@@ -126,6 +194,31 @@ module Apia
|
|
|
126
194
|
"#{first_part}#{last_part}"
|
|
127
195
|
end
|
|
128
196
|
|
|
197
|
+
# Returns an array of tags representing the group hierarchy for a given group.
|
|
198
|
+
#
|
|
199
|
+
# @param group [Group] The group for which to retrieve the tags.
|
|
200
|
+
# @return [Array<String>] An array of tags representing the group hierarchy.
|
|
201
|
+
def get_group_tags(group)
|
|
202
|
+
tags = []
|
|
203
|
+
current_group = group
|
|
204
|
+
|
|
205
|
+
while current_group
|
|
206
|
+
# Add tags to the spec global tags if they don't already exist
|
|
207
|
+
# Include a description if the group has one.
|
|
208
|
+
unless @spec[:tags].any? { |t| t[:name] == current_group.name }
|
|
209
|
+
global_tag = { name: current_group.name }
|
|
210
|
+
global_tag[:description] = current_group.description if current_group.description
|
|
211
|
+
@spec[:tags] << global_tag
|
|
212
|
+
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
tags.unshift(current_group.name)
|
|
216
|
+
current_group = current_group.parent
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
tags
|
|
220
|
+
end
|
|
221
|
+
|
|
129
222
|
end
|
|
130
223
|
end
|
|
131
224
|
end
|
data/lib/apia/open_api/rack.rb
CHANGED
|
@@ -29,6 +29,18 @@ module Apia
|
|
|
29
29
|
@options[:base_url] || "https://api.example.com/api/v1"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
def security_schemes
|
|
33
|
+
@options[:security_schemes] || {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def external_docs
|
|
37
|
+
@options[:external_docs] || {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def info
|
|
41
|
+
@options[:info] || {}
|
|
42
|
+
end
|
|
43
|
+
|
|
32
44
|
def call(env)
|
|
33
45
|
if @options[:hosts]&.none? { |host| host == env["HTTP_HOST"] }
|
|
34
46
|
return @app.call(env)
|
|
@@ -38,10 +50,15 @@ module Apia
|
|
|
38
50
|
return @app.call(env)
|
|
39
51
|
end
|
|
40
52
|
|
|
41
|
-
specification = Specification.new(api_class, base_url, @options[:name]
|
|
53
|
+
specification = Specification.new(api_class, base_url, @options[:name],
|
|
54
|
+
{
|
|
55
|
+
info: info,
|
|
56
|
+
external_docs: external_docs,
|
|
57
|
+
security_schemes: security_schemes
|
|
58
|
+
})
|
|
42
59
|
body = specification.json
|
|
43
60
|
|
|
44
|
-
[200, { "
|
|
61
|
+
[200, { "content-type" => "application/json", "content-length" => body.bytesize.to_s }, [body]]
|
|
45
62
|
end
|
|
46
63
|
|
|
47
64
|
end
|
|
@@ -14,21 +14,33 @@ module Apia
|
|
|
14
14
|
|
|
15
15
|
OPEN_API_VERSION = "3.0.0" # The Ruby client generator currently only supports v3.0.0 https://openapi-generator.tech/
|
|
16
16
|
|
|
17
|
-
def initialize(api, base_url, name)
|
|
17
|
+
def initialize(api, base_url, name, additions = {})
|
|
18
|
+
default_additions = { info: {}, external_docs: {}, security_schemes: {} }
|
|
19
|
+
additions = default_additions.merge(additions)
|
|
20
|
+
|
|
18
21
|
@api = api
|
|
19
22
|
@base_url = base_url
|
|
20
23
|
@name = name || "Core" # will be suffixed with 'Api' and used in the client generator
|
|
21
24
|
@spec = {
|
|
22
25
|
openapi: OPEN_API_VERSION,
|
|
23
|
-
info:
|
|
26
|
+
info: additions[:info],
|
|
27
|
+
externalDocs: additions[:external_docs],
|
|
24
28
|
servers: [],
|
|
25
29
|
paths: {},
|
|
26
30
|
components: {
|
|
27
31
|
schemas: {}
|
|
28
32
|
},
|
|
29
|
-
security: []
|
|
33
|
+
security: [],
|
|
34
|
+
tags: [],
|
|
35
|
+
"x-tagGroups": []
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
if @spec[:externalDocs].nil? || @spec[:externalDocs].empty?
|
|
39
|
+
@spec.delete(:externalDocs)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
add_additional_security_schemes(additions[:security_schemes])
|
|
43
|
+
|
|
32
44
|
# path_ids is used to keep track of all the IDs of all the paths we've generated, to avoid duplicates
|
|
33
45
|
# refer to the Path object for more info
|
|
34
46
|
@path_ids = []
|
|
@@ -41,20 +53,31 @@ module Apia
|
|
|
41
53
|
|
|
42
54
|
private
|
|
43
55
|
|
|
56
|
+
def sort_hash_by_nested_tag(hash)
|
|
57
|
+
hash.sort_by do |_, nested_hash|
|
|
58
|
+
nested_hash.values.first[:tags]&.first
|
|
59
|
+
end.to_h
|
|
60
|
+
end
|
|
61
|
+
|
|
44
62
|
def build_spec
|
|
45
63
|
add_info
|
|
46
64
|
add_servers
|
|
47
|
-
add_paths
|
|
48
65
|
add_security
|
|
66
|
+
add_paths
|
|
67
|
+
add_tag_groups
|
|
68
|
+
|
|
69
|
+
@spec[:paths] = sort_hash_by_nested_tag(@spec[:paths])
|
|
49
70
|
end
|
|
50
71
|
|
|
51
72
|
def add_info
|
|
52
73
|
title = @api.definition.name || @api.definition.id
|
|
53
|
-
@spec[:info] =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
@spec[:info][:version] = "1.0.0" if @spec[:info][:version].nil?
|
|
75
|
+
@spec[:info][:title] = title if @spec[:info][:title].nil?
|
|
76
|
+
|
|
77
|
+
return unless @spec[:info][:description].nil?
|
|
78
|
+
|
|
79
|
+
@spec[:info][:description] =
|
|
80
|
+
@api.definition.description || "Welcome to the documentation for the #{title}"
|
|
58
81
|
end
|
|
59
82
|
|
|
60
83
|
def add_servers
|
|
@@ -85,6 +108,71 @@ module Apia
|
|
|
85
108
|
end
|
|
86
109
|
end
|
|
87
110
|
|
|
111
|
+
def get_tag_group_index(tag)
|
|
112
|
+
@spec[:"x-tagGroups"].each_with_index do |group, index|
|
|
113
|
+
return index if !group.nil? && group[:name] == tag
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Adds tag groups to the OpenAPI specification.
|
|
120
|
+
#
|
|
121
|
+
# This method iterates over the paths in the specification and adds tag groups
|
|
122
|
+
# based on the tags specified for each method.
|
|
123
|
+
# It ensures that each tag is included in the `tags` array of the specification,
|
|
124
|
+
# and creates tag groups based on the order of the tags.
|
|
125
|
+
# Tag groups are represented by the `x-tagGroups` property in the specification.
|
|
126
|
+
# Tag groups are nested groups and are used to group tags together in documentation.
|
|
127
|
+
# A Tag *must* be included in a tag group. So if it is not part of a group / has no parent tag,
|
|
128
|
+
# it will be added to a group with the same name as the tag.
|
|
129
|
+
#
|
|
130
|
+
# @return [void]
|
|
131
|
+
def add_tag_groups
|
|
132
|
+
@spec[:paths].each_value do |methods|
|
|
133
|
+
methods.each_value do |method_spec|
|
|
134
|
+
tags = method_spec[:tags]
|
|
135
|
+
|
|
136
|
+
tags.each_with_index do |tag, tag_index|
|
|
137
|
+
next if tag_index.zero?
|
|
138
|
+
|
|
139
|
+
parent_tag = tags[tag_index - 1]
|
|
140
|
+
parent_index = get_tag_group_index(parent_tag)
|
|
141
|
+
|
|
142
|
+
if parent_index.nil? && tags.size > 1
|
|
143
|
+
@spec[:"x-tagGroups"] << { name: parent_tag, tags: [parent_tag] }
|
|
144
|
+
parent_index = @spec[:"x-tagGroups"].size - 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
unless @spec[:"x-tagGroups"][parent_index][:tags].include?(tag)
|
|
148
|
+
@spec[:"x-tagGroups"][parent_index][:tags] << tag
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Set the last tag as the tag for the method
|
|
153
|
+
# After we have built the tag group.
|
|
154
|
+
method_spec[:tags] = [tags.last] if tags.any?
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
@spec[:tags].each do |tag|
|
|
159
|
+
unless @spec[:"x-tagGroups"].any? { |group| group[:tags].include?(tag[:name]) }
|
|
160
|
+
@spec[:"x-tagGroups"] << { name: tag[:name], tags: [tag[:name]] }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
@spec[:"x-tagGroups"].sort_by! { |group| group[:name] }
|
|
165
|
+
@spec[:"x-tagGroups"].each { |group| group[:tags].sort! }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def add_additional_security_schemes(security_schemes)
|
|
169
|
+
security_schemes.each do |key, value|
|
|
170
|
+
@spec[:components][:securitySchemes] ||= {}
|
|
171
|
+
@spec[:components][:securitySchemes][key] = value
|
|
172
|
+
@spec[:security] << { key => [] }
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
88
176
|
end
|
|
89
177
|
end
|
|
90
178
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: apia-open_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paul Sturgess
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2024-08-
|
|
11
|
+
date: 2024-08-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|