Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add spec tests; bring implementation into compliance

  • Loading branch information...
commit db5ae909eb1f07f5b84a1a11a746dd782a0fe7e9 1 parent 2d3a976
@mbklein mbklein authored
View
1  .gitignore
@@ -3,3 +3,4 @@
Gemfile.lock
pkg/*
.yardoc
+coverage
View
18 README.md
@@ -66,18 +66,10 @@ off the inner call.)
You can even
- config.project 'other-project'
- config.github.configure do
- url 'http://www.github.com/somefork/other-project'
- branch 'pre-1.0'
- end
-
-or
-
config.project = 'other-project'
config.github = { :url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0' }
-The configure method will even do a deep merge for you if you pass it a hash or hash-like object
+The configure method will even a deep merge for you if you pass it a hash or hash-like object
(anything that responds to `each_pair`)
config.configure({:project => 'other-project', :github => {:url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0'}})
@@ -115,14 +107,6 @@ define read-only, dynamic configuration attributes
config.github.client
=> #<RestClient::Resource:0x1035d5bc0 @options={}, @url="http://www.github.com/somefork/other-project", @block=nil>
-If a config block has a proc named `after_config!`, it will be called after that block
-is configured.
-
- config.github[:after_config!] = lambda { puts "Finished github configuration!" }
- config.github { url 'http://www.github.com/somefork/other-project' }
- Finished github configuration!
- => {:branch=>"master", :url=>"http://www.github.com/somefork/other-project"}
-
`push!` and `pop!` methods allow you to temporarily override some or all of your configuration values
config.github.url
View
28 lib/confstruct/configuration.rb
@@ -22,7 +22,10 @@ class << self; attr_accessor :default_values; end
def initialize hash=nil, &block
super(hash || {})
initialize_default_values! if hash.nil?
- configure &block
+ configure &block if block_given?
+ end
+
+ def after_config! obj
end
def initialize_default_values!
@@ -35,24 +38,15 @@ def configure *args, &block
if args[0].respond_to?(:each_pair)
self.deep_merge!(args[0])
end
-
- if block_given?
- eval_or_yield self, &block
- if self[:after_config!].is_a?(Proc)
- p = self[:after_config!]
- eval_or_yield self, &p
- end
- end
+ eval_or_yield self, &block
+ after_config! self
self
end
-
- def method_missing sym, *args, &block
- super(sym, *args) { |x| x.configure(&block) }
- end
-
- def push! &block
+
+ def push! *args, &block
(self[:@stash] ||= []).push(self.deep_copy)
- configure &block if block_given?
+ configure *args, &block if args.length > 0 or block_given?
+ self
end
def pop!
@@ -64,7 +58,9 @@ def pop!
self.clear
self[:@stash] = s unless s.empty?
self.merge! obj
+ after_config! self
end
+ self
end
end
View
82 lib/confstruct/hash_with_struct_access.rb
@@ -1,45 +1,24 @@
require 'delegate'
require 'confstruct/utils'
-##############
-# Confstruct::HashWithStructAccess is a Hash wrapper that provides deep struct access
-#
-# Initialize from a hash:
-# h = Confstruct::HashWithStructAccess.from_hash({ :one => 1, :two => {:three => 3, :four => 4} })
-#
-# Access it like a hash or a struct, all the way down:
-# h[:one]
-# => 1
-# Or a struct:
-# h.one
-# => 1
-# h.two.respond_to?(:three)
-# => true
-# h.two.three
-# => 3
-# h[:two][:three]
-# => 3
-# h.two.five = 5
-# => 5
-#
-# Yield sub-structs:
-# h.two { |t| t.three = 'three' }
-#
-# h
-# => {:one=>1, :two=>{:three=>"three", :four=>4, :five=>5}}
-#############
-
module Confstruct
class HashWithStructAccess < DelegateClass(Hash)
class << self
def from_hash hash
- symbolized_hash = hash.inject({}) { |h,(k,v)| h[symbolize k] = v; h }
+ symbolized_hash = symbolize_hash hash
self.new(symbolized_hash)
end
+ def symbolize_hash hash
+ hash.inject({}) do |h,(k,v)|
+ h[symbolize k] = v.is_a?(Hash) ? symbolize_hash(v) : v
+ h
+ end
+ end
+
def symbolize key
- (key.to_s.gsub(/\s+/,'_').to_sym rescue key) || key
+ (key.to_s.gsub(/\s+/,'_').to_sym rescue key.to_sym) || key
end
end
@@ -49,8 +28,8 @@ def initialize hash = {}
def [] key
result = super(symbolize(key))
- if result.is_a?(Hash) and not result.is_a?(self.class)
- result = self.class.new(result)
+ if result.is_a?(Hash) and not result.is_a?(HashWithStructAccess)
+ result = HashWithStructAccess.new(result)
end
result
end
@@ -64,10 +43,6 @@ def []= key,value
end
end
- def is_a? klazz
- klazz == Hash or super
- end
-
def deep_copy
result = self.class.new({})
self.each_pair do |k,v|
@@ -94,30 +69,39 @@ def inspect
"{#{r.compact.join(', ')}}"
end
+ def is_a? klazz
+ klazz == Hash or super
+ end
+
alias_method :_keys, :keys
def keys
_keys.reject { |k| self[k].is_a?(Proc) or k.to_s =~ /^@/ }
end
+ def values
+ keys.collect { |k| self[k] }
+ end
+
def method_missing sym, *args, &block
- (name, setter) = sym.to_s.scan(/^(.+?)(=)?$/).flatten
- setter = args.length > 0
- accessor = setter ? args.length == 1 : args.length == 0
- if accessor
- result = setter ? self[name.to_sym] = args[0] : self[name.to_sym]
- if result.nil? and args.length == 0 and block_given?
- result = self[name.to_sym] = self.class.new
+ if args.length > 1
+ super(sym,*args,&block)
+ end
+
+ name = sym.to_s.chomp('=').to_sym
+ if args.length == 1
+ self[name] = args[0]
+ else
+ result = self[name]
+ if result.nil? and block_given?
+ result = self[name] = HashWithStructAccess.new({})
end
-
+
if result.is_a?(HashWithStructAccess) and block_given?
eval_or_yield result, &block
elsif result.is_a?(Proc)
- eval_or_yield self, &result
- else
- result
+ result = eval_or_yield self, &result
end
- else
- super(sym,*args,&block)
+ result
end
end
View
17 lib/tasks/rspec.rake
@@ -0,0 +1,17 @@
+require 'rspec/core/rake_task'
+
+desc 'Default: run specs.'
+task :default => :spec
+
+desc "Run specs"
+RSpec::Core::RakeTask.new do |t|
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
+ # Put spec opts in a file named .rspec in root
+end
+
+desc "Generate code coverage"
+RSpec::Core::RakeTask.new(:coverage) do |t|
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
+ t.rcov = true
+ t.rcov_opts = ['--exclude', '/gems/,/Library/,/usr/,spec,lib/tasks']
+end
View
122 spec/confstruct/configuration_spec.rb
@@ -0,0 +1,122 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require 'confstruct/configuration'
+
+describe Confstruct::Configuration do
+
+ it "should initialize empty" do
+ conf = Confstruct::Configuration.new
+ conf.is_a?(Hash).should be_true
+ conf.is_a?(Confstruct::Configuration).should be_true
+ conf.should == {}
+ end
+
+ context "subclass" do
+ before :all do
+ @defaults = {
+ :project => 'confstruct',
+ :github => {
+ :url => 'http://www.github.com/mbklein/confstruct',
+ :branch => 'master'
+ }
+ }
+ @test_config_class = Confstruct::ConfigClass(@defaults)
+ end
+
+ before :each do
+ @config = @test_config_class.new
+ end
+
+ it "should have the correct defaults" do
+ @test_config_class.default_values.should == @defaults
+ end
+
+ it "should instantiate with the correct default values" do
+ @config.should == @defaults
+ @config.object_id.should_not == @defaults.object_id
+ end
+
+ it "can be defined in block mode" do
+ block_config_class = Confstruct.ConfigClass do
+ project 'confstruct'
+ github do
+ url 'http://www.github.com/mbklein/confstruct'
+ branch 'master'
+ end
+ end
+ block_config_class.default_values.should == @defaults
+ block_config_class.new.should == @defaults
+ end
+ end
+
+ context "configuration" do
+ before :all do
+ @defaults = {
+ :project => 'confstruct',
+ :github => {
+ :url => 'http://www.github.com/mbklein/confstruct',
+ :branch => 'master'
+ }
+ }
+
+ @configured = {
+ :project => 'other-project',
+ :github => {
+ :url => 'http://www.github.com/mbklein/other-project',
+ :branch => 'master'
+ }
+ }
+
+ TestConfigClass = Confstruct::ConfigClass(@defaults)
+ end
+
+ before :each do
+ @config = TestConfigClass.new
+ end
+
+ it "should deep merge a hash" do
+ @config.configure({ :project => 'other-project', :github => { :url => 'http://www.github.com/mbklein/other-project' } })
+ @config.should == @configured
+ end
+
+ it "should configure as a struct" do
+ @config.project = 'other-project'
+ @config.github.url = 'http://www.github.com/mbklein/other-project'
+ @config.should == @configured
+ end
+
+ it "should configure as a block" do
+ @config.configure do
+ project 'other-project'
+ github do
+ url 'http://www.github.com/mbklein/other-project'
+ end
+ end
+ @config.should == @configured
+ end
+
+ it "should save and restore state via #push! and #pop!" do
+ @config.push!({ :project => 'other-project', :github => { :url => 'http://www.github.com/mbklein/other-project' } })
+ @configured.each_pair { |k,v| @config[k].should == v }
+ @config.pop!
+ @defaults.each_pair { |k,v| @config[k].should == v }
+ end
+
+ it "should raise an exception when popping an empty stash" do
+ lambda { @config.pop! }.should raise_error(IndexError)
+ end
+
+ it "should call #after_config! when configuration is complete" do
+ postconfigurator = RSpec::Mocks::Mock.new('after_config!')
+ postconfigurator.should_receive(:configured!).once.with(@config)
+ def @config.after_config! obj
+ obj.project.should == 'other-project'
+ obj.mock.configured!(obj)
+ end
+ @config.configure do
+ project 'other-project'
+ mock postconfigurator
+ end
+ end
+ end
+
+end
View
167 spec/confstruct/hash_with_struct_access_spec.rb
@@ -0,0 +1,167 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require 'confstruct/hash_with_struct_access'
+
+describe Confstruct::HashWithStructAccess do
+
+ it "should initialize empty" do
+ hwsa = Confstruct::HashWithStructAccess.new
+ hwsa.is_a?(Hash).should be_true
+ hwsa.is_a?(Confstruct::HashWithStructAccess).should be_true
+ hwsa.should == {}
+ end
+
+ context "data manipulation" do
+ before :all do
+ @hash = {
+ :project => 'confstruct',
+ :github => {
+ :url => 'http://www.github.com/mbklein/confstruct',
+ :default_branch => 'master'
+ }
+ }
+ end
+
+ before :each do
+ @hwsa = Confstruct::HashWithStructAccess.from_hash(@hash)
+ end
+
+ it "should initialize from a hash" do
+ hwsa = Confstruct::HashWithStructAccess.from_hash({
+ 'project' => 'confstruct',
+ :github => {
+ :url => 'http://www.github.com/mbklein/confstruct',
+ 'default branch' => 'master'
+ }
+ })
+
+ hwsa.should == @hwsa
+ hwsa.should == @hash
+ end
+
+ it "should provide hash access" do
+ @hwsa[:project].should == @hash[:project]
+ @hwsa['project'].should == @hash[:project]
+ @hwsa[:github].should == @hash[:github]
+ @hwsa[:github][:url].should == @hash[:github][:url]
+ end
+
+ it "should provide struct access" do
+ @hwsa.project.should == @hash[:project]
+ @hwsa.github.should == @hash[:github]
+ @hwsa.github.url.should == @hash[:github][:url]
+ end
+
+ it "should provide block access" do
+ u = @hash[:github][:url]
+ @hwsa.github do
+ url.should == u
+ end
+
+ @hwsa.github do |g|
+ g.url.should == @hash[:github][:url]
+ end
+ end
+
+ it "should provide introspection" do
+ @hwsa.should_respond_to(:project)
+ @hash.keys.each do |m|
+ @hwsa.methods.should include("#{m}")
+ @hwsa.methods.should include("#{m}=")
+ end
+ end
+
+ it "should #deep_merge" do
+ hwsa = @hwsa.deep_merge({ :new_foo => 'bar', :github => { :default_branch => 'develop' } })
+ @hwsa.should == @hash
+ hwsa.should_not == @hwsa
+ hwsa.should_not == @hash
+ hwsa.should == {
+ :new_foo => 'bar',
+ :project => 'confstruct',
+ :github => {
+ :url => 'http://www.github.com/mbklein/confstruct',
+ :default_branch => 'develop'
+ }
+ }
+ end
+
+ it "should #deep_merge!" do
+ @hwsa.deep_merge!({ :github => { :default_branch => 'develop' } })
+ @hwsa.should_not == @hash
+ @hwsa.should == {
+ :project => 'confstruct',
+ :github => {
+ :url => 'http://www.github.com/mbklein/confstruct',
+ :default_branch => 'develop'
+ }
+ }
+ end
+
+ it "should create values on demand" do
+ @hwsa.github.foo = 'bar'
+ @hwsa.github.should == {
+ :foo => 'bar',
+ :url => 'http://www.github.com/mbklein/confstruct',
+ :default_branch => 'master'
+ }
+
+ @hwsa.baz do
+ quux 'default_for_quux'
+ end
+ @hwsa[:baz].should == { :quux => 'default_for_quux' }
+ end
+
+ it "should replace an existing hash" do
+ @hwsa.github = { :url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0' }
+ @hwsa.github.has_key?(:default_branch).should == false
+ @hwsa.github.should == { :url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0' }
+ end
+
+ it "should fail on other method signatures" do
+ lambda { @hwsa.error(1, 2, 3) }.should raise_error(NoMethodError)
+ end
+ end
+
+ context "Proc values as virtual methods" do
+ before :all do
+ @hash = {
+ :project => 'confstruct',
+ :github => {
+ :url => 'http://www.github.com/mbklein/confstruct',
+ :default_branch => 'master',
+ :reverse_url => lambda { self.url.reverse },
+ :upcase_url => lambda { |c| c.url.upcase }
+ }
+ }
+ end
+
+ before :each do
+ @hwsa = Confstruct::HashWithStructAccess.from_hash(@hash)
+ end
+
+ it "should instance_eval the proc with no params" do
+ @hwsa.github.reverse_url.should == @hash[:github][:url].reverse
+ end
+
+ it "should call the proc with params" do
+ @hwsa.github.upcase_url.should == @hash[:github][:url].upcase
+ end
+
+ it "should ignore procs when enumerating keys" do
+ @hash[:github].keys.length.should == 4
+ @hwsa.github.keys.length.should == 2
+ end
+
+ it "should ignore procs when enumerating values" do
+ @hash[:github].values.length.should == 4
+ @hwsa.github.values.length.should == 2
+ end
+
+ it "should ignore procs when inspecting" do
+ s = @hwsa.inspect
+ s.should =~ /:github=>/
+ s.should =~ /:url=>/
+ s.should_not =~ /:reverse_url=>/
+ end
+ end
+end
View
32 spec/confstruct/utils_spec.rb
@@ -0,0 +1,32 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+require 'confstruct/utils'
+
+describe "Kernel.eval_or_yield" do
+ before :all do
+ @obj = RSpec::Mocks::Mock.new('obj')
+ end
+
+ it "should instance_eval when the block takes no params" do
+ @obj.should_receive(:test).and_return('OK')
+ eval_or_yield(@obj) {
+ self.should_not == @obj
+ self.test.should == 'OK'
+ }
+ end
+
+ it "should yield when the block takes a param" do
+ @obj.should_receive(:test).and_return('OK')
+ eval_or_yield(@obj) { |o|
+ self.should_not == @obj
+ o.should == @obj
+ lambda { self.test }.should raise_error(NoMethodError)
+ o.test.should == 'OK'
+ }
+ end
+
+ it "should return the object when no block is given" do
+ eval_or_yield(@obj).should == @obj
+ end
+
+end
+
View
13 spec/spec_helper.rb
@@ -0,0 +1,13 @@
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'bundler/setup'
+require 'rspec'
+require 'rspec/autorun'
+
+require 'rubygems'
+require 'confstruct'
+
+RSpec.configure do |config|
+
+end
Please sign in to comment.
Something went wrong with that request. Please try again.