Skip to content

[OpenAPI]: Refactor: replace Prism AST parser with Blueprinter Reflection API#323

Merged
rsamoilov merged 3 commits into
rage-rb:mainfrom
Abishekcs:openapi/blueprinter-reflection-api
Jun 12, 2026
Merged

[OpenAPI]: Refactor: replace Prism AST parser with Blueprinter Reflection API#323
rsamoilov merged 3 commits into
rage-rb:mainfrom
Abishekcs:openapi/blueprinter-reflection-api

Conversation

@Abishekcs

@Abishekcs Abishekcs commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What this PR does?

Switch the Blueprinter parser from Prism-based AST parsing to Blueprinter's built-in Reflection API. This removes the need for a Visitor pattern, temp file parsing, and manual AST traversal. The Reflection API provides direct access to fields, views, and associations making the parser significantly simpler.

Example Code and Screenshot for basic Blueprinter fields: identifier, field, and fields

  • routes.rb
Rage.routes.draw do
  root to: ->(env) { [200, {}, ["It works!"]] }

  get "/users/:id/blue", to: "users#show_blue"
end
  • products_controller.rb
class UsersController < RageController::API
  
  # @response UserBlueprint
  def show_blue
    user = [{ uuid: 1, id: 101, name: "Alice", email: "alice@example.com", age: 25, first_name: "Alice", last_name: "loom", abishek: 'abi' }]
    render json: UserBlueprint.render(user)
  end
end
  • user_blueprint.rb
class UserBlueprint < Blueprinter::Base
  identifier :uuid
  
  fields :id, :name, :email, :age
  
  field :email, name: :login   # -> "login": string

  field :abishek, name: :ad   # -> "login": string
  
  fields :first_name, :last_name

  field(:full_name) { |u| "#{u[:first_name]} #{u[:last_name]}" }
end
basic_fields

Example Code and Screnshoot of Blueprinter Inheritance

  • routes.rb
Rage.routes.draw do
  root to: ->(env) { [200, {}, ["It works!"]] }

  get "/data_mining", to: "data_mining#show_data_mining"
end
  • data_mining_controller.rb
class DataMiningController < RageController::API

  # @response DataMining
  def show_data_mining
    data_mining_details = { uuid: 'Third Edition', email: 'MorganKaufmann@publishers.com', name: 'Han Kamber Pei', first_name: 'Morgan', last_name: 'Kaufmann', hello_world: 'Earth', subject: "DataMining"  }
    render json: DataMining.render(data_mining_details)
  end
end
  • data_mining_base.rb
class DataMiningBase < Blueprinter::Base
  field :hello_world
  field :email, name: :something
  fields :subject
end
  • data_mining.rb
class DataMining < DataMiningBase
  identifier :uuid
  field "email", name: :login
  field "first_name"
  field :hello_world
end
  • Blueprinter Response for route /data_mining
{
  "uuid": "Third Edition",
  "first_name": "Morgan",
  "hello_world": "Earth",
  "login": "MorganKaufmann@publishers.com",
  "something": "MorganKaufmann@publishers.com",
  "subject": "DataMining"
}
  • OpenAPI response output for # @response DataMining
{
  "uuid": "string",
  "first_name": "string",
  "hello_world": "string",
  "login": "string",
  "something": "string",
  "subject": "string"
}
blueprinter_inheritance

Abishekcs added 2 commits June 9, 2026 20:44
Switch the Blueprinter parser from Prism-based AST parsing to Blueprinter's built-in Reflection API. This removes the need for a Visitor pattern, temp file parsing, and manual AST traversal. The Reflection API provides direct access to fields, views, and associations making the parser significantly simpler.
@context = VisitorContext.new
yield
@context
view.instance_variable_get(:@view_collection).instance_variable_get(:@views)[view_name].instance_variable_get(:@fields).each_with_object({}) do |(_, field), hash|

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.

At first I was using something like this:

    reflections = klass.reflections

    identifier_fields = {}
    default_fields = {}

    # identifier fields
    if (identifier_view = reflections[:identifier])
      identifier_view.fields.each do |_, field|
        identifier_fields[field.name.to_s] = { "type" => "string" }
      end
    end

    # default view fields
    if (default_view = reflections[:default])
      default_view.fields.each do |_, field|
        default_fields[field.name.to_s] = { "type" => "string" }
      end
    end

But .fields of Relection API seems to break for

 fields "first_name", :last_name
Image

or for this too

field :email, name: "login"
Image

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.

fields "first_name", :last_name

This seems to work fine for me with Blueprinter 1.2.1. But in any way, with this approach, we should only be relying on the official API. If some cases are not supported by the reflection API, it's safe to ignore them.

@Abishekcs

Copy link
Copy Markdown
Contributor Author

@rsamoilov I have refactor Blueprinter parser using Reflection API just a quick summary:

  • Added Gem Blueprinter
  • Added a new mock class i.e spec/support/contexts/mocked_blueprinter_classes.rb. I am not sure if this was the right way or I should have made changes to the existing mocked_classess.rb.
  • Existing test cases logic is same with just some minor changes.

@Abishekcs Abishekcs marked this pull request as ready for review June 9, 2026 16:18
@Abishekcs

Copy link
Copy Markdown
Contributor Author

Only works with

  • basic Blueprinter fields: identifier, field, and fields.
  • Blueprinter Inheritance

end

klass = Class.new(parent)
klass.class_eval(block.call) if block

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.

This is the only newly added line. Rest is same as mocked_classes.rb

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.

It should be safe to add this line to mocked_classes.rb. And you won't need a separate context then.

The Reflection API requires DSL calls to be evaluated in the class context at runtime. Updated let_class to call class_eval with the block's string content when the parent is a Blueprinter class, while preserving the original behavior for all other classes (Alba, controllers, etc).

Switched blueprinter_spec to use mocked_classes instead of the separate mocked_blueprinter_classes context, and removed mocked_blueprinter_classes from spec_helper since it's no longer needed.

Also switched to view.fields official API using field.display_name instead of instance_variable_get hacks, and dropped unsupported mixed string/symbol field test cases.
@Abishekcs Abishekcs force-pushed the openapi/blueprinter-reflection-api branch from b125ebf to 87977ff Compare June 10, 2026 07:53
@Abishekcs

Abishekcs commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

@rsamoilov PR is ready to be reviewed again. Thank you!

  • This test case was fully removed
  • Switched back to using the official API
  • Removed mocked_blueprinter_classes.rb and added few new lines in mocked_classes.rb
    which is:
      if block
        if defined?(Blueprinter::Base) && parent.ancestors.include?(Blueprinter::Base)
          klass.class_eval(block.call)
        else
          klass.class_eval(&block)
        end
      end

@Abishekcs Abishekcs changed the title Refactor: replace Prism AST parser with Blueprinter Reflection API [OpenAPI]: Refactor: replace Prism AST parser with Blueprinter Reflection API Jun 12, 2026

@rsamoilov rsamoilov left a comment

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.

🚀

Comment on lines +29 to +35
if block
if defined?(Blueprinter::Base) && parent.ancestors.include?(Blueprinter::Base)
klass.class_eval(block.call)
else
klass.class_eval(&block)
end
end

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
if block
if defined?(Blueprinter::Base) && parent.ancestors.include?(Blueprinter::Base)
klass.class_eval(block.call)
else
klass.class_eval(&block)
end
end
if block
if defined?(Blueprinter::Base) && parent.ancestors.include?(Blueprinter::Base)
klass.class_eval(block.call)
end
end

The else branch doesn't seem to be used, so it'd be better to remove it in the next PR.

@rsamoilov rsamoilov merged commit cf7f319 into rage-rb:main Jun 12, 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.

2 participants