Skip to content
This repository has been archived by the owner on Sep 20, 2023. It is now read-only.

Make array like classes configurable #6

Merged
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ This will result in JSON that looks something like this:
]
```


You can also configure other classes to be treated like collections. For example, if you are using Mongoid, you can configure it to treat `Mongoid::Criteria` objects as collections:

```ruby
Blueprinter.configure do |config|
config.array_like_classes = [Mongoid::Criteria]
end
```

Or if you wanted it to treat the `Set` class as a collection:

```ruby
Blueprinter.configure do |config|
config.array_like_classes = [Set]
end
Copy link

Choose a reason for hiding this comment

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

can you pls create a changelog entry and bump the patch version?

```

---
</details>


Expand Down
3 changes: 2 additions & 1 deletion lib/blueprinter/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module Blueprinter
class Configuration
attr_accessor :association_default, :datetime_format, :deprecations, :field_default, :generator, :if, :method, :sort_fields_by, :unless, :extractor_default, :default_transformers
attr_accessor :association_default, :datetime_format, :deprecations, :field_default, :generator, :if, :method, :sort_fields_by, :unless, :extractor_default, :default_transformers, :array_like_classes

VALID_CALLABLES = %i(if unless).freeze

Expand All @@ -18,6 +18,7 @@ def initialize
@unless = nil
@extractor_default = AutoExtractor
@default_transformers = []
@array_like_classes = []
end

def jsonify(blob)
Expand Down
14 changes: 10 additions & 4 deletions lib/blueprinter/helpers/type_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
module Blueprinter
module TypeHelpers
private
def active_record_relation?(object)
!!(defined?(ActiveRecord::Relation) &&
object.is_a?(ActiveRecord::Relation))

def array_like_classes
[
Array,
defined?(ActiveRecord::Relation) && ActiveRecord::Relation,
Copy link
Author

Choose a reason for hiding this comment

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

if it's not defined this becomes nil and the .compact at the end gets rid of it.

Copy link

@ritikesh ritikesh Jan 31, 2023

Choose a reason for hiding this comment

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

since this is config and won't change at runtime, should we consider overriding the default getter or setter and inject this behaviour there? that way, it'll be memoized and we won't create/compute this array at runtime for each render call.
PS: sprinkling a memoization logic here might look ugly since this is a helper class.

*Blueprinter.configuration.array_like_classes
].compact
end

def array_like?(object)
object.is_a?(Array) || active_record_relation?(object)
array_like_classes.any? do |klass|
Copy link
Author

Choose a reason for hiding this comment

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

I got rid of the array_like_class? helper method since all this method did was call that method, but if you prefer it for readability I can move this logic back to that method and just call that method from here.

object.is_a?(klass)
end
end
end
end
51 changes: 51 additions & 0 deletions spec/integrations/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,29 @@
end
end

context 'Given passed object is array-like' do
let(:blueprint) { blueprint_with_block }
let(:additional_object) { OpenStruct.new(obj_hash.merge(id: 2)) }
let(:obj) { Set.new([object_with_attributes, additional_object]) }

context 'and is an instance of a configured array-like class' do
before do
Blueprinter.configure { |config| config.array_like_classes = [Set] }
end
after { reset_blueprinter_config! }

it 'should return the expected array of hashes' do
should eq('[{"id":1,"position_and_company":"Manager at Procore"},{"id":2,"position_and_company":"Manager at Procore"}]')
end
end

context 'and is not an instance of a configured array-like class' do
it 'should raise an error' do
expect { blueprint.render(obj) }.to raise_error(NoMethodError)
Copy link
Author

Choose a reason for hiding this comment

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

there's not really anything else to assert here. We already have tests for that cover a hash being handled properly, and hashes are "array-like" in that they are Enumerable, so here I just mimicked what would happen if you pass it a collection for which you didn't pass the class of to array_like_classes.

end
end
end

context 'Inside Rails project' do
include FactoryBot::Syntax::Methods
let(:obj) { create(:user) }
Expand Down Expand Up @@ -383,6 +406,34 @@ def extract(association_name, object, _local_options, _options={})
end
end
end

context 'Given passed object is an instance of a configured array-like class' do
let(:blueprint) do
Class.new(Blueprinter::Base) do
identifier :id
fields :make
end
end
let(:vehicle1) { build(:vehicle, id: 1) }
let(:vehicle2) { build(:vehicle, id: 2, make: 'Mediocre Car') }
let(:vehicle3) { build(:vehicle, id: 3, make: 'Terrible Car') }
let(:vehicles) { [vehicle1, vehicle2, vehicle3] }
let(:obj) { Set.new(vehicles) }
let(:result) do
vehicles_json = vehicles.map do |vehicle|
"{\"id\":#{vehicle.id},\"make\":\"#{vehicle.make}\"}"
end.join(',')
"[#{vehicles_json}]"
end

before do
Blueprinter.configure do |config|
config.array_like_classes = [Set]
end
end

it('returns the expected result') { should eq(result) }
end
end
end
describe '::render_as_hash' do
Expand Down