Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## [Unreleased]

### Added

- [OpenAPI] Add `openapi:validate` Rake task for OpenAPI tags validation (#163).

## [1.27.0] - 2026-08-06

### Added
Expand Down
11 changes: 11 additions & 0 deletions lib/rage/openapi/openapi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,20 @@ def self.__resolve_resource(klass_str, namespace)

# @private
def self.__log_warn(log)
@__warnings << log if @__warnings
puts "[OpenAPI] WARNING: #{log}"
end

# @private
# @return [Array<String>] the warnings logged inside the block
def self.__collect_warnings
@__warnings = []
yield
@__warnings
ensure
@__warnings = nil
Comment on lines +209 to +214

@alex-rogachev alex-rogachev Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @rsamoilov
What do you think about implementing the warnings accumulator as a wrapper around Rage::OpenAPI.build? I feel like this approach works well because the additional logic doesn't add overhead to the original __log_warn implementation and remains optional, which aligns with the idea that new features shouldn't affect performance for users who don't use them.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the current approach, but I'm not entirely sure what a wrapper around Rage::OpenAPI.build would look like.

I've approved the PR and will be happy to merge it. Let me know if you'd like to rebuild it though.

end

module Nodes
end

Expand Down
5 changes: 5 additions & 0 deletions lib/rage/tasks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Rage::Tasks
class << self
def init
load_db_tasks if defined?(StandaloneMigrations)
load_rage_tasks
load_app_tasks
end

Expand Down Expand Up @@ -40,5 +41,9 @@ def configuration_file
def load_app_tasks
Dir[Rage.root.join("lib/tasks/**/*.rake")].each { |file| load file }
end

def load_rage_tasks
Dir[File.expand_path("tasks/**/*.rake", __dir__)].each { |file| load file }
end
end
end
14 changes: 14 additions & 0 deletions lib/rage/tasks/openapi.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace :openapi do
desc "Validate OpenAPI tags and fail if any warnings were produced"
task :validate do
abort "OpenAPI validation requires a booted application." unless Rage.config.internal.initialized?

warnings = Rage::OpenAPI.__collect_warnings { Rage::OpenAPI.build }

if warnings.any?
abort "OpenAPI validation failed with #{warnings.size} warning(s)."
else
puts "OpenAPI validation passed without warnings."
end
end
end
49 changes: 49 additions & 0 deletions spec/openapi/openapi_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,55 @@
end
end

describe ".__log_warn" do
it "prints a warning" do
expect { described_class.__log_warn("something went wrong") }.
to output("[OpenAPI] WARNING: something went wrong\n").to_stdout
end

it "does not retain the warning when not collecting" do
allow(described_class).to receive(:puts)

described_class.__log_warn("something went wrong")

expect(described_class.instance_variable_get(:@__warnings)).to be_nil
end
end

describe ".__collect_warnings" do
before do
allow(described_class).to receive(:puts)
end

it "returns the warnings logged inside the block" do
warnings = described_class.__collect_warnings do
described_class.__log_warn("first")
described_class.__log_warn("second")
end

expect(warnings).to eq(["first", "second"])
end

it "returns an empty array when nothing was logged" do
expect(described_class.__collect_warnings {}).to eq([])
end

it "stops collecting once the block returns" do
described_class.__collect_warnings { described_class.__log_warn("first") }
described_class.__log_warn("second")

expect(described_class.__collect_warnings {}).to eq([])
end

it "stops collecting if the block raises" do
expect {
described_class.__collect_warnings { raise "boom" }
}.to raise_error("boom")

expect(described_class.instance_variable_get(:@__warnings)).to be_nil
end
end

describe ".__type_to_spec" do
subject { described_class.__type_to_spec(type) }

Expand Down
123 changes: 123 additions & 0 deletions spec/rage/tasks_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# frozen_string_literal: true

require "rake"
require "rage/tasks"

RSpec.describe Rage::Tasks do
# `Rake.application` is a process-wide singleton owning the task registry; a fresh one per example
# keeps the registry empty and tasks re-invokable, while restoring it avoids leaking into other specs.
around do |example|
original_application = Rake.application
Rake.application = Rake::Application.new
example.run
ensure
Rake.application = original_application
end

describe ".load_rage_tasks" do
it "loads the openapi:validate task" do
expect(Rake::Task.task_defined?("openapi:validate")).to eq(false)

described_class.send(:load_rage_tasks)

expect(Rake::Task.task_defined?("openapi:validate")).to eq(true)
end
end
end

RSpec.describe "openapi:validate" do
include_context "mocked_classes"
include_context "mocked_rage_routes"

around do |example|
original_application = Rake.application
Rake.application = Rake::Application.new
Rage::Tasks.send(:load_rage_tasks)
example.run
ensure
Rake.application = original_application
end

before do
allow(Rage.config.internal).to receive(:initialized?).and_return(true)
end

let(:routes) do
{ "GET /users" => "UsersController#index" }
end

# returns the `SystemExit` error the task exited with, or `nil` if it didn't exit
subject(:invoke_task) do
Rake::Task["openapi:validate"].invoke
nil
rescue SystemExit => e
e
end

context "when the application is not booted" do
before do
allow(Rage.config.internal).to receive(:initialized?).and_return(false)
end

it "doesn't build the spec" do
expect(Rage::OpenAPI).not_to receive(:build)

expect { invoke_task }.
to output(/OpenAPI validation requires a booted application\./).to_stderr
end

it "exits with status 1" do
exit_error = nil

expect { exit_error = invoke_task }.to output.to_stderr

expect(exit_error.status).to eq(1)
end
end

context "when the spec builds without warnings" do
let_class("UsersController", parent: RageController::API) do
<<~'RUBY'
# @response { id: Integer, full_name: String }
def index
end
RUBY
end

it "prints a success message" do
expect { invoke_task }.to output(/OpenAPI validation passed without warnings\./).to_stdout
end

it "doesn't exit with an error" do
allow($stdout).to receive(:puts)

expect(invoke_task).to be_nil
end
end

context "when the build produces warnings" do
let_class("UsersController", parent: RageController::API) do
<<~'RUBY'
# @response UnknownResource
def index
end
RUBY
end

it "prints the warnings and the number of failures" do
expect { invoke_task }.
to output(/unrecognized `@response` tag detected/).to_stdout.
and output(/OpenAPI validation failed with 1 warning\(s\)\./).to_stderr
end

it "exits with status 1" do
allow($stdout).to receive(:puts)

exit_error = nil

expect { exit_error = invoke_task }.to output.to_stderr

expect(exit_error.status).to eq(1)
end
end
end