apia-open_api 0.1.5 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7c3d54619c699cf9edca73ebc76e68819c2656f0259adbd6d5e411d4e415ca6
4
- data.tar.gz: f31e758e511dd57af96896cd9ca956e6455c29747fc8389d5bda59a6c5938f3d
3
+ metadata.gz: ce32b2c3d98cff03c533669e56c860dcda6b97a0deb554d423f8c208645ea87a
4
+ data.tar.gz: 9f9d5b952f2cc7141a0c207fa8d43c9d5d7a2a617dabb1819bb07e6a7decc691
5
5
  SHA512:
6
- metadata.gz: 1f96b6eca24cc4d97d4e248892adf62bce141c4d4cce4697c8141cd1b844465d7ce9ef33036daddee81bf908dd41b4cc1bcad2b4436bc2cd1aef431e8fba8602
7
- data.tar.gz: 1754bd390af23a0c88658c9a58b7c367f1743df4021a2f47c186720b3b3ff8235be743adaf62d0a3613502ae22267264f6aea71ae44ab716ba5ca8b86257345d
6
+ metadata.gz: 5b83929f1bc986f6ce7d8d736f30b76992868bd6442d72b9f8456d83537bc91b1b8d4297ad94655aaf86720d40468305215f2e28fcd61f63253ee9d24fc59874
7
+ data.tar.gz: 13c108fcb4596a5912781732192e4bac034c6dd7a441e951edff6b740515e188a3ee03ba73cb63c518f235bff5a57b09a0dd3a8f3dcff77f79ccfb81edfbb61c
data/Gemfile CHANGED
@@ -5,12 +5,13 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in apia-open_api.gemspec
6
6
  gemspec
7
7
 
8
- gem "apia", "~> 3.5"
8
+ gem "apia", "~> 3.7"
9
9
  gem "rake", "~> 13.0"
10
10
  gem "rspec", "~> 3.0"
11
11
  gem "rubocop", "~> 1.21"
12
12
 
13
13
  group :test do
14
+ gem "openapi3_parser"
14
15
  gem "pry"
15
16
  gem "simplecov", require: false
16
17
  end
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Apia OpenAPI Specification
2
2
 
3
- This gem can generate an [OpenAPI](https://www.openapis.org/) compatible schema from an API implemented using [Apia](https://github.com/krystal/apia).
3
+ This gem can generate an [OpenAPI](https://www.openapis.org/) compatible schema from an API implemented using [Apia](https://github.com/apiaframework/apia).
4
4
 
5
5
  This gem is in the early phases of development and breaking changes may be introduced whilst the gem is in the 0.1.x version range.
6
6
 
@@ -12,7 +12,7 @@ Install the gem and add to the application's Gemfile by executing:
12
12
 
13
13
  ## Usage
14
14
 
15
- The schema can be mounted in much the same way as an [Apia API](https://github.com/krystal/apia) itself.
15
+ The schema can be mounted in much the same way as an [Apia API](https://github.com/apiaframework/apia) itself.
16
16
 
17
17
  For example, for a Ruby on Rails application:
18
18
 
@@ -85,7 +85,7 @@ You can also run `bin/console` for an interactive prompt that will allow you to
85
85
 
86
86
  ## Contributing
87
87
 
88
- Bug reports and pull requests are welcome on GitHub at https://github.com/krystal/apia-open_api.
88
+ Bug reports and pull requests are welcome on GitHub at https://github.com/apiaframework/apia-open_api.
89
89
 
90
90
  ## License
91
91
 
@@ -8,13 +8,13 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Paul Sturgess"]
9
9
 
10
10
  spec.summary = "Apia OpenAPI spec generator"
11
- spec.homepage = "https://github.com/krystal/apia-openapi"
11
+ spec.homepage = "https://github.com/apiaframework/apia-openapi"
12
12
  spec.license = "MIT"
13
13
  spec.required_ruby_version = ">= 2.7.0"
14
14
 
15
15
  spec.metadata["homepage_uri"] = spec.homepage
16
- spec.metadata["source_code_uri"] = "https://github.com/krystal/apia-openapi"
17
- spec.metadata["changelog_uri"] = "https://github.com/krystal/apia-openapi/changelog.md"
16
+ spec.metadata["source_code_uri"] = "https://github.com/apiaframework/apia-openapi"
17
+ spec.metadata["changelog_uri"] = "https://github.com/apiaframework/apia-openapi/changelog.md"
18
18
 
19
19
  spec.metadata["rubygems_mfa_required"] = "false" # rubocop:disable Gemspec/RequireMFA (enabling MFA means we cannot auto publish via the CI)
20
20
 
@@ -52,17 +52,26 @@ module Apia
52
52
  schema
53
53
  end
54
54
 
55
- def generate_schema_ref(definition, id: nil, **schema_opts)
55
+ def generate_schema_ref(definition, id: nil, sibling_props: false, **schema_opts)
56
56
  id ||= generate_id_from_definition(definition.type.klass.definition)
57
57
  success = add_to_components_schemas(definition, id, **schema_opts)
58
58
 
59
- if success
59
+ # sibling_props indicates we want to allow sibling properties (typically setting nullable: true)
60
+ # In OpenAPI 3.0 sibling properties are not allowed for $refs (but are allowed in 3.1)
61
+ # Using allOf is a workaround to allow us to set a ref as `nullable` in OpenAPI 3.0
62
+ if success && sibling_props
63
+ {
64
+ allOf: [{ "$ref": "#/components/schemas/#{id}" }]
65
+ }
66
+ elsif success
60
67
  { "$ref": "#/components/schemas/#{id}" }
61
68
  else # no properties were defined, so just declare an object with unknown properties
62
69
  { type: "object" }
63
70
  end
64
71
  end
65
72
 
73
+ # Converts the definition id to a short version:
74
+ # e.g. CoreAPI/Objects/TimeZone => TimeZone
66
75
  def generate_id_from_definition(definition)
67
76
  definition.id.split("/").last
68
77
  end
@@ -36,6 +36,7 @@ module Apia
36
36
  @route_spec = route_spec
37
37
  @api_authenticator = api_authenticator
38
38
  @http_status = @endpoint.definition.http_status
39
+ @response_type = @endpoint.definition.response_type
39
40
  end
40
41
 
41
42
  def add_to_spec
@@ -46,9 +47,14 @@ module Apia
46
47
  private
47
48
 
48
49
  def add_sucessful_response_schema
49
- content_schema = {
50
- properties: generate_properties_for_successful_response
51
- }
50
+ if @response_type == Apia::Response::PLAIN
51
+ content_schema = { type: "string" }
52
+ else
53
+ content_schema = {
54
+ properties: generate_properties_for_successful_response
55
+ }
56
+ end
57
+
52
58
  required_fields = @endpoint.definition.fields.select { |_, field| field.condition.nil? }
53
59
  content_schema[:required] = required_fields.keys if required_fields.any?
54
60
 
@@ -56,7 +62,7 @@ module Apia
56
62
  "#{@http_status}": {
57
63
  description: @endpoint.definition.description || "",
58
64
  content: {
59
- "application/json": {
65
+ @response_type => {
60
66
  schema: content_schema
61
67
  }
62
68
  }
@@ -98,15 +104,15 @@ module Apia
98
104
  else
99
105
  # We assume the partially selected attributes must be present in all of the polymorph options
100
106
  # and that each option returns the same data type for that attribute.
101
- # The same 'allOf workaround' is used here as for objects and enums below.
102
107
  ref = generate_schema_ref(
103
108
  field.type.klass.definition.options.values.first,
104
109
  id: generate_field_id(field_name),
110
+ sibling_props: field.description.present? || field.null?,
105
111
  endpoint: @endpoint,
106
112
  path: [field]
107
113
  )
108
114
 
109
- properties[field_name] = { allOf: [ref] }
115
+ properties[field_name] = ref
110
116
  end
111
117
  properties[field_name][:description] = field.description if field.description.present?
112
118
  end
@@ -135,28 +141,32 @@ module Apia
135
141
  properties[field_name][:description] = field.description if field.description.present?
136
142
  end
137
143
 
138
- # Using allOf is a 'workaround' so that we can include a description for the field
139
- # In OpenAPI 3.0 sibling properties are not allowed for $refs (but are allowed in 3.1)
140
144
  # We don't want to put the description on the $ref itself because the description is
141
145
  # specific to the endpoint and not necessarily applicable to all uses of the $ref.
142
146
  def build_properties_for_object_or_enum(field_name, field, properties)
143
- properties[field_name] = {}
144
- properties[field_name][:description] = field.description if field.description.present?
147
+ sibling_props = field.description.present? || field.null?
145
148
  if field_includes_all_properties?(field)
146
- ref = generate_schema_ref(field)
149
+ ref = generate_schema_ref(field, sibling_props: sibling_props)
147
150
  else
148
151
  ref = generate_schema_ref(
149
152
  field,
150
153
  id: generate_field_id(field_name),
154
+ sibling_props: sibling_props,
151
155
  endpoint: @endpoint,
152
156
  path: [field]
153
157
  )
154
158
  end
155
- properties[field_name][:allOf] = [ref]
159
+
160
+ properties[field_name] = {
161
+ description: (field.description if field.description.present?),
162
+ **ref
163
+ }.compact
156
164
  end
157
165
 
166
+ # We used to check if field.include was nil, but explicitly checking for a string is more robust.
167
+ # As some apia definitions declare `include: true`, which is not the correct way to use the option.
158
168
  def field_includes_all_properties?(field)
159
- field.include.nil?
169
+ !field.include.is_a?(String)
160
170
  end
161
171
 
162
172
  def generate_field_id(field_name)
@@ -173,7 +183,11 @@ module Apia
173
183
  d.http_status_code.to_s.to_sym
174
184
  end
175
185
 
176
- sorted_grouped_potential_errors = grouped_potential_errors.sort_by do |http_status_code, _|
186
+ deduplicated_potential_errors = grouped_potential_errors.map do |http_status_code, potential_errors|
187
+ [http_status_code, potential_errors.uniq { |error| error.code }]
188
+ end
189
+
190
+ sorted_grouped_potential_errors = deduplicated_potential_errors.sort_by do |http_status_code, _|
177
191
  http_status_code.to_s.to_i
178
192
  end
179
193
 
@@ -270,6 +284,7 @@ module Apia
270
284
  oneOf: definitions.map { |d| generate_ref("schemas", http_status_code, [d]) }
271
285
  }
272
286
 
287
+ # we don't need the ref allOf workaround here because these responses are not nullable
273
288
  schema = { "$ref": "#/components/schemas/#{one_of_id}" }
274
289
  end
275
290
 
@@ -40,7 +40,7 @@ module Apia
40
40
  @definition = definition
41
41
  @schema = schema
42
42
  @id = id
43
- @endpoint = endpoint # endpoint gets specified when we are dealing with a partial response
43
+ @endpoint = endpoint # endpoint gets specified when we are dealing with a partial response (see below)
44
44
  @path = path
45
45
  @children = []
46
46
  end
@@ -90,10 +90,10 @@ module Apia
90
90
 
91
91
  return if @children.empty?
92
92
 
93
- all_properties_included = error_definition? || enum_definition? || @endpoint.nil?
94
93
  @children.each do |child|
95
94
  next unless @endpoint.nil? || (!enum_definition? && @endpoint.include_field?(@path + [child]))
96
95
 
96
+ all_properties_included = all_properties_included_for_child?(child)
97
97
  if child.respond_to?(:array?) && child.array?
98
98
  generate_schema_for_child_array(@schema, child, all_properties_included)
99
99
  else
@@ -102,6 +102,25 @@ module Apia
102
102
  end
103
103
  end
104
104
 
105
+ # We need to check if all properties are included, because if they aren't we cannot use
106
+ # an existing schema $ref. We need to generate a new schema only for the endpoint.
107
+ # A field is considered 'partial' when declared in Apia with the `include` option, e.g.:
108
+ # ```
109
+ # field :zones,
110
+ # type: [Objects::Zone],
111
+ # include: 'id,name,permalink,data_center[reference]'
112
+ # ```
113
+ # the above example means we only want to include the id, name and permalink fields for the zone
114
+ # and data_center is a nested object in zone, which should only include reference.
115
+ # so the parent zone is a partial object and the child data_center is also a partial object.
116
+ def all_properties_included_for_child?(child)
117
+ return true if error_definition? || enum_definition? || @endpoint.nil? || !child.type.object?
118
+
119
+ child.type.klass.definition.fields.values.all? do |child_field|
120
+ @endpoint.include_field?(@path + [child, child_field])
121
+ end
122
+ end
123
+
105
124
  def generate_schema_for_child_array(schema, child, all_properties_included)
106
125
  child_schema = generate_schema_for_child({}, child, all_properties_included)
107
126
  items = child_schema.dig(:properties, child.name.to_s)
@@ -115,22 +134,23 @@ module Apia
115
134
  end
116
135
 
117
136
  def generate_schema_for_child(schema, child, all_properties_included)
137
+ nullable = child.try(:null?)
118
138
  if enum_definition?
119
139
  schema[:type] = "string"
120
140
  schema[:enum] = @children.map { |c| c[:name] }
121
141
  elsif child.type.argument_set? || child.type.enum? || child.type.polymorph?
122
142
  schema[:type] = "object"
123
143
  schema[:properties] ||= {}
124
- schema[:properties][child.name.to_s] = generate_schema_ref(child)
144
+ schema[:properties][child.name.to_s] = generate_schema_ref(child, sibling_props: nullable)
125
145
  elsif child.type.object?
126
- generate_properties_for_object(schema, child, all_properties_included)
146
+ generate_properties_for_object(schema, child, all_properties_included, nullable)
127
147
  else # scalar
128
148
  schema[:type] = "object"
129
149
  schema[:properties] ||= {}
130
150
  schema[:properties][child.name.to_s] = generate_scalar_schema(child)
131
151
  end
132
152
 
133
- if child.try(:null?)
153
+ if nullable
134
154
  schema[:properties][child.name.to_s][:nullable] = true
135
155
  end
136
156
 
@@ -141,11 +161,11 @@ module Apia
141
161
  schema
142
162
  end
143
163
 
144
- def generate_properties_for_object(schema, child, all_properties_included)
164
+ def generate_properties_for_object(schema, child, all_properties_included, nullable)
145
165
  schema[:type] = "object"
146
166
  schema[:properties] ||= {}
147
167
  if all_properties_included
148
- ref = generate_schema_ref(child)
168
+ ref = generate_schema_ref(child, sibling_props: nullable)
149
169
  else
150
170
  child_path = @path.nil? ? nil : @path + [child]
151
171
 
@@ -156,13 +176,13 @@ module Apia
156
176
  ref = generate_schema_ref(
157
177
  child,
158
178
  id: "#{truncated_id}Part_#{child.name}".camelize,
179
+ sibling_props: nullable,
159
180
  endpoint: @endpoint,
160
181
  path: child_path
161
182
  )
162
183
  end
163
184
 
164
- # Using allOf is a workaround to allow us to set a ref as `nullable` in OpenAPI 3.0
165
- schema[:properties][child.name.to_s] = { allOf: [ref] }
185
+ schema[:properties][child.name.to_s] = ref
166
186
  end
167
187
 
168
188
  end
@@ -28,6 +28,9 @@ module Apia
28
28
  },
29
29
  security: []
30
30
  }
31
+
32
+ # path_ids is used to keep track of all the IDs of all the paths we've generated, to avoid duplicates
33
+ # refer to the Path object for more info
31
34
  @path_ids = []
32
35
  build_spec
33
36
  end
@@ -60,7 +63,9 @@ module Apia
60
63
 
61
64
  def add_paths
62
65
  @api.definition.route_set.routes.each do |route|
63
- next unless route.endpoint.definition.schema? # not all routes should be documented
66
+ # not all routes should be documented
67
+ next unless route.group.nil? || route.group.schema?
68
+ next unless route.endpoint.definition.schema?
64
69
 
65
70
  Objects::Path.new(
66
71
  spec: @spec,
@@ -3,7 +3,7 @@
3
3
  module Apia
4
4
  module OpenApi
5
5
 
6
- VERSION = "0.1.5"
6
+ VERSION = "0.1.7"
7
7
 
8
8
  end
9
9
  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.5
4
+ version: 0.1.7
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-01-24 00:00:00.000000000 Z
11
+ date: 2024-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -47,13 +47,13 @@ files:
47
47
  - lib/apia/open_api/rack.rb
48
48
  - lib/apia/open_api/specification.rb
49
49
  - lib/apia/open_api/version.rb
50
- homepage: https://github.com/krystal/apia-openapi
50
+ homepage: https://github.com/apiaframework/apia-openapi
51
51
  licenses:
52
52
  - MIT
53
53
  metadata:
54
- homepage_uri: https://github.com/krystal/apia-openapi
55
- source_code_uri: https://github.com/krystal/apia-openapi
56
- changelog_uri: https://github.com/krystal/apia-openapi/changelog.md
54
+ homepage_uri: https://github.com/apiaframework/apia-openapi
55
+ source_code_uri: https://github.com/apiaframework/apia-openapi
56
+ changelog_uri: https://github.com/apiaframework/apia-openapi/changelog.md
57
57
  rubygems_mfa_required: 'false'
58
58
  post_install_message:
59
59
  rdoc_options: []