Nenv
Using ENV in Ruby is like using raw SQL statements - it feels wrong, because it is.
If you agree, this gem is for you.
The benefits over using ENV directly:
- much friendlier stubbing in tests
- you no longer have to care whether false is "0" or "false" or whatever
- NO MORE ALL CAPS EVERYWHERE!
- keys become methods
- namespaces which can be passed around as objects
- you can subclass!
- you can marshal/unmarshal your own types automatically!
- strict mode saves you from doing validation yourself
- and there's more to come...
Other benefits (and compared to other solutions):
- should still work with Ruby 1.8 (in case anyone is still stuck with it)
- it's designed to be as lightweight and as fast as possible compared to ENV
- designed to be both hackable and convenient
Installation
Add this line to your application's Gemfile:
gem 'nenv'
And then execute:
$ bundle
Or install it yourself as:
$ gem install nenv
Examples !!!
Automatic booleans
You no longer have to care whether the value is "0" or "false" or "no" or "FALSE" or ... whatever
# Without Nenv
t.verbose = (ENV['CI'] == 'true')
ok = ENV['RUBYGEMS_GEMDEPS'] == "1" || ENV.key?('BUNDLE_GEMFILE']
ENV['DEBUG'] = "true"
now becomes:
t.verbose = Nenv.ci?
gemdeps = Nenv.rubygems_gemdeps? || Nenv.bundle_gemfile?
Nenv.debug = true
"Namespaces"
# Without Nenv
puts ENV['GIT_BROWSER']
puts ENV['GIT_PAGER']
puts ENV['GIT_EDITOR']
now becomes:
git = Nenv :git
puts git.browser
puts git.pager
puts git.editor
Custom type handling
# Code without Nenv
paths = [ENV['GEM_HOME`]] + ENV['GEM_PATH'].split(':')
enable_logging if Integer(ENV['WEB_CONCURRENCY']) > 1
mydata = YAML.load(ENV['MY_DATA'])
ENV['VERBOSE'] = debug ? "1" : nil
can become:
# setup
gem = Nenv :gem
gem.instance.create_method(:path) { |p| p.split(':') }
web = Nenv :web
web.instance.create_method(:concurrency) { |c| Integer(c) }
my = Nenv :my
my.instance.create_method(:data) { |d| YAML.load(d) }
Nenv.instance.create_method(:verbose=) { |v| v ? 1 : nil }
# and then you can simply do:
paths = [gem.home] + gem.path
enable_logging if web.concurrency > 1
mydata = my.data
Nenv.verbose = debug
Automatic conversion to string
ENV['RUBYGEMS_GEMDEPS'] = 1 # TypeError: no implicit conversion of Fixnum (...)
Nenv automatically uses to_s
:
Nenv.rubygems_gemdeps = 1 # no problem here
Custom assignment
data = YAML.load(ENV['MY_DATA'])
data[:foo] = :bar
ENV['MY_DATA'] = YAML.dump(data)
can now become:
my = Nenv :my
my.instance.create_method(:data) { |d| YAML.load(d) }
my.instance.create_method(:data=) { |d| YAML.dump(d) }
data = my.data
data[:foo] = :bar
my.data = data
Strict mode
# Without Nenv
fail 'home not allowed' if ENV['HOME'] = Dir.pwd # BUG! Assignment instead of comparing!
puts ENV['HOME'] # Now contains clobbered value
Now, clobbering can be prevented:
env = Nenv::Environment.new
env.create_method(:home)
fail 'home not allowed' if env.home = Dir.pwd # Fails with NoMethodError
puts env.home # works
Mashup mode
You can first define all the load/dump logic globally in one place
Nenv.instance.create_method(:web_concurrency) { |d| Integer(d) }
Nenv.instance.create_method(:web_concurrency=)
Nenv.instance.create_method(:path) { |p| Pathname(p.split(File::PATH_SEPARATOR)) }
Nenv.instance.create_method(:path=) { |array| array.map(&:to_s).join(File::PATH_SEPARATOR) }
# And now, anywhere in your app:
Nenv.web_concurrency += 3
Nenv.path += Pathname.pwd + "foo"
Your own class (recommended version for simpler unit tests)
MyEnv = Nenv::Builder.build do
create_method(:foo?)
end
MyEnv.new('my').foo? # same as ENV['MY_FOO'][/^(?:false|no|n|0)/i,1].nil?
Your own class (dynamic version - not recommended because harder to test)
class MyEnv < Nenv::Environment
def initialize
super("my")
create_method(:foo?)
end
end
MyEnv.new.foo? # same as ENV['MY_FOO'][/^(?:false|no|n|0)/i,1].nil?
NOTES
Still, avoid using environment variables if you can.
At least, avoid actually setting them - especially in multithreaded apps.
As for Nenv, while you can access the same variable with or without namespaces, filters are tied to instances, e.g.:
Nenv.instance.create_method(:foo_bar) { |d| Integer(d) }
Nenv('foo').instance.create_method(:bar) { |d| Float(d) }
env = Nenv::Environment.new(:foo).tap { |e| e.create_method(:bar) }
all work on the same variable, but each uses a different filter for reading the value.
What's wrong with ENV?
Well sure, having ENV act like a Hash is much better than calling "getenv".
Unfortunately, the advantages of using ENV make no sense:
- it's faster but ... environment variables are rarely used thousands of times in tight loops
- it's already an object ... but there's not much you can do with it (try ENV.class)
- it's globally available ... but you can't isolate it in tests (you need to reset it every time)
- you can use it to set variables ... but it's named like a const
- it allows you to use keys regardless of case ... but by convention lowercase shouldn't be used except for local variables (which are only really used by shell scripts)
- it's supposed to look ugly to discourage use ... but often your app/gem is forced to use them anyway
- it's a simple class ... but either you encapsulate it in your own classes - or all the value mapping/validation happens everywhere you want the data (yuck!)
But the BIGGEST disadvantage is in specs, e.g.:
allow(ENV).to receive(:[]).with('MY_VARIABLE').and_return("old data")
allow(ENV).to receive(:[]=).with('MY_VARIABLE', "new data")
which could instead be completely isolated as:
let(:env) { instance_double(Nenv::Environment) }
before { allow(Nenv::Environment).to receive(:new).with(:my).and_return(env) }
allow(env).to receive(:variable).and_return("old data")
allow(env).to receive(:variable=).with("new data")
Contributing
- Fork it ( https://github.com/[my-github-username]/nenv/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request