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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
### Added

- Implement `FiberScheduler#fiber_interrupt` (#283).
- Add Blueprinter parser scaffold and class detection (#287)
- [OpenAPI] Add Blueprinter parser scaffold and class detection (#287)
- [OpenAPI] Extend @response / @request tag syntax to accept serializer options (#299)

## [1.24.0] - 2026-05-12

Expand Down
31 changes: 31 additions & 0 deletions lib/rage/openapi/openapi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,37 @@ def self.__try_parse_collection(str)
end
end

# @private
# @return [Array<Boolean, String, Hash>] a tuple of (is_collection, serializer, args)
def self.__parse_serializer_args(str)
Comment on lines +130 to +132
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.

Suggested change
# @private
def self.__parse_serializer_args(str)
# @private
# @return [Array<Boolean, String, Hash>] a tuple of (is_collection, serializer, args)
def self.__parse_serializer_args(str)

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.

Done

is_collection, inner = __try_parse_collection(str)

if is_collection
# discard is_collection since we already know this is a collection from the outer call
_, clean_inner, args = __parse_serializer_args(inner)
if args.any?
[is_collection, clean_inner, args]
else
[is_collection, clean_inner, {}]
Comment on lines +140 to +141
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.

This branch is not covered with tests.

Copy link
Copy Markdown
Contributor Author

@Abishekcs Abishekcs May 22, 2026

Choose a reason for hiding this comment

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

It's covered by this two test (Line 94) and (Line 99) cases:

context "with a collection using square brackets without options" do
  let(:str) { "[UserBlueprint]" }
  it { is_expected.to eq([true, "UserBlueprint", {}]) }
end

context "with a collection using Array syntax without options" do
  let(:str) { "Array<UserBlueprint>" }
  it { is_expected.to eq([true, "UserBlueprint", {}]) }
end

end
elsif str =~ /^([\w:]+)\(([^)]+)\)$/
[is_collection, $1, __parse_keywords($2)]
else
[is_collection, str, {}]
end
end

# @private
def self.__parse_keywords(str)
return {} if str.nil? || str.empty?

str.split(",").each_with_object({}) do |part, hash|
option = YAML.load(part)
return nil unless option.is_a?(Hash)
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.

This condition is not covered with tests.

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.

Added a new test case for this (Line 183)

context "with an invalid option (not a key value pair)" do
  let(:str) { "extended" }
    it { is_expected.to be_nil }
  end

hash.merge!(option.transform_keys!(&:to_sym))
end
end

# @private
def self.__module_parent(klass)
klass.name =~ /::[^:]+\z/ ? Object.const_get($`) : Object
Expand Down
2 changes: 1 addition & 1 deletion lib/rage/openapi/parsers/ext/blueprinter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def initialize(namespace: Object, root: Rage::OpenAPI::Nodes::Root.new, **)
end

def known_definition?(str)
_, str = Rage::OpenAPI.__try_parse_collection(str)
_, str, _ = Rage::OpenAPI.__parse_serializer_args(str)
defined?(Blueprinter::Base) && @namespace.const_get(str).ancestors.include?(Blueprinter::Base)
rescue NameError
false
Expand Down
113 changes: 113 additions & 0 deletions spec/openapi/openapi_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,119 @@
end
end

describe ".__parse_serializer_options" do
subject { described_class.__parse_serializer_args(str) }

context "with a plain class name" do
let(:str) { "UserBlueprint" }
it { is_expected.to eq([false, "UserBlueprint", {}]) }
end

context "with a view option" do
let(:str) { "UserBlueprint(view: :extended)" }
it { is_expected.to eq([false, "UserBlueprint", { view: :extended }]) }
end

context "with multiple options" do
let(:str) { "UserBlueprint(view: :extended, root: :user)" }
it { is_expected.to eq([false, "UserBlueprint", { view: :extended, root: :user }]) }
end

context "with a collection using square brackets without options" do
let(:str) { "[UserBlueprint]" }
it { is_expected.to eq([true, "UserBlueprint", {}]) }
end

context "with a collection using Array syntax without options" do
let(:str) { "Array<UserBlueprint>" }
it { is_expected.to eq([true, "UserBlueprint", {}]) }
end

context "with a collection using square brackets with options" do
let(:str) { "[UserBlueprint(view: :extended)]" }
it { is_expected.to eq([true, "UserBlueprint", { view: :extended }]) }
end

context "with a collection using Array syntax with options" do
let(:str) { "Array<UserBlueprint(view: :extended)>" }
it { is_expected.to eq([true, "UserBlueprint", { view: :extended }]) }
end

context "with unknown options" do
let(:str) { "UserBlueprint(unknown_option: :something)" }

it "does not raise" do
expect { subject }.not_to raise_error
end

it { is_expected.to eq([false, "UserBlueprint", { unknown_option: :something }]) }
end

context "with existing Alba syntax unchanged" do
let(:str) { "UserResource" }
it { is_expected.to eq([false, "UserResource", {}]) }
end

context "with a collection using Array syntax with multiple options" do
let(:str) { "Array<UserBlueprint(view: :extended, root: :user)>" }
it { is_expected.to eq([true, "UserBlueprint", { view: :extended, root: :user }]) }
end

context "with a plain string value without quotes" do
let(:str) { "UserBlueprint(root: users)" }
it { is_expected.to eq([false, "UserBlueprint", { root: "users" }]) }
end
end

describe ".__parse_keywords" do
subject { described_class.__parse_keywords(str) }

context "with nil" do
let(:str) { nil }
it { is_expected.to eq({}) }
end

context "with empty string" do
let(:str) { "" }
it { is_expected.to eq({}) }
end

context "with a symbol value" do
let(:str) { "view: :extended" }
it { is_expected.to eq({ view: :extended }) }
end

context "with a string value" do
let(:str) { 'name: "hello"' }
it { is_expected.to eq({ name: "hello" }) }
end

context "with a boolean true value" do
let(:str) { "active: true" }
it { is_expected.to eq({ active: true }) }
end

context "with a boolean false value" do
let(:str) { "admin: false" }
it { is_expected.to eq({ admin: false }) }
end

context "with a nil value" do
let(:str) { "key:" }
it { is_expected.to eq({ key: nil }) }
end

context "with multiple options" do
let(:str) { "view: :extended, root: :user" }
it { is_expected.to eq({ view: :extended, root: :user }) }
end

context "with an invalid option (not a key value pair)" do
let(:str) { "extended" }
it { is_expected.to be_nil }
end
end

describe ".__module_parent" do
subject { described_class.__module_parent(klass) }

Expand Down