Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
/tmp/
/benchmarks/profiles
.tool-versions
*coverage.html
# rspec failure tracking
.rspec_status
Gemfile.*.lock
Expand Down
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Metrics/BlockLength:
Exclude:
- "spec/**/*.rb"
- "*.gemspec"
Metrics/ClassLength:
Enabled: false
Layout/LineLength:
Exclude:
- "spec/**/*.rb"
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## Unreleased
- Added `OpenapiFirst::Test::Coverage::HtmlReporter` that writes a self-contained HTML coverage report to `coverage/openapi_coverage.html`.
- Renamed `OpenapiFirst::Test::Coverage::TerminalFormatter` to `TerminalReporter` and `Test::Configuration#coverage_formatter(_options)` to `#coverage_reporter(_options)`. The old names continue to work but emit a deprecation warning.
- Apps using request validation middleware can call Failure.fail! to produce an error result
- The `after_request_validation` now supports throwing a Failure, which will result in a failed request
- Added new hook `before_request_validation`. Called after a request routed to an operation but before the request is validated. You can throw via `Failure.fail!` to abort request validation immediately.
Expand Down
7 changes: 4 additions & 3 deletions examples/rails_app/Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
PATH
remote: ../..
specs:
openapi_first (3.0.0)
openapi_first (3.3.1)
drb (~> 2.0)
hana (~> 1.3)
json_schemer (>= 2.1, < 3.0)
openapi_parameters (>= 0.7.0, < 2.0)
openapi_parameters (>= 0.10.0, < 2.0)
rack (>= 2.2, < 4.0)

PATH
Expand Down Expand Up @@ -154,7 +155,7 @@ GEM
racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4)
openapi_parameters (0.8.0)
openapi_parameters (0.10.0)
rack (>= 2.2)
pp (0.6.2)
prettyprint
Expand Down
1 change: 1 addition & 0 deletions examples/rails_app/test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

require 'openapi_first'
OpenapiFirst::Test.setup do |test|
test.coverage_reporter = OpenapiFirst::Test::Coverage::HtmlReporter
test.register Rails.root.join('../../spec/data/train-travel-api/openapi.yaml')
test.register Rails.root.join('../../spec/data/attachments_openapi.yaml'), as: :attachments
test.register Rails.root.join('../../spec/data/weather_openapi.yaml'), as: :weather
Expand Down
2 changes: 1 addition & 1 deletion lib/openapi_first/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
module OpenapiFirst
# Builds parts of a Definition
# This knows how to read a resolved OpenAPI document and build {Request} and {Response} objects.
class Builder # rubocop:disable Metrics/ClassLength
class Builder
REQUEST_METHODS = %w[get head post put patch delete trace options query].freeze

# Builds a router from a resolved OpenAPI document.
Expand Down
5 changes: 5 additions & 0 deletions lib/openapi_first/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ def initialize(contents, filepath = nil, path_prefix = nil)
# @return [Enumerable[Router::Route]]
def_delegators :@router, :routes

# @return [String,nil] The title from the OpenAPI document's `info.title`, if any.
def title
self['info']&.[]('title')
end

# Returns a unique identifier for this API definition
# @return [String] A unique key for this API definition
def key
Expand Down
15 changes: 10 additions & 5 deletions lib/openapi_first/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ def self.handle_exit
return unless configuration.report_coverage

report_coverage(
formatter: configuration.coverage_formatter,
**configuration.coverage_formatter_options
reporter: configuration.coverage_reporter,
**configuration.coverage_reporter_options
)
return unless configuration.report_coverage == true

Expand All @@ -91,10 +91,15 @@ def self.handle_exit
end

# Print the coverage report
# @param formatter A formatter to define the report.
# @param reporter A reporter class to render the report.
# @param formatter @deprecated Alias for `reporter:`.
# @return [IO] An output where to puts the report.
def self.report_coverage(formatter: Coverage::TerminalFormatter, **)
formatter.new(**).report(Coverage.result)
def self.report_coverage(reporter: Coverage::TerminalReporter, formatter: nil, **)
if formatter
warn 'DEPRECATION WARNING: Test.report_coverage(formatter:) is deprecated, use reporter: instead.'
reporter = formatter
end
reporter.new(**).report(Coverage.result)
end

# Returns the Rack app wrapped with silent request, response validation
Expand Down
40 changes: 37 additions & 3 deletions lib/openapi_first/test/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ module Test
class Configuration
def initialize
@minimum_coverage = 100
@coverage_formatter = Coverage::TerminalFormatter
@coverage_formatter_options = {}
@coverage_reporter = Coverage::HtmlReporter
@coverage_reporter_options = {}
@skip_response_coverage = nil
@skip_coverage = nil
@response_raise_error = true
Expand All @@ -34,10 +34,34 @@ def observe(app, api: :default)
Observe.observe(app, api:)
end

attr_accessor :coverage_formatter_options, :coverage_formatter, :response_raise_error,
attr_accessor :coverage_reporter, :coverage_reporter_options, :response_raise_error,
:ignore_unknown_requests, :ignore_unknown_response_status, :minimum_coverage, :logger
attr_reader :report_coverage, :ignored_unknown_status

# @deprecated Use {#coverage_reporter} instead.
def coverage_formatter
warn_coverage_formatter_deprecation
coverage_reporter
end

# @deprecated Use {#coverage_reporter=} instead.
def coverage_formatter=(value)
warn_coverage_formatter_deprecation
self.coverage_reporter = value
end

# @deprecated Use {#coverage_reporter_options} instead.
def coverage_formatter_options
warn_coverage_formatter_deprecation
coverage_reporter_options
end

# @deprecated Use {#coverage_reporter_options=} instead.
def coverage_formatter_options=(value)
warn_coverage_formatter_deprecation
self.coverage_reporter_options = value
end

# Set ignored unknown status codes.
# @param [Array<Integer>] status Status codes that are okay not to cover in an OAD
def ignored_unknown_status=(status)
Expand Down Expand Up @@ -106,6 +130,16 @@ def raise_response_error?(validated_response, rack_request)

true
end

private

def warn_coverage_formatter_deprecation
return if @coverage_formatter_warned

warn 'DEPRECATION WARNING: Test::Configuration#coverage_formatter(_options) is deprecated, ' \
'use #coverage_reporter(_options) instead.'
@coverage_formatter_warned = true
end
end
end
end
6 changes: 5 additions & 1 deletion lib/openapi_first/test/coverage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ module Test
# to assess if all parts of the API description have been tested.
# Currently it does not care about unknown requests that are not part of any API description.
module Coverage
autoload :TerminalFormatter, 'openapi_first/test/coverage/terminal_formatter'
autoload :TerminalReporter, 'openapi_first/test/coverage/terminal_reporter'
autoload :HtmlReporter, 'openapi_first/test/coverage/html_reporter'

# @deprecated Use {TerminalReporter} instead.
TerminalFormatter = TerminalReporter

Result = Data.define(:plans, :coverage)

Expand Down
179 changes: 179 additions & 0 deletions lib/openapi_first/test/coverage/html_reporter.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
*, *::before, *::after { box-sizing: border-box; }

body {
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
background: #0f1117;
color: #e2e8f0;
margin: 0;
padding: 2rem;
line-height: 1.6;
}

h1 {
font-size: 1.1rem;
font-weight: 600;
margin-block: 0 1rem;
color: #f8fafc;

& .pct {
font-size: 0.95rem;
font-weight: 400;
color: #94a3b8;
margin-inline-start: 0.5rem;
}
}

details.plan {
border: 1px solid #1e293b;
padding: 1.25rem 1.5rem;
margin-block-end: 1.5rem;
background: #161b27;

&[open] { border-color: #334155; }
}

details.plan > summary {
font-size: 1rem;
font-weight: 600;
color: #cbd5e1;
cursor: pointer;
list-style: none;
display: flex;
align-items: baseline;
gap: 0.5rem;
user-select: none;

& .pct {
font-size: 0.8rem;
font-weight: 400;
color: #64748b;
text-transform: none;
letter-spacing: 0;
}

&::before {
content: '▶';
font-size: 0.6rem;
color: #475569;
transition: transform 0.15s ease;
align-self: center;
}

details[open] > &::before { transform: rotate(90deg); }
}

.plan-content {
margin-block-start: 1rem;
}

details.route {
border-inline-start: 2px solid #1e293b;
padding-inline-start: 1rem;
margin-block: 0.75rem;

&[open] { border-color: #334155; }
}

details.route > summary {
font-weight: 600;
color: #94a3b8;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.04em;
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 0.4rem;
padding-block: 0.2rem;
user-select: none;

&::before {
content: '▶';
font-size: 0.6rem;
color: #475569;
transition: transform 0.15s ease;
}

details[open] > &::before { transform: rotate(90deg); }
}

.operation-label {
font-size: 0.8rem;
font-weight: 400;
color: #64748b;
text-transform: none;
letter-spacing: 0;
}

.request-status,
.response-summary {
font-size: 0.8rem;
font-weight: 400;
text-transform: none;
letter-spacing: 0;
}

.route-content {
margin-block-start: 0.4rem;
}

ul.tasks {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.2rem;
}

ul.responses {
padding-inline-start: 1.5rem;
}

li {
font-size: 0.875rem;
padding: 0.2rem 0.4rem;
display: flex;
align-items: baseline;
gap: 0.5rem;
}

.covered { color: #4ade80; }

.uncovered {
background: #1f1315;
font-weight: 500;
}

.status {
color: #e2e8f0;
font-weight: 600;
min-width: 3ch;
}

.content-type {
color: #64748b;
font-size: 0.8rem;
}

.problem {
color: #f87171;
font-size: 0.8rem;
}

.method {
color: #7dd3fc;
font-weight: 700;
}

.path {
color: #e2e8f0;
}

.warning {
color: #fbbf24;
background: #1c1a10;
border: 1px solid #3b3010;
padding: 0.75rem 1rem;
}
Loading
Loading