diff --git a/lib/openapi3_parser/node_factory/paths.rb b/lib/openapi3_parser/node_factory/paths.rb index 715e064..8d2074a 100644 --- a/lib/openapi3_parser/node_factory/paths.rb +++ b/lib/openapi3_parser/node_factory/paths.rb @@ -38,6 +38,7 @@ def build_node(data, node_context) def validate(validatable) paths = validatable.input.keys.grep_v(NodeFactory::EXTENSION_REGEX) validate_paths(validatable, paths) + validate_operation_ids(validatable) end def validate_paths(validatable, paths) @@ -66,6 +67,31 @@ def conflicting_paths(paths) grouped_paths.select { |group| group.size > 1 }.flatten end + + def validate_operation_ids(validatable) + operation_ids = [] + + validatable.input.each_value do |path_item| + next unless path_item.is_a?(Hash) + + %w[get put post delete options head patch trace].each do |method| + operation = path_item[method] + next unless operation.is_a?(Hash) + + operation_id = operation["operationId"] + operation_ids << operation_id if operation_id + end + end + + counts = operation_ids.each_with_object(Hash.new(0)) { |id, hash| hash[id] += 1 } + dupes = counts.select { |_id, count| count > 1 }.keys + + return if dupes.empty? + + validatable.add_error( + "Duplicate operationId values: #{dupes.sort.join(', ')}" + ) + end end end end diff --git a/spec/lib/openapi3_parser/node_factory/openapi_spec.rb b/spec/lib/openapi3_parser/node_factory/openapi_spec.rb index 0bddbe1..f2e6f76 100644 --- a/spec/lib/openapi3_parser/node_factory/openapi_spec.rb +++ b/spec/lib/openapi3_parser/node_factory/openapi_spec.rb @@ -59,6 +59,78 @@ end end + describe "validating operationIds" do + it "is valid when operationIds are unique" do + factory_context = create_node_factory_context( + minimal_openapi_definition.merge( + "paths" => { + "/pets" => { + "get" => { + "operationId" => "listPets", + "responses" => { "200" => { "description" => "Success" } } + } + }, + "/pets/{id}" => { + "get" => { + "operationId" => "getPet", + "responses" => { "200" => { "description" => "Success" } } + } + } + } + ) + ) + expect(described_class.new(factory_context)).to be_valid + end + + it "is valid when some operations have no operationId" do + factory_context = create_node_factory_context( + minimal_openapi_definition.merge( + "paths" => { + "/pets" => { + "get" => { + "operationId" => "listPets", + "responses" => { "200" => { "description" => "Success" } } + } + }, + "/pets/{id}" => { + "get" => { + "responses" => { "200" => { "description" => "Success" } } + } + } + } + ) + ) + expect(described_class.new(factory_context)).to be_valid + end + + it "is invalid when operationIds are duplicated" do + factory_context = create_node_factory_context( + minimal_openapi_definition.merge( + "paths" => { + "/pets" => { + "get" => { + "operationId" => "getPets", + "responses" => { "200" => { "description" => "Success" } } + } + }, + "/pets/{id}" => { + "get" => { + "operationId" => "getPets", + "responses" => { "200" => { "description" => "Success" } } + } + } + } + ) + ) + + instance = described_class.new(factory_context) + expect(instance).not_to be_valid + expect(instance) + .to have_validation_error("#/paths") + .with_message("Duplicate operationId values: getPets") + end + end + describe "default values for servers" do it "contains a basic root server when servers input is nil" do node = create_node(minimal_openapi_definition.merge({ "servers" => nil }))