confstruct 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .yardoc
6
+ coverage
7
+ rdoc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in confstruct.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # confstruct
2
+
3
+ <b>Confstruct</b> is yet another configuration gem. Definable and configurable by
4
+ hash, struct, or block, confstruct aims to provide the flexibility to do things your
5
+ way, while keeping things simple and intuitive.
6
+
7
+ [![Build Status](https://secure.travis-ci.org/mbklein/confstruct.png)](http://travis-ci.org/mbklein/confstruct])
8
+
9
+ ## Usage
10
+
11
+ First, either create an empty `ConfStruct::Configuration` object:
12
+
13
+ config = Confstruct::Configuration.new
14
+
15
+ Or with some default values:
16
+
17
+ config = Confstruct::Configuration.new({
18
+ :project => 'confstruct',
19
+ :github => {
20
+ :url => 'http://www.github.com/mbklein/confstruct',
21
+ :branch => 'master'
22
+ }
23
+ })
24
+
25
+ The above can also be done in block form:
26
+
27
+ config = Confstruct::Configuration.new do
28
+ project 'confstruct'
29
+ github do
30
+ url 'http://www.github.com/mbklein/confstruct'
31
+ branch 'master'
32
+ end
33
+ end
34
+
35
+ There are many ways to access and configure the resulting `config` object...
36
+
37
+ As a struct...
38
+
39
+ config.project = 'other-project'
40
+ config.github.url = 'http://www.github.com/somefork/other-project'
41
+ config.github.branch = 'pre-1.0'
42
+
43
+ As a block...
44
+
45
+ config.configure do
46
+ project 'other-project'
47
+ github.url 'http://www.github.com/somefork/other-project'
48
+ github.branch 'pre-1.0'
49
+ end
50
+
51
+ As a hash...
52
+
53
+ config[:github][:url] = 'http://www.github.com/somefork/other-project'
54
+
55
+ Or even as a crazy struct/hash hybrid...
56
+
57
+ config.github[:url] = 'http://www.github.com/somefork/other-project'
58
+ config[:github].branch = 'pre-1.0'
59
+
60
+ Each sub-hash/struct is a configuration object in its own right, and can be
61
+ treated as such. (Note the ability to leave the `configure` method
62
+ off the inner call.)
63
+
64
+ config.configure do
65
+ project 'other-project'
66
+ github do
67
+ url 'http://www.github.com/somefork/other-project'
68
+ branch 'pre-1.0'
69
+ end
70
+ end
71
+
72
+ You can even
73
+
74
+ config.project = 'other-project'
75
+ config.github = { :url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0' }
76
+
77
+ The configure method will even a deep merge for you if you pass it a hash or hash-like object
78
+ (anything that responds to `each_pair`)
79
+
80
+ config.configure({:project => 'other-project', :github => {:url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0'}})
81
+
82
+ Because it's "hashes all the way down," you can store your defaults and/or configurations
83
+ in YAML files, or in Ruby scripts if you need to evaluate expressions at config-time.
84
+
85
+ However you do it, the resulting configuration object can be accessed either as a
86
+ hash or a struct:
87
+
88
+ config.project
89
+ => "other-project"
90
+ config[:project]
91
+ => "other-project"
92
+ config.github
93
+ => {:url=>"http://www.github.com/somefork/other-project", :branch=>"pre-1.0"}
94
+ config.github.url
95
+ => "http://www.github.com/somefork/other-project"
96
+ config.github[:url]
97
+ => "http://www.github.com/somefork/other-project"
98
+ config[:github]
99
+ => {:url=>"http://www.github.com/somefork/other-project", :branch=>"pre-1.0"}
100
+
101
+ ### Advanced Tips & Tricks
102
+
103
+ Any configuration value of class `Confstruct::Deferred` will be evaluated on access, allowing you to
104
+ define read-only, dynamic configuration attributes
105
+
106
+ config[:github][:client] = Confstruct::Deferred.new { |c| RestClient::Resource.new(c.url) }
107
+ => #<Proc:0x00000001035eb240>
108
+ config.github.client
109
+ => #<RestClient::Resource:0x1035e3b30 @options={}, @url="http://www.github.com/mbklein/confstruct", @block=nil>
110
+ config.github.url = 'http://www.github.com/somefork/other-project'
111
+ => "http://www.github.com/somefork/other-project"
112
+ config.github.client
113
+ => #<RestClient::Resource:0x1035d5bc0 @options={}, @url="http://www.github.com/somefork/other-project", @block=nil>
114
+
115
+ As a convenience, `Confstruct.deferred(&block)` and `Confstruct::HashWithStructAccess#deferred!(&block)`
116
+ act like `lambda`, making the following two assignments equivalent to the above:
117
+
118
+ config.github.client = Confstruct.deferred { |c| RestClient::Resource.new(c.url) }
119
+ config.github do
120
+ client deferred! { |c| RestClient::Resource.new(c.url) }
121
+ end
122
+
123
+ `push!` and `pop!` methods allow you to temporarily override some or all of your configuration values. This can be
124
+ useful in spec tests where you need to change values but don't want to worry about messing up tests that depend
125
+ on the same global configuration object.
126
+
127
+ config.github.url
128
+ => "http://www.github.com/mbklein/confstruct"
129
+ config.push! { github.url 'http://www.github.com/somefork/other-project' }
130
+ => {:project=>"confstruct", :github=>{:branch=>"master", :url=>"http://www.github.com/somefork/other-project"}}
131
+ config.github.url
132
+ => "http://www.github.com/somefork/other-project"
133
+ config.pop!
134
+ => {:project=>"confstruct", :github=>{:branch=>"master", :url=>"http://www.github.com/mbklein/confstruct"}}
135
+ config.github.url
136
+ => "http://www.github.com/mbklein/confstruct"
137
+
138
+ ### Notes
139
+
140
+ * Confstruct will attempt to use ordered hashes internally when available.
141
+ * In Ruby 1.9 and above, this is automatic.
142
+ * In Rubies earlier than 1.9, Confstruct will try to require and use ActiveSupport::OrderedHash,
143
+ falling back to a regular Hash if necessary. The class/instance method `ordered?` can be used
144
+ to determine if the hash is ordered or not.
145
+ * In order to support struct access, all hash keys are converted to symbols, and are accessible
146
+ both as strings and symbols (like a `HashWithIndifferentAccess`). In other words, config['foo']
147
+ and config[:foo] refer to the same value.
148
+
149
+ ## Release History
150
+
151
+ ## Contributing to confstruct
152
+
153
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
154
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
155
+ * Fork the project
156
+ * Start a feature/bugfix branch
157
+ * Commit and push until you are happy with your contribution
158
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
159
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
160
+
161
+ ## Copyright
162
+
163
+ Copyright (c) 2011 Michael B. Klein. See LICENSE.txt for further details.
164
+
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ Dir.glob('lib/tasks/*.rake').each { |r| import r }
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "confstruct"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "confstruct"
7
+ s.version = Confstruct::VERSION
8
+ s.authors = ["Michael Klein"]
9
+ s.email = ["mbklein@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{A simple, hash/struct-based configuration object}
12
+ s.description = %q{A simple, hash/struct-based configuration object}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_development_dependency "rake", ">=0.8.7"
20
+ s.add_development_dependency "rcov"
21
+ s.add_development_dependency "rdiscount"
22
+ s.add_development_dependency "rdoc"
23
+ s.add_development_dependency "rspec"
24
+ s.add_development_dependency "yard"
25
+
26
+ end
data/lib/confstruct.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Confstruct
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,53 @@
1
+ require "confstruct/hash_with_struct_access"
2
+
3
+ module Confstruct
4
+
5
+ class Configuration < HashWithStructAccess
6
+
7
+ def initialize hash=@@hash_class.new, &block
8
+ super({})
9
+ @default_values = hash.is_a?(HashWithStructAccess) ? hash : HashWithStructAccess.new(hash)
10
+ eval_or_yield @default_values, &block
11
+ reset_defaults!
12
+ end
13
+
14
+ def after_config! obj
15
+ end
16
+
17
+ def configure *args, &block
18
+ if args[0].respond_to?(:each_pair)
19
+ self.deep_merge!(args[0])
20
+ end
21
+ eval_or_yield self, &block
22
+ after_config! self
23
+ self
24
+ end
25
+
26
+ def push! *args, &block
27
+ _stash.push(self.deep_copy)
28
+ configure *args, &block if args.length > 0 or block_given?
29
+ self
30
+ end
31
+
32
+ def pop!
33
+ if _stash.empty?
34
+ raise IndexError, "Stash is empty"
35
+ else
36
+ obj = _stash.pop
37
+ self.clear
38
+ self.merge! obj
39
+ after_config! self
40
+ end
41
+ self
42
+ end
43
+
44
+ def reset_defaults!
45
+ self.replace(default_values.deep_copy)
46
+ end
47
+
48
+ protected
49
+ def _stash
50
+ @stash ||= []
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,214 @@
1
+ require 'delegate'
2
+ require 'confstruct/utils'
3
+
4
+ module Confstruct
5
+ class Deferred < Proc; end
6
+ def self.deferred &block; Deferred.new(&block); end
7
+
8
+ if ::RUBY_VERSION < '1.9'
9
+ begin
10
+ require 'active_support/ordered_hash'
11
+ class HashWithStructAccess < DelegateClass(ActiveSupport::OrderedHash); @@ordered = true; @@hash_class = ActiveSupport::OrderedHash; end
12
+ rescue LoadError, NameError
13
+ class HashWithStructAccess < DelegateClass(Hash); @@ordered = false; @@hash_class = Hash; end
14
+ end
15
+ else
16
+ class HashWithStructAccess < DelegateClass(Hash); @@ordered = true; @@hash_class = Hash; end
17
+ end
18
+
19
+ class HashWithStructAccess
20
+ attr_accessor :default_values
21
+ @default_values = {}
22
+
23
+ class << self
24
+ def from_hash hash
25
+ symbolized_hash = symbolize_hash hash
26
+ self.new(symbolized_hash)
27
+ end
28
+
29
+ def ordered?
30
+ @@ordered
31
+ end
32
+
33
+ def structurize hash
34
+ result = hash
35
+ if result.is_a?(Hash) and not result.is_a?(HashWithStructAccess)
36
+ result = HashWithStructAccess.new(result)
37
+ end
38
+ result
39
+ end
40
+
41
+ def symbolize_hash hash
42
+ hash.inject(@@hash_class.new) do |h,(k,v)|
43
+ h[symbolize k] = v.is_a?(Hash) ? symbolize_hash(v) : v
44
+ h
45
+ end
46
+ end
47
+
48
+ def symbolize key
49
+ (key.to_s.gsub(/\s+/,'_').to_sym rescue key.to_sym) || key
50
+ end
51
+ end
52
+
53
+ def initialize hash = @@hash_class.new
54
+ super(hash)
55
+ end
56
+
57
+ def [] key
58
+ result = structurize! super(symbolize!(key))
59
+ if result.is_a?(Deferred)
60
+ result = eval_or_yield self, &result
61
+ end
62
+ result
63
+ end
64
+
65
+ def []= key,value
66
+ k = symbolize!(key)
67
+ v = structurize! value
68
+ if v.is_a?(Hash) and self[k].is_a?(Hash)
69
+ self[k].replace(v)
70
+ else
71
+ super(k, v)
72
+ end
73
+ end
74
+
75
+ def deep_copy
76
+ result = self.class.new(@@hash_class.new)
77
+ self.each_pair do |k,v|
78
+ if v.respond_to?(:deep_copy)
79
+ result[k] = v.deep_copy
80
+ else
81
+ result[k] = Marshal.load(Marshal.dump(v)) rescue v.dup
82
+ end
83
+ end
84
+ result
85
+ end
86
+ alias_method :inheritable_copy, :deep_copy
87
+
88
+ def deep_merge hash
89
+ do_deep_merge! hash, self.deep_copy
90
+ end
91
+
92
+ def deep_merge! hash
93
+ do_deep_merge! hash, self
94
+ end
95
+
96
+ def deferred! &block
97
+ Deferred.new(&block)
98
+ end
99
+
100
+ def has? key_path
101
+ val = self
102
+ keys = key_path.split(/\./)
103
+ keys.each do |key|
104
+ return false if val.nil?
105
+ if val.respond_to?(:has_key?) and val.has_key?(key.to_sym)
106
+ val = val[key.to_sym]
107
+ else
108
+ return false
109
+ end
110
+ end
111
+ return true
112
+ end
113
+
114
+ def inspect
115
+ r = self.keys.collect { |k| "#{k.inspect}=>#{self[k].inspect}" }
116
+ "{#{r.compact.join(', ')}}"
117
+ end
118
+
119
+ def is_a? klazz
120
+ klazz == @@hash_class or super
121
+ end
122
+
123
+ def lookup! key_path
124
+ val = self
125
+ keys = key_path.split(/\./)
126
+ keys.each do |key|
127
+ return nil if val.nil?
128
+ if val.respond_to?(:has_key?) and val.has_key?(key.to_sym)
129
+ val = val[key.to_sym]
130
+ else
131
+ return nil
132
+ end
133
+ end
134
+ return val
135
+ end
136
+
137
+ def method_missing sym, *args, &block
138
+ name = sym.to_s.chomp('=').to_sym
139
+ result = nil
140
+
141
+ if name.to_s =~ /^add_(.+)!$/
142
+ name = $1.to_sym
143
+ self[name] = [] unless self.has_key?(name)
144
+ unless self[name].is_a?(Array)
145
+ raise TypeError, "Cannot #add! to a #{self[name].class}"
146
+ end
147
+ if args.length > 0
148
+ local_args = args.collect { |a| structurize! a }
149
+ result = self[name].push *local_args
150
+ elsif block_given?
151
+ result = HashWithStructAccess.new(@@hash_class.new)
152
+ self[name].push result
153
+ end
154
+ elsif args.length == 1
155
+ result = self[name] = args[0]
156
+ elsif args.length > 1
157
+ super(sym,*args,&block)
158
+ else
159
+ result = self[name]
160
+ if result.nil? and block_given?
161
+ result = self[name] = HashWithStructAccess.new(@@hash_class.new)
162
+ end
163
+ end
164
+ if block_given?
165
+ eval_or_yield result, &block
166
+ end
167
+ result
168
+ end
169
+
170
+ def methods
171
+ key_methods = keys.collect do |k|
172
+ self[k].is_a?(Deferred) ? k.to_s : [k.to_s, "#{k}="]
173
+ end
174
+ super + key_methods.compact.flatten
175
+ end
176
+
177
+ def ordered?
178
+ self.class.ordered?
179
+ end
180
+
181
+ def respond_to? arg
182
+ super(arg) || keys.include?(symbolize!(arg.to_s.sub(/=$/,'')))
183
+ end
184
+
185
+ def structurize! hash
186
+ self.class.structurize(hash)
187
+ end
188
+
189
+ def symbolize! key
190
+ self.class.symbolize(key)
191
+ end
192
+
193
+ def values
194
+ keys.collect { |k| self[k] }
195
+ end
196
+
197
+ protected
198
+ def do_deep_merge! source, target
199
+ source.each_pair do |k,v|
200
+ if target.has_key?(k)
201
+ if v.respond_to?(:each_pair) and target[k].respond_to?(:merge)
202
+ do_deep_merge! v, target[k]
203
+ elsif v != target[k]
204
+ target[k] = v
205
+ end
206
+ else
207
+ target[k] = v
208
+ end
209
+ end
210
+ target
211
+ end
212
+
213
+ end
214
+ end
@@ -0,0 +1,13 @@
1
+ module Kernel
2
+ def eval_or_yield obj, &block
3
+ if block_given?
4
+ if block.arity < 1
5
+ obj.instance_eval(&block)
6
+ else
7
+ block.call(obj)
8
+ end
9
+ else
10
+ obj
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,32 @@
1
+ desc "Generate RDoc"
2
+ task :doc => ['doc:generate']
3
+
4
+ namespace :doc do
5
+ project_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
6
+ doc_destination = File.join(project_root, 'rdoc')
7
+
8
+ begin
9
+ require 'yard'
10
+ require 'yard/rake/yardoc_task'
11
+
12
+ YARD::Rake::YardocTask.new(:generate) do |yt|
13
+ yt.files = Dir.glob(File.join(project_root, 'lib', '*.rb')) +
14
+ Dir.glob(File.join(project_root, 'lib', '**', '*.rb')) +
15
+ [ File.join(project_root, 'README.rdoc') ] +
16
+ [ File.join(project_root, 'LICENSE') ]
17
+
18
+ yt.options = ['--output-dir', doc_destination, '--readme', 'README.rdoc']
19
+ end
20
+ rescue LoadError
21
+ desc "Generate YARD Documentation"
22
+ task :generate do
23
+ abort "Please install the YARD gem to generate rdoc."
24
+ end
25
+ end
26
+
27
+ desc "Remove generated documenation"
28
+ task :clean do
29
+ rm_r doc_destination if File.exists?(doc_destination)
30
+ end
31
+
32
+ end
@@ -0,0 +1,17 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ desc 'Default: run specs.'
4
+ task :default => :spec
5
+
6
+ desc "Run specs"
7
+ RSpec::Core::RakeTask.new do |t|
8
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
9
+ # Put spec opts in a file named .rspec in root
10
+ end
11
+
12
+ desc "Generate code coverage"
13
+ RSpec::Core::RakeTask.new(:coverage) do |t|
14
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
15
+ t.rcov = true
16
+ t.rcov_opts = ['--exclude', '/gems/,/Library/,/usr/,spec,lib/tasks']
17
+ end
@@ -0,0 +1,123 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'confstruct/configuration'
3
+
4
+ describe Confstruct::Configuration do
5
+
6
+ it "should initialize empty" do
7
+ conf = Confstruct::Configuration.new
8
+ conf.is_a?(Hash).should be_true
9
+ conf.is_a?(Confstruct::Configuration).should be_true
10
+ conf.should == {}
11
+ end
12
+
13
+ context "default values" do
14
+ before :all do
15
+ @defaults = {
16
+ :project => 'confstruct',
17
+ :github => {
18
+ :url => 'http://www.github.com/mbklein/confstruct',
19
+ :branch => 'master'
20
+ }
21
+ }
22
+ end
23
+
24
+ before :each do
25
+ @config = Confstruct::Configuration.new(@defaults)
26
+ end
27
+
28
+ it "should have the correct defaults" do
29
+ @config.default_values.should == @defaults
30
+ @config.should == @defaults
31
+ end
32
+
33
+ it "can be defined in block mode" do
34
+ config = Confstruct::Configuration.new do
35
+ project 'confstruct'
36
+ github do
37
+ url 'http://www.github.com/mbklein/confstruct'
38
+ branch 'master'
39
+ end
40
+ end
41
+ config.default_values.should == @defaults
42
+ config.should == @defaults
43
+ end
44
+ end
45
+
46
+ context "configuration" do
47
+ before :all do
48
+ @defaults = {
49
+ :project => 'confstruct',
50
+ :github => {
51
+ :url => 'http://www.github.com/mbklein/confstruct',
52
+ :branch => 'master'
53
+ }
54
+ }
55
+
56
+ @configured = {
57
+ :project => 'other-project',
58
+ :github => {
59
+ :url => 'http://www.github.com/mbklein/other-project',
60
+ :branch => 'master'
61
+ }
62
+ }
63
+ end
64
+
65
+ before :each do
66
+ @config = Confstruct::Configuration.new(@defaults)
67
+ end
68
+
69
+ it "should deep merge a hash" do
70
+ @config.configure({ :project => 'other-project', :github => { :url => 'http://www.github.com/mbklein/other-project' } })
71
+ @config.should == @configured
72
+ end
73
+
74
+ it "should configure as a struct" do
75
+ @config.project = 'other-project'
76
+ @config.github.url = 'http://www.github.com/mbklein/other-project'
77
+ @config.should == @configured
78
+ end
79
+
80
+ it "should configure as a block" do
81
+ @config.configure do
82
+ project 'other-project'
83
+ github do
84
+ url 'http://www.github.com/mbklein/other-project'
85
+ end
86
+ end
87
+ @config.should == @configured
88
+ end
89
+
90
+ it "should save and restore state via #push! and #pop!" do
91
+ @config.push!({ :project => 'other-project', :github => { :url => 'http://www.github.com/mbklein/other-project' } })
92
+ @configured.each_pair { |k,v| @config[k].should == v }
93
+ @config.pop!
94
+ @defaults.each_pair { |k,v| @config[k].should == v }
95
+ end
96
+
97
+ it "should raise an exception when popping an empty stash" do
98
+ lambda { @config.pop! }.should raise_error(IndexError)
99
+ end
100
+
101
+ it "should #reset_defaults!" do
102
+ @config.project = 'other-project'
103
+ @config.github.url = 'http://www.github.com/mbklein/other-project'
104
+ @config.should == @configured
105
+ @config.reset_defaults!
106
+ @config.should == @defaults
107
+ end
108
+
109
+ it "should call #after_config! when configuration is complete" do
110
+ postconfigurator = RSpec::Mocks::Mock.new('after_config!')
111
+ postconfigurator.should_receive(:configured!).once.with(@config)
112
+ def @config.after_config! obj
113
+ obj.project.should == 'other-project'
114
+ obj.mock.configured!(obj)
115
+ end
116
+ @config.configure do
117
+ project 'other-project'
118
+ mock postconfigurator
119
+ end
120
+ end
121
+ end
122
+
123
+ end
@@ -0,0 +1,226 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'confstruct/hash_with_struct_access'
3
+
4
+ describe Confstruct::HashWithStructAccess do
5
+
6
+ it "should initialize empty" do
7
+ hwsa = Confstruct::HashWithStructAccess.new
8
+ hwsa.is_a?(Hash).should be_true
9
+ hwsa.is_a?(Confstruct::HashWithStructAccess).should be_true
10
+ hwsa.should == {}
11
+ end
12
+
13
+ it "should respond to #ordered?" do
14
+ hwsa = Confstruct::HashWithStructAccess.new
15
+ [true,false].should include(hwsa.ordered?)
16
+ end
17
+
18
+ context "data manipulation" do
19
+ before :all do
20
+ @hash = {
21
+ :project => 'confstruct',
22
+ :github => {
23
+ :url => 'http://www.github.com/mbklein/confstruct',
24
+ :default_branch => 'master'
25
+ }
26
+ }
27
+ end
28
+
29
+ before :each do
30
+ @hwsa = Confstruct::HashWithStructAccess.from_hash(@hash)
31
+ end
32
+
33
+ it "should initialize from a hash" do
34
+ hwsa = Confstruct::HashWithStructAccess.from_hash({
35
+ 'project' => 'confstruct',
36
+ :github => {
37
+ :url => 'http://www.github.com/mbklein/confstruct',
38
+ 'default branch' => 'master'
39
+ }
40
+ })
41
+
42
+ hwsa.should == @hwsa
43
+ hwsa.should == @hash
44
+ end
45
+
46
+ it "should provide hash access" do
47
+ @hwsa[:project].should == @hash[:project]
48
+ @hwsa['project'].should == @hash[:project]
49
+ @hwsa[:github].should == @hash[:github]
50
+ @hwsa[:github][:url].should == @hash[:github][:url]
51
+ end
52
+
53
+ it "should provide struct access" do
54
+ @hwsa.project.should == @hash[:project]
55
+ @hwsa.github.should == @hash[:github]
56
+ @hwsa.github.url.should == @hash[:github][:url]
57
+ end
58
+
59
+ it "should provide block access" do
60
+ u = @hash[:github][:url]
61
+ @hwsa.github do
62
+ url.should == u
63
+ end
64
+
65
+ @hwsa.github do |g|
66
+ g.url.should == @hash[:github][:url]
67
+ end
68
+ end
69
+
70
+ it "should properly respond to #has?" do
71
+ @hwsa.has?('github.url').should be_true
72
+ @hwsa.has?('github.foo.bar.baz').should be_false
73
+ end
74
+
75
+ it "should properly respond to #lookup!" do
76
+ @hwsa.lookup!('github.url').should == @hash[:github][:url]
77
+ @hwsa.lookup!('github.foo.bar.baz').should be_nil
78
+ end
79
+
80
+ it "should provide introspection" do
81
+ @hwsa.should_respond_to(:project)
82
+ @hash.keys.each do |m|
83
+ @hwsa.methods.should include("#{m}")
84
+ @hwsa.methods.should include("#{m}=")
85
+ end
86
+ end
87
+
88
+ it "should #deep_merge" do
89
+ hwsa = @hwsa.deep_merge({ :new_foo => 'bar', :github => { :default_branch => 'develop' } })
90
+ @hwsa.should == @hash
91
+ hwsa.should_not == @hwsa
92
+ hwsa.should_not == @hash
93
+ hwsa.should == {
94
+ :new_foo => 'bar',
95
+ :project => 'confstruct',
96
+ :github => {
97
+ :url => 'http://www.github.com/mbklein/confstruct',
98
+ :default_branch => 'develop'
99
+ }
100
+ }
101
+ end
102
+
103
+ it "should #deep_merge!" do
104
+ @hwsa.deep_merge!({ :github => { :default_branch => 'develop' } })
105
+ @hwsa.should_not == @hash
106
+ @hwsa.should == {
107
+ :project => 'confstruct',
108
+ :github => {
109
+ :url => 'http://www.github.com/mbklein/confstruct',
110
+ :default_branch => 'develop'
111
+ }
112
+ }
113
+ end
114
+
115
+ it "should create values on demand" do
116
+ @hwsa.github.foo = 'bar'
117
+ @hwsa.github.should == {
118
+ :foo => 'bar',
119
+ :url => 'http://www.github.com/mbklein/confstruct',
120
+ :default_branch => 'master'
121
+ }
122
+
123
+ @hwsa.baz do
124
+ quux 'default_for_quux'
125
+ end
126
+ @hwsa[:baz].should == { :quux => 'default_for_quux' }
127
+ end
128
+
129
+ it "should replace an existing hash" do
130
+ @hwsa.github = { :url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0' }
131
+ @hwsa.github.has_key?(:default_branch).should == false
132
+ @hwsa.github.should == { :url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0' }
133
+ end
134
+
135
+ it "should eval_or_yield all types" do
136
+ @hwsa.github do
137
+ items([]) do
138
+ self.should == []
139
+ push 1
140
+ push 'two'
141
+ push :III
142
+ self.should == [1,'two',:III]
143
+ end
144
+ end
145
+ @hwsa.github.items.should == [1,'two',:III]
146
+ end
147
+
148
+ it "should fail on other method signatures" do
149
+ lambda { @hwsa.error(1, 2, 3) }.should raise_error(NoMethodError)
150
+ end
151
+ end
152
+
153
+ context "Proc values as virtual methods" do
154
+ before :all do
155
+ @hash = {
156
+ :project => 'confstruct',
157
+ :github => {
158
+ :url => 'http://www.github.com/mbklein/confstruct',
159
+ :default_branch => 'master',
160
+ :regular_proc => lambda { |a,b,c| puts "#{a}: #{b} #{c}" },
161
+ :reverse_url => Confstruct.deferred { self.url.reverse },
162
+ :upcase_url => Confstruct.deferred { |c| c.url.upcase }
163
+ }
164
+ }
165
+ end
166
+
167
+ before :each do
168
+ @hwsa = Confstruct::HashWithStructAccess.from_hash(@hash)
169
+ end
170
+
171
+ it "should only evaluate Confstruct::Deferred procs" do
172
+ @hwsa.github.regular_proc.is_a?(Proc).should be_true
173
+ @hwsa.github.upcase_url.is_a?(Proc).should be_false
174
+ @hwsa.github.reverse_url.is_a?(Proc).should be_false
175
+ end
176
+
177
+ it "should instance_eval the proc with no params" do
178
+ @hwsa.github.reverse_url.should == @hash[:github][:url].reverse
179
+ end
180
+
181
+ it "should call the proc with params" do
182
+ @hwsa.github.upcase_url.should == @hash[:github][:url].upcase
183
+ end
184
+
185
+ it "should evaluate deferreds when enumerating values" do
186
+ @hash[:github].values.should_not include(@hash[:github][:url].reverse)
187
+ @hwsa.github.values.should include(@hash[:github][:url].reverse)
188
+ end
189
+
190
+ it "should evaluate deferreds when inspecting" do
191
+ s = @hwsa.inspect
192
+ s.should =~ %r[:reverse_url=>"tcurtsfnoc/nielkbm/moc.buhtig.www//:ptth"]
193
+ s.should =~ %r[:regular_proc=>#<Proc:]
194
+ end
195
+
196
+ it "should allow definition of deferreds in block mode" do
197
+ @hwsa.github do
198
+ defproc deferred! { reverse_url + upcase_url }
199
+ regproc lambda { reverse_url + upcase_url }
200
+ end
201
+ @hwsa.github.defproc.is_a?(Proc).should be_false
202
+ @hwsa.github.defproc.should == @hwsa.github.reverse_url + @hwsa.github.upcase_url
203
+ @hwsa.github.regproc.is_a?(Proc).should be_true
204
+ end
205
+
206
+ it "should create arrays on the fly" do
207
+ @hwsa.github do
208
+ add_roles!({:jeeves => :valet}, {:wooster => :dolt})
209
+ add_roles! do
210
+ psmith :chum
211
+ end
212
+ end
213
+ @hwsa.github.roles.should == [{:jeeves => :valet}, {:wooster => :dolt}, {:psmith => :chum}]
214
+ @hwsa.github.roles.first.jeeves.should == :valet
215
+ end
216
+
217
+ it "should not allow #add!ing to non-Array types" do
218
+ lambda {
219
+ @hwsa.github do
220
+ add_url! 'https://github.com/mbklein/busted'
221
+ end
222
+ }.should raise_error(TypeError)
223
+ end
224
+
225
+ end
226
+ end
@@ -0,0 +1,32 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'confstruct/utils'
3
+
4
+ describe "Kernel.eval_or_yield" do
5
+ before :all do
6
+ @obj = RSpec::Mocks::Mock.new('obj')
7
+ end
8
+
9
+ it "should instance_eval when the block takes no params" do
10
+ @obj.should_receive(:test).and_return('OK')
11
+ eval_or_yield(@obj) {
12
+ self.should_not == @obj
13
+ self.test.should == 'OK'
14
+ }
15
+ end
16
+
17
+ it "should yield when the block takes a param" do
18
+ @obj.should_receive(:test).and_return('OK')
19
+ eval_or_yield(@obj) { |o|
20
+ self.should_not == @obj
21
+ o.should == @obj
22
+ lambda { self.test }.should raise_error(NoMethodError)
23
+ o.test.should == 'OK'
24
+ }
25
+ end
26
+
27
+ it "should return the object when no block is given" do
28
+ eval_or_yield(@obj).should == @obj
29
+ end
30
+
31
+ end
32
+
@@ -0,0 +1,13 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'bundler/setup'
5
+ require 'rspec'
6
+ require 'rspec/autorun'
7
+
8
+ require 'rubygems'
9
+ require 'confstruct'
10
+
11
+ RSpec.configure do |config|
12
+
13
+ end
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: confstruct
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Michael Klein
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-11-14 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rake
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 49
29
+ segments:
30
+ - 0
31
+ - 8
32
+ - 7
33
+ version: 0.8.7
34
+ type: :development
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: rcov
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 3
45
+ segments:
46
+ - 0
47
+ version: "0"
48
+ type: :development
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: rdiscount
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ type: :development
63
+ version_requirements: *id003
64
+ - !ruby/object:Gem::Dependency
65
+ name: rdoc
66
+ prerelease: false
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ hash: 3
73
+ segments:
74
+ - 0
75
+ version: "0"
76
+ type: :development
77
+ version_requirements: *id004
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ prerelease: false
81
+ requirement: &id005 !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ hash: 3
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ type: :development
91
+ version_requirements: *id005
92
+ - !ruby/object:Gem::Dependency
93
+ name: yard
94
+ prerelease: false
95
+ requirement: &id006 !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ type: :development
105
+ version_requirements: *id006
106
+ description: A simple, hash/struct-based configuration object
107
+ email:
108
+ - mbklein@gmail.com
109
+ executables: []
110
+
111
+ extensions: []
112
+
113
+ extra_rdoc_files: []
114
+
115
+ files:
116
+ - .gitignore
117
+ - Gemfile
118
+ - README.md
119
+ - Rakefile
120
+ - confstruct.gemspec
121
+ - lib/confstruct.rb
122
+ - lib/confstruct/configuration.rb
123
+ - lib/confstruct/hash_with_struct_access.rb
124
+ - lib/confstruct/utils.rb
125
+ - lib/tasks/rdoc.rake
126
+ - lib/tasks/rspec.rake
127
+ - spec/confstruct/configuration_spec.rb
128
+ - spec/confstruct/hash_with_struct_access_spec.rb
129
+ - spec/confstruct/utils_spec.rb
130
+ - spec/spec_helper.rb
131
+ homepage: ""
132
+ licenses: []
133
+
134
+ post_install_message:
135
+ rdoc_options: []
136
+
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ none: false
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ hash: 3
145
+ segments:
146
+ - 0
147
+ version: "0"
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ hash: 3
154
+ segments:
155
+ - 0
156
+ version: "0"
157
+ requirements: []
158
+
159
+ rubyforge_project:
160
+ rubygems_version: 1.8.6
161
+ signing_key:
162
+ specification_version: 3
163
+ summary: A simple, hash/struct-based configuration object
164
+ test_files:
165
+ - spec/confstruct/configuration_spec.rb
166
+ - spec/confstruct/hash_with_struct_access_spec.rb
167
+ - spec/confstruct/utils_spec.rb
168
+ - spec/spec_helper.rb
169
+ has_rdoc: