Search code examples
rubybindingconstantsscoping

Shadowing a top-level constant within a binding


I would like to shadow ENV within a templating method, so that I can raise an error if keys are requested which are not present in the real ENV. Obviously I don't want to shadow the constant elsewhere - just within a specific method (specific binding). Is this even possible?

Explainer: - I know about the existence of Hash#fetch and I use it all the time and everywhere. However, I want to use this in an ERB template generating a config file. This config file is likely to be touched by more people than usual, and not everyone is familiar with the Ruby behavior of returning a nil for a missing Hash key. I am also working on a system where, of late, configuration mishaps (or straight out misconfigurations, or misunderstandings of a format) caused noticeable production failures. The failures were operator error. Therefore, I would like to establish a convention, within that template only, that would cause a raise. Moreover, I have a gem, strict_env, that does just that already - but you have to remember to use STRICT_ENV instead of just ENV, and every "you have to" statement for this specific workflow, in this specific case, raises a red flag for me since I want more robustness. I could of course opt for a stricter templating language and use that language's logic for raising (for example, Mustache), but since the team already has some familiarity with ERB, and Rails endorses ERB-templated-YML as a viable config approach (even though you might not agree with that) it would be nice if I could stick to that workflow too. That's why I would like to alter the behavior of ENV[] locally.


Solution

  • ERB#result takes an optional binding:

    require 'erb'
    
    class Foo
      ENV = { 'RUBY_VERSION' => '1.2.3' }
      def get_binding
        binding
      end
    end
    
    template = "Ruby version: <%= ENV['RUBY_VERSION'] %>"
    
    ERB.new(template).result
    #=> "Ruby version: 2.1.3"
    
    b = Foo.new.get_binding
    
    ERB.new(template).result b
    #=> "Ruby version: 1.2.3"