Skip to content

Instantly share code, notes, and snippets.

@jiggneshhgohel
Last active April 15, 2024 12:43
Show Gist options
  • Save jiggneshhgohel/1f0137e5dc294cc94141785eaf836c87 to your computer and use it in GitHub Desktop.
Save jiggneshhgohel/1f0137e5dc294cc94141785eaf836c87 to your computer and use it in GitHub Desktop.
Rabl-Rails: How to directly render objects in versions greater than 0.4.3.

Using the suggestions in ccocchi/rabl-rails#88 (comment) I was able to resolve my problem I described in ccocchi/rabl-rails#88 (comment) but I needed few customizations to be made. Following is the list of customizations I made:

  1. Created lib folder under app folder.

  2. Created rabl-rails folder under app/lib folder.

  3. Created renderers folder under app/lib/rabl-rails folder.

  4. Created renderer.rb file under app/lib/rabl-rails folder.

  5. Created hash_supporting_locals.rb file under app/lib/rabl-rails/renderers folder and added following content to it

  module RablRails
    module Renderers
      module HashSupportingLocals
        include Renderers::Hash
        extend self

        #
        # Render a template.
        # Uses the compiled template source to get a hash with the actual
        # data and then format the result according to the `format_result`
        # method defined by the renderer.
        #
        def render(template, context, locals = nil)
          visitor = Visitors::ToHash.new(context)

          collection_or_resource = if template.data
            if context.respond_to?(template.data)
              context.send(template.data)
            else
              visitor.instance_variable_get(template.data)
            end
          end
           
          # Note: This is the line added to original implementation.
          collection_or_resource ||= locals[:resource] if locals

          render_with_cache(template.cache_key, collection_or_resource) do
            output_hash = if collection?(collection_or_resource)
              render_collection(collection_or_resource, template.nodes, visitor)
            else
              render_resource(collection_or_resource, template.nodes, visitor)
            end

            format_output(output_hash, root_name: template.root_name, params: context.params)
          end
        end
      end
    end
  end
  1. Created json_supporting_locals.rb file under app/lib/rabl-rails/renderers folder and added following content to it
  module RablRails
    module Renderers
      module JSONSupportingLocals
        include Renderers::JSON
        include Renderers::HashSupportingLocals
        extend self

      end
    end
  end
  1. Created library_extended.rb file under app/lib/rabl-rails folder and added following content to it
  module RablRails
    class LibraryExtended < Library

      # This is the customized-version of RENDERER_MAP in super class.
      RENDERER_MAP = begin
        h = Library::RENDERER_MAP.dup

        h.merge!(
          json: Renderers::JSONSupportingLocals,
          ruby: Renderers::HashSupportingLocals
        )

        h.freeze
      end

      # Overridden method so that `RENDERER_MAP` defined in this file
      # gets used by this method.
      def get_rendered_template(source, view, locals = nil)
        compiled_template = compile_template_from_source(source, view)
        format = view.lookup_context.formats.first || :json
        raise UnknownFormat, "#{format} is not supported in rabl-rails" unless RENDERER_MAP.key?(format)
        RENDERER_MAP[format].render(compiled_template, view, locals)
      end
    end
  end
  1. Copied the contents of https://github.com/ccocchi/rabl-rails/blob/v0.4.3/lib/rabl-rails/renderer.rb file and added them to the file app/lib/rabl-rails/renderer.rb.

  2. Update app/lib/rabl-rails/renderer.rb by making following changes:

  • Replaced following

      require 'rabl-rails/renderers/hash'
      require 'rabl-rails/renderers/json'
    

    WITH

      require 'rabl-rails/renderers/hash_supporting_locals'
      require 'rabl-rails/renderers/json_supporting_locals'
    
  • Added require 'rabl-rails/library_extended' as the last require statement.

  • In RablRails::Renderer::LookupContext class made following changes:

    • removed method rendered_format

    • added following method

      def formats
        [ @format ].map(&:to_sym)
      end
    
  • In render method replaced Library with LibraryExtended.

  1. Finally you should have a ruby file in which there should be a method which has following line
  RablRails.render(serializable_object, <template_name>, view_path: <template_path>, format: :json)

At the top of that file add following code:

  require 'rabl-rails/renderer'

  module RablRails
    extend RablRails::Renderer
  end

and that method should work without needing any changes.

@jiggneshhgohel
Copy link
Author

jiggneshhgohel commented Mar 8, 2024

Details behind the customizations listed in https://gist.github.com/jiggneshhgohel/1f0137e5dc294cc94141785eaf836c87#file-rabl-rails-directly-use-object-md.

Copied https://github.com/ccocchi/rabl-rails/blob/v0.4.3/lib/rabl-rails/renderer.rb as it is in app/lib/rabl-rails/renderer.rb file as was suggested at ccocchi/rabl-rails#88 (comment) to fix the problem mentioned in that issue.

However using this file with the codebase in the latest version of rabl-rails (which was 0.6.2 at the time of writing this (ref: ccocchi/rabl-rails@12c7103) ) it raised error

   Failure/Error: Library.instance.get_rendered_template(t.source, c, resource: object)

   NoMethodError:
     undefined method `formats' for #<RablRails::Renderer::LookupContext:0x00005595f03ab638>
   # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/library.rb:28:in `get_rendered_template'
   # ./app/lib/rabl-rails/renderer.rb:94:in `render'

Investigating the problem it was found that in v0.4.3 codebase in RablRails::Renderer::LookupContext was manually defined and following method was defined in it

   def rendered_format
     @format.to_sym
   end

which was used by RablRails::Library#get_rendered_template(....) method at https://github.com/ccocchi/rabl-rails/blob/v0.4.3/lib/rabl-rails/library.rb#L29.

But in the versions greater-than 0.4.3 the RablRails::Renderer module itself is removed and consequently RablRails::Renderer::LookupContext also is removed and in the latest version RablRails::Library#get_rendered_template(....)
is updated to use

   format = view.lookup_context.formats.first || :json

in which view.lookup_context returns an instance of ActionView::LookupContext and which has a formats method. In Rails latest version (v 7.1.3.2 at the time of writing this) docs at lookup_context method doc can be found at https://api.rubyonrails.org/classes/ActionView/ViewPaths.html#method-i-lookup_context and ActionView::LookupContext.formats method can be found at https://github.com/rails/rails/blob/v7.1.3.2/actionview/lib/action_view/lookup_context.rb#L50.

So to make the code in this file compatible with latest version of RablRails::Library#get_rendered_template(....) we needed to replace

     def rendered_format
       @format.to_sym
     end

with

    def formats
      [ @format ].map(&:to_sym)
    end

After doing above change and testing it following error was encountered

    Failure/Error: Library.instance.get_rendered_template(t.source, c, resource: object)

    NoMethodError:
      undefined method `<method_invoked_in_template>' for nil:NilClass
    # (eval):1:in `block in compile_source'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/visitors/to_hash.rb:56:in `instance_exec'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/visitors/to_hash.rb:56:in `visit_Code'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/visitors/visitor.rb:18:in `dispatch'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/visitors/visitor.rb:8:in `block in visit_Array'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/visitors/visitor.rb:8:in `each'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/visitors/visitor.rb:8:in `visit_Array'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/visitors/visitor.rb:18:in `dispatch'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/visitors/visitor.rb:4:in `visit'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/renderers/hash.rb:54:in `render_resource'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/renderers/hash.rb:28:in `block in render'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/renderers/hash.rb:79:in `render_with_cache'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/renderers/hash.rb:24:in `render'
    # ...../gems/rabl-rails-0.6.2/lib/rabl-rails/library.rb:36:in `get_rendered_template'
    # ./app/lib/rabl-rails/renderer.rb:165:in `render'

That was unexpected.

Adding following debug statements at the start of RablRails::Library#get_rendered_template(source, view, locals = nil)
method (see https://github.com/ccocchi/rabl-rails/blob/12c7103432f734acff949d514045194b408d9736/lib/rabl-rails/library.rb#L26 which is the link to rabl-rails's latest-version code at the time of writing this (which was 0.6.2) which was invoked by the code in this file)

  puts ">>>>>>>> source: #{source}"
  puts ">>>>>>>> view: #{view}"
  puts ">>>>>>>> locals: #{locals}"

following was found

  >>>>>>>> source: <the template's source>
  >>>>>>>> view: #<RablRails::Renderer::ViewContext:0x000055fc259642a0>
  >>>>>>>> locals: {:resource=>#<the object which contained methods which were invoked in the template's source> }

And the line which was causing problem was found to be

https://github.com/ccocchi/rabl-rails/blob/12c7103432f734acff949d514045194b408d9736/lib/rabl-rails/library.rb#L30

So debugged the method at

https://github.com/ccocchi/rabl-rails/blob/12c7103432f734acff949d514045194b408d9736/lib/rabl-rails/renderers/hash.rb#L13

and found that in the 0.6.2 version, the locals passed to that method wasn't used in the method which was the case in 0.4.3 version of that method (see https://github.com/ccocchi/rabl-rails/blob/v0.4.3/lib/rabl-rails/renderers/hash.rb#L23) and in our case locals contained the "object which contained methods which were invoked in the template's source".

So the solution was to make set data to the object (which was received in locals) on the template so that if template.data on line

https://github.com/ccocchi/rabl-rails/blob/12c7103432f734acff949d514045194b408d9736/lib/rabl-rails/renderers/hash.rb#L16

evaluates to true.

template was found to be an instance of RablRails::CompiledTemplate. So explored the implementation of following related files

https://github.com/ccocchi/rabl-rails/blob/12c7103432f734acff949d514045194b408d9736/lib/rabl-rails/compiler.rb
https://github.com/ccocchi/rabl-rails/blob/12c7103432f734acff949d514045194b408d9736/lib/rabl-rails/template.rb

but couldn't find a straightforward/reliable way to set the data on template instance.

Thus the last option was to override following method

https://github.com/ccocchi/rabl-rails/blob/12c7103432f734acff949d514045194b408d9736/lib/rabl-rails/renderers/hash.rb#L13

in our application and restore the usage of locals in overridden version of the method.

But taking that approach I ended up creating RablRails::Renderer::HashSupportingLocals which included RablRails::Renderer::Hash and overridden render(template, context, locals = nil) by restoring the line collection_or_resource ||= locals[:resource] if locals which was there in RablRails::Renderer::Hash's code in 0.4.3 version.

After doing that and trying it out another need came up to customize RablRails::Library::RENDERER_MAP constant such that ruby key is mapped to Renderers::HashSupportingLocals. But to achieve that there was no choice except to extend RablRails::Library class and override that constant.

So I ended up implementing a RablRails::LibraryExtended and overriding the RENDERER_MAP constant in it in following manner

    RENDERER_MAP = begin
      h = Library::RENDERER_MAP.dup

      h.merge!(
        ruby: Renderers::HashSupportingLocals
      )

      h.freeze
    end

Then I replaced Library, in RablRails::Render#render(object, template, options = {}) method in file app/lib/rabl-rails/renderer.rb , with LibraryExtended.

But still it didn't worked because RablRails::Library#get_rendered_template(source, view, locals = nil) method's RENDERER_MAP still used the Hash set in its rabl-rails based version of RablRails::Library class instead of the one defined in my class RablRails::LibraryExtended.

So another step I took was to override RablRails::Library#get_rendered_template(source, view, locals = nil) method in my class RablRails::LibraryExtended but I realized that if we override that method then there is no need to use customized Renderers::HashSupportingLocals because in RablRails::LibraryExtended#get_rendered_template(source, view, locals = nil)
method I could set following

  compiled_template.data ||= locals[:resource] if locals

which should make it compatible with the RablRails::Renderer::Hash#render(template, context, locals = nil) method. So I decided to discard RablRails::Renderer::HashSupportingLocals.

So the changes in RablRails::LibraryExtended#get_rendered_template(source, view, locals = nil) worked but following new error was encountered:

  TypeError:
    #<object which contained methods which were invoked in the template's source"> is not a symbol nor a string
  # ....../gems/rabl-rails-0.6.2/lib/rabl-rails/renderers/hash.rb:19:in `respond_to?'
  # ....../gems/rabl-rails-0.6.2/lib/rabl-rails/renderers/hash.rb:19:in `render'
  # ./app/lib/rabl-rails/library_extended.rb:31:in `get_rendered_template'
  # ./app/lib/rabl-rails/renderer.rb:245:in `render'

That error occurred on following line

https://github.com/ccocchi/rabl-rails/blob/12c7103432f734acff949d514045194b408d9736/lib/rabl-rails/renderers/hash.rb#L17

So I debugged context and it was found to be an instance of RablRails::Renderer::ViewContext which is defined in file app/lib/rabl-rails/renderer.rb.

So I defined an overridden version of respond_to? method like following in RablRails::Renderer::ViewContext class defined in file app/lib/rabl-rails/renderer.rb.

  # Overridden version
  def respond_to?(symbol_or_str)
    begin
      super

    rescue StandardError => se
      Rails.logger.error ">>>>>>>>> #{self.class.name}'s overridden :respond_to? method rescued following exception: #{se.message}"
      false
    end
  end

And that worked but following new error was encountered:

```
  TypeError:
  #<object which contained methods which were invoked in the template's source"> is not a symbol nor a string
  # ..../gems/rabl-rails-0.6.2/lib/rabl-rails/renderers/hash.rb:22:in `instance_variable_get'
  # ..../rabl-rails-0.6.2/lib/rabl-rails/renderers/hash.rb:22:in `render'
  # ./app/lib/rabl-rails/library_extended.rb:31:in `get_rendered_template'
  # ./app/lib/rabl-rails/renderer.rb:308:in `render'
```

That error occurred on following line

https://github.com/ccocchi/rabl-rails/blob/12c7103432f734acff949d514045194b408d9736/lib/rabl-rails/renderers/hash.rb#L20

and the error made sense and there was no reliable way to avoid that.

So I came back to my earlier idea of creating RablRails::Renderer::HashSupportingLocals an overridden version of RablRails::Renderer::Hash but I also realized that to make this idea work we also needed to create RablRails::Renderer::JSONSupportingLocals because original RablRails::Renderer::JSON included RablRails::Renderer::Hash
and thus for json format templates it would invoke that original version of RablRails::Renderer::Hash which doesn't support locals.

So this way I was able to fix the problem referenced about in https://gist.github.com/jiggneshhgohel/1f0137e5dc294cc94141785eaf836c87#file-rabl-rails-directly-render-object-md by making the listed customizations in that file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment