apia-open_api 0.1.5 → 0.1.7

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 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: []