Skip to content

Commit

Permalink
Merge manually added oneOf schemas (#174)
Browse files Browse the repository at this point in the history
  • Loading branch information
hss-mateus authored Feb 17, 2024
1 parent f57f6e3 commit 3932a49
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 51 deletions.
30 changes: 25 additions & 5 deletions lib/rspec/openapi/components_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def update!(base, fresh)
# 0 1 2 ^...............................^
# ["components", "schema", "Table", "properties", "owner", "properties", "company", "$ref"]
# 0 1 2 ^...........................................^
needle = paths.slice(2, paths.size - 3)
needle = paths.reject { |path| path.is_a?(Integer) || path == 'oneOf' }
needle = needle.slice(2, needle.size - 3)
nested_schema = fresh_schemas.dig(*needle)

# Skip if the property using $ref is not found in the parent schema. The property may be removed.
Expand All @@ -44,27 +45,36 @@ def build_fresh_schemas(references, base, fresh)
references.inject({}) do |acc, paths|
ref_link = dig_schema(base, paths)['$ref']
schema_name = ref_link.gsub('#/components/schemas/', '')
schema_body = dig_schema(fresh, paths)
schema_body = dig_schema(fresh, paths.reject { |path| path.is_a?(Integer) })

RSpec::OpenAPI::SchemaMerger.merge!(acc, { schema_name => schema_body })
end
end

def dig_schema(obj, paths)
obj.dig(*paths, 'schema', 'items') || obj.dig(*paths, 'schema')
item_schema = obj.dig(*paths, 'schema', 'items')
object_schema = obj.dig(*paths, 'schema')
one_of_schema = obj.dig(*paths.take(paths.size - 1), 'schema', 'oneOf', paths.last)

item_schema || object_schema || one_of_schema
end

def paths_to_top_level_refs(base)
request_bodies = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.requestBody.content.application/json')
responses = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.responses.*.content.application/json')
(request_bodies + responses).select do |paths|
dig_schema(base, paths)&.dig('$ref')&.start_with?('#/components/schemas/')
(request_bodies + responses).flat_map do |paths|
object_paths = find_object_refs(base, paths)
one_of_paths = find_one_of_refs(base, paths)

object_paths || one_of_paths || []
end
end

def find_non_top_level_nested_refs(base, generated_names)
nested_refs = [
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.$ref'),
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.items.$ref'),
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'oneOf.*.$ref'),
]
# Reject already-generated schemas to reduce unnecessary loop
nested_refs.reject do |paths|
Expand All @@ -73,4 +83,14 @@ def find_non_top_level_nested_refs(base, generated_names)
generated_names.include?(schema_name)
end
end

def find_one_of_refs(base, paths)
dig_schema(base, paths)&.dig('oneOf')&.map&.with_index do |schema, index|
paths + [index] if schema&.dig('$ref')&.start_with?('#/components/schemas/')
end&.compact
end

def find_object_refs(base, paths)
[paths] if dig_schema(base, paths)&.dig('$ref')&.start_with?('#/components/schemas/')
end
end
4 changes: 4 additions & 0 deletions lib/rspec/openapi/hash_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ def paths_to_all_fields(obj)
k = k.to_s
[[k]] + paths_to_all_fields(v).map { |x| [k, *x] }
end
when Array
obj.flat_map.with_index do |value, i|
[[i]] + paths_to_all_fields(value).map { |x| [i, *x] }
end
else
[]
end
Expand Down
41 changes: 41 additions & 0 deletions lib/rspec/openapi/schema_merger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ def normalize_keys(spec)
#
# TODO: Should we probably force-merge `summary` regardless of manual modifications?
def merge_schema!(base, spec)
if (options = base['oneOf'])
merge_closest_match!(options, spec)

return base
end

spec.each do |key, value|
if base[key].is_a?(Hash) && value.is_a?(Hash)
merge_schema!(base[key], value) unless base[key].key?('$ref')
Expand Down Expand Up @@ -67,4 +73,39 @@ def merge_parameters(base, key, value)
all_parameters.uniq! { |param| param.slice('name', 'in') }
base[key] = all_parameters
end

SIMILARITY_THRESHOLD = 0.5

def merge_closest_match!(options, spec)
score, option = options.map { |option| [similarity(option, spec), option] }.max_by(&:first)

return if option&.key?('$ref')

if score.to_f > SIMILARITY_THRESHOLD
merge_schema!(option, spec)
else
options.push(spec)
end
end

def similarity(first, second)
return 1 if first == second

score =
case [first.class, second.class]
when [Array, Array]
(first & second).size / [first.size, second.size].max.to_f
when [Hash, Hash]
return 1 if first.merge(second).key?('$ref')

intersection = first.keys & second.keys
total_size = [first.size, second.size].max.to_f

intersection.sum { |key| similarity(first[key], second[key]) } / total_size
else
0
end

score.finite? ? score : 0
end
end
100 changes: 74 additions & 26 deletions spec/rails/doc/smart/expected.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,46 @@ paths:
description:
type: string
database:
type: object
properties:
id:
type: integer
name:
type: string
required:
- id
- name
discriminator:
propertyName: name
oneOf:
- type: array
items:
type: object
properties:
name:
type: string
host:
type: string
port:
type: integer
user:
type: string
schema:
type: string
- type: object
properties:
host:
type: string
port:
type: integer
user:
type: string
schema:
type: string
required:
- host
- port
- schema
- type: object
properties:
id:
type: integer
name:
type: string
required:
- id
- name
null_sample:
nullable: true
storage_size:
Expand Down Expand Up @@ -161,7 +192,9 @@ paths:
content:
application/json:
schema:
"$ref": "#/components/schemas/PostUsersRequest"
oneOf:
- "$ref": "#/components/schemas/PostUsersRequest"
- type: string
example:
name: alice
avatar_url: "https://example.com/avatar.png"
Expand Down Expand Up @@ -293,22 +326,37 @@ components:
- name
- column_type
User:
type: object
properties:
name:
type: string
relations:
type: object
properties:
avatar:
"$ref": "#/components/schemas/Avatar"
pets:
type: array
items:
"$ref": "#/components/schemas/Pet"
required:
- avatar
- pets
discriminator:
propertyName: name
oneOf:
- type: object
properties:
name:
type: string
foo:
type: string
bar:
type: string
baz:
type: string
quux:
type: string
- type: object
properties:
name:
type: string
relations:
type: object
properties:
avatar:
"$ref": "#/components/schemas/Avatar"
pets:
type: array
items:
"$ref": "#/components/schemas/Pet"
required:
- avatar
- pets
Avatar:
type: object
properties:
Expand Down
82 changes: 62 additions & 20 deletions spec/rails/doc/smart/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,37 @@ paths:
description:
type: string
database:
type: object
properties:
id:
type: integer
name:
type: string
discriminator:
propertyName: name
oneOf:
- type: array
items:
type: object
properties:
name:
type: string
host:
type: string
port:
type: integer
user:
type: string
schema:
type: string
- type: object
properties:
host:
type: string
port:
type: integer
user:
type: string
schema:
type: string
required:
- host
- port
- schema
null_sample:
nullable: true
storage_size:
Expand Down Expand Up @@ -212,7 +237,9 @@ paths:
content:
application/json:
schema:
"$ref": "#/components/schemas/PostUsersRequest"
oneOf:
- "$ref": "#/components/schemas/PostUsersRequest"
- type: string
responses:
'201':
description: returns a user
Expand Down Expand Up @@ -279,16 +306,31 @@ components:
id:
type: integer
User:
type: object
properties:
name:
type: string
relations:
type: object
properties:
avatar:
"$ref": "#/components/schemas/Avatar"
pets:
type: array
items:
"$ref": "#/components/schemas/Pet"
discriminator:
propertyName: name
oneOf:
- type: object
properties:
name:
type: string
foo:
type: string
bar:
type: string
baz:
type: string
quux:
type: string
- type: object
properties:
name:
type: string
relations:
type: object
properties:
avatar:
"$ref": "#/components/schemas/Avatar"
pets:
type: array
items:
"$ref": "#/components/schemas/Pet"

0 comments on commit 3932a49

Please sign in to comment.