Skip to content

[OpenAPI]: Extend @response / @request tag syntax to accept serializer options#299

Merged
rsamoilov merged 2 commits into
rage-rb:mainfrom
Abishekcs:openapi/serializer-options-tag-parser
May 25, 2026
Merged

[OpenAPI]: Extend @response / @request tag syntax to accept serializer options#299
rsamoilov merged 2 commits into
rage-rb:mainfrom
Abishekcs:openapi/serializer-options-tag-parser

Conversation

@Abishekcs
Copy link
Copy Markdown
Contributor

@Abishekcs Abishekcs commented May 16, 2026

What this PR does

Extends the tag parser to recognize serializer options like (Few Examples) :
a) UserBlueprint
b) UserBlueprint(view: :extended)
c) UserBlueprint(view: :extended, root: :user)
d) Array<UserBlueprint>
e) Array<UserBlueprint(view: :extended, root: :user)
... .

As, recmoneded in the issue description changes have been made in a way so Alba and any future serializer can use the same syntax.

References

  • Blueprinter Usage Documentation.

AI usage

  • Used AI for generating the the test cases. I also went through all the test cases that was generated by the AI just to be sure it's okay.

@Abishekcs Abishekcs force-pushed the openapi/serializer-options-tag-parser branch 2 times, most recently from 15170bc to bae4515 Compare May 19, 2026 06:15
@Abishekcs Abishekcs changed the title [WIP]: Extend @response / @request tag syntax to accept serializer options [OpenAPI]: Extend @response / @request tag syntax to accept serializer options May 19, 2026
@Abishekcs Abishekcs marked this pull request as ready for review May 19, 2026 06:50
Comment thread lib/rage/openapi/openapi.rb Outdated
Comment on lines +153 to +168
key, value = part.split(":", 2).map(&:strip)
next unless key

if value.nil?
hash[key.to_sym] = nil
elsif value.start_with?(":")
hash[key.to_sym] = value[1..].to_sym
elsif value.start_with?('"') && value.end_with?('"')
hash[key.to_sym] = value[1..-2]
elsif value == "true"
hash[key.to_sym] = true
elsif value == "false"
hash[key.to_sym] = false
else
hash[key.to_sym] = value
end
Copy link
Copy Markdown
Member

@rsamoilov rsamoilov May 20, 2026

Choose a reason for hiding this comment

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

Can we substitute this logic with YAML parsing? For example:

Suggested change
key, value = part.split(":", 2).map(&:strip)
next unless key
if value.nil?
hash[key.to_sym] = nil
elsif value.start_with?(":")
hash[key.to_sym] = value[1..].to_sym
elsif value.start_with?('"') && value.end_with?('"')
hash[key.to_sym] = value[1..-2]
elsif value == "true"
hash[key.to_sym] = true
elsif value == "false"
hash[key.to_sym] = false
else
hash[key.to_sym] = value
end
option = YAML.load(part)
return nil unless option.is_a?(Hash) # TODO: handle the nil response in `OpenAPI::Parser`
hash.merge!(option.transform_keys!(&:to_sym))

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.

Okay, I didn't knew about YAML.load ... 😅 make things much simpler. Thanks.

Comment thread lib/rage/openapi/parser.rb Outdated
elsif response_data.nil?
node.responses[status] = nil
else
response_data, serializer_options = Rage::OpenAPI.__parse_serializer_options(response_data)
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.

If you check the existing parsers, you'll see that each of them internally calls the similar __try_parse_collection call.

This creates a fair amount of duplication, but this is by design - parsers are intended to be independent systems that receive raw response tags and decide what to do with them.

That being said, let's move this call inside the Blueprinter parser. It's currently the only one using these options, which will also allow us to leave out the ** signature updates for parsers that don't support options. This will also allow the parsers to correctly issue a warning if someone passes options to the parser that doesn't support them.

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.

Got it.

Comment thread lib/rage/openapi/openapi.rb Outdated
end

# @private
def self.__parse_serializer_options(str)
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.

What about args instead of options?

Suggested change
def self.__parse_serializer_options(str)
def self.__parse_serializer_args(str)

…r options

- Updated the tag parser(Rage::OpenAPI) to recognise an options hash after the serializer class name.

- The parsed options are forwarded as-is to whatever serializer parser from it is invoked, so this change is generic - Alba and any future serializer can use the same syntax.

- The tag parser should is tolerant for unknown options i.e it ignores and don't raise so existing tags stay valid.
@Abishekcs Abishekcs force-pushed the openapi/serializer-options-tag-parser branch from bae4515 to 27d1726 Compare May 21, 2026 08:56
@Abishekcs
Copy link
Copy Markdown
Contributor Author

Abishekcs commented May 21, 2026

Hi @rsamoilov can you review the changes again whenever you're free. Thank you! Here's a quick summary:

  • Moved the .__parse_serializer_args call (previously .__parse_serializer_options) from parser.rb into blueprinter.rb
  • Removed ** from methods where it was added
  • .__parse_serializer_args (previously .__parse_serializer_options) used to return [str, options], now changed to return [is_collection, klass_str, options] where klass_str is the clean serializer class name stripped of any collection syntax or options
  • Updated test case.

Copy link
Copy Markdown
Member

@rsamoilov rsamoilov left a comment

Choose a reason for hiding this comment

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

Hey @Abishekcs,

This looks really nice.

I left a couple of minor suggestions - one to add a YARD comment and another to update the name of the variable.

There're also multiple statements not covered with tests. Let's either remove them if the situations they guard against can't happen or add tests to cover them.

Comment thread lib/rage/openapi/openapi.rb Outdated
Comment on lines +136 to +138
_, clean_inner, options = __parse_serializer_args(inner)
if options.any?
[is_collection, clean_inner, options]
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
_, clean_inner, options = __parse_serializer_args(inner)
if options.any?
[is_collection, clean_inner, options]
_, clean_inner, args = __parse_serializer_args(inner)
if args.any?
[is_collection, clean_inner, args]

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

Comment on lines +130 to +131
# @private
def self.__parse_serializer_args(str)
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

Comment on lines +139 to +140
else
[is_collection, clean_inner, {}]
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


# @private
def self.__parse_keywords(str)
return {} if str.nil? || str.empty?
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.

The str.empty? clause 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.

This is the test case (Line 148):

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

While poking around I noticed removing str.empty? clause didn't break the test context "with empty string" because "".split(",") returns an empty array [], so each_with_object({}) just returns {} naturally without the block ever running.

image

So str.empty? is technically redundant, but I think keeping it will be nice since it's not immediately obvious that "".split(",").each_with_object({}) returns {} , the explicit check makes the intent clearer for anyone reading the code later.

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.

Does this make sense😅 ? or should i remove 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

@Abishekcs
Copy link
Copy Markdown
Contributor Author

There're also multiple statements not covered with tests. Let's either remove them if the situations they guard against can't happen or add tests to cover them.

I have to check this one to be sure. Thanks for the review.

- Adds a test for when `YAML.load` returns a non-Hash value (e.g. `UserBlueprint(extended)`) confirming `__parse_keywords` returns `nil` instead of crashing.
@Abishekcs Abishekcs force-pushed the openapi/serializer-options-tag-parser branch from b75861e to 14b6422 Compare May 22, 2026 09:12
@Abishekcs Abishekcs requested a review from rsamoilov May 25, 2026 12:47
Copy link
Copy Markdown

@roman-softserve roman-softserve left a comment

Choose a reason for hiding this comment

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

Looks great!

@rsamoilov rsamoilov merged commit a07e45b into rage-rb:main May 25, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants