├── .document ├── .gitignore ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── lib ├── maybe.rb └── maybe │ └── core_ext.rb └── test ├── core_ext_test.rb ├── maybe_test.rb └── test_helper.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | *.sw? 3 | .DS_Store 4 | coverage 5 | rdoc 6 | pkg 7 | maybe.gemspec 8 | *.rbc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2018 Ben Brinckerhoff 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | maybe 2 | ===== 3 | 4 | A library for treating nil and non-nil objects in a similar manner. Technically speaking, Maybe is an implemenation of the maybe monad. 5 | 6 | Synopsis 7 | -------- 8 | 9 | The Maybe class wraps any value (nil or non-nil) and lets you treat it as non-nil. 10 | 11 | require "maybe" 12 | "hello".upcase #=> "HELLO" 13 | nil.upcase #=> NoMethodError: undefined method `upcase' for nil:NilClass 14 | Maybe.new("hello").upcase.__value__ #=> "HELLO" 15 | Maybe.new(nil).upcase.__value__ #=> nil 16 | 17 | You can also use the method `Maybe` for convenience. The following are equivalent: 18 | 19 | Maybe.new("hello").__value__ #=> "hello" 20 | Maybe("hello").__value__ #=> "hello" 21 | 22 | You can also optionally patch `Object` to include a `#maybe` method: 23 | 24 | require "maybe/core_ext" 25 | "hello".maybe.upcase #=> "HELLO" 26 | 27 | When you call `Maybe.new` with a value, that value is wrapped in a Maybe object. Whenever you call methods on that object, it does a simple check: if the wrapped value is nil, then it returns another Maybe object that wraps nil. If the wrapped object is not nil, it calls the method on that object, then wraps it back up in a Maybe object. 28 | 29 | This is especially handy for long chains of method calls, any of which could return nil. 30 | 31 | # foo, bar, and/or baz could return nil, but this will still work 32 | Maybe.new(foo).bar(1).baz(:x) 33 | 34 | Here's a real world example. Instead of writing this: 35 | 36 | if(customer && customer.order && customer.order.id==newest_customer_id) 37 | # ... do something with customer 38 | end 39 | 40 | just write this: 41 | 42 | if(Maybe.new(customer).order.id.__value__==newest_customer_id) 43 | # ... do something with customer 44 | end 45 | 46 | If your wrapped object does not have a `#value` method, you can call 47 | 48 | Maybe.new(obj).value 49 | 50 | instead of 51 | 52 | Maybe.new(obj).__value__ 53 | 54 | Examples 55 | -------- 56 | 57 | require "maybe" 58 | 59 | Maybe.new("10") #=> A Maybe object, wrapping "10" 60 | 61 | Maybe.new("10").to_i #=> A Maybe object, wrapping 10 62 | 63 | Maybe.new("10").to_i.__value__ #=> 10 64 | 65 | Maybe.new(nil) #=> A Maybe object, wrapping nil 66 | 67 | Maybe.new(nil).to_i #=> A Maybe object, still wrapping nil 68 | 69 | Maybe.new(nil).to_i.__value__ #=> nil 70 | 71 | Related Reading 72 | --------------- 73 | 74 | * [MenTaLguY has a great tutorial on Monads in Ruby over at Moonbase](http://moonbase.rydia.net/mental/writings/programming/monads-in-ruby/00introduction.html) 75 | * [Oliver Steele explores the problem in depth and looks at a number of different solutions](http://osteele.com/archives/2007/12/cheap-monads) 76 | * [Reg Braithwaite explores this same problem and comes up with a different, but very cool solution in Ruby](http://weblog.raganwald.com/2008/01/objectandand-objectme-in-ruby.html) 77 | * [Weave Jester has another solution, inspired by the Maybe monad](http://weavejester.com/node/10) 78 | 79 | Note on Patches/Pull Requests 80 | ------ 81 | 82 | * Fork the project. 83 | * Make your feature addition or bug fix. 84 | * Add tests for it. This is important so I don't break it in a 85 | future version unintentionally. 86 | * Commit, do not mess with rakefile, version, or history. 87 | (if you want to have your own version, that is fine but 88 | bump version in a commit by itself I can ignore when I pull) 89 | * Send me a pull request. Bonus points for topic branches. 90 | 91 | Copyright 92 | ---- 93 | 94 | Copyright (c) 2009-2018 Ben Brinckerhoff. See LICENSE for details. 95 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "maybe" 8 | gem.summary = %Q{A library for treating nil and non-nil objects in a similar manner.} 9 | gem.description = %Q{A library for treating nil and non-nil objects in a similar manner. Technically speaking, Maybe is an implemenation of the maybe monad. The Maybe class wraps any value (nil or non-nil) and lets you treat it as non-nil.} 10 | gem.email = "ben@devver.net" 11 | gem.homepage = "http://github.com/bhb/maybe" 12 | gem.authors = ["Ben Brinckerhoff"] 13 | gem.add_development_dependency("mocha", "~> 0.9.8") 14 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 15 | end 16 | Jeweler::GemcutterTasks.new 17 | rescue LoadError 18 | puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" 19 | end 20 | 21 | require 'rake/testtask' 22 | Rake::TestTask.new(:test) do |test| 23 | test.libs << 'lib' << 'test' 24 | test.pattern = 'test/**/*_test.rb' 25 | test.verbose = true 26 | end 27 | 28 | begin 29 | require 'rcov/rcovtask' 30 | Rcov::RcovTask.new do |test| 31 | test.libs << 'test' 32 | test.pattern = 'test/**/*_test.rb' 33 | test.verbose = true 34 | end 35 | rescue LoadError 36 | task :rcov do 37 | abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" 38 | end 39 | end 40 | 41 | task :test => :check_dependencies 42 | 43 | task :default => :test 44 | 45 | require 'rake/rdoctask' 46 | Rake::RDocTask.new do |rdoc| 47 | if File.exist?('VERSION') 48 | version = File.read('VERSION') 49 | else 50 | version = "" 51 | end 52 | 53 | rdoc.rdoc_dir = 'rdoc' 54 | rdoc.title = "maybe #{version}" 55 | rdoc.rdoc_files.include('README*') 56 | rdoc.rdoc_files.include('lib/**/*.rb') 57 | end 58 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.0 -------------------------------------------------------------------------------- /lib/maybe.rb: -------------------------------------------------------------------------------- 1 | def Maybe(value) 2 | Maybe.new(value) 3 | end 4 | 5 | class Maybe 6 | 7 | LEGACY_METHODS = %w{value pass fmap join} 8 | 9 | instance_methods.reject { |method_name| method_name.to_s =~ /^__/ || ['object_id','respond_to?', 'methods'].include?(method_name.to_s) }.each { |method_name| undef_method method_name } 10 | 11 | # Initializes a new Maybe object 12 | # @param value Any Ruby object (nil or non-nil) 13 | def initialize(value) 14 | @value = value 15 | __join__ 16 | end 17 | 18 | def respond_to?(method_name) 19 | return true if LEGACY_METHODS.include?(method_name.to_s) 20 | super || @value.respond_to?(method_name) 21 | end 22 | 23 | def methods 24 | super + @value.methods + LEGACY_METHODS 25 | end 26 | 27 | def respond_to_missing?(method_name, *args, &block) 28 | # For Ruby 1.9 support 29 | super 30 | end 31 | 32 | def method_missing(method_name, *args, &block) 33 | if LEGACY_METHODS.include?(method_name.to_s) 34 | if @value.respond_to?(method_name) 35 | @value.send(method_name, *args, &block) 36 | else 37 | __send__("__#{method_name}__", *args, &block) 38 | end 39 | else 40 | __fmap__ do |value| 41 | value.send(method_name,*args) do |*block_args| 42 | yield(*block_args) if block_given? 43 | end 44 | end 45 | end 46 | end 47 | 48 | # Unwraps the Maybe object. If the wrapped object does not define #value 49 | # you may call #value instead of \_\_value\_\_ 50 | # @param value_if_nil A value to return if the wrapped value is nil. 51 | # @return the wrapped object 52 | def __value__(value_if_nil=nil) 53 | if(@value==nil) 54 | value_if_nil 55 | else 56 | @value 57 | end 58 | end 59 | 60 | def nil? 61 | @value==nil 62 | end 63 | 64 | # Only included to provide a complete Monad interface. Not recommended 65 | # for general use. 66 | # (Technically: Given that the value is of type A 67 | # takes a function from A->M[B] and returns 68 | # M[B] (a monad with a value of type B)) 69 | def __pass__ 70 | __fmap__ {|*block_args| yield(*block_args)}.__join__ 71 | end 72 | 73 | # Only included to provide a complete Monad interface. Not recommended 74 | # for general use. 75 | # (Technically: Given that the value is of type A 76 | # takes a function from A->B and returns 77 | # M[B] (a monad with a value of type B)) 78 | def __fmap__ 79 | if(@value==nil) 80 | self 81 | else 82 | Maybe.new(yield(@value)) 83 | end 84 | end 85 | 86 | # Only included to provide a complete Monad interface. Not recommended 87 | # for general use. 88 | # (Technically: M[M[A]] is equivalent to M[A], that is, monads should be flat 89 | # rather than nested) 90 | def __join__ 91 | if(@value.is_a?(Maybe)) 92 | @value = @value.__value__ 93 | end 94 | self 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /lib/maybe/core_ext.rb: -------------------------------------------------------------------------------- 1 | require 'maybe' 2 | 3 | class Object 4 | def maybe 5 | Maybe.new(self) 6 | end 7 | end 8 | 9 | -------------------------------------------------------------------------------- /test/core_ext_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("test_helper", File.dirname(__FILE__)) 2 | 3 | class CoreExtTest < Test::Unit::TestCase 4 | 5 | context "Object#maybe" do 6 | 7 | should "not include patching by default" do 8 | assert_raises NoMethodError do 9 | "foo".maybe 10 | end 11 | end 12 | 13 | should "allow calling foo#maybe rather than Maybe.new(foo)" do 14 | require 'maybe/core_ext' 15 | assert_kind_of Maybe, "foo".maybe 16 | end 17 | 18 | teardown do 19 | Object.send(:undef_method, :maybe) if Object.new.respond_to?(:maybe) 20 | end 21 | 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /test/maybe_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require File.expand_path("test_helper", File.dirname(__FILE__)) 3 | require 'cgi' 4 | require 'maybe' 5 | 6 | class MaybeTest < Test::Unit::TestCase 7 | 8 | context "#initialize" do 9 | 10 | should "perform join" do 11 | assert_equal 1, Maybe.new(Maybe.new(1)).__value__ 12 | end 13 | 14 | should "never call pass on nested maybe" do 15 | Maybe.any_instance.expects(:__pass__).never 16 | Maybe.new(Maybe.new(1)).__value__ 17 | end 18 | 19 | end 20 | 21 | context "when calling methods" do 22 | 23 | should "return correct value for match operator" do 24 | assert_equal nil, (Maybe.new(nil)=~/b/).__value__ 25 | assert_equal 1, (Maybe.new('abc')=~/b/).__value__ 26 | end 27 | 28 | should "return correct value for to_s" do 29 | assert_equal nil, (Maybe.new(nil).to_s).__value__ 30 | assert_equal "1", (Maybe.new(1).to_s).__value__ 31 | end 32 | 33 | should "return correct value for to_int" do 34 | assert_equal nil, Maybe.new(nil).to_int.__value__ 35 | assert_equal 2, Maybe.new(2.3).to_int.__value__ 36 | end 37 | 38 | should "work if method call takes a block" do 39 | assert_equal nil, Maybe.new(nil).map{|x|x*2}.__value__ 40 | assert_equal [2,4,6], Maybe.new([1,2,3]).map{|x|x*2}.__value__ 41 | end 42 | 43 | should "work if methods takes args and a block" do 44 | assert_equal nil, Maybe.new(nil).gsub(/x/) {|m| m.upcase}.__value__ 45 | str = Maybe.new('x').gsub(/x/) do |m| 46 | m.upcase 47 | end 48 | assert_equal 'X', str.__value__ 49 | end 50 | 51 | should "not change the value" do 52 | x = Maybe.new(1) 53 | x.to_s 54 | assert_equal 1, x.__value__ 55 | end 56 | 57 | end 58 | 59 | context "when calling object_id" do 60 | 61 | should "have different object id than wrapped object" do 62 | wrapped = "hello" 63 | maybe = Maybe.new(wrapped) 64 | assert_kind_of Fixnum, maybe.object_id 65 | assert_not_equal wrapped.object_id, maybe.object_id 66 | assert_equal wrapped.object_id, maybe.__value__.object_id 67 | end 68 | 69 | end 70 | 71 | context "#join" do 72 | 73 | should "not call #pass" do 74 | Maybe.any_instance.expects(:__pass__).never 75 | m = Maybe.new(nil) 76 | m.instance_variable_set(:@value, Maybe.new(1)) 77 | m.join 78 | m.__join__ 79 | end 80 | 81 | should "call the wrapped object's #join if defined" do 82 | wrapped = %w{a b c} 83 | assert_equal "a b c", Maybe.new(wrapped).join(' ') 84 | assert_equal "a b c", Maybe.new(Maybe.new(wrapped)).join(' ') 85 | end 86 | 87 | end 88 | 89 | context "respond_to?" do 90 | 91 | should "respond correctly" do 92 | klass = Class.new do 93 | def fmap 94 | end 95 | 96 | def foo 97 | end 98 | end 99 | 100 | wrapped = klass.new 101 | maybe = Maybe.new(wrapped) 102 | 103 | assert_equal false, wrapped.respond_to?(:bar) 104 | assert_equal true, wrapped.respond_to?(:foo) 105 | assert_equal true, wrapped.respond_to?(:fmap) 106 | assert_equal false, wrapped.respond_to?(:join) 107 | assert_equal false, wrapped.respond_to?(:value) 108 | assert_equal false, wrapped.respond_to?(:pass) 109 | assert_equal false, wrapped.respond_to?(:__fmap__) 110 | assert_equal false, wrapped.respond_to?(:__join__) 111 | assert_equal false, wrapped.respond_to?(:__value__) 112 | assert_equal false, wrapped.respond_to?(:__pass__) 113 | 114 | assert_equal false, maybe.respond_to?(:bar) 115 | assert_equal true, maybe.respond_to?(:foo) 116 | assert_equal true, maybe.respond_to?(:fmap) 117 | assert_equal true, maybe.respond_to?(:join) 118 | assert_equal true, maybe.respond_to?(:value) 119 | assert_equal true, maybe.respond_to?(:pass) 120 | assert_equal true, maybe.respond_to?(:__fmap__) 121 | assert_equal true, maybe.respond_to?(:__join__) 122 | assert_equal true, maybe.respond_to?(:__value__) 123 | assert_equal true, maybe.respond_to?(:__pass__) 124 | end 125 | 126 | end 127 | 128 | context "#methods" do 129 | 130 | should "contain methods from wrapped method and wrapper" do 131 | klass = Class.new do 132 | def fmap 133 | end 134 | 135 | def foo 136 | end 137 | end 138 | 139 | wrapped = klass.new 140 | maybe = Maybe.new(wrapped) 141 | 142 | methods = maybe.methods.map{|x| x.to_sym} 143 | 144 | assert_equal false, methods.include?(:far) 145 | assert_equal true, methods.include?(:foo) 146 | 147 | assert_equal true, methods.include?(:fmap) 148 | assert_equal true, methods.include?(:value) 149 | assert_equal true, methods.include?(:pass) 150 | assert_equal true, methods.include?(:join) 151 | 152 | assert_equal true, methods.include?(:__fmap__) 153 | assert_equal true, methods.include?(:__value__) 154 | assert_equal true, methods.include?(:__pass__) 155 | assert_equal true, methods.include?(:__join__) 156 | end 157 | 158 | end 159 | 160 | context "#pass" do 161 | 162 | should "not conflict with wrapped object's #pass method" do 163 | ball = Object.new 164 | def ball.pass 165 | "success" 166 | end 167 | assert_equal "success", Maybe.new(ball).pass 168 | end 169 | 170 | should "work with CGI.unescape" do 171 | # using CGI::unescape because that's the first function I had problems with 172 | # when implementing Maybe 173 | assert_equal nil, Maybe.new(nil).pass {|v|CGI.unescapeHTML(v)}.value 174 | assert_equal '&', Maybe.new('&').pass {|v|CGI.unescapeHTML(v)}.value 175 | 176 | assert_equal nil, Maybe.new(nil).__pass__ {|v|CGI.unescapeHTML(v)}.__value__ 177 | assert_equal '&', Maybe.new('&').__pass__ {|v|CGI.unescapeHTML(v)}.__value__ 178 | end 179 | 180 | end 181 | 182 | context "#nil?" do 183 | 184 | should "be true for nil value" do 185 | assert_equal true, Maybe.new(nil).nil? 186 | end 187 | 188 | should "be false for non-nil value" do 189 | assert_equal false, Maybe.new(1).nil? 190 | end 191 | 192 | end 193 | 194 | context "#value" do 195 | 196 | should "return value if wrapped object does not define #value" do 197 | assert_equal nil, Maybe.new(nil).value 198 | assert_equal 1, Maybe.new(1).value 199 | end 200 | 201 | should "call wrapped object's #value if defined" do 202 | wrapped = Object.new 203 | def wrapped.value 204 | "foo" 205 | end 206 | assert_equal "foo", Maybe.new(wrapped).value 207 | assert_equal "foo", Maybe.new(Maybe.new(wrapped)).value 208 | end 209 | 210 | should "call wrapped object's #value if defined (with params and block)" do 211 | wrapped = Object.new 212 | def wrapped.value(value) 213 | value * yield 214 | end 215 | assert_equal 4, Maybe.new(wrapped).value(2) { 2 } 216 | assert_equal 4, Maybe.new(Maybe.new(wrapped)).value(2) { 2 } 217 | end 218 | 219 | end 220 | 221 | context "#__value__" do 222 | 223 | should "return value with no params" do 224 | assert_equal nil, Maybe.new(nil).__value__ 225 | assert_equal 1, Maybe.new(1).__value__ 226 | end 227 | 228 | context "when default is provided" do 229 | 230 | should "return default is value is nil" do 231 | assert_equal "", Maybe.new(nil).__value__("") 232 | assert_equal nil, Maybe.new(nil).__value__(nil) 233 | assert_equal false, Maybe.new(nil).__value__(false) 234 | end 235 | 236 | should "return value if value is non-nil" do 237 | assert_equal 1, Maybe.new(1).__value__("1") 238 | assert_equal true, Maybe.new(true).__value__(nil) 239 | assert_equal 1, Maybe.new(1).__value__(false) 240 | end 241 | 242 | end 243 | 244 | end 245 | 246 | context "#fmap" do 247 | 248 | should "call wrapped object's #fmap if defined" do 249 | wrapped = Object.new 250 | def wrapped.fmap 251 | "x" 252 | end 253 | assert_equal "x", Maybe.new(wrapped).fmap 254 | assert_equal "x", Maybe.new(Maybe.new(wrapped)).fmap 255 | end 256 | 257 | end 258 | 259 | context "when testing monad rules" do 260 | 261 | # the connection between fmap and pass (translated from) 262 | # http://james-iry.blogspot.com/2007/10/monads-are-elephants-part-3.html 263 | # scala version: m map f ≡ m flatMap {x => unit(f(x))} 264 | # note that in my code map == fmap && unit==Maybe.new && flatMap==pass 265 | should "satisfy monad rule #0" do 266 | f = lambda {|x| x*2} 267 | m = Maybe.new(5) 268 | assert_equal m.fmap(&f), m.pass {|x| Maybe.new(f[x])} 269 | assert_equal m.__fmap__(&f), m.__pass__ {|x| Maybe.new(f[x])} 270 | end 271 | 272 | # monad rules taken from http://moonbase.rydia.net/mental/writings/programming/monads-in-ruby/01identity 273 | # and http://james-iry.blogspot.com/2007_10_01_archive.html 274 | 275 | #1. Calling pass on a newly-wrapped value should have the same effect as giving that value directly to the block. 276 | # (this is actually the second law at http://james-iry.blogspot.com/2007/10/monads-are-elephants-part-3.html) 277 | # scala version: unit(x) flatMap f ≡ f(x) 278 | should "satisfy monad rule #1" do 279 | f = lambda {|y| Maybe.new(y.to_s)} 280 | x = 1 281 | assert_equal f[x], Maybe.new(x).pass {|y| f[y]}.value 282 | assert_equal f[x], Maybe.new(x).__pass__ {|y| f[y]}.__value__ 283 | end 284 | 285 | #2. pass with a block that simply calls wrap on its value should produce the exact same values, wrapped up again. 286 | # (this is actually the first law at http://james-iry.blogspot.com/2007/10/monads-are-elephants-part-3.html) 287 | # scala version: m flatMap unit ≡ m 288 | should "satisfy monad rule #2" do 289 | x = Maybe.new(1) 290 | assert_equal x.value, x.pass {|y| Maybe.new(y)}.value 291 | assert_equal x.__value__, x.pass {|y| Maybe.new(y)}.__value__ 292 | end 293 | 294 | #3. nesting pass blocks should be equivalent to calling them sequentially 295 | should "satisfy monad rule #3" do 296 | f = lambda {|x| Maybe.new(x*2)} 297 | g = lambda {|x| Maybe.new(x+1)} 298 | m = Maybe.new(3) 299 | n = Maybe.new(3) 300 | assert_equal m.pass{|x| f[x]}.pass{|x|g[x]}.value, n.pass{|x| f[x].pass{|y|g[y]}}.value 301 | assert_equal m.__pass__{|x| f[x]}.__pass__{|x|g[x]}.__value__, n.__pass__{|x| f[x].__pass__{|y|g[y]}}.__value__ 302 | end 303 | 304 | end 305 | 306 | end 307 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | require 'mocha' 4 | require 'shoulda' 5 | 6 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 7 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 8 | 9 | class Test::Unit::TestCase 10 | end 11 | --------------------------------------------------------------------------------