From a18a5adbe9674b071c1fe9ce48dafb321edc704d Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Thu, 30 May 2024 21:29:02 +0200 Subject: [PATCH] Remove Operation --- CHANGELOG.md | 6 ++ lib/openapi_first/doc.rb | 10 ++-- lib/openapi_first/operation.rb | 104 -------------------------------- lib/openapi_first/request.rb | 8 +-- spec/doc_spec.rb | 11 ++-- spec/operation_spec.rb | 105 --------------------------------- 6 files changed, 17 insertions(+), 227 deletions(-) delete mode 100644 lib/openapi_first/operation.rb delete mode 100644 spec/operation_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a6c72417..46644a7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ - after_request_body_property_validation - after_request_parameter_property_validation +### Breaking Changes + +- `Definition` was renamed to `Doc` +- `Operation` was removed +- Instead of `Doc#operations` you can use `Doc#routes`, which returns a list of routes. Routes have a `#path`, `#request_method`, `#requests` and `#responses`. + ## 1.4.3 - Allow using json_schemer 2...3 diff --git a/lib/openapi_first/doc.rb b/lib/openapi_first/doc.rb index 85637671..d341b248 100644 --- a/lib/openapi_first/doc.rb +++ b/lib/openapi_first/doc.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require_relative 'operation' require_relative 'failure' require_relative 'router' require_relative 'request' @@ -11,8 +10,6 @@ module OpenapiFirst # This is returned by OpenapiFirst.load. class Doc attr_reader :filepath, :openapi_version, :config, :paths - # Openapi 3 specific - attr_reader :operations REQUEST_METHODS = %w[get head post put patch delete trace options].freeze private_constant :REQUEST_METHODS @@ -25,7 +22,7 @@ def initialize(resolved, filepath = nil) @openapi_version = detect_version(resolved) @router = Router.new - @operations = resolved['paths'].flat_map do |path, path_item_object| + resolved['paths'].each do |path, path_item_object| path_item_object.slice(*REQUEST_METHODS).keys.map do |request_method| operation_object = path_item_object[request_method] path_item_parameters = path_item_object['parameters'] @@ -46,7 +43,6 @@ def initialize(resolved, filepath = nil) response_content_type: response.content_type ) end - Operation.new(path, request_method, operation_object, path_item_parameters:) end end @paths = resolved['paths'].keys @@ -55,6 +51,10 @@ def initialize(resolved, filepath = nil) @config.freeze end + def routes + @router.routes + end + # Validates the request against the API description. # @param rack_request [Rack::Request] The Rack request object. # @param raise_error [Boolean] Whether to raise an error if validation fails. diff --git a/lib/openapi_first/operation.rb b/lib/openapi_first/operation.rb deleted file mode 100644 index 9c91c5b2..00000000 --- a/lib/openapi_first/operation.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -require 'forwardable' -require 'openapi_parameters' - -module OpenapiFirst - # Represents an operation object in the OpenAPI 3.X specification. - # Use this class to access information about the operation. Use `#[key]` to read the raw data. - # When using the middleware you can access the operation object via `env[OpenapiFirst::REQUEST].operation`. - class Operation - extend Forwardable - - def_delegators :operation_object, :[], :dig - - def initialize(path, request_method, operation_object, path_item_parameters:) - @path = path - @method = request_method - @operation_object = operation_object - @path_item_parameters = path_item_parameters - end - - # @return [String] path The path of the operation as in the API description. - attr_reader :path - - # @attr_reader [String] method The (downcased) request method of the operation. - # Example: "get" - attr_reader :method - alias request_method method - - # Returns the operation ID as defined in the API description. - # @return [String, nil] - def operation_id - operation_object['operationId'] - end - - # Returns a unique name for this operation. Used for generating error messages. - # @visibility private - def name - @name ||= "#{method.upcase} #{path}".freeze - end - - # These return [Hash] - %i[path query cookie].each do |location| - define_method(:"#{location}_parameters") do - all_parameters[location] - end - end - - IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze - private_constant :IGNORED_HEADERS - - def header_parameters - all_parameters[:header]&.reject { IGNORED_HEADERS.include?(_1['name']) } - end - - # These return a Schema instance for each type of parameters - %i[path query header cookie].each do |location| - define_method(:"#{location}_schema") do - build_parameters_schema(send(:"#{location}_parameters")) - end - end - - private - - WRITE_METHODS = Set.new(%w[post put patch delete]).freeze - private_constant :WRITE_METHODS - - IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze - private_constant :IGNORED_HEADERS - - attr_reader :operation_object - - def build_parameters_schema(parameters) - return unless parameters - - properties = {} - required = [] - parameters.each do |parameter| - schema = parameter['schema'] - name = parameter['name'] - properties[name] = schema if schema - required << name if parameter['required'] - end - - { - 'properties' => properties, - 'required' => required - } - end - - def all_parameters - @all_parameters ||= begin - result = {} - @path_item_parameters&.each do |parameter| - (result[parameter['in'].to_sym] ||= []) << parameter - end - self['parameters']&.each do |parameter| - (result[parameter['in'].to_sym] ||= []) << parameter - end - result - end - end - end -end diff --git a/lib/openapi_first/request.rb b/lib/openapi_first/request.rb index e5e754b8..c24e7d80 100644 --- a/lib/openapi_first/request.rb +++ b/lib/openapi_first/request.rb @@ -28,7 +28,7 @@ def initialize(path:, request_method:, operation_id:, parameters:, content_type: @validator = RequestValidator.new(self, hooks:, openapi_version:) end - attr_reader :content_type, :content_schema, :operation_id, :request_method + attr_reader :content_type, :content_schema, :operation_id, :request_method, :path def validate(request, route_params:) parsed = @parser.parse(request, route_params:) @@ -47,12 +47,6 @@ def required_request_body? @required_request_body end - def inspect - result = "Request:#{request_method} #{path}" - result << ":#{content_type}" if content_type - result - end - private IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze diff --git a/spec/doc_spec.rb b/spec/doc_spec.rb index d2ad1e31..405c5e7f 100644 --- a/spec/doc_spec.rb +++ b/spec/doc_spec.rb @@ -200,13 +200,12 @@ def build_request(path, method: 'GET') end end - describe '#operations' do - let(:definition) { OpenapiFirst.load('./spec/data/petstore.yaml') } + describe '#routes' do + let(:definition) { OpenapiFirst.load('./spec/data/train-travel-api/openapi.yaml') } - it 'returns a list of operations' do - expect(definition.operations.length).to eq 3 - expected_ids = %w[listPets createPets showPetById] - expect(definition.operations.map(&:operation_id)).to eq expected_ids + it 'returns routes' do + routes = definition.routes.map { |route| "#{route.request_method} #{route.path}" } + expect(routes).to match_array ['GET /stations', 'GET /trips', 'GET /bookings', 'POST /bookings', 'GET /bookings/{bookingId}', 'DELETE /bookings/{bookingId}', 'POST /bookings/{bookingId}/payment'] end end diff --git a/spec/operation_spec.rb b/spec/operation_spec.rb deleted file mode 100644 index e5d84748..00000000 --- a/spec/operation_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe OpenapiFirst::Operation do - let(:operation) do - described_class.new('/pets/{pet_id}', 'post', operation_object, path_item_parameters: {}) - end - - let(:operation_object) do - { - 'operationId' => 'create_pet', - 'parameters' => [ - { 'name' => 'limit', 'in' => 'query', 'schema' => { 'type' => 'integer' } } - ], - 'requestBody' => { - 'required' => true, - 'content' => { - 'application/json' => { - 'schema' => { 'type' => 'object' } - }, - 'application/xml' => { - 'schema' => { 'type' => 'object' } - } - } - }, - 'responses' => { - '200' => { - 'content' => { - 'application/json' => { - 'schema' => { 'type' => 'array' } - }, - 'application/vnd.api+json' => { - 'schema' => { 'type' => 'object', required: ['data'] } - } - } - }, - 'default' => { - 'description' => 'unexpected error' - } - } - } - end - - describe '#operation_id' do - it 'returns the operationId' do - expect(operation.operation_id).to eq 'create_pet' - end - end - - describe '#query_parameters' do - it 'returns the query parameters of path and operation level' do - path_item_parameters = [ - { - 'in' => 'query', - 'name' => 'other' - } - ] - operation = described_class.new('/pets/{pet_id}', 'get', operation_object, path_item_parameters:) - expect(operation.query_parameters.map { |p| p['name'] }).to eq %w[other limit] - end - end - - describe '#path_parameters' do - it 'returns the path parameters on operation level' do - expect(operation.path_parameters).to be_nil - end - end - - describe '#header_parameters' do - it 'returns the header parameters' do - expect(operation.header_parameters).to be_nil - end - end - - describe '#cookie_parameters' do - it 'returns cookie parameters' do - expect(operation.cookie_parameters).to be_nil - end - end - - describe '#[]' do - it 'allows to access the resolved hash' do - expect(operation['operationId']).to eq 'create_pet' - expect(operation['responses'].dig('200', 'content', 'application/json', 'schema', 'type')).to eq 'array' - expect(operation['responses'].dig('default', 'description')).to eq 'unexpected error' - end - end - - describe '#name' do - it 'returns a human readable name' do - expect(operation.name).to eq 'POST /pets/{pet_id}' - end - end - - describe '#method' do - let(:spec) { OpenapiFirst.load('./spec/data/petstore-expanded.yaml') } - - it 'returns get' do - expect(spec.operations.first.method).to eq 'get' - end - - it 'returns post' do - expect(spec.operations[1].method).to eq 'post' - end - end -end