require 'spec_helper' describe Figgy do it "reads YAML config files" do write_config 'values', <<-YML foo: 1 bar: 2 YML expect(test_config.values).to eq({ "foo" => 1, "bar" => 2 }) end it "raises an exception if the file can't be found" do expect { test_config.values }.to raise_error(Figgy::FileNotFound) end it "has a useful #inspect method" do write_config 'values', 'foo: 1' write_config 'wtf', 'bar: 2' config = test_config expect(config.inspect).to eq("#") config.values expect(config.inspect).to eq("#") config.wtf expect(config.inspect).to eq("#") end context "multiple extensions" do it "supports .yaml" do write_config 'values.yaml', 'foo: 1' expect(test_config.values.foo).to eq(1) end it "supports .yml.erb and .yaml.erb" do write_config 'values.yml.erb', '<%= "foo" %>: <%= 1 %>' write_config 'values.yaml.erb', '<%= "foo" %>: <%= 2 %>' expect(test_config.values.foo).to eq(2) end it "supports .json" do write_config "values.json", '{ "json": true }' expect(test_config.values.json).to be true end it "loads in the order named" do write_config 'values.yml', 'foo: 1' write_config 'values.yaml', 'foo: 2' config = test_config do |config| config.define_handler('yml', 'yaml') { |body| YAML.load(body) } end expect(config.values.foo).to eq(2) end end context "hash contents" do it "makes the hash result dottable and indifferent" do write_config 'values', <<-YML outer: also: dottable YML config = test_config expect(config.values.outer).to eq({ "also" => "dottable" }) expect(config.values["outer"]).to eq({ "also" => "dottable" }) expect(config.values[:outer]).to eq({ "also" => "dottable" }) end it "makes a hash inside the hash result dottable and indifferent" do write_config 'values', <<-YML outer: also: dottable YML config = test_config expect(config.values.outer.also).to eq("dottable") expect(config.values.outer["also"]).to eq("dottable") expect(config.values.outer[:also]).to eq("dottable") end it "makes a hash inside an array result dottable and indifferent" do write_config 'values', <<-YML outer: - in: an array: it is - still: a dottable hash YML config = test_config expect(config.values.outer.size).to eq(2) first, second = *config.values.outer expect(first).to eq({ "in" => "an", "array" => "it is" }) expect(first[:in]).to eq("an") expect(first.array).to eq("it is") expect(second.still).to eq("a dottable hash") expect(second[:still]).to eq("a dottable hash") expect(second["still"]).to eq("a dottable hash") end it "supports dottable and indifferent setting" do write_config 'values', "number: 1" config = test_config config.values["number"] = 2 expect(config.values.number).to eq(2) config.values[:number] = 3 expect(config.values.number).to eq(3) config.values.number = 4 expect(config.values.number).to eq(4) end it "answers respond_to? accurately for known keys" do write_config 'values', <<-YML outer: inner: still: a hash YML config = test_config expect(config).to respond_to(:values) expect(config).not_to respond_to(:unknown) expect(config.values).to respond_to(:outer) expect(config.values.outer).to respond_to(:inner) expect(config.values.outer.inner).to respond_to(:still) # Ensure plain ol' respond_to? still works, too. expect(config).to respond_to(:inspect) expect(config.values).to respond_to(:[]) end it "supports indifferent hash notation on the top-level config object" do write_config 'values', "number: 1" config = test_config expect(config['values']).to eq(config.values) expect(config[:values]).to eq(config.values) end context "performing basic hash operations" do let(:config) do write_config 'values', <<-YML with: one: 1 two: 2 without: two: 2 another: three: 3 altogether: one: 1 two: 2 three: 3 YML test_config end it "can delete a key" do expect(config.values.with.delete(:one)).to eq(1) expect(config.values.with).to eq(config.values.without) end it "can look up values for a list of keys" do expect(config.values.with.values_at(:one,:two)).to eq([1,2]) end it "can merge with another hash" do expect(config.values.with.merge(config.values.another)).to eq(config.values.altogether) end end end context 'oddities' do it "returns false for empty files (cf. YAML.load(''))" do write_config 'empty', '' expect(test_config.empty).to eq(false) end it "returns false for files containing a literal false" do write_config 'maybe', 'false' expect(test_config.maybe).to eq(false) end it "returns nil when explicitly set to that value in the YAML file" do write_config 'reason_to_do_this', nil.to_yaml expect(test_config.reason_to_do_this).to eq(nil) end it "prioritizes hash methods over keys in the YAML file" do write_config 'bad', { 'default' => 'something'}.to_yaml expect(test_config.bad.default).to eq(nil) end end context "multiple roots" do it "can be told to read from multiple directories" do write_config 'root1/values', 'foo: 1' write_config 'root2/values', 'bar: 2' config = test_config do |config| config.root = File.join(current_dir, 'root1') config.add_root File.join(current_dir, 'root2') end expect(config.values.foo).to eq(1) expect(config.values.bar).to eq(2) end it "supports overlays in each root" do write_config 'root1/values', 'foo: 1' write_config 'root1/prod/values', 'foo: 2' write_config 'root2/values', 'bar: 1' write_config 'root2/prod/values', 'bar: 2' config = test_config do |config| config.root = File.join(current_dir, 'root1') config.add_root File.join(current_dir, 'root2') config.define_overlay :environment, 'prod' end expect(config.values.foo).to eq(2) expect(config.values.bar).to eq(2) end it "reads from roots in *reverse* order of definition" do write_config 'root1/values', 'foo: 1' write_config 'root1/prod/values', 'foo: 2' write_config 'root2/prod/values', 'foo: 3' config = test_config do |config| config.root = File.join(current_dir, 'root1') config.add_root File.join(current_dir, 'root2') config.define_overlay :environment, 'prod' end expect(config.values.foo).to eq(2) end end context "overlays" do it "defaults to no overlay, thus reading directly from the config root" do write_config 'values', "foo: 1" expect(test_config.values).to eq({ "foo" => 1 }) end it "interprets a nil overlay value as an indication to read from the config root" do write_config 'values', "foo: 1" config = test_config do |config| config.define_overlay :default, nil end expect(config.values).to eq({ "foo" => 1 }) end it "allows the overlay's value to be the result of a block" do write_config 'prod/values', "foo: 1" config = test_config do |config| config.define_overlay(:environment) { 'prod' } end expect(config.values).to eq({ "foo" => 1 }) end it "overwrites values if the config file does not define a hash" do write_config 'some_string', "foo bar baz" write_config 'prod/some_string', "foo bar baz quux" config = test_config do |config| config.define_overlay :default, nil config.define_overlay :environment, 'prod' end expect(config.some_string).to eq("foo bar baz quux") end it "deep merges hash contents from overlays" do write_config 'defaults/values', <<-YML foo: bar: 1 baz: 2 YML write_config 'prod/values', <<-YML foo: baz: 3 quux: hi! YML config = test_config do |config| config.define_overlay :default, 'defaults' config.define_overlay :environment, 'prod' end expect(config.values).to eq({ "foo" => { "bar" => 1, "baz" => 3 }, "quux" => "hi!" }) end it "can use both a nil overlay and an overlay with a value" do write_config 'values', "foo: 1\nbar: 2" write_config 'prod/values', "foo: 2" config = test_config do |config| config.define_overlay :default, nil config.define_overlay :environment, 'prod' end expect(config.values).to eq({ "foo" => 2, "bar" => 2 }) end it "reads from overlays in order of definition" do write_config 'defaults/values', <<-YML foo: 1 bar: 1 baz: 1 YML write_config 'prod/values', <<-YML bar: 2 baz: 2 YML write_config 'local/values', <<-YML baz: 3 YML config = test_config do |config| config.define_overlay :default, 'defaults' config.define_overlay :environment, 'prod' config.define_overlay :local, 'local' end expect(config.values).to eq({ "foo" => 1, "bar" => 2, "baz" => 3 }) end end context "combined overlays" do it "allows new overlays to be defined from the values of others" do write_config 'keys', "foo: 1" write_config 'prod/keys', "foo: 2" write_config 'prod_US/keys', "foo: 3" config = test_config do |config| config.define_overlay :default, nil config.define_overlay :environment, 'prod' config.define_overlay :country, 'US' config.define_combined_overlay :environment, :country end expect(config.keys).to eq({ "foo" => 3 }) end end context "reloading" do it "can reload on each access when config.always_reload = true" do write_config 'values', 'foo: 1' config = test_config do |config| config.always_reload = true end expect(config.values).to eq({ "foo" => 1 }) write_config 'values', 'foo: bar' expect(config.values).to eq({ "foo" => "bar" }) end it "does not reload when config.always_reload = false" do write_config 'values', 'foo: 1' config = test_config do |config| config.always_reload = false end expect(config.values).to eq({ "foo" => 1 }) write_config 'values', 'foo: bar' expect(config.values).to eq({ "foo" => 1 }) end end context "preloading" do it "can preload all available configs when config.preload = true" do write_config 'values', 'foo: 1' write_config 'prod/values', 'foo: 2' write_config 'prod/prod_only', 'bar: baz' config = test_config do |config| config.define_overlay :default, nil config.define_overlay :environment, 'prod' config.preload = true end write_config 'prod/values', 'foo: 3' write_config 'prod_only', 'bar: quux' expect(config.values['foo']).to eq(2) expect(config.prod_only['bar']).to eq('baz') end it "still works with multiple extension support" do write_config 'values.yaml', 'foo: 1' write_config 'values.json', '{ "foo": 2 }' write_config 'prod/lonely.yml', 'only: yml' write_config 'local/json_values.json', '{ "json": true }' config = test_config do |config| config.define_overlay :default, nil config.define_overlay :environment, 'prod' config.define_overlay :local, 'local' end finder = config.instance_variable_get(:@finder) expect(finder.all_key_names).to eq(['values', 'lonely', 'json_values']) end it "still supports reloading when preloading is enabled" do write_config 'values', 'foo: 1' config = test_config do |config| config.preload = true config.always_reload = true end expect(config.values['foo']).to eq(1) write_config 'values', 'foo: 2' expect(config.values['foo']).to eq(2) end end context "freezing" do it "leaves results unfrozen by default" do write_config 'values', "foo: '1'" expect(test_config.values.foo).not_to be_frozen end it "freezes the results when config.freeze = true" do write_config 'values', "foo: '1'" config = test_config do |config| config.freeze = true end expect(config.values).to be_frozen end it "freezes all the way down" do write_config 'values', <<-YML outer: key: value array: - some string - another string - and: an inner hash YML config = test_config do |config| config.freeze = true end expect { config.values.outer.array[2]['and'] = 'foo' }.to raise_error(/can't modify frozen/) assert_deeply_frozen(config.values) end def assert_deeply_frozen(obj) expect(obj).to be_frozen case obj when Hash then obj.each { |k, v| assert_deeply_frozen(k); assert_deeply_frozen(v) } when Array then obj.each { |v| assert_deeply_frozen(v) } end end end end