Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit 0a3210140eb1b337177c64d725085e5520f43770 0 parents
Alex Fortuna authored
11 .gitignore
@@ -0,0 +1,11 @@
+# General Ruby.
+.ref-*
+.old*
+*-old*
+
+# Project-specific.
+/*.rb
+/doc/
+/pkg/
+/.rvmrc
+/.yardoc
2  .rspec
@@ -0,0 +1,2 @@
+--color
+-fn # Tree-like progress.
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem dependencies in `PROJECT.gemspec`.
+gemspec
26 Gemfile.lock
@@ -0,0 +1,26 @@
+PATH
+ remote: .
+ specs:
+ smart_hash (0.1.0)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ diff-lcs (1.1.3)
+ rspec (2.8.0)
+ rspec-core (~> 2.8.0)
+ rspec-expectations (~> 2.8.0)
+ rspec-mocks (~> 2.8.0)
+ rspec-core (2.8.0)
+ rspec-expectations (2.8.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.8.0)
+ yard (0.7.5)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ rspec
+ smart_hash!
+ yard
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2012 Alex Fortuna
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
152 README.md
@@ -0,0 +1,152 @@
+
+A smarter alternative to OpenStruct
+===================================
+
+Introduction
+------------
+
+If you're unhappy with `OpenStruct` (like myself), you might consider using `SmartHash` because of these major features:
+
+* You can access attributes as methods or keys. Both `person.name` and `person[:name]` will work.
+* Attribute access is strict by default. `person.invalid_stuff` will raise an exception instead of returning the stupid `nil`.
+* You can use **any** attribute names. `person.size = "XL"` will work as intended.
+* `SmartHash` descends from `Hash` and inherits its rich feature set.
+
+
+Setup
+-----
+
+~~~
+$ gem install smart_hash
+~~~
+
+, or via Bundler's `Gemfile`:
+
+~~~
+gem "smart_hash"
+#gem "smart_hash", :git => "git://github.com/dadooda/smart_hash.git" # Edge version.
+~~~
+
+
+Usage
+-----
+
+Create an object and set a few attributes:
+
+~~~
+>> person = SmartHash[]
+>> person.name = "John"
+>> person.age = 25
+
+>> person
+=> {:name=>"John", :age=>25}
+~~~
+
+Read attributes:
+
+~~~
+>> person.name
+=> "John"
+>> person[:name]
+=> "John"
+~~~
+
+Access an unset attribute:
+
+~~~
+>> person.invalid_stuff
+KeyError: key not found: :invalid_stuff
+>> person[:invalid_stuff]
+=> nil
+~~~
+
+Please note that `[]` access is always non-strict since `SmartHash` behaves as `Hash` here.
+
+Manipulate attributes which exist as methods:
+
+~~~
+>> person = SmartHash[:name => "John"]
+>> person.size
+=> 1
+>> person.size = "XL"
+>> person.size
+=> "XL"
+~~~
+
+**IMPORTANT:** You can use any attribute names excluding these: `default`, `default_proc`, `strict`.
+
+Use `Hash` features, e.g. merge:
+
+~~~
+>> person = SmartHash[:name => "John"]
+>> person.merge(:surname => "Smith", :age => 25)
+=> {:name=>"John", :surname=>"Smith", :age=>25}
+~~~
+
+, or iterate:
+
+~~~
+>> person.each {|k, v| puts "#{k}: #{v}"}
+name: John
+surname: Smith
+age: 25
+~~~
+
+Suppose you want to disable the strict mode:
+
+~~~
+>> person = SmartHash[]
+>> person.strict = false
+
+>> person.name
+=> nil
+>> person.age
+=> nil
+~~~
+
+`SmartHash::Loose` is non-strict upon construction:
+
+~~~
+>> person = SmartHash::Loose[]
+>> person.name
+=> nil
+>> person.age
+=> nil
+~~~
+
+Suppose you **know** you will use the `size` attribute and you don't want any interference from the `#size` method. Use attribute declaration:
+
+~~~
+>> person = SmartHash[]
+>> person.declare(:size)
+>> person.size
+KeyError: key not found: :size
+>> person.size = "XL"
+>> person.size
+=> "XL"
+~~~
+
+Suppose you set an attribute and want to ensure that it's not overwritten. Use attribute protection:
+
+~~~
+>> person = SmartHash[]
+>> person.name = "John"
+>> person.protect(:name)
+
+>> person.name = "Bob"
+ArgumentError: Attribute 'name' is protected
+~~~
+
+
+Copyright
+---------
+
+Copyright © 2012 Alex Fortuna.
+
+Licensed under the MIT License.
+
+
+Feedback
+--------
+
+Send bug reports, suggestions and criticisms through [project's page on GitHub](http://github.com/dadooda/smart_hash).
1  Rakefile
@@ -0,0 +1 @@
+require "bundler/gem_tasks"
241 lib/smart_hash.rb
@@ -0,0 +1,241 @@
+require "set"
+
+# Get us `SmartHash::Loose`. It's usually `Dir[]` in other gems, but we've only got 1 file at the moment.
+require File.expand_path("../smart_hash/loose", __FILE__)
+
+# == A smarter alternative to OpenStruct
+#
+# Major features:
+#
+# * You can access attributes as methods or keys.
+# * Attribute access is strict by default.
+# * You can use <b>any</b> attribute names.
+# * Descends from `Hash` and inherits its rich feature set.
+#
+# === Basic Usage
+#
+# Create an object and set a few attributes:
+#
+# >> person = SmartHash[]
+# >> person.name = "John"
+# >> person.age = 25
+#
+# >> person
+# => {:name=>"John", :age=>25}
+#
+# Read attributes:
+#
+#
+# >> person.name
+# => "John"
+# >> person[:name]
+# => "John"
+#
+# Access an unset attribute:
+#
+# >> person.invalid_stuff
+# KeyError: key not found: :invalid_stuff
+# >> person[:invalid_stuff]
+# => nil
+#
+# Please note that `[]` access is always non-strict since `SmartHash` behaves as `Hash` here.
+#
+# Manipulate attributes which exist as methods:
+#
+# >> person = SmartHash[:name => "John"]
+# >> person.size
+# => 1
+# >> person.size = "XL"
+# >> person.size
+# => "XL"
+#
+# **IMPORTANT:** You can use any attribute names excluding these: `default`, `default_proc`, `strict`.
+#
+# Use `Hash` features, e.g. merge:
+#
+# >> person = SmartHash[:name => "John"]
+# >> person.merge(:surname => "Smith", :age => 25)
+# => {:name=>"John", :surname=>"Smith", :age=>25}
+#
+# , or iterate:
+#
+# >> person.each {|k, v| puts "#{k}: #{v}"}
+# name: John
+# surname: Smith
+# age: 25
+class SmartHash < Hash
+ # Attribute name regexp without delimiters.
+ ATTR_REGEXP = /[a-zA-Z_]\w*/
+
+ # Attribute names that are forbidden.
+ # Forbidden attrs cannot be manupulated as such and are handled as methods only.
+ FORBIDDEN_ATTRS = [:default, :default_proc, :strict]
+
+ # Gem version.
+ VERSION = "0.1.0"
+
+ # See #declare.
+ attr_reader :declared_attrs
+
+ # See #protect.
+ attr_reader :protected_attrs
+
+ # Strict mode. Default is <tt>true</tt>.
+ attr_accessor :strict
+
+ def initialize(*args)
+ super
+ _smart_hash_init
+ end
+
+ # Alternative constructor.
+ #
+ # h = SmartHash[]
+ def self.[](*args)
+ super.tap do |_|
+ _.instance_eval do
+ _smart_hash_init
+ end
+ end
+ end
+
+ # Declare that specific key(s) are going to be used as attributes.
+ # Thus, exception will be raised when trying to access declared attributes if they are not set,
+ # even if the corresponding method exists in Hash class.
+ #
+ # r.declare(:size)
+ # r.size # `KeyError` or `IndexError` will be raised.
+ #
+ # r.declare(:count, :size) # Declare more than one attribute at once.
+ def declare(*attrs)
+ raise ArgumentError, "No attrs specified" if attrs.empty?
+ attrs.each do |attr|
+ (v = attr).is_a?(klass = Symbol) or raise ArgumentError, "#{klass} expected, #{v.class} (#{v.inspect}) given"
+ attr.to_s.match /\A#{ATTR_REGEXP}\z/ or raise ArgumentError, "Incorrect attribute name '#{attr}'"
+ @declared_attrs << attr # `Set` is returned.
+ end
+ end
+
+ # Protect attributes against assignment.
+ #
+ # r.protect(:size)
+ # r.size = 1 # Exception.
+ def protect(*attrs)
+ raise ArgumentError, "No attrs specified" if attrs.empty?
+ attrs.each do |attr|
+ (v = attr).is_a?(klass = Symbol) or raise ArgumentError, "#{klass} expected, #{v.class} (#{v.inspect}) given"
+ attr.to_s.match /\A#{ATTR_REGEXP}\z/ or raise ArgumentError, "Incorrect attribute name '#{attr}'"
+ @protected_attrs << attr
+ end
+ end
+
+ def undeclare(*attrs)
+ raise ArgumentError, "No attrs specified" if attrs.empty?
+ attrs.each do |attr|
+ @declared_attrs.delete(attr) # `Set` is returned.
+ end
+ end
+
+ def unprotect(*attrs)
+ raise ArgumentError, "No attrs specified" if attrs.empty?
+ attrs.each do |attr|
+ @protected_attrs.delete(attr)
+ end
+ end
+
+ private
+
+ # Make private copies of methods we need.
+ [:fetch, :instance_eval].each do |method_name|
+ my_method_name = "_smart_hash_#{method_name}".to_sym
+ alias_method my_method_name, method_name
+ private my_method_name
+ end
+
+ # Common post-initialize routine.
+ def _smart_hash_init
+ @declared_attrs = Set[]
+ @strict = true
+
+ # Protect only the bare minimum. Technically speaking, assigning ANYTHING that exists as a method is potentially dangerous
+ # or confusing. So it's fairly pointless to try to protect everything. If the person wants to screw everything up on purpose,
+ # he'll find a way to do it anyway.
+ @protected_attrs = Set[:inspect, :to_s]
+
+ # Suppress warnings.
+ vrb, $VERBOSE = $VERBOSE, nil
+
+ # Insert lookup routine for existing methods, such as <tt>size</tt>.
+ methods.map(&:to_s).each do |method_name|
+ # Install control routine on correct attribute access methods only.
+ # NOTE: Check longer REs first.
+ case method_name
+ when /\A(#{ATTR_REGEXP})=\z/
+ # Case "r.attr=".
+ attr = $1.to_sym
+ next if FORBIDDEN_ATTRS.include? attr
+ _smart_hash_instance_eval <<-EOT
+ def #{method_name}(value)
+ raise ArgumentError, "Attribute '#{attr}' is protected" if @protected_attrs.include? :#{attr}
+ self[:#{attr}] = value
+ end
+ EOT
+ when /\A#{ATTR_REGEXP}\z/
+ # Case "r.attr".
+ next if FORBIDDEN_ATTRS.include? attr
+ _smart_hash_instance_eval <<-EOT
+ def #{method_name}(*args)
+ if @declared_attrs.include?(:#{method_name}) or has_key?(:#{method_name})
+ if @strict
+ _smart_hash_fetch(:#{method_name})
+ else
+ self[:#{method_name}]
+ end
+ else
+ super
+ end
+ end
+ EOT
+ end # case
+ end # each
+
+ # Restore warnings.
+ $VERBOSE = vrb
+ end
+
+ def method_missing(method_name, *args)
+ # NOTE: No need to check for forbidden attrs here, since they exist as methods by definition.
+
+ case method_name
+ when /\A(.+)=\z/
+ # Case "r.attr=". Attribute assignment. Method name is pre-validated for us by Ruby.
+ attr = $1.to_sym
+ raise ArgumentError, "Attribute '#{attr}' is protected" if @protected_attrs.include? attr
+
+ self[attr] = args[0]
+ when /\A#{ATTR_REGEXP}\z/
+ # Case "r.attr".
+ if @strict
+ _smart_hash_fetch(method_name)
+ else
+ self[method_name]
+ end
+ else
+ super
+ end
+ end
+end
+
+#--------------------------------------- Junk
+
+# Decided to keep original Hash#inspect, it's more sane with log output.
+if false
+ def inspect
+ [
+ self.class.to_s,
+ "[",
+ super[1..-2],
+ "]",
+ ].join
+ end
+end
24 lib/smart_hash/loose.rb
@@ -0,0 +1,24 @@
+class SmartHash < Hash
+ # Non-strict SmartHash
+ #
+ # person = SmartHash::Loose[]
+ #
+ # is equivalent to:
+ #
+ # person = SmartHash[]
+ # person.strict = false
+ class Loose < ::SmartHash
+ # See SmartHash#initialize.
+ def initialize(*args)
+ super
+ @strict = false
+ end
+
+ # See SmartHash::[].
+ def self.[](*args)
+ super.tap do |_|
+ _.strict = false
+ end
+ end
+ end
+end
22 smart_hash.gemspec
@@ -0,0 +1,22 @@
+require File.expand_path("../lib/smart_hash", __FILE__)
+
+Gem::Specification.new do |s|
+ s.name = "smart_hash"
+ s.version = SmartHash::VERSION
+ s.authors = ["Alex Fortuna"]
+ s.email = ["alex.r@askit.org"]
+ s.homepage = "http://github.com/dadooda/smart_hash"
+
+ # Copy these from class's description, adjust markup.
+ s.summary = %q{A smarter alternative to OpenStruct}
+ s.description = %q{A smarter alternative to OpenStruct}
+ # end of s.description=
+
+ 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 "rspec"
+ s.add_development_dependency "yard"
+end
293 spec/smart_hash_spec.rb
@@ -0,0 +1,293 @@
+require File.expand_path("../spec_helper", __FILE__)
+
+describe SmartHash do
+ before do
+ # Get key error exception sample. Should be `KeyError` for 1.9, `IndexError` for 1.8.
+ @key_error = {}.fetch(:kk) rescue $!.class
+ end
+
+ it "should generally work" do
+ r = described_class.new
+ r.should == {}
+
+ r = described_class[]
+ r.should == {}
+
+ r = described_class[:name => "John"]
+ r.should == {:name => "John"}
+
+ r = described_class[]
+ r[:name].should == nil
+ lambda {r.name}.should raise_error @key_error
+ r.name = "John"
+ r.should == {:name => "John"}
+
+ r = described_class[]
+ r[:young?] = true # This is allowed, we're `Hash`.
+ lambda {r.young?}.should raise_error NoMethodError # Because `young?` is not a valid attribute name.
+ r[:go!] = true
+ lambda {r.go!}.should raise_error NoMethodError # Same as above.
+
+ # Existing method shadowed by an attribute.
+ r = described_class[:name => "John"]
+ r.size.should == 1
+ (r.size = "XL").should == "XL"
+ r.size.should == "XL"
+
+ # Existing `attr=` method shadowed by an attribute.
+ # NOTE: `:something` and `:something=` are methods we've added in `Hash`, see spec helper.
+ r = described_class[]
+ r.method(:something=).should be_a Method
+ r.something = 99
+ r.something.should == 99
+ end # it "should generally work"
+
+ it "should allow to redefine `fetch`" do
+ r = described_class[]
+ r.fetch = 99
+ r.fetch.should == 99
+ r.name = "John"
+ r.name.should == "John"
+ r.should == {:fetch => 99, :name => "John"}
+ end
+
+ describe "ATTR_REGEXP" do
+ before :each do
+ re = described_class.const_get(:ATTR_REGEXP)
+ @access_regexp = /\A#{re}\z/
+ @assign_regexp = /\A#{re}=\z/
+ end
+
+ it "should generally work" do
+ # Errors first, OKs second.
+ @access_regexp.tap do |_|
+ "".should_not match _
+ "911".should_not match _
+ "911abc".should_not match _
+ "young?".should_not match _
+ "go!".should_not match _
+ end
+
+ @access_regexp.tap do |_|
+ "good_attr".should match _
+ "_".should match _
+ "__abc99__".should match _
+ end
+
+ @assign_regexp.tap do |_|
+ "good_attr=".should match _
+ end
+ end
+ end
+
+ describe "attribute declaration" do
+ it "should generally work" do
+ r = described_class[:name => "John"]
+ r.declare(:size)
+ r.declared_attrs.should include :size
+ lambda {r.size}.should raise_error @key_error
+ r.undeclare(:size)
+ r.size.should == 1
+
+ # Direct modification.
+ r = described_class[:name => "John"]
+ r.declared_attrs << :size
+ lambda {r.size}.should raise_error @key_error
+ r.declared_attrs.clear
+ r.size.should == 1
+ end
+
+ describe "#declare" do
+ it "should not accept empty attrs list" do
+ r = described_class[]
+ lambda {r.declare}.should raise_error ArgumentError
+ end
+
+ it "should accept multiple attrs" do
+ r = described_class[]
+ r.declare(:count, :size)
+ r.declared_attrs.should include :count
+ r.declared_attrs.should include :size
+ end
+
+ it "should accept `Symbol` only" do
+ r = described_class[]
+ lambda {r.declare(1)}.should raise_error ArgumentError
+ lambda {r.declare("some_attr")}.should raise_error ArgumentError
+ lambda {r.declare(:some_attr)}.should_not raise_error
+ end
+
+ it "should validate attribute name" do
+ r = described_class[]
+ lambda {r.declare(:good_attr)}.should_not raise_error
+ lambda {r.declare(:bad_attr!)}.should raise_error ArgumentError
+ lambda {r.declare(:bad_attr?)}.should raise_error ArgumentError
+ end
+ end # describe "#declare"
+
+ describe "#undeclare" do
+ it "should not accept empty attrs list" do
+ r = described_class[]
+ lambda {r.undeclare}.should raise_error ArgumentError
+ end
+
+ it "should accept multiple attrs" do
+ r = described_class[:name => "John"]
+ r.declare(:count, :size)
+ lambda {r.count}.should raise_error @key_error
+ lambda {r.size}.should raise_error @key_error
+ r.undeclare(:count, :size)
+ r.count.should == 1
+ r.size.should == 1
+ end
+
+ it "should generally work" do
+ r = described_class[:name => "John"]
+ r.declare(:size)
+ lambda {r.size}.should raise_error @key_error
+ r.undeclare(:size)
+ r.size.should == 1
+ end
+
+ it "should not be strict" do
+ r = described_class[]
+ lambda {r.undeclare("young?")}.should_not raise_error
+ lambda {r.undeclare(5)}.should_not raise_error
+ lambda {r.undeclare([])}.should_not raise_error
+ end
+ end
+ end # describe "attribute declaration"
+
+ describe "attribute protection" do
+ it "should generally work" do
+ # General.
+ r = described_class[]
+ r.protect(:name)
+ lambda {r.name = "John"}.should raise_error ArgumentError
+
+ # Direct modification.
+ r = described_class[]
+ r.protected_attrs << :name
+ lambda {r.name = "John"}.should raise_error ArgumentError
+ r.protected_attrs.delete :name
+ lambda {r.name = "John"}.should_not raise_error ArgumentError
+
+ # Existing method.
+ r = described_class[:name => "John"]
+ r.protect(:size)
+ lambda {r.size = "XL"}.should raise_error ArgumentError
+ r.size.should == 1
+
+ # Protect/unprotect.
+ r = described_class[]
+ r.protect(:name)
+ lambda {r.name = "John"}.should raise_error ArgumentError
+ r.unprotect(:name)
+ r.name = "Johnny"
+ r.name.should == "Johnny"
+
+ # Existing "attr=".
+ r = described_class[]
+ r.something.should == "something"
+ r.protect(:something)
+ lambda {r.something = "other"}.should raise_error ArgumentError
+ r.unprotect(:something)
+ r.something = "other"
+ r.something.should == "other"
+ r.delete(:something)
+ r.something.should == "something"
+ end
+
+ it "should protect sensitive attrs by default" do
+ r = described_class[]
+ lambda {r.inspect = 99}.should raise_error ArgumentError
+ lambda {r.to_s = 99}.should raise_error ArgumentError
+ end
+
+ it "should allow to unprotect sensitive attrs if needed" do
+ r = described_class[]
+ r.unprotect(:inspect)
+ r.inspect = "top_secret"
+ r.inspect.should == "top_secret"
+
+ r = described_class[]
+ r.unprotect(:to_s)
+ r.to_s = "top_secret"
+ "#{r}".should == "top_secret"
+ end
+
+ describe "#protect" do
+ it "should not accept empty attrs list" do
+ r = described_class[]
+ lambda {r.protect}.should raise_error ArgumentError
+ end
+
+ it "should accept multiple attrs" do
+ r = described_class[]
+ r.protect(:count, :size)
+ r.protected_attrs.should include :count
+ r.protected_attrs.should include :size
+ end
+ end # describe "#protect"
+
+ describe "#unprotect" do
+ it "should not accept empty attrs list" do
+ r = described_class[]
+ lambda {r.unprotect}.should raise_error ArgumentError
+ end
+
+ it "should accept multiple attrs" do
+ r = described_class[]
+ r.protect(:count, :size)
+ r.protected_attrs.should include :count
+ r.protected_attrs.should include :size
+ r.unprotect(:count, :size)
+ r.protected_attrs.should_not include :count
+ r.protected_attrs.should_not include :size
+ end
+ end
+ end # describe "attribute declaration"
+
+ describe "defaults" do
+ it "should generally work" do
+ # Default, scalar.
+ r = described_class[]
+ r.default = 99
+ r.should == {} # Because `default` is a forbidden (non-attribute) name, but built-in method exists and it responds.
+ r[:anything].should == 99 # Because this is the default `Hash` behavior.
+ lambda {r.anything}.should raise_error @key_error # Because attribute access is more strict and default doesn't affect it.
+
+ # Default, proc.
+ r = described_class[]
+ r.default_proc = lambda {|h, k| "<#{k}>"}
+ r.should == {}
+ r[:anything].should == "<anything>"
+ r[:other].should == "<other>"
+ lambda {r.anything}.should raise_error @key_error
+
+ # Loose mode.
+ r = described_class[]
+ r.strict = false
+ r.anything.should == nil
+
+ # Loose mode with default.
+ r = described_class[]
+ r.strict = false
+ r.default = 99
+ r[:anything].should == 99
+ r.anything.should == 99
+ end
+ end # describe "defaults"
+end # describe SmartHash do
+
+describe SmartHash::Loose do
+ it "should generally work" do
+ r = described_class[]
+ r.anything.should == nil
+
+ r = described_class[]
+ r.default = 99
+ r[:anything].should == 99
+ r.anything.should == 99
+ end
+end
39 spec/spec_helper.rb
@@ -0,0 +1,39 @@
+# NOTE: I usually support `STANDALONE` mode in specs for Rails projects' components
+# to be able to test them without loading the environment. This project does not
+# depend on Rails *BUT* I still want a consistent RSpec file structure.
+# If this is confusing, feel free to propose something better. :)
+
+# No Rails, we're always standalone... and free! :)
+STANDALONE = 1
+
+if STANDALONE
+ # Provide root path object.
+ module Standalone
+ eval <<-EOT
+ def self.root
+ # This is an absolute path, it's perfectly safe to do a `+` and then `require`.
+ Pathname("#{File.expand_path('../..', __FILE__)}")
+ end
+ EOT
+ end
+
+ # Load stuff.
+ [
+ "lib/**/*.rb",
+ ].each do |fmask|
+ Dir[Standalone.root + fmask].each do |fn|
+ require fn
+ end
+ end
+end # if STANDALONE
+
+# Extend `Hash` with a non-forbidden `attr=` to test shadowing.
+class ::Hash
+ def something
+ "something"
+ end
+
+ def something=(value)
+ "something="
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.