dacs 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,10 @@
1
+ == 0.2.0 2010-01-13
2
+ * Major enhancements
3
+ * Added new mini-DSL for defining known configuration keys
4
+ * AppConfig now knows where each configured value came from
5
+ (default, file, or environment)
6
+ * Init will generate a starter config file if one doesn't exist
7
+
8
+ * Minor enhancements
9
+ * Warns when unknown keys are encountered
10
+ * Checks config file for sanity
data/Rakefile CHANGED
@@ -10,8 +10,10 @@ begin
10
10
  gem.email = "devs@devver.net"
11
11
  gem.homepage = "http://github.com/avdi/dacs"
12
12
  gem.authors = ["Devver, Inc."]
13
+ gem.add_dependency "ruport", "~> 1.6"
13
14
  gem.add_development_dependency "rspec", ">= 1.2.9"
14
15
  gem.add_development_dependency "yard", ">= 0"
16
+ gem.add_development_dependency "devver-construct", "~> 1.1"
15
17
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
18
  end
17
19
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
data/example/example.rb CHANGED
@@ -1,20 +1,33 @@
1
1
  #!/usr/bin/env ruby
2
+ require 'rubygems'
2
3
  require File.expand_path('../lib/dacs', File.dirname(__FILE__))
3
4
  ENV['EXAMPLE_BAR'] = 'env_bar'
4
5
  Dacs::AppConfig.init!('example',
5
- :defaults => {
6
- 'foo' => 'default_foo',
7
- 'bar' => 'default_bar',
8
- 'baz' => 'default_baz' },
9
6
  :config_path => File.expand_path('example.yml', File.dirname(__FILE__)),
10
7
  :environment => 'development',
11
- :logger => Logger.new($stdout))
8
+ :logger => Logger.new($stdout)) do |config|
9
+
10
+ config.key 'foo', :default => 'default_foo'
11
+ config.key 'bar', :default => 'default_bar'
12
+ config.key 'baz', :default => 'default_baz'
13
+ end
12
14
 
15
+ puts "Running in #{Dacs::AppConfig.environment} mode"
13
16
  puts "foo: #{Dacs::AppConfig['foo']}"
14
17
  puts "bar: #{Dacs::AppConfig['bar']}"
15
18
  puts "baz: #{Dacs::AppConfig['baz']}"
19
+ puts Dacs::AppConfig.dump # => nil
16
20
 
17
- # >> I, [2010-01-27T18:59:15.223408 #23940] INFO -- : Found config file /devver-repos/dacs/example/example.yml.
21
+ # >> I, [2010-02-13T02:34:46.344705 #25573] INFO -- : Found config file /devver-repos/dacs/example/example.yml.
22
+ # >> W, [2010-02-13T02:34:46.345300 #25573] WARN -- : Unknown configuration key 'fuz' in file example.yml
23
+ # >> Running in development mode
18
24
  # >> foo: file_foo
19
25
  # >> bar: env_bar
20
26
  # >> baz: default_baz
27
+ # >> +--------------------------------------+
28
+ # >> | Key | Value | Source |
29
+ # >> +--------------------------------------+
30
+ # >> | baz | default_baz | defaults |
31
+ # >> | foo | file_foo | file example.yml |
32
+ # >> | bar | env_bar | environment |
33
+ # >> +--------------------------------------+
data/example/example.yml CHANGED
@@ -1,3 +1,4 @@
1
1
  development:
2
2
  foo: file_foo
3
3
  bar: file_bar
4
+ fuz: file_fuz
@@ -3,6 +3,7 @@ require 'singleton'
3
3
  require 'logger'
4
4
  require 'pathname'
5
5
  require 'yaml'
6
+ require 'ruport'
6
7
 
7
8
  module Dacs
8
9
  # This configuration system is for deployment-specific values, such as AWS keys
@@ -34,23 +35,55 @@ module Dacs
34
35
  # variables. For instance, to set key 'foo' = 'bar', set APPNAME_FOO='bar'
35
36
  # in the process environment.
36
37
  class AppConfig < Hash
37
- include Singleton
38
38
 
39
39
  class << self
40
40
  extend Forwardable
41
41
 
42
42
  # Delegate a subset of Hash methods to the singleton instance
43
- def_delegators :instance, :[], :fetch, :key?, :merge, :merge!, :app_name
43
+ def_delegators :instance, :[], :fetch, :key?, :merge, :merge!, :app_name,
44
+ :source, :dump
45
+
46
+ def_delegators :schema, :required?, :optional?, :keys, :default_value
44
47
  end
45
48
 
49
+ # Usage:
50
+ #
51
+ # Dacs::AppConfig.init!('example',
52
+ # :environment => 'development',
53
+ # :logger => Logger.new($stdout)) do |config|
54
+ #
55
+ # config.key 'foo', :default => 'default_foo'
56
+ # config.key 'bar', :default => 'default_bar'
57
+ # config.key 'baz', :default => 'default_baz'
58
+ # end
46
59
  def self.init!(app_name, options={})
47
- @__instance__ = nil
60
+ @instance = nil
48
61
  @@options = options.merge(:app_name => app_name)
49
62
  @@options[:app_root] ||= Pathname(Dir.pwd)
50
63
  @@options[:config_path] ||= @@options[:app_root] + 'config' + "#{app_name}.yml"
51
64
  @@options[:logger] ||= ::Logger.new($stderr)
52
65
  @@options[:environment] ||= :development
53
66
  @@options[:defaults] ||= {}
67
+ if block_given?
68
+ schema = Schema.new
69
+ yield(schema)
70
+ @@schema = schema
71
+ else
72
+ @@schema = PermissiveSchema.new(@@options[:defaults])
73
+ end
74
+ self.instance
75
+ end
76
+
77
+ def self.instance
78
+ @instance ||= new
79
+ end
80
+
81
+ def self.schema
82
+ @@schema
83
+ end
84
+
85
+ def self.environment
86
+ @@options[:environment]
54
87
  end
55
88
 
56
89
  attr_reader :app_name
@@ -60,28 +93,94 @@ module Dacs
60
93
 
61
94
  def initialize
62
95
  raise "You must initialize with Dacs::AppConfig.init!()" unless @@options
63
- @app_name = @@options[:app_name]
64
- @config_path = @@options[:config_path]
65
- @logger = @@options[:logger]
66
- @environment = @@options[:environment]
67
- @defaults = @@options[:defaults]
68
- @local_config = if Pathname(config_path).exist?
69
- logger.info "Found config file #{config_path}."
70
- YAML.load_file(config_path.to_s).fetch(environment.to_s) {{}}
71
- else
72
- logger.info "#{config_path} does not exist;" +
73
- " config will be from environment."
74
- {}
75
- end
76
-
77
- replace(@defaults)
78
-
79
- # Local config takes priority over defaults
80
- merge!(@local_config)
81
-
82
- # Environment takes priority over local config
83
- ENV.keys.grep(/^#{app_name.upcase}_(.*)$/) do |key|
84
- self[$1.downcase] = ENV[key]
96
+ @app_name = @@options[:app_name]
97
+ @config_path = @@options[:config_path]
98
+ @logger = @@options[:logger]
99
+ @environment = @@options[:environment]
100
+ @defaults = self.class.schema.defaults
101
+ find_or_create_config_file!
102
+
103
+ defaults_source = DefaultSource.new(@defaults)
104
+ file_source = FileSource.new(config_path, @environment)
105
+ env_source = EnvironmentSource.new(@app_name)
106
+
107
+ load_values!(self.class.schema, env_source, file_source, defaults_source)
108
+ end
109
+
110
+ def source(key)
111
+ configured_value = Hash.instance_method(:fetch).bind(self).call(key.to_s) do
112
+ raise ConfigurationError, "No such key '#{key}'"
113
+ end
114
+ configured_value.source.to_s
115
+ end
116
+
117
+ def [](key)
118
+ super(key).value
119
+ end
120
+
121
+ def fetch(key, &block)
122
+ case result = super(key, &block)
123
+ when ConfiguredValue then result.value
124
+ else result
125
+ end
126
+ end
127
+
128
+ def dump
129
+ table = Table(%w[Key Value Source])
130
+ each_pair do |key, configured_value|
131
+ table << [key, configured_value.value, configured_value.source.to_s]
132
+ end
133
+ table.as(:text)
134
+ end
135
+
136
+ private
137
+
138
+ def find_or_create_config_file!
139
+ if Pathname(config_path).exist?
140
+ logger.info "Found config file #{config_path}."
141
+ else
142
+ logger.info "#{config_path} does not exist"
143
+ create_starter_config_file!
144
+ end
145
+ end
146
+
147
+ def create_starter_config_file!
148
+ path = Pathname(config_path)
149
+ path.dirname.mkpath
150
+ path.open('w+') do |f|
151
+ f << starter_config_content
152
+ end
153
+ logger.info "Starter config file created at #{config_path}. " +
154
+ "Please customize it to your needs."
155
+ end
156
+
157
+ def starter_config_content
158
+ <<END
159
+ # This is an auto-generated starter configuration file. Feel free to customize
160
+ # it to your needs.
161
+
162
+ development:
163
+ example_key: example_value
164
+
165
+ test:
166
+ example_key: example_value
167
+
168
+ production:
169
+ example_key: example_value
170
+
171
+ END
172
+ end
173
+
174
+ def load_values!(schema, *sources)
175
+ sources.reverse_each do |source|
176
+ source.each do |configured_value|
177
+ if schema.defined?(configured_value.key)
178
+ self[configured_value.key] = configured_value
179
+ else
180
+ key = configured_value.key
181
+ logger.warn "Unknown configuration key '#{key}' in #{source}"
182
+ end
183
+ end
85
184
  end
86
185
  end
87
186
 
@@ -0,0 +1,5 @@
1
+ module Dacs
2
+ ConfiguredValue = Struct.new(:source, :key, :value) do
3
+ # ...
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ module Dacs
2
+ class DefaultSource
3
+ def initialize(defaults)
4
+ @defaults = defaults
5
+ end
6
+
7
+ def to_s
8
+ "defaults"
9
+ end
10
+
11
+ def each
12
+ @defaults.each do |key, value|
13
+ yield ConfiguredValue.new(self, key.to_s, value)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module Dacs
2
+ class EnvironmentSource
3
+ def initialize(app_name, environment=ENV)
4
+ @app_name = app_name.to_s.downcase
5
+ @environment = environment
6
+ end
7
+
8
+ def to_s
9
+ "environment"
10
+ end
11
+
12
+ def each
13
+ @environment.each_pair do |key, value|
14
+ if match = /^#{@app_name}_(.*)$/.match(key.downcase)
15
+ yield ConfiguredValue.new(self, match[1], value)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module Dacs
2
+ ConfigurationError = Class.new(Exception)
3
+ end
@@ -0,0 +1,28 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module Dacs
5
+ class FileSource
6
+ def initialize(config_path, environment)
7
+ @path = Pathname(config_path)
8
+ @environment = environment.to_s
9
+ end
10
+
11
+ def to_s
12
+ "file #{@path.relative_path_from(Pathname.pwd).to_s}"
13
+ end
14
+
15
+ def each
16
+ @path.open('r') do |f|
17
+ environments = YAML.load(f)
18
+ environment = environments.fetch(@environment) do
19
+ raise ConfigurationError,
20
+ "File #{@path} contains no #{environment} section"
21
+ end
22
+ environment.each_pair do |key, value|
23
+ yield ConfiguredValue.new(self, key.to_s, value)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,34 @@
1
+ module Dacs
2
+ class PermissiveSchema
3
+ def initialize(defaults={})
4
+ @defaults = defaults.inject({}) { |h, (k,v)|
5
+ h[k.to_s] = v
6
+ h
7
+ }
8
+ end
9
+
10
+ def keys
11
+ @defaults.keys
12
+ end
13
+
14
+ def optional?(key)
15
+ true
16
+ end
17
+
18
+ def required?(key)
19
+ false
20
+ end
21
+
22
+ def defined?(key)
23
+ true
24
+ end
25
+
26
+ def default_value(key)
27
+ @defaults[key.to_s]
28
+ end
29
+
30
+ def defaults
31
+ @defaults
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,53 @@
1
+ module Dacs
2
+ class Schema
3
+
4
+ # Add a key definition
5
+ def key(name, options={})
6
+ key_defs << { :name => name.to_s }.merge(options)
7
+ end
8
+
9
+ def keys
10
+ key_defs.map{|d| d[:name]}
11
+ end
12
+
13
+ def required?(key)
14
+ assert_key_defined!(key)
15
+ kd = key_def(key.to_s)
16
+ kd && !kd.key?(:default)
17
+ end
18
+
19
+ def optional?(key)
20
+ !required?(key.to_s)
21
+ end
22
+
23
+ def defined?(key)
24
+ !!key_def(key)
25
+ end
26
+
27
+ def default_value(key)
28
+ assert_key_defined!(key)
29
+ key_def(key.to_s)[:default]
30
+ end
31
+
32
+ def defaults
33
+ key_defs.inject({}) { |h, key_def|
34
+ h[key_def[:name]] = key_def[:default]
35
+ h
36
+ }
37
+ end
38
+
39
+ private
40
+
41
+ def assert_key_defined!(key)
42
+ !!key_def(key) or raise ConfigurationError, "No configuration key '#{key}' is defined"
43
+ end
44
+
45
+ def key_defs
46
+ @key_defs ||= []
47
+ end
48
+
49
+ def key_def(key)
50
+ key_defs.detect{|kd| kd[:name] == key.to_s}
51
+ end
52
+ end
53
+ end
data/lib/dacs.rb CHANGED
@@ -1,2 +1,10 @@
1
1
  $:.unshift(File.dirname(__FILE__))
2
+ require 'dacs/errors'
3
+ require 'dacs/schema'
4
+ require 'dacs/permissive_schema'
5
+ require 'dacs/configured_value'
6
+ require 'dacs/environment_source'
7
+ require 'dacs/file_source'
8
+ require 'dacs/default_source'
9
+ require 'dacs/environment_source'
2
10
  require 'dacs/app_config'
@@ -0,0 +1,166 @@
1
+ require File.expand_path('../spec_helper', File.dirname(__FILE__))
2
+
3
+ module Dacs
4
+ describe AppConfig do
5
+ include Construct::Helpers
6
+ before :each do
7
+ @construct = create_construct
8
+ @start_dir = Dir.pwd
9
+ Dir.chdir(@construct)
10
+ @app_name = "foo_app"
11
+ @logger = stub("logger").as_null_object
12
+ end
13
+
14
+ after(:each) do
15
+ Dir.chdir(@start_dir)
16
+ @construct.destroy!
17
+ end
18
+
19
+ context "when first initialized" do
20
+ it "should write an example config file" do
21
+ AppConfig.init!(@app_name, :logger => @logger)
22
+ AppConfig.instance
23
+ (@construct+'config'+'foo_app.yml').should exist
24
+ end
25
+
26
+ it "should know its environment" do
27
+ AppConfig.environment.should be == :development
28
+ end
29
+ end
30
+
31
+ context "given a config file lacking the expected environment key" do
32
+ append_before :each do
33
+ @construct.file("config/foo_app.yml") do |f|
34
+ YAML.dump({'production'=>{}}, f)
35
+ end
36
+ end
37
+
38
+ it "should raise an error on init" do
39
+ lambda do
40
+ AppConfig.init!(@app_name,
41
+ :environment => :development,
42
+ :logger => @logger)
43
+ end.should raise_error(ConfigurationError)
44
+ end
45
+ end
46
+
47
+ context "given a config file with an unknown key" do
48
+ before :each do
49
+ @construct.file("config/foo_app.yml") do |f|
50
+ YAML.dump({'development'=>{'undefined' => 'xyz'}}, f)
51
+ end
52
+ end
53
+
54
+ it "should warn the user" do
55
+ @logger.should_receive(:warn).with("Unknown configuration key 'undefined' in file config/foo_app.yml")
56
+ AppConfig.init!(@app_name, :logger => @logger) do |config|
57
+ config.key "foo", :default => 42
58
+ end
59
+ end
60
+ end
61
+
62
+ context "without explicit key definitions" do
63
+ before :each do
64
+ AppConfig.init!(@app_name,
65
+ :logger => @logger,
66
+ :defaults => {
67
+ 'foo' => 24,
68
+ 'bar' => false,
69
+ 'baz' => 3.14
70
+ }
71
+ )
72
+ end
73
+
74
+ it "should derive key list from defaults option" do
75
+ AppConfig.keys.sort.should be == ['foo', 'bar', 'baz'].sort
76
+ end
77
+
78
+ it "should use defaults provided in options" do
79
+ AppConfig.default_value('foo').should be == 24
80
+ AppConfig.default_value('bar').should be == false
81
+ AppConfig.default_value('baz').should be == 3.14
82
+ end
83
+
84
+ it "should consider all keys to be optional" do
85
+ AppConfig.optional?('foo').should be_true
86
+ AppConfig.required?(:foo).should be_false
87
+ AppConfig.optional?('faz').should be_true
88
+ end
89
+
90
+ end
91
+
92
+ context "with explicit key definitions" do
93
+ before :each do
94
+ AppConfig.init!(@app_name, :logger => @logger) do |config|
95
+ config.key :foo, :default => 42
96
+ config.key 'bar'
97
+ end
98
+ end
99
+
100
+ it "should be able to list known keys" do
101
+ AppConfig.keys.should == ['foo', 'bar']
102
+ end
103
+
104
+ it "should remember defaults provided in definition" do
105
+ AppConfig.default_value('foo').should be == 42
106
+ end
107
+
108
+ it "should consider keys with no default to be required" do
109
+ AppConfig.required?('bar').should be_true
110
+ AppConfig.optional?(:bar).should be_false
111
+ end
112
+
113
+ it "should consider keys with no default to be optional" do
114
+ AppConfig.required?('foo').should be_false
115
+ AppConfig.optional?(:foo).should be_true
116
+ end
117
+
118
+ end
119
+
120
+ context "with a mix of default, file, and environment settings" do
121
+ before :each do
122
+ @construct.file("config/foo_app.yml") do |f|
123
+ YAML.dump({
124
+ 'development'=>{
125
+ 'bar' => 'file_bar',
126
+ 'baz' => 'file_buz'
127
+ }
128
+ },
129
+ f)
130
+ end
131
+ ENV['FOO_APP_BUZ'] = 'env_buz'
132
+ AppConfig.init!(@app_name, :logger => @logger) do |config|
133
+ config.key :foo, :default => 42
134
+ config.key 'bar', :default => "baz"
135
+ config.key 'buz', :default => "ribbit"
136
+ end
137
+ end
138
+
139
+ it "should have the correct values for each" do
140
+ AppConfig['foo'].should be == 42
141
+ AppConfig['bar'].should be == 'file_bar'
142
+ AppConfig['buz'].should be == 'env_buz'
143
+ end
144
+
145
+ it "should be able to tell where each came from" do
146
+ AppConfig.source('foo').should match(/defaults/)
147
+ AppConfig.source('bar').should match(
148
+ /file config\/foo_app.yml/)
149
+ AppConfig.source('buz').should be == 'environment'
150
+ end
151
+
152
+ it "should be able to show a table of values" do
153
+ AppConfig.dump.should == <<END
154
+ +------------------------------------------+
155
+ | Key | Value | Source |
156
+ +------------------------------------------+
157
+ | foo | 42 | defaults |
158
+ | buz | env_buz | environment |
159
+ | bar | file_bar | file config/foo_app.yml |
160
+ +------------------------------------------+
161
+ END
162
+ end
163
+
164
+ end
165
+ end
166
+ end
data/spec/dacs_spec.rb CHANGED
@@ -1,7 +1,3 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
- describe "Dacs" do
4
- it "fails" do
5
- fail "hey buddy, you should probably rename this file and start specing for real"
6
- end
7
- end
3
+
data/spec/spec_helper.rb CHANGED
@@ -3,7 +3,7 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
3
  require 'dacs'
4
4
  require 'spec'
5
5
  require 'spec/autorun'
6
+ require 'construct'
6
7
 
7
8
  Spec::Runner.configure do |config|
8
-
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dacs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Devver, Inc.
@@ -9,9 +9,19 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-27 00:00:00 -05:00
12
+ date: 2010-02-13 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: ruport
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: "1.6"
24
+ version:
15
25
  - !ruby/object:Gem::Dependency
16
26
  name: rspec
17
27
  type: :development
@@ -32,6 +42,16 @@ dependencies:
32
42
  - !ruby/object:Gem::Version
33
43
  version: "0"
34
44
  version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: devver-construct
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: "1.1"
54
+ version:
35
55
  description: Devver Application Configuration System
36
56
  email: devs@devver.net
37
57
  executables: []
@@ -44,6 +64,7 @@ extra_rdoc_files:
44
64
  files:
45
65
  - .document
46
66
  - .gitignore
67
+ - History.txt
47
68
  - LICENSE
48
69
  - README.rdoc
49
70
  - Rakefile
@@ -52,6 +73,14 @@ files:
52
73
  - example/example.yml
53
74
  - lib/dacs.rb
54
75
  - lib/dacs/app_config.rb
76
+ - lib/dacs/configured_value.rb
77
+ - lib/dacs/default_source.rb
78
+ - lib/dacs/environment_source.rb
79
+ - lib/dacs/errors.rb
80
+ - lib/dacs/file_source.rb
81
+ - lib/dacs/permissive_schema.rb
82
+ - lib/dacs/schema.rb
83
+ - spec/dacs/app_config_spec.rb
55
84
  - spec/dacs_spec.rb
56
85
  - spec/spec.opts
57
86
  - spec/spec_helper.rb
@@ -85,4 +114,5 @@ specification_version: 3
85
114
  summary: Devver Application Configuration System
86
115
  test_files:
87
116
  - spec/spec_helper.rb
117
+ - spec/dacs/app_config_spec.rb
88
118
  - spec/dacs_spec.rb