Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rabl-rails assumes any object that respond to each is a collection #55

Closed
jrunning opened this issue Feb 28, 2014 · 12 comments
Closed

rabl-rails assumes any object that respond to each is a collection #55

jrunning opened this issue Feb 28, 2014 · 12 comments

Comments

@jrunning
Copy link

I'd like to use rabl-rails to render an object, but since the object responds to each, RablRails::Renderers::Base tries to render it as a collection. Here's code that reproduces the problem:

# app/views/controllers/config_controller.rb
class ConfigController < ApplicationController
  respond_to :json

  def show
    struct = Struct.new(:foo)
    @config = struct.new("bar")
  end
end
# app/views/config/show.json.rabl
object :@config
attribute :foo

I would expect this to render {"foo":"bar"} since @config.foo returns "bar". However, because @config responds to each, Renderers::Base#render calls render_collection on it instead of render_resource, yielding an error:

undefined method `foo' for "bar":String
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:68:in `block in render_resource'
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:64:in `each'
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:64:in `inject'
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:64:in `render_resource'
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:116:in `block in render_collection'
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:116:in `each'
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:116:in `map'
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:116:in `render_collection'
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:32:in `block in render'
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:55:in `render_with_cache'
  (gem) rabl-rails-0.3.3/lib/rabl-rails/renderers/base.rb:31:in `render'
  ...

(You can see the complete backtrace here: https://gist.github.com/jrunning/9279963)

This might seem like a contrived example, but I ran into it in a real scenario (I was trying to render attributes of an ActiveSupport::OrderedOptions rather than a struct). The same issue would manifest if someone defined each on an ActiveRecord model.

I'm not sure what the best solution is. Perhaps object could take an option that would explicitly tell it not to render the object as a collection?

For now my only option seems to be falling back to the regular rabl gem, which lacks some of the Railsy niceties of rabl-rails.

@ccocchi
Copy link
Owner

ccocchi commented Oct 12, 2014

FIxed in 781ec8d.

@ccocchi ccocchi closed this as completed Oct 12, 2014
@jrunning
Copy link
Author

The referenced commit doesn't actually fix the issue, unfortunately. The fix is specific to Structs but the issue is not. As I wrote above, I discovered it when trying to render an attribute of an ActiveSupport::OrderedOptions. The issue still manifests with any object that responds to each and isn't a collection or a Struct, including if someone were to define an ActiveRecord model that responds to each (which I have seen in the wild).

@ccocchi
Copy link
Owner

ccocchi commented Oct 14, 2014

What about this b2eb5be ? You can now define your own objects that respond_to each but should be treated as objects.

@ccocchi ccocchi reopened this Oct 14, 2014
@jrunning
Copy link
Author

Thanks for taking the time to reopen this, @ccocchi. I think this solution is better, with one reservation. I wonder if specifying class names instead of actual classes (i.e. ["Struct", "NotACollection"] instead of [Struct, NotACollection]) would be better, especially when it comes to autoloading. For example, if you have a Rails model named MyModel, if you do this in your configuration:

RablRails.configure do |config|
  config.non_collection_classes << MyModel
end

...Rails will autoload MyModel as soon as that block is called.

If instead you could do this:

RablRails.configure do |config|
  config.non_collection_classes << "MyModel"
end

...and then in lib/rabl-rails/helpers.rb:

resource && resource.respond_to?(:each) &&
  RablRails.configuration.non_collection_classes.none? { |k| klass <= k.constantize }

...then MyModel will only be loaded when absolutely necessary.

While a typical Rails app will load all of its models eventually, often Rake tasks or workers only need one or two of the app's models, and I think those tend to benefit from optimizations like this.

@ccocchi
Copy link
Owner

ccocchi commented Oct 19, 2014

Using strings results in a big performance drawback, see this https://gist.github.com/ccocchi/b74045f5e3c15db44dce

Calculating -------------------------------------
               klass     68989 i/100ms
               const     20790 i/100ms
             strings     20112 i/100ms
-------------------------------------------------
               klass  1685134.9 (±5.6%) i/s -    8416658 in   5.009936s
               const   318492.8 (±5.5%) i/s -    1600830 in   5.042154s
             strings   269006.3 (±7.8%) i/s -    1347504 in   5.040542s

I guess having one or two models autoloaded at boot time is a lesser matter. This method is at least called once per object rendered so kind of a hot path. Workers and Rails would eventually load all models so we should be good. Maybe some rake tasks will load more models with this but I think rendering views in a task is a marginal case.

I will test this in a live application to see performance results and I'll release a new version soon.

@bsedin
Copy link

bsedin commented Oct 19, 2014

Got problem with non_collection_classes in development environment.
When i edit my class, rails removes class constant and initializes new fresh one, which not equal one i've put in initializer to RablRails.configuration.non_collection_classes

@ccocchi
Copy link
Owner

ccocchi commented Oct 19, 2014

Ok ok, let's go with strings 😞
This commit 3a05ec4 should fix your reload problem

@jrunning
Copy link
Author

If performance is an issue for some people, we could have our cake and eat it too by using the string technique in development and the class technique in production. Or you could hook into Rails' reloader to make sure rabl-rails' configuration is updated when classes are reloaded. But that still leaves the autoloading issue. I suppose the third way would be to allow either classes or strings, which would let users choose between the autoloading penalty and the constantize penatly.

@ccocchi
Copy link
Owner

ccocchi commented Dec 30, 2014

From v0.4.1, classes that should not be treated as collection are configurable.

@ccocchi ccocchi closed this as completed Dec 30, 2014
@pkuykendall
Copy link

I found good results with changing the collection? helper to include a class check for Enumerable

resource && resource.respond_to?(:each) && resource.kind_of?(Enumerable) &&
        klass.ancestors.none? { |a| RablRails.configuration.non_collection_classes.include? a.name }

What are your thoughts, @ccocchi?

@myxrome
Copy link

myxrome commented Feb 28, 2019

it is not compatible with sequel-rails. the code
partial "path/to/partial", object: object
will fail on
https://github.com/ccocchi/rabl-rails/blob/master/lib/rabl-rails/visitors/to_hash.rb#L127
because of sequel model does respond to each method. and it does not respond to map method.
could you please fix it.

@ccocchi
Copy link
Owner

ccocchi commented Mar 1, 2019

Hello,

I don't understand what your problem is. If a sequel model (I'm assuming it is an equivalent of ActiveRecord::Base) does not respond to the each method, it won't be treated as a collection but as an object, which is the expected behavior.

Where am I mistaken?

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

No branches or pull requests

5 participants