dacs 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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