Robin
Robin

Reputation: 915

Ruby: Capturing multiple lines in a block

I have a rails generator that creates files based on some erb templates.

My goal is:

To have not too complicated template files. Something like this:

# example of erb template file
<%= wrap_in_modules do
  "class Engine < ::Rails::Engine"
  if mountable?
    "isolate_namespace #{modules.first}"
  end
  "end"
end %>

And after parsing and creating, the example above should become:

module Naming
  module Extension
    class Engine < ::Rails::Engine
      isolate_namespace Naming
    end
  end
end

The method that defines the wrap_in_modules should take all the lines between the do..end in the template file to process it with that module nesting.

But currently it does not. It only takes the last line.

This is my actual output for the generated file (for the same example as above)

module Naming
  module Extension
    end
  end
end

As you can see, only the last line between the do..end is being taken and wrapped in to the modules.

Okay, now here comes the actual wrap_in_modules method.

def wrap_in_modules
  modules.reverse.inject(yield) do |content, mod|
    "module #{mod}\n" << content.lines.map { |line| "  #{line}" }.join << "\nend"
  end
end

I know, its documented: yield only returns the last expression evaluated on the block. And also does the proc.call.

I took a look into the source of the rails content_tag helper and tried to copy it because it does basically the same what I want here. But passing the &block with an ampersand to the method and capturing it also does not work. Its just empty!?

Btw: another thing is that this notation will fail with a syntax error coming from erb:

<%= wrap_in_modules do -%>
  Test test
<% end %>

lib/ruby/2.0.0/erb.rb:849:in `eval': (erb):1: syntax error, unexpected ')' (SyntaxError) ...r.concat(( wrap_in_modules do ).to_s); @output_buffer.concat...
...                               ^
(erb):4: syntax error, unexpected end-of-input, expecting ')'

; @output_buffer.force_encoding(ENCODING)

I wrack my brain for many hours now... how can I get more than just that last line to the method... Does someone has an idea?

Upvotes: 1

Views: 1332

Answers (3)

Allerin
Allerin

Reputation: 1108

This will work:

<%= wrap_in_modules do
  <<-STRING 
    class Engine < ::Rails::Engine
      #{mountable? ? ('isolate_namespace ' + modules.first.to_s) : ''}
    end
  STRING 
end %>

Upvotes: 0

Phrogz
Phrogz

Reputation: 303136

You can't implement any Ruby code to use your code block exactly as written to do what you want. You have multiple literal values but don't do anything with them. It would be the same as asking how to do this:

sum do
  1
  2
  3
end #=> 6

It can't be done. Instead, you need to invoke some sort of method to accumulate the strings. For example:

# Evaluate the block in a context where `s` is defined to add strings to an array
wrap_in_modules do
  s "class Engine < ::Rails::Engine"
  if mountable?
    s "isolate_namespace #{modules.first}"
  end
  s "end"
end

or

# Monkeypatch String class unary plus during this block invocation
# to add to your own collection
wrap_in_modules do
  +"class Engine < ::Rails::Engine"
  if mountable?
    +"isolate_namespace #{modules.first}"
  end
  +"end"
end

Here's an example using refinements and global variable (bleah!):

module UglyAggregator
  refine String do
    def +@
      $all << self
    end
  end
end

# Do this within your actual scope    
using UglyAggregator
def many_strings(&block)
  $all = []
  yield
  $all.join
end

p many_strings{
  +"a"
  +"b"
}

#=> "ab"

Upvotes: 1

Danny
Danny

Reputation: 6025

I have customized several generators myself, and you basically use them similar to rails views: all text that is "fixed" is just put "as is". The ruby around it is meant to create the variable stuff

Something like :

require 'spec_helper'

<% module_namespacing do -%>
describe <%= class_name %> do
<% for attribute in attributes -%>
  it { should validate_presence_of(:<%= attribute.name %>) }  
  it { should validate_uniqueness_of(:<%= attribute.name %>) }  
  it { should ensure_length_of(:<%= attribute.name %>).is_at_least(3).is_at_most(255) }
<% end -%>
end
<% end -%>

This is a totally different approach, but much easier, as you can simply write your code, and "customize" it afterwards using ruby...

Upvotes: 0

Related Questions