Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit b7fc3963e54d9ea15243bd7630012e946130dd84 @mbklein committed Nov 9, 2011
Showing with 470 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +4 −0 Gemfile
  3. +154 −0 README.md
  4. +3 −0 Rakefile
  5. +27 −0 confstruct.gemspec
  6. +3 −0 lib/confstruct.rb
  7. +78 −0 lib/confstruct/configuration.rb
  8. +165 −0 lib/confstruct/hash_with_struct_access.rb
  9. +32 −0 lib/tasks/rdoc.rake
4 .gitignore
@@ -0,0 +1,4 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in confstruct.gemspec
+gemspec
154 README.md
@@ -0,0 +1,154 @@
+# confstruct
+
+<b>Confstruct</b> is yet another configuration gem. Definable and configurable by
+hash, struct, or block, confstruct aims to provide the flexibility to do things your
+way, while keeping things simple and intuitive.
+
+## Usage
+
+First, either create an empty `ConfStruct::Configuration` object:
+
+ config = Confstruct::Confguration.new
+
+Or use `ConfigClass` to define a subclass with default values:
+
+ MyConfigClass = Confstruct::ConfigClass({
+ :project => 'confstruct',
+ :github => {
+ :url => 'http://www.github.com/mbklein/confstruct',
+ :branch => 'master'
+ }
+ })
+ config = MyConfigClass.new
+
+The above can also be done in block form:
+
+ # Confstruct::ConfigClass with no parentheses will be treated as a
+ # constant, not a method. In block form, use either Confstruct.ConfigClass
+ # or Confstruct::ConfigClass()
+
+ MyConfigClass = Confstruct.ConfigClass do
+ project 'confstruct'
+ github do
+ url 'http://www.github.com/mbklein/confstruct'
+ branch 'master'
+ end
+ end
+ config = MyConfigClass.new
+
+There are many ways to configure the resulting `config` object...
+
+The Struct-like way
+
+ config.project = 'other-project'
+ config.github.url = 'http://www.github.com/somefork/other-project'
+ config.github.branch = 'pre-1.0'
+
+The block-oriented way
+
+ config.configure do
+ project 'other-project'
+ github.url 'http://www.github.com/somefork/other-project'
+ github.branch 'pre-1.0'
+ end
+
+Each sub-hash/struct is a configuration object in its own right, and can be
+treated as such. (Note the ability to leave the `configure` method
+off the inner call.)
+
+ config.configure do
+ project 'other-project'
+ github do
+ url 'http://www.github.com/somefork/other-project'
+ branch 'pre-1.0'
+ end
+ end
+
+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
+(anything that responds to `each_pair`)
+
+ config.configure({:project => 'other-project', :github => {:url => 'http://www.github.com/somefork/other-project', :branch => 'pre-1.0'}})
+
+Because it's "hashes all the way down," you can store your defaults and/or configurations
+in YAML files, or in Ruby scripts if you need to evaluate expressions at config-time.
+
+However you do it, the resulting configuration object can be accessed either as a
+hash or a struct:
+
+ config.project
+ => "other-project"
+ config[:project]
+ => "other-project"
+ config.github
+ => {:url=>"http://www.github.com/somefork/other-project", :branch=>"pre-1.0"}
+ config.github.url
+ => "http://www.github.com/somefork/other-project"
+ config.github[:url]
+ => "http://www.github.com/somefork/other-project"
+ config[:github]
+ => {:url=>"http://www.github.com/somefork/other-project", :branch=>"pre-1.0"}
+
+### Advanced Tips & Tricks
+
+Any configuration value of class `Proc` will be evaluated on access, allowing you to
+define read-only, dynamic configuration attributes
+
+ config[:github][:client] = Proc.new { |c| RestClient::Resource.new(c.url) }
+ => #<Proc:0x00000001035eb240>
+ config.github.client
+ => #<RestClient::Resource:0x1035e3b30 @options={}, @url="http://www.github.com/mbklein/confstruct", @block=nil>
+ config.github.url = 'http://www.github.com/somefork/other-project'
+ => "http://www.github.com/somefork/other-project"
+ 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
+ => "http://www.github.com/mbklein/confstruct"
+ config.push! { github.url 'http://www.github.com/somefork/other-project' }
+ => {:project=>"confstruct", :github=>{:branch=>"master", :url=>"http://www.github.com/somefork/other-project"}}
+ config.github.url
+ => "http://www.github.com/somefork/other-project"
+ config.pop!
+ => {:project=>"confstruct", :github=>{:branch=>"master", :url=>"http://www.github.com/mbklein/confstruct"}}
+ config.github.url
+ => "http://www.github.com/mbklein/confstruct"
+
+## Release History
+
+## Contributing to confstruct
+
+* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
+* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
+* Fork the project
+* Start a feature/bugfix branch
+* Commit and push until you are happy with your contribution
+* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
+* 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.
+
+## Copyright
+
+Copyright (c) 2011 Michael B. Klein. See LICENSE.txt for further details.
+
3 Rakefile
@@ -0,0 +1,3 @@
+require "bundler/gem_tasks"
+
+Dir.glob('lib/tasks/*.rake').each { |r| import r }
27 confstruct.gemspec
@@ -0,0 +1,27 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "confstruct"
+
+Gem::Specification.new do |s|
+ s.name = "confstruct"
+ s.version = Confstruct::VERSION
+ s.authors = ["Michael Klein"]
+ s.email = ["mbklein@gmail.com"]
+ s.homepage = ""
+ s.summary = %q{A simple, hash/struct-based configuration object}
+ s.description = %q{A simple, hash/struct-based configuration object}
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ s.add_development_dependency "rake", ">=0.8.7"
+ s.add_development_dependency "rcov"
+ s.add_development_dependency "rdiscount"
+ s.add_development_dependency "rdoc"
+ s.add_development_dependency "rspec"
+ s.add_development_dependency "ruby-debug"
+ s.add_development_dependency "yard"
+
+end
3 lib/confstruct.rb
@@ -0,0 +1,3 @@
+module Confstruct
+ VERSION = '0.1.0'
+end
78 lib/confstruct/configuration.rb
@@ -0,0 +1,78 @@
+require "confstruct/hash_with_struct_access"
+
+module Confstruct
+
+ def self.ConfigClass defaults=nil, &block
+ klazz = Class.new(Confstruct::Configuration)
+ klazz.instance_eval do
+ @default_values = defaults
+ if @default_values.nil?
+ @default_values = HashWithStructAccess.new({})
+ if block_given?
+ if block.arity == -1
+ @default_values.instance_eval(&block)
+ else
+ yield @default_values
+ end
+ end
+ end
+ end
+ klazz
+ end
+
+ class Configuration < HashWithStructAccess
+
+ class << self; attr_accessor :default_values; end
+ @default_values = {}
+
+ def initialize hash=nil, &block
+ super(hash || {})
+ initialize_default_values! if hash.nil?
+ configure &block
+ end
+
+ def initialize_default_values!
+ self.class.new(self.class.default_values).deep_copy.each do |k,v|
+ self[k] ||= v
+ end
+ end
+
+ def configure *args, &block
+ if args[0].respond_to?(:each_pair)
+ self.deep_merge!(args[0])
+ end
+
+ if block_given?
+ if block.arity == -1
+ self.instance_eval(&block)
+ else
+ yield self
+ end
+ self[:after_config!].call if self[:after_config!].is_a?(Proc)
+ end
+ self
+ end
+
+ def method_missing sym, *args, &block
+ super(sym, *args) { |x| x.configure(&block) }
+ end
+
+ def push! &block
+ (self[:@stash] ||= []).push(self.deep_copy)
+ configure &block if block_given?
+ end
+
+ def pop!
+ s = self[:@stash]
+ if s.nil? or s.empty?
+ raise IndexError, "Stash is empty"
+ else
+ obj = s.pop
+ self.clear
+ self[:@stash] = s unless s.empty?
+ self.merge! obj
+ end
+ end
+
+ end
+end
165 lib/confstruct/hash_with_struct_access.rb
@@ -0,0 +1,165 @@
+require 'delegate'
+
+##############
+# 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 }
+ self.new(symbolized_hash)
+ end
+
+ def symbolize key
+ (key.to_s.gsub(/\s+/,'_').to_sym rescue key) || key
+ end
+ end
+
+ def initialize hash = {}
+ super(hash)
+ end
+
+ def [] key
+ result = super(symbolize(key))
+ if result.is_a?(Hash) and not result.is_a?(self.class)
+ result = self.class.new(result)
+ end
+ result
+ end
+
+ def []= key,value
+ k = symbolize(key)
+ if value.is_a?(Hash) and self[k].is_a?(Hash)
+ self[k].replace(value)
+ else
+ result = super(k, 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|
+ if v.respond_to?(:deep_copy)
+ result[k] = v.deep_copy
+ else
+ result[k] = Marshal.load(Marshal.dump(v)) rescue v.dup
+ end
+ end
+ result
+ end
+ alias_method :inheritable_copy, :deep_copy
+
+ def deep_merge hash
+ do_deep_merge! hash, self.deep_copy
+ end
+
+ def deep_merge! hash
+ do_deep_merge! hash, self
+ end
+
+ def inspect
+ r = self.keys.collect { |k| self[k].is_a?(Proc) or k.to_s =~ /^@/ ? nil : "#{k.inspect}=>#{self[k].inspect}" }
+ "{#{r.compact.join(', ')}}"
+ end
+
+ alias_method :_keys, :keys
+ def keys
+ _keys.reject { |k| self[k].is_a?(Proc) or k.to_s =~ /^@/ }
+ 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
+ end
+
+ if result.is_a?(HashWithStructAccess) and block_given?
+ if block.arity == -1
+ result.instance_eval(&block)
+ else
+ yield result
+ end
+ end
+
+ if result.is_a?(Proc)
+ result.call(self)
+ else
+ result
+ end
+ else
+ super(sym,*args,&block)
+ end
+ end
+
+ def methods
+ key_methods = _keys.collect do |k|
+ if k.to_s =~ /^@/
+ nil
+ else
+ self[k].is_a?(Proc) ? k.to_s : [k.to_s, "#{k}="]
+ end
+ end
+ super + key_methods.compact.flatten
+ end
+
+ def respond_to? arg
+ super(arg) || keys.include?(symbolize(arg.to_s.sub(/=$/,'')))
+ end
+
+ def symbolize key
+ self.class.symbolize(key)
+ end
+
+ protected
+ def do_deep_merge! source, target
+ source.each_pair do |k,v|
+ if target.has_key?(k)
+ if v.respond_to?(:each_pair) and target[k].respond_to?(:merge)
+ do_deep_merge! v, target[k]
+ elsif v != target[k]
+ target[k] = v
+ end
+ else
+ target[k] = v
+ end
+ end
+ target
+ end
+
+ end
+end
32 lib/tasks/rdoc.rake
@@ -0,0 +1,32 @@
+desc "Generate RDoc"
+task :doc => ['doc:generate']
+
+namespace :doc do
+ project_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
+ doc_destination = File.join(project_root, 'rdoc')
+
+ begin
+ require 'yard'
+ require 'yard/rake/yardoc_task'
+
+ YARD::Rake::YardocTask.new(:generate) do |yt|
+ yt.files = Dir.glob(File.join(project_root, 'lib', '*.rb')) +
+ Dir.glob(File.join(project_root, 'lib', '**', '*.rb')) +
+ [ File.join(project_root, 'README.rdoc') ] +
+ [ File.join(project_root, 'LICENSE') ]
+
+ yt.options = ['--output-dir', doc_destination, '--readme', 'README.rdoc']
+ end
+ rescue LoadError
+ desc "Generate YARD Documentation"
+ task :generate do
+ abort "Please install the YARD gem to generate rdoc."
+ end
+ end
+
+ desc "Remove generated documenation"
+ task :clean do
+ rm_r doc_destination if File.exists?(doc_destination)
+ end
+
+end

0 comments on commit b7fc396

Please sign in to comment.