Skip to content

Commit

Permalink
Refactor internal API to reduce duplication
Browse files Browse the repository at this point in the history
  • Loading branch information
bkeepers committed Jan 20, 2024
1 parent e68028c commit 0009fe1
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 81 deletions.
49 changes: 16 additions & 33 deletions lib/dotenv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,41 @@ class << self

module_function

def load(*filenames)
with(*filenames) do |f|
ignoring_nonexistent_files do
env = Environment.new(f, true)
instrument("dotenv.load", env: env) { env.apply }
end
def load(*filenames, **kwargs)
parse(*filenames, **kwargs) do |env|
instrument("dotenv.load", env: env) { env.apply }
end
end

# same as `load`, but raises Errno::ENOENT if any files don't exist
def load!(*filenames)
with(*filenames) do |f|
env = Environment.new(f, true)
instrument("dotenv.load", env: env) { env.apply }
end
load(*filenames, ignore: false)
end

# same as `load`, but will override existing values in `ENV`
def overload(*filenames)
with(*filenames.reverse) do |f|
ignoring_nonexistent_files do
env = Environment.new(f, false)
instrument("dotenv.overload", env: env) { env.apply! }
end
end
load(*filenames, overwrite: true)
end

# same as `overload`, but raises Errno::ENOENT if any files don't exist
def overload!(*filenames)
with(*filenames.reverse) do |f|
env = Environment.new(f, false)
instrument("dotenv.overload", env: env) { env.apply! }
end
load(*filenames, overwrite: true, ignore: false)
end

# returns a hash of parsed key/value pairs but does not modify ENV
def parse(*filenames)
with(*filenames) do |f|
ignoring_nonexistent_files do
Environment.new(f, false)
end
end
end

# Internal: Helper to expand list of filenames.
#
# Returns a hash of all the loaded environment variables.
def with(*filenames)
def parse(*filenames, overwrite: false, ignore: true, &block)
filenames << ".env" if filenames.empty?
filenames = filenames.reverse if overwrite

filenames.reduce({}) do |hash, filename|
hash.merge!(yield(File.expand_path(filename)) || {})
begin
env = Environment.new(File.expand_path(filename), overwrite: overwrite)
env = block.call(env) if block
rescue Errno::ENOENT
raise unless ignore
end

hash.merge! env || {}
end
end

Expand Down
21 changes: 12 additions & 9 deletions lib/dotenv/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@ module Dotenv
class Environment < Hash
attr_reader :filename

def initialize(filename, is_load = false)
def initialize(filename, overwrite: false)
@filename = filename
load(is_load)
@overwrite = overwrite
load
end

def load(is_load = false)
update Parser.call(read, is_load)
def load
update Parser.call(read, overwrite: @overwrite)
end

def read
File.open(@filename, "rb:bom|utf-8", &:read)
end

def apply
each { |k, v| ENV[k] ||= v }
end

def apply!
each { |k, v| ENV[k] = v }
each do |k, v|
if @overwrite
ENV[k] = v
else
ENV[k] ||= v
end
end
end
end
end
10 changes: 5 additions & 5 deletions lib/dotenv/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ class Parser
class << self
attr_reader :substitutions

def call(string, is_load = false)
new(string, is_load).call
def call(...)
new(...).call
end
end

def initialize(string, is_load = false)
def initialize(string, overwrite: false)
@string = string
@hash = {}
@is_load = is_load
@overwrite = overwrite
end

def call
Expand Down Expand Up @@ -104,7 +104,7 @@ def unescape_value(value, maybe_quote)
def perform_substitutions(value, maybe_quote)
if maybe_quote != "'"
self.class.substitutions.each do |proc|
value = proc.call(value, @hash, @is_load)
value = proc.call(value, @hash, overwrite: @overwrite)
end
end
value
Expand Down
2 changes: 1 addition & 1 deletion lib/dotenv/substitutions/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class << self
)
/x

def call(value, _env, _is_load)
def call(value, _env, overwrite: false)
# Process interpolated shell commands
value.gsub(INTERPOLATED_SHELL_COMMAND) do |*|
# Eliminate opening and closing parentheses
Expand Down
4 changes: 2 additions & 2 deletions lib/dotenv/substitutions/variable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class << self
\}? # closing brace
/xi

def call(value, env, is_load)
combined_env = is_load ? env.merge(ENV) : ENV.to_h.merge(env)
def call(value, env, overwrite: false)
combined_env = overwrite ? ENV.to_h.merge(env) : env.merge(ENV)
value.gsub(VARIABLE) do |variable|
match = $LAST_MATCH_INFO
substitute(match, variable, combined_env)
Expand Down
28 changes: 15 additions & 13 deletions spec/dotenv/environment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

it "fails if file does not exist" do
expect do
Dotenv::Environment.new(".does_not_exists", true)
Dotenv::Environment.new(".does_not_exists")
end.to raise_error(Errno::ENOENT)
end
end
Expand All @@ -27,27 +27,29 @@
subject.apply
expect(ENV["OPTION_A"]).to eq("predefined")
end
end

describe "apply!" do
it "sets variables in the ENV" do
subject.apply!
expect(ENV["OPTION_A"]).to eq("1")
end
context "with overwrite: true" do
subject { env("OPTION_A=1\nOPTION_B=2", overwrite: true) }

it "overrides defined variables" do
ENV["OPTION_A"] = "predefined"
subject.apply!
expect(ENV["OPTION_A"]).to eq("1")
it "sets variables in the ENV" do
subject.apply
expect(ENV["OPTION_A"]).to eq("1")
end

it "overrides defined variables" do
ENV["OPTION_A"] = "predefined"
subject.apply
expect(ENV["OPTION_A"]).to eq("1")
end
end
end

require "tempfile"
def env(text)
def env(text, ...)
file = Tempfile.new("dotenv")
file.write text
file.close
env = Dotenv::Environment.new(file.path, true)
env = Dotenv::Environment.new(file.path, ...)
file.unlink
env
end
Expand Down
2 changes: 1 addition & 1 deletion spec/dotenv/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

describe Dotenv::Parser do
def env(string)
Dotenv::Parser.call(string, true)
Dotenv::Parser.call(string)
end

it "parses unquoted values" do
Expand Down
33 changes: 16 additions & 17 deletions spec/dotenv_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
let(:env_files) { [] }

it "defaults to .env" do
expect(Dotenv::Environment).to receive(:new).with(expand(".env"), anything)
.and_return(double(apply: {}, apply!: {}))
expect(Dotenv::Environment).to receive(:new).with(expand(".env"), anything).and_call_original
subject
end
end
Expand All @@ -23,7 +22,7 @@
expected = expand("~/.env")
allow(File).to receive(:exist?) { |arg| arg == expected }
expect(Dotenv::Environment).to receive(:new).with(expected, anything)
.and_return(double(apply: {}, apply!: {}))
.and_return(Dotenv::Environment.new(".env"))
subject
end
end
Expand Down Expand Up @@ -84,9 +83,9 @@

it_behaves_like "load"

it "initializes the Environment with a truthy is_load" do
expect(Dotenv::Environment).to receive(:new).with(anything, true)
.and_return(double(apply: {}, apply!: {}))
it "initializes the Environment with overwrite: false" do
expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false)
.and_call_original
subject
end

Expand All @@ -106,9 +105,9 @@

it_behaves_like "load"

it "initializes Environment with truthy is_load" do
expect(Dotenv::Environment).to receive(:new).with(anything, true)
.and_return(double(apply: {}, apply!: {}))
it "initializes Environment with overwrite: false" do
expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false)
.and_call_original
subject
end

Expand All @@ -127,9 +126,9 @@
it_behaves_like "load"
it_behaves_like "overload"

it "initializes the Environment with a falsey is_load" do
expect(Dotenv::Environment).to receive(:new).with(anything, false)
.and_return(double(apply: {}, apply!: {}))
it "initializes the Environment overwrite: true" do
expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: true)
.and_call_original
subject
end

Expand Down Expand Up @@ -161,9 +160,9 @@
it_behaves_like "load"
it_behaves_like "overload"

it "initializes the Environment with a falsey is_load" do
expect(Dotenv::Environment).to receive(:new).with(anything, false)
.and_return(double(apply: {}, apply!: {}))
it "initializes the Environment with overwrite: true" do
expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: true)
.and_call_original
subject
end

Expand Down Expand Up @@ -271,8 +270,8 @@
end
end

it "initializes the Environment with a falsey is_load" do
expect(Dotenv::Environment).to receive(:new).with(anything, false)
it "initializes the Environment with overwrite: false" do
expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false)
subject
end

Expand Down

0 comments on commit 0009fe1

Please sign in to comment.