yacht 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,4 +1,7 @@
1
1
  coverage
2
+ tmp
3
+ doc
4
+ .yardoc
2
5
  *.gem
3
6
  .DS_Store
4
7
  Gemfile.lock
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --title "Yacht: Yet Another Configuration Helper Tool"
2
+ --no-private
3
+ lib/**/*.rb
4
+ Gemfile
5
+ features//*.feature
6
+ features//*.rb
data/Gemfile CHANGED
@@ -2,4 +2,6 @@ source :rubygems
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'rake'
5
+ gem 'rake'
6
+ gem 'ruby-debug19', :platform => :ruby_19
7
+ gem 'ruby-debug', :platform => :ruby_18
data/README.rdoc CHANGED
@@ -4,6 +4,7 @@ Yacht is an application configuration gem that lets you define settings for mult
4
4
  * use of ClassyStruct for improved performance over OpenStruct
5
5
  * protection of sensitive settings by specifying a whitelist in a YAML file
6
6
  * easy override of nested keys (not pretty with YAML references)
7
+ * no need for an initializer or constant to store loaded values (just use Yacht.my_key)
7
8
 
8
9
  == Installation
9
10
 
@@ -39,8 +40,13 @@ First create two (or more) YAML files in the same directory to define your setti
39
40
 
40
41
  # config/yacht/whitelist.yml (required)
41
42
  # any keys specified here can be used as a whitelist filter:
42
- # YachtLoader.to_hash(:apply_whitelist? => true)
43
+ # Yacht::Loader.to_hash(:apply_whitelist? => true)
44
+ # or
45
+ # Yacht::Loader.to_classy_struct(:apply_whitelist? => true)
43
46
  # (by default the whitelist is ignored)
47
+ # NOTE: the whitelist is ignored when using Yacht.my_key or Yacht['my_key']
48
+ # you have to use Yacht::Loader#to_hash or
49
+ # Yacht::Loader#to_classy_struct to use the whitelist
44
50
  - super_secret_info
45
51
 
46
52
  # config/yacht/local.yml (optional)
@@ -49,28 +55,16 @@ First create two (or more) YAML files in the same directory to define your setti
49
55
  production:
50
56
  cdn_host: localhost
51
57
 
52
- === Step 2: Define a constant
58
+ === Step 2: Use Yacht.my_key or Yacht['my_key']
53
59
 
54
60
  * <b>Rails</b>:
55
- # config/initializers/01_yacht.rb
56
- # Define a constant that will store all settings
57
- # looks for the following YAML files:
58
- # * config/yacht/base.yml
59
- # * config/yacht/local.yml
60
- # * config/yacht/whitelist.yml
61
- Yacht = YachtLoader.to_classy_struct
62
61
  # now you can access any key set in your YAML files with:
63
62
  # Yacht.my_key
64
63
 
65
- * Outside of rails, you need to tell +YachtLoader+ where your YAML files are stored, and what environment you want to use.
66
- YachtLoader.dir = '/path/to/YAML/dir'
67
- YachtLoader.environment = 'my_environment'
68
- Yacht = YachtLoader.to_classy_struct
69
-
70
-
71
- == How it works
72
-
73
- Currently, the +Yacht+ gem defines a class called +YachtLoader+ that will read your YAML files and output them as a regular +Hash+ or a +ClassyStruct+ . Then you use <tt>YachtLoader#to_classy_struct</tt> (or <tt>YachtLoader#to_hash</tt>) to define a constant. You can name the constant whatever you like, but you should use +Yacht+. This is because a planned feature is to have +Yacht+ defined when the gem is required and have everything namespaced under a single +Yacht+ class.
64
+ * Outside of rails, you need to tell +Yacht+ where your YAML files are stored, and what environment you want to use.
65
+ Yacht::Loader.dir = '/path/to/YAML/dir'
66
+ Yacht::Loader.environment = 'my_environment'
67
+ Yacht.my_key
74
68
 
75
69
  == License
76
70
 
data/Rakefile CHANGED
@@ -6,4 +6,4 @@ Bundler::GemHelper.install_tasks
6
6
 
7
7
  Dir['gem_tasks/**/*.rake'].each { |rake| load rake }
8
8
 
9
- task :default => [:spec]
9
+ task :default => [:spec, :features]
@@ -0,0 +1,80 @@
1
+ Feature: Load configuration settings
2
+ In order to organize my configuration settings
3
+ As a developer using Yacht
4
+ I want to load configuration settings from an external source like a YAML file
5
+
6
+ Background:
7
+ Given a file named "base.yml" with:
8
+ """
9
+ default:
10
+ :api_key: some_fake_key
11
+ :partner_sites:
12
+ - twitter
13
+ - github
14
+ :mail:
15
+ :host: localhost
16
+ :from: Our great company
17
+ development:
18
+ :api_key: some_development_key
19
+ production:
20
+ :api_key: the_real_mccoy
21
+ :partner_sites:
22
+ - facebook
23
+ :mail:
24
+ host: example.com
25
+ reply-to: info@example.com
26
+ """
27
+
28
+ Scenario: Load from YAML
29
+ When I load Yacht with environment: "development"
30
+ Then Yacht should contain the following hash:
31
+ """
32
+ {
33
+ :api_key => 'some_development_key',
34
+ :partner_sites => [
35
+ 'twitter',
36
+ 'github'
37
+ ],
38
+ :mail => {
39
+ :host => 'localhost',
40
+ :from => 'Our great company'
41
+ }
42
+ }
43
+ """
44
+
45
+ Scenario: Local overrides with local.yml
46
+ Given a file named "local.yml" with:
47
+ """
48
+ :api_key: some_crazy_local_key
49
+ """
50
+ When I load Yacht with environment: "development"
51
+ Then Yacht should contain the following hash:
52
+ """
53
+ {
54
+ :api_key => 'some_crazy_local_key',
55
+ :partner_sites => [
56
+ 'twitter',
57
+ 'github'
58
+ ],
59
+ :mail => {
60
+ :host => 'localhost',
61
+ :from => 'Our great company'
62
+ }
63
+ }
64
+ """
65
+
66
+ Scenario: Whitelisting with whitelist.yml
67
+ Given a file named "whitelist.yml" with:
68
+ """
69
+ - :partner_sites
70
+ """
71
+ When I define the constant "MyYacht" with environment: "development" using a whitelist
72
+ Then the constant "MyYacht" should contain the following hash:
73
+ """
74
+ {
75
+ :partner_sites => [
76
+ 'twitter',
77
+ 'github'
78
+ ]
79
+ }
80
+ """
@@ -0,0 +1,30 @@
1
+ Feature: Handle missing YAML files reasonably
2
+ In order to ensure robustness and correctness of data
3
+ Missing YAML files should cause an error to be raised when reasonable
4
+
5
+ Background:
6
+ Given a file named "base.yml" with:
7
+ """
8
+ development:
9
+ api_key: some_fake_key
10
+ """
11
+
12
+ Scenario: No base.yml
13
+ Given a file named "base.yml" does not exist
14
+ When I try to use Yacht
15
+ Then Yacht should raise an error with message: "Couldn't load base config"
16
+
17
+ Scenario: No local.yml
18
+ Given a file named "local.yml" does not exist
19
+ When I try to use Yacht
20
+ Then Yacht should not raise an error
21
+
22
+ Scenario: No whitelist.yml but whitelist not used
23
+ Given a file named "whitelist.yml" does not exist
24
+ When I try to use Yacht
25
+ Then Yacht should not raise an error
26
+
27
+ Scenario: No whitelist.yml and whitelist used
28
+ Given a file named "whitelist.yml" does not exist
29
+ When I try to use Yacht with a whitelist
30
+ Then Yacht should raise an error with message: "Couldn't load whitelist"
@@ -0,0 +1,5 @@
1
+ Given /^a file named "([^"]*)" does not exist$/ do |file_name|
2
+ in_current_dir do
3
+ FileUtils.rm(file_name) if File.file?(file_name)
4
+ end
5
+ end
@@ -0,0 +1,27 @@
1
+ Then /^Yacht should raise an error with message: "([^"]*)"$/ do |message|
2
+ @last_yacht.class.should be(Yacht::LoadError)
3
+ @last_yacht.message.should == message
4
+ end
5
+
6
+ When /^I try to use Yacht( with a whitelist)?$/ do |whitelist|
7
+ @last_yacht = use_yacht(!!whitelist)
8
+ end
9
+
10
+ Then /^Yacht should not raise an error$/ do
11
+ @last_yacht.class.should_not <= Exception
12
+ end
13
+
14
+ module ErrorHandlingHelpers
15
+ # Try to use Yacht and return output of Yacht#to_classy_struct if no error raised
16
+ # If an error is raised, return it instead
17
+ def use_yacht(whitelist=false)
18
+ in_current_dir do
19
+ Yacht::Loader.dir = '.'
20
+ Yacht::Loader.environment = 'development'
21
+ Yacht::Loader.to_classy_struct(:apply_whitelist? => whitelist)
22
+ end
23
+ rescue Exception => e
24
+ e
25
+ end
26
+ end
27
+ World(ErrorHandlingHelpers)
@@ -0,0 +1,28 @@
1
+ When /^I define the constant "([^"]*)" with environment: "([^"]*)"( using a whitelist)?$/ do |constant_name, env, whitelist|
2
+ in_current_dir do
3
+ Yacht::Loader.dir = '.'
4
+ Yacht::Loader.environment = env
5
+ Object.const_set( constant_name, Yacht::Loader.to_classy_struct(:apply_whitelist? => whitelist ) )
6
+ end
7
+ end
8
+
9
+
10
+ Then /^the constant "([^"]*)" should contain the following hash:$/ do |constant_name, stringified|
11
+ hash = eval(stringified)
12
+ Object.const_get(constant_name).to_hash.should == hash # don't forget to tack on to_hash to avoid weird errors
13
+ # TODO: make ClassyStruct more Hash-like so
14
+ # i t can be compared against hashes using `==`
15
+ end
16
+
17
+ When /^I load Yacht with environment: "([^"]*)"$/ do |env|
18
+ Yacht::Loader.dir = '.'
19
+ Yacht::Loader.environment = env
20
+ end
21
+
22
+ Then /^Yacht should contain the following hash:$/ do |stringified|
23
+ hash = eval(stringified)
24
+
25
+ in_current_dir do
26
+ Yacht::Loader.to_hash.should == hash
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ After do
2
+ Yacht::Loader.environment = nil
3
+ Yacht::Loader.dir = nil
4
+
5
+ Yacht::Loader.instance_variable_set(:@config_file_names, nil)
6
+ Yacht::Loader.instance_variable_set(:@classy_struct_instance, nil)
7
+
8
+ Yacht.instance_variable_set(:@_loader, nil)
9
+ end
@@ -0,0 +1,4 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../../lib')
2
+
3
+ require 'aruba/cucumber'
4
+ require 'yacht'
@@ -0,0 +1,5 @@
1
+ require 'cucumber/rake/task'
2
+
3
+ Cucumber::Rake::Task.new(:features) do |t|
4
+ t.fork = false
5
+ end
data/lib/yacht/base.rb ADDED
@@ -0,0 +1,14 @@
1
+ class Yacht < BasicObject
2
+ class LoadError < ::StandardError
3
+ end
4
+
5
+ class << self
6
+ def [](key)
7
+ self._hash[key]
8
+ end
9
+
10
+ def _hash
11
+ @_hash ||= Loader.to_hash
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ require 'classy_struct'
2
+
3
+ class Yacht < BasicObject
4
+ class Loader
5
+ class << self
6
+ def classy_struct_instance
7
+ @classy_struct_instance ||= ClassyStruct.new
8
+ end
9
+
10
+ def to_classy_struct(opts={})
11
+ classy_struct_instance.new( self.to_hash(opts) )
12
+ rescue StandardError => e
13
+ # don't do anything to our own custom errors
14
+ if e.is_a? Yacht::LoadError
15
+ raise e
16
+ else
17
+ raise Yacht::LoadError.new("Error creating ClassyStruct: #{e.message}")
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ class << self
24
+ def method_missing(method, *args, &block)
25
+ _classy_struct.send(method)
26
+ end
27
+
28
+ def _classy_struct
29
+ @_classy_struct ||= Loader.to_classy_struct
30
+ end
31
+ end
32
+ end
@@ -1,9 +1,6 @@
1
1
  # TODO: Rename YachtLoader to Yacht and somehow incorporate ClassyStruct
2
2
 
3
- class YachtLoader
4
- class LoadError < StandardError
5
- end
6
-
3
+ class Yacht::Loader
7
4
  class << self
8
5
  def environment
9
6
  @environment ||= 'default'
@@ -20,7 +17,7 @@ class YachtLoader
20
17
  end
21
18
 
22
19
  def config_file_for(config_type)
23
- raise LoadError.new "#{config_type} is not a valid config type" unless valid_config_types.include?(config_type.to_s)
20
+ raise Yacht::LoadError.new "#{config_type} is not a valid config type" unless valid_config_types.include?(config_type.to_s)
24
21
 
25
22
  full_file_path_for_config(config_type)
26
23
  end
@@ -34,7 +31,7 @@ class YachtLoader
34
31
  end
35
32
 
36
33
  def to_hash(opts={})
37
- opts[:apply_whitelist?] = false unless opts.has_key?(:apply_whitelist?)
34
+ opts[:apply_whitelist?] ||= false unless opts.has_key?(:apply_whitelist?)
38
35
  self.environment = opts[:env] if opts.has_key?(:env)
39
36
 
40
37
  if opts[:apply_whitelist?]
@@ -45,60 +42,47 @@ class YachtLoader
45
42
  end
46
43
 
47
44
  def whitelist
48
- load_config_file(:whitelist, :expect_to_load => Array) || begin
49
- raise LoadError.new("Couldn't load whitelist")
50
- end
45
+ load_config_file(:whitelist, :expect_to_load => Array) || raise( Yacht::LoadError.new("Couldn't load whitelist") )
51
46
  end
52
47
 
53
48
  def base_config
54
- load_config_file(:base) || begin
55
- raise LoadError.new("Couldn't load base config")
56
- end
49
+ load_config_file(:base) || raise( Yacht::LoadError.new("Couldn't load base config") )
57
50
  end
58
51
 
59
52
  def local_config
60
- load_config_file(:local, :require_presence? => false) || {}
53
+ load_config_file(:local) || {}
61
54
  end
62
55
 
63
56
  protected
57
+ # Wrap the YAML.load for easier mocking
58
+ def _load_config_file(file_name)
59
+ YAML.load( File.read(file_name) ) if File.exists?(file_name)
60
+ end
61
+
62
+ # Load a config file with plenty of error-checking
64
63
  def load_config_file(file_type, opts={})
65
64
  # by default, expect a Hash to be loaded
66
65
  expected_class = opts[:expect_to_load] || Hash
67
66
 
68
- # by default, raise error if file missing or empty
69
- presence_required = if opts.has_key?(:require_presence?)
70
- opts[:require_presence?]
71
- else
72
- true
73
- end
74
-
75
67
  file_name = self.config_file_for(file_type)
76
-
77
- loaded = if File.exists?(file_name)
78
- YAML.load( File.read(file_name) )
79
- else
80
- nil
81
- end
82
-
83
- # an empty YAML file will be converted to boolean false
84
- raise LoadError.new "#{file_name} cannot be empty" if presence_required && loaded === false
68
+ loaded = self._load_config_file(file_name)
85
69
 
86
70
  # YAML contained the wrong type
87
- raise LoadError.new "#{file_name} must contain #{expected_class} (got #{loaded.class})" if loaded && !loaded.is_a?(expected_class)
71
+ raise Yacht::LoadError.new "#{file_name} must contain #{expected_class} (got #{loaded.class})" if loaded && !loaded.is_a?(expected_class)
88
72
 
89
73
  loaded
90
74
  rescue => e
91
75
  # don't do anything to our own custom errors
92
- if e.is_a? YachtLoader::LoadError
76
+ if e.is_a? Yacht::LoadError
93
77
  raise e
94
78
  else
95
- # convert other errors to YachtLoader::LoadError
96
- raise LoadError.new "ERROR: loading config file: '#{file_type}': #{e}"
79
+ # convert other errors to YachtLoader::Yacht::LoadError
80
+ raise Yacht::LoadError.new "ERROR: loading config file: '#{file_type}': #{e}"
97
81
  end
98
82
  end
99
83
 
100
84
  def chain_configs(config, env)
101
- raise LoadError.new "environment '#{env}' does not exist" unless config.has_key?(env)
85
+ raise Yacht::LoadError.new "environment '#{env}' does not exist" unless config.has_key?(env)
102
86
 
103
87
  parent = if config[env]['_parent']
104
88
  chain_configs(config, config[env]['_parent'])
@@ -109,4 +93,8 @@ class YachtLoader
109
93
  parent.deep_merge(config[env])
110
94
  end
111
95
  end
112
- end
96
+
97
+ end
98
+
99
+ # Alias for Yacht::Loader for backwards compatibility
100
+ Object.const_set(:YachtLoader, Yacht::Loader)
@@ -1,4 +1,4 @@
1
- class YachtLoader
1
+ class Yacht::Loader
2
2
  class << self
3
3
  # use the current rails environment by default
4
4
  def environment
@@ -0,0 +1,3 @@
1
+ class Yacht < BasicObject
2
+ VERSION = "0.2.0"
3
+ end
data/lib/yacht.rb CHANGED
@@ -1,12 +1,7 @@
1
- # This file is named yacht.rb
2
- # so that Bundler will automatically load YachtLoader
3
- #
4
- # In the future, once we figure out how to integrate ClassyStruct,
5
- # YachtLoader will be renamed to Yacht
1
+ require "yacht/base"
2
+ require "yacht/loader"
3
+ require "yacht/classy_struct"
4
+ require "yacht/version"
6
5
 
7
- require "yacht_loader/base"
8
- require "yacht_loader/classy_struct"
9
- require "yacht_loader/version"
10
-
11
- require "yacht_loader/rails" if Object.const_defined?(:Rails)
12
- require 'monkeypatches/hash'
6
+ require "yacht/rails" if Object.const_defined?(:Rails)
7
+ require 'monkeypatches/hash'
data/spec/spec_helper.rb CHANGED
@@ -15,101 +15,8 @@ require 'yacht'
15
15
 
16
16
  RSpec.configure do |config|
17
17
  config.after :each do
18
- YachtLoader.environment = nil
19
- YachtLoader.dir = nil
20
- YachtLoader.instance_variable_set(:@config_file_names, nil)
21
- end
22
- end
23
-
24
- BASE_CONFIG_FILE = <<EOF
25
- default:
26
- name: default
27
- defaultkey: defaultvalue
28
- dog: schnauzer
29
- hashkey:
30
- foo: bar
31
- baz: wurble
32
- xyzzy: thud
33
- an_environment:
34
- name: an_environment
35
- hashkey:
36
- baz: yay
37
- dog: terrier
38
- a_child_environment:
39
- _parent: an_environment
40
- name: a_child_environment
41
- hashkey:
42
- foo: kung
43
- test:
44
- baloney: delicious
45
- EOF
46
-
47
- WHITELIST_CONFIG_FILE = <<EOF
48
- - defaultkey
49
- - hashkey
50
- EOF
51
-
52
- EMPTY_WHITELIST_CONFIG_FILE = <<EOF
53
- EOF
54
-
55
- INVALID_WHITELIST_CONFIG_FILE = <<EOF
56
- somenonsenseorother
57
- EOF
58
-
59
- LOCAL_CONFIG_FILE = <<EOF
60
- localkey: localvalue
61
- EOF
62
-
63
- EMPTY_LOCAL_CONFIG_FILE = <<EOF
64
- EOF
65
-
66
- INVALID_LOCAL_CONFIG_FILE = <<EOF
67
- someinvalidstufforother
68
- EOF
69
-
70
-
71
- # ===================================
72
- # = Helpers to mock file operations =
73
- # ===================================
74
- def banish_config_file_from_prefix(prefix)
75
- file_name = YachtLoader.config_file_for(prefix)
76
- banish_file(file_name)
77
- end
78
-
79
- # shortcut to mock config file
80
- def conjure_config_file_from_prefix(prefix, file_contents=nil)
81
- file_name = YachtLoader.config_file_for(prefix)
82
- file_contents ||= "#{prefix.upcase}_CONFIG_FILE".constantize
83
-
84
- conjure_file(file_name, file_contents)
85
- end
86
-
87
- def conjure_bad_config_file_from_prefix(prefix)
88
- file_name = "#{prefix}_config_file"
89
- file_contents = file_name.upcase.constantize
90
-
91
- conjure_file(file_name, file_contents)
92
- end
93
-
94
- # mock file existence & contents
95
- def conjure_file(file_name, file_contents)
96
- File.stub!(:exists?).with(file_name).and_return true
97
- File.stub!(:read).with(file_name).and_return file_contents
98
- end
99
-
100
- # mock file non-existence
101
- def banish_file(file_name)
102
- File.stub!(:exists?).with(file_name).and_return(false)
103
- File.stub!(:read).with(file_name).and_raise Errno::ENOENT.new("No such file or directory - #{file_name}")
104
- end
105
-
106
- # ================================================
107
- # = Railsy helper to turn strings into constants =
108
- # ================================================
109
- if !String.instance_methods.include?(:constantize)
110
- class String
111
- def constantize # NOT as awesome as ActiveSupport::Inflector#constantize
112
- Object.const_get(self)
113
- end
18
+ Yacht::Loader.environment = nil
19
+ Yacht::Loader.dir = nil
20
+ Yacht::Loader.instance_variable_set(:@config_file_names, nil)
114
21
  end
115
22
  end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Yacht do
4
+ subject { Yacht }
5
+
6
+ describe :[] do
7
+ it "should retrieve value of key from Yacht::Loader.to_hash" do
8
+ mock_hash = {}
9
+
10
+ Yacht::Loader.should_receive(:to_hash).and_return(mock_hash)
11
+ mock_hash.should_receive(:[]).with(:foo)
12
+
13
+ subject[:foo]
14
+ end
15
+ end
16
+ end