├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── Changelog.md ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.markdown ├── Rakefile ├── lib ├── naught.rb └── naught │ ├── basic_object.rb │ ├── conversions.rb │ ├── null_class_builder.rb │ ├── null_class_builder │ ├── command.rb │ ├── commands.rb │ └── commands │ │ ├── define_explicit_conversions.rb │ │ ├── define_implicit_conversions.rb │ │ ├── impersonate.rb │ │ ├── mimic.rb │ │ ├── pebble.rb │ │ ├── predicates_return.rb │ │ ├── singleton.rb │ │ └── traceable.rb │ └── version.rb ├── naught.gemspec └── spec ├── base_object_spec.rb ├── basic_null_object_spec.rb ├── blackhole_spec.rb ├── explicit_conversions_spec.rb ├── functions ├── actual_spec.rb ├── just_spec.rb ├── maybe_spec.rb └── null_spec.rb ├── implicit_conversions_spec.rb ├── mimic_spec.rb ├── naught ├── null_object_builder │ └── command_spec.rb └── null_object_builder_spec.rb ├── naught_spec.rb ├── pebble_spec.rb ├── predicate_spec.rb ├── singleton_null_object_spec.rb ├── spec_helper.rb └── support ├── convertable_null.rb ├── jruby.rb ├── rubinius.rb └── ruby_18.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | /naught.org 19 | /naught.html 20 | /bin 21 | /TAGS 22 | /gems.tags 23 | /tags 24 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order random 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Lint/NestedMethodDefinition: 2 | Enabled: false 3 | 4 | Metrics/BlockNesting: 5 | Max: 2 6 | 7 | Metrics/LineLength: 8 | AllowURI: true 9 | Max: 93 # TODO: Lower to 80 10 | 11 | Metrics/MethodLength: 12 | CountComments: false 13 | Max: 21 # TODO: Lower to 15 14 | 15 | Metrics/ParameterLists: 16 | Max: 4 17 | CountKeywordArgs: true 18 | 19 | Style/AccessModifierIndentation: 20 | EnforcedStyle: outdent 21 | 22 | Style/ClassVars: 23 | Enabled: false 24 | 25 | Style/CollectionMethods: 26 | Enabled: true 27 | PreferredMethods: 28 | map: 'collect' 29 | map!: 'collect!' 30 | reduce: 'inject' 31 | find: 'detect' 32 | find_all: 'select' 33 | 34 | Style/Documentation: 35 | Enabled: false 36 | 37 | Style/DotPosition: 38 | EnforcedStyle: trailing 39 | 40 | Style/DoubleNegation: 41 | Enabled: false 42 | 43 | Style/EachWithObject: 44 | Enabled: false 45 | 46 | Style/Encoding: 47 | Enabled: false 48 | 49 | Style/HashSyntax: 50 | EnforcedStyle: hash_rockets 51 | 52 | Style/Lambda: 53 | Enabled: false 54 | 55 | Style/MethodName: 56 | Enabled: false 57 | 58 | Style/RaiseArgs: 59 | EnforcedStyle: compact 60 | 61 | Style/SpaceInsideHashLiteralBraces: 62 | EnforcedStyle: no_space 63 | 64 | Style/TrailingComma: 65 | EnforcedStyleForMultiline: 'comma' 66 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: gem update bundler 2 | bundler_args: --without development --retry=3 --jobs=3 3 | cache: bundler 4 | env: 5 | global: 6 | - JRUBY_OPTS="$JRUBY_OPTS --debug" 7 | language: ruby 8 | rvm: 9 | - 1.8.7 10 | - 1.9.3 11 | - 2.0.0 12 | - 2.1 13 | - 2.2 14 | - jruby-9000 15 | - jruby-head 16 | - rbx-2 17 | - ruby-head 18 | matrix: 19 | allow_failures: 20 | - rvm: jruby-head 21 | - rvm: rbx-2 22 | - rvm: ruby-head 23 | fast_finish: true 24 | sudo: false 25 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ## 1.1.0 2 | 3 | - [Make it possible to supply an example object to mimic, with no class.](https://github.com/avdi/naught/commit/df2b62c027812760ce200177ce056929b5aea339) 4 | - [Define implicit conversion for to_hash](https://github.com/avdi/naught/commit/e20dc472d3bc71ba927d6ddb0fb0032e1646df77) 5 | - [Define implicit conversion for to_int](https://github.com/avdi/naught/commit/d32d4ea32a9a847bffd6cf18f480bdfaaf7a3641) 6 | 7 | ## 1.0.0 8 | 9 | - [Replace `::BasicObject` with `Naught::BasicObject`](https://github.com/avdi/naught/commit/8defad0bf9eb65e33054bf0a6e9c625c87c3e6df) 10 | - [Delegate explicit conversions to nil instead of defining them explicitly](https://github.com/avdi/naught/commit/85c195de80ed56993b88f47e09112c903a92a167) 11 | - Add support for (and run tests on) Ruby 1.8, 1.9, 2.0, 2.1, JRuby, and Rubinius 12 | 13 | ## 0.0.3 14 | 15 | Features: 16 | 17 | - New "pebble" mode (Guilherme Carvalho) 18 | 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in naught.gemspec 4 | gemspec 5 | 6 | gem 'rake' 7 | 8 | group :development do 9 | platforms :ruby_19, :ruby_20, :ruby_21, :ruby_22 do 10 | gem 'guard' 11 | gem 'guard-bundler' 12 | gem 'guard-rspec' 13 | end 14 | gem 'pry' 15 | end 16 | 17 | group :test do 18 | gem 'coveralls', :require => false 19 | gem 'json', :platforms => [:jruby, :rbx, :ruby_18, :ruby_19] 20 | gem 'libnotify' 21 | gem 'mime-types', '~> 1.25', :platforms => [:jruby, :ruby_18] 22 | gem 'rest-client', '~> 1.6.0', :platforms => [:jruby, :ruby_18] 23 | gem 'rspec', '>= 2.14' 24 | gem 'rubocop', '~> 0.34.0', :platforms => [:ruby_19, :ruby_20, :ruby_21, :ruby_22] 25 | end 26 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'bundler' do 2 | watch('Gemfile') 3 | watch(/^.+\.gemspec/) 4 | end 5 | 6 | guard :rspec, :cli => '-fs --color --order rand' do 7 | watch(%r{^spec/.+_spec\.rb$}) 8 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 9 | watch('spec/spec_helper.rb') { 'spec' } 10 | end 11 | 12 | guard 'ctags-bundler', :emacs => true, :src_path => ['lib', 'spec/support'] do 13 | watch(%r{^(lib|spec/support)/.*\.rb$}) 14 | watch('Gemfile.lock') 15 | end 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Avdi Grimm 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/naught.svg)][gem] 2 | [![Build Status](https://travis-ci.org/avdi/naught.svg?branch=master)][travis] 3 | [![Dependency Status](https://gemnasium.com/avdi/naught.svg)][gemnasium] 4 | [![Code Climate](https://codeclimate.com/github/avdi/naught/badges/gpa.svg)][codeclimate] 5 | [![Coverage Status](https://coveralls.io/repos/avdi/naught/badge.svg?branch=master&service=github)][coveralls] 6 | [![Inline docs](http://inch-ci.org/github/avdi/naught.svg?branch=master)][docs] 7 | 8 | [gem]: https://rubygems.org/gems/naught 9 | [travis]: https://travis-ci.org/avdi/naught 10 | [gemnasium]: https://gemnasium.com/avdi/naught 11 | [codeclimate]: https://codeclimate.com/github/avdi/naught 12 | [coveralls]: https://coveralls.io/github/avdi/naught?branch=master 13 | [docs]: http://inch-ci.org/github/avdi/naught 14 | 15 | A quick intro to Naught 16 | ------------------------- 17 | 18 | #### What's all this now then? 19 | 20 | Naught is a toolkit for building [Null 21 | Objects](http://en.wikipedia.org/wiki/Null_Object_pattern) in Ruby. 22 | 23 | #### What's that supposed to mean? 24 | 25 | Null Objects can make your code more 26 | [confident](http://confidentruby.com). 27 | 28 | Here's a method that's not very sure of itself. 29 | 30 | ```ruby 31 | class Geordi 32 | def make_it_so(logger=nil) 33 | logger && logger.info("Reversing the flux phase capacitance!") 34 | logger && logger.info("Bounding a tachyon particle beam off of Data's cat!") 35 | logger && logger.warn("Warning, bogon levels are rising!") 36 | end 37 | end 38 | ``` 39 | 40 | Now, observe as we give it a dash of confidence with the Null Object 41 | pattern! 42 | 43 | ```ruby 44 | class NullLogger 45 | def debug(*); end 46 | def info(*); end 47 | def warn(*); end 48 | def error(*); end 49 | def fatal(*); end 50 | end 51 | 52 | class Geordi 53 | def make_it_so(logger=NullLogger.new) 54 | logger.info "Reversing the flux phase capacitance!" 55 | logger.info "Bounding a tachyon particle beam off of Data's cat!" 56 | logger.warn "Warning, bogon levels are rising!" 57 | end 58 | end 59 | ``` 60 | 61 | By providing a `NullLogger` which implements [some of] the `Logger` 62 | interface as no-op methods, we've gotten rid of those unsightly `&&` 63 | operators. 64 | 65 | #### That was simple enough. Why do I need a library for it? 66 | 67 | You don't! The Null Object pattern is a very simple one at its core. 68 | 69 | #### And yet here we are… 70 | 71 | Yes. While you don't *need* a Null Object library, this one offers some 72 | conveniences you probably won't find elsewhere. 73 | 74 | But there's an even more important reason I wrote this library. In the 75 | immortal last words of James T. Kirk: "It was… *fun!*" 76 | 77 | #### OK, so how do I use this thing? 78 | 79 | Well, what would you like to do? 80 | 81 | #### I dunno, gimme an object that responds to any message with nil 82 | 83 | Sure thing! 84 | 85 | ```ruby 86 | require 'naught' 87 | 88 | NullObject = Naught.build 89 | 90 | null = NullObject.new 91 | null.foo # => nil 92 | null.bar # => nil 93 | ``` 94 | 95 | #### That was… weird. What's with this "build" business? 96 | 97 | Naught is a *toolkit* for building null object classes. It is not a 98 | one-size-fits-all solution. 99 | 100 | What else can I make for you? 101 | 102 | #### How about a "black hole" null object that supports infinite chaining of methods? 103 | 104 | OK. 105 | 106 | ```ruby 107 | require 'naught' 108 | 109 | BlackHole = Naught.build do |config| 110 | config.black_hole 111 | end 112 | 113 | null = BlackHole.new 114 | null.foo # => 115 | null.foo.bar.baz # => 116 | null << "hello" << "world" # => 117 | ``` 118 | 119 | #### What's that "config" thing? 120 | 121 | That's what you use to customize the generated class to your 122 | liking. Internally, Naught uses the [Builder 123 | Pattern](http://en.wikipedia.org/wiki/Builder_pattern) to make this work.. 124 | 125 | #### Whatever. What if I want a null object that has conversions to Integer, String, etc. using sensible conversions to "zero values"? 126 | 127 | We can do that. 128 | 129 | ```ruby 130 | require 'naught' 131 | 132 | NullObject = Naught.build do |config| 133 | config.define_explicit_conversions 134 | end 135 | 136 | null = NullObject.new 137 | 138 | null.to_s # => "" 139 | null.to_i # => 0 140 | null.to_f # => 0.0 141 | null.to_a # => [] 142 | null.to_h # => {} 143 | null.to_c # => (0+0i) 144 | null.to_r # => (0/1) 145 | ``` 146 | 147 | #### Ah, but what about implicit conversions such as `#to_str`? Like what if I want a null object that implicitly splats the same way as an empty array? 148 | 149 | Gotcha covered. 150 | 151 | ```ruby 152 | require 'naught' 153 | 154 | NullObject = Naught.build do |config| 155 | config.define_implicit_conversions 156 | end 157 | 158 | null = NullObject.new 159 | 160 | null.to_str # => "" 161 | null.to_ary # => [] 162 | 163 | a, b, c = [] 164 | a # => nil 165 | b # => nil 166 | c # => nil 167 | x, y, z = null 168 | x # => nil 169 | y # => nil 170 | z # => nil 171 | ``` 172 | 173 | #### How about a null object that only stubs out the methods from a specific class? 174 | 175 | That's what `mimic` is for. 176 | 177 | ```ruby 178 | require 'naught' 179 | 180 | NullIO = Naught.build do |config| 181 | config.mimic IO 182 | end 183 | 184 | null_io = NullIO.new 185 | 186 | null_io << "foo" # => nil 187 | null_io.readline # => nil 188 | null_io.foobar # => 189 | # ~> -:11:in `
': undefined method `foobar' for 190 | # :NullIO (NoMethodError) 191 | ``` 192 | 193 | There is also `impersonate` which takes `mimic` one step further. The 194 | generated null class will be derived from the impersonated class. This 195 | is handy when refitting legacy code that contains type checks. 196 | 197 | ```ruby 198 | require 'naught' 199 | 200 | NullIO = Naught.build do |config| 201 | config.impersonate IO 202 | end 203 | 204 | null_io = NullIO.new 205 | IO === null_io # => true 206 | 207 | case null_io 208 | when IO 209 | puts "Yep, checks out!" 210 | null_io << "some output" 211 | else 212 | raise "Hey, I expected an IO!" 213 | end 214 | # >> Yep, checks out! 215 | ``` 216 | 217 | #### My objects are unique and special snowflakes, with new methods added to them at runtime. How are you gonna mimic *that*, hotshot? 218 | 219 | So long as you can create an object to serve as an example, Naught can copy the interface of that object (both the methods defined by its class, and its singleton methods). 220 | 221 | ```ruby 222 | require "naught" 223 | require "logging" 224 | 225 | log = Logging.logger["test"] 226 | log.info 227 | 228 | NullLog = Naught.build do |config| 229 | config.mimic example: log 230 | end 231 | 232 | null_log = NullLog.new 233 | null_log.info # => nil 234 | ``` 235 | 236 | #### What about predicate methods? You know, the ones that end with question marks? Shouldn't they return `false` instead of `nil`? 237 | 238 | Sure, if you'd like. 239 | 240 | ```ruby 241 | require 'naught' 242 | 243 | NullObject = Naught.build do |config| 244 | config.predicates_return false 245 | end 246 | 247 | null = NullObject.new 248 | null.foo # => nil 249 | null.bar? # => false 250 | null.nil? # => false 251 | ``` 252 | 253 | #### Alright smartypants. What if I want to add my own methods? 254 | 255 | Not a problem, just define them in the `.build` block. 256 | 257 | ```ruby 258 | require 'naught' 259 | 260 | NullObject = Naught.build do |config| 261 | config.define_explicit_conversions 262 | config.predicates_return false 263 | def to_path 264 | "/dev/null" 265 | end 266 | 267 | # You can override methods generated by Naught 268 | def to_s 269 | "NOTHING TO SEE HERE MOVE ALONG" 270 | end 271 | 272 | def nil? 273 | true 274 | end 275 | end 276 | 277 | null = NullObject.new 278 | null.to_path # => "/dev/null" 279 | null.to_s # => "NOTHING TO SEE HERE MOVE ALONG" 280 | null.nil? # => true 281 | ``` 282 | 283 | #### Got anything else up your sleeve? 284 | 285 | Well, we can make the null class a singleton, since null objects 286 | generally have no state. 287 | 288 | ```ruby 289 | require 'naught' 290 | 291 | NullObject = Naught.build do |config| 292 | config.singleton 293 | end 294 | 295 | null = NullObject.instance 296 | 297 | null.__id__ # => 17844080 298 | NullObject.instance.__id__ # => 17844080 299 | NullObject.new # => 300 | # ~> -:11:in `
': private method `new' called for 301 | # NullObject:Class (NoMethodError) 302 | ``` 303 | 304 | Speaking of null objects with state, we can also enable tracing. This is 305 | handy for playing "where'd that null come from?!" Try doing *that* with 306 | `nil`! 307 | 308 | ```ruby 309 | require 'naught' 310 | 311 | NullObject = Naught.build do |config| 312 | config.traceable 313 | end 314 | 315 | null = NullObject.new # line 7 316 | 317 | null.__file__ # => "example.rb" 318 | null.__line__ # => 7 319 | ``` 320 | 321 | We can even conditionally enable either singleton mode (for production) 322 | or tracing (for development). Here's an example of using the `$DEBUG` 323 | global variable (set with the `-d` option to ruby) to choose which one. 324 | 325 | ```ruby 326 | require 'naught' 327 | 328 | NullObject = Naught.build do |config| 329 | if $DEBUG 330 | config.traceable 331 | else 332 | config.singleton 333 | end 334 | end 335 | ``` 336 | 337 | The only caveat is that when swapping between singleton and 338 | non-singleton implementations, you should be careful to always 339 | instantiate your null objects with `NullObject.get`, not `.new` or 340 | `.instance`. `.get` will work whether the class is implemented as a 341 | singleton or not. 342 | 343 | ```ruby 344 | NullObject.get # => 345 | ``` 346 | 347 | #### And if I want to know legacy code better? 348 | 349 | Naught can make a null object behave as a pebble object. 350 | 351 | ```ruby 352 | require 'naught' 353 | 354 | NullObject = Naught.build do |config| 355 | if $DEBUG 356 | config.pebble 357 | else 358 | config.black_hole 359 | end 360 | end 361 | ``` 362 | 363 | Now you can pass the pebble object to your code and see which messages are sent to the pebble. 364 | 365 | ```ruby 366 | null = NullObject.new 367 | 368 | class MyConsumer < Struct.new(:producer) 369 | def consume 370 | producer.produce 371 | end 372 | end 373 | 374 | MyConsumer.new(null).consume 375 | # >> produce() from consume 376 | # => 377 | ``` 378 | 379 | #### Are you done yet? 380 | 381 | Just one more thing. For maximum convenience, Naught-generated null 382 | classes also come with a full suite of conversion functions which can be 383 | included into your classes. 384 | 385 | ```ruby 386 | require 'naught' 387 | 388 | NullObject = Naught.build 389 | 390 | include NullObject::Conversions 391 | 392 | # Convert nil to null objects. Everything else passes through. 393 | Maybe(42) # => 42 394 | Maybe(nil) # => 395 | Maybe(NullObject.get) # => 396 | Maybe{ 42 } # => 42 397 | 398 | # Insist on a non-null (or nil) value 399 | Just(42) # => 42 400 | Just(nil) rescue $! # => # 401 | Just(NullObject.get) rescue $! # => #> 402 | 403 | # nils and nulls become nulls. Everything else is rejected. 404 | Null() # => 405 | Null(42) rescue $! # => # 406 | Null(nil) # => 407 | Null(NullObject.get) # => 408 | 409 | # Convert nulls back to nils. Everything else passes through. Useful 410 | # for preventing null objects from "leaking" into public API return 411 | # values. 412 | Actual(42) # => 42 413 | Actual(nil) # => nil 414 | Actual(NullObject.get) # => nil 415 | Actual { 42 } # => 42 416 | ``` 417 | 418 | Installation 419 | -------------- 420 | 421 | ``` {.example} 422 | gem install naught 423 | ``` 424 | 425 | Requirements 426 | -------------- 427 | 428 | - Ruby 429 | 430 | Contributing 431 | -------------- 432 | 433 | - Fork, branch, submit PR, blah blah blah. Don't forget tests. 434 | 435 | Who's responsible 436 | ------------------- 437 | 438 | Naught is by [Avdi Grimm](http://devblog.avdi.org/). 439 | 440 | Prior Art 441 | --------- 442 | 443 | This isn't the first Ruby Null Object library. Others to check out include: 444 | 445 | - [NullAndVoid](https://github.com/jfelchner/null_and_void) 446 | - [BlankSlate](https://github.com/saturnflyer/blank_slate) 447 | 448 | The Book 449 | -------- 450 | 451 | If you've read this far, you might be interested in the short ebook, [*Much Ado About Naught*](https://shiprise.dpdcart.com/cart/add?product_id=64334&method_id=66165), I (Avdi) wrote as I developed this library. It's a fun exploration of Ruby metaprogramming techniques as applied to writing a Ruby gem. You can [read the introduction here](http://devblog.avdi.org/introduction-to-much-ado-about-naught/). 452 | 453 | Further reading 454 | ----------------- 455 | 456 | - [Null Object: Something for 457 | Nothing](http://www.two-sdg.demon.co.uk/curbralan/papers/europlop/NullObject.pdf) 458 | (PDF) by Kevlin Henney 459 | - [The Null Object 460 | Pattern](http://www.cs.oberlin.edu/~jwalker/refs/woolf.ps) (PS) by 461 | Bobby Woolf 462 | - [NullObject](http://www.c2.com/cgi/wiki?NullObject) on WikiWiki 463 | - [Null Object 464 | pattern](http://en.wikipedia.org/wiki/Null_Object_pattern) on 465 | Wikipedia 466 | - [Null Objects and 467 | Falsiness](http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/), 468 | by Avdi Grimm 469 | 470 | Libraries Using Naught 471 | ----------------------- 472 | 473 | - [ActiveNull](https://github.com/Originate/active_null) Null Model support for ActiveRecord. 474 | - [Twitter](https://github.com/sferik/twitter) A Ruby interface to the Twitter API. 475 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | begin 7 | require 'rubocop/rake_task' 8 | RuboCop::RakeTask.new 9 | rescue LoadError 10 | task :rubocop do 11 | $stderr.puts 'Rubocop is disabled' 12 | end 13 | end 14 | 15 | task :default => [:spec, :rubocop] 16 | -------------------------------------------------------------------------------- /lib/naught.rb: -------------------------------------------------------------------------------- 1 | require 'naught/version' 2 | require 'naught/null_class_builder' 3 | require 'naught/null_class_builder/commands' 4 | 5 | module Naught 6 | def self.build(&customization_block) 7 | builder = NullClassBuilder.new 8 | builder.customize(&customization_block) 9 | builder.generate_class 10 | end 11 | module NullObjectTag 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/naught/basic_object.rb: -------------------------------------------------------------------------------- 1 | module Naught 2 | if defined? ::BasicObject 3 | class BasicObject < ::BasicObject 4 | end 5 | else 6 | class BasicObject #:nodoc: 7 | keep = %w( 8 | ! != == __id__ __send__ equal? instance_eval instance_exec 9 | method_missing singleton_method_added singleton_method_removed 10 | singleton_method_undefined 11 | ) 12 | instance_methods.each do |method_name| 13 | undef_method(method_name) unless keep.include?(method_name) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/naught/conversions.rb: -------------------------------------------------------------------------------- 1 | module Naught 2 | module Conversions 3 | def self.included(null_class) 4 | unless class_variable_defined?(:@@included) && @@included 5 | @@null_class = null_class 6 | @@null_equivs = null_class::NULL_EQUIVS 7 | @@included = true 8 | end 9 | super 10 | end 11 | 12 | def Null(object = :nothing_passed) 13 | case object 14 | when NullObjectTag 15 | object 16 | when :nothing_passed, *@@null_equivs 17 | @@null_class.get(:caller => caller(1)) 18 | else 19 | fail(ArgumentError.new("#{object.inspect} is not null!")) 20 | end 21 | end 22 | 23 | def Maybe(object = nil) 24 | object = yield if block_given? 25 | case object 26 | when NullObjectTag 27 | object 28 | when *@@null_equivs 29 | @@null_class.get(:caller => caller(1)) 30 | else 31 | object 32 | end 33 | end 34 | 35 | def Just(object = nil) 36 | object = yield if block_given? 37 | case object 38 | when NullObjectTag, *@@null_equivs 39 | fail(ArgumentError.new("Null value: #{object.inspect}")) 40 | else 41 | object 42 | end 43 | end 44 | 45 | def Actual(object = nil) 46 | object = yield if block_given? 47 | case object 48 | when NullObjectTag 49 | nil 50 | else 51 | object 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder.rb: -------------------------------------------------------------------------------- 1 | require 'naught/basic_object' 2 | require 'naught/conversions' 3 | 4 | module Naught 5 | class NullClassBuilder # rubocop:disable ClassLength 6 | # make sure this module exists 7 | module Commands 8 | end 9 | 10 | attr_accessor :base_class, :inspect_proc, :interface_defined 11 | alias_method :interface_defined?, :interface_defined 12 | 13 | def initialize 14 | @interface_defined = false 15 | @base_class = Naught::BasicObject 16 | @inspect_proc = lambda { '' } 17 | @stub_strategy = :stub_method_returning_nil 18 | define_basic_methods 19 | end 20 | 21 | def customize(&customization_block) 22 | return unless customization_block 23 | customization_module.module_exec(self, &customization_block) 24 | end 25 | 26 | def customization_module 27 | @customization_module ||= Module.new 28 | end 29 | 30 | def null_equivalents 31 | @null_equivalents ||= [nil] 32 | end 33 | 34 | def generate_class # rubocop:disable AbcSize 35 | respond_to_any_message unless interface_defined? 36 | generation_mod = Module.new 37 | customization_mod = customization_module # get a local binding 38 | builder = self 39 | 40 | apply_operations(operations, generation_mod) 41 | 42 | null_class = Class.new(@base_class) do 43 | const_set :GeneratedMethods, generation_mod 44 | const_set :Customizations, customization_mod 45 | const_set :NULL_EQUIVS, builder.null_equivalents 46 | include Conversions 47 | remove_const :NULL_EQUIVS 48 | Conversions.instance_methods.each do |instance_method| 49 | undef_method(instance_method) 50 | end 51 | const_set :Conversions, Conversions 52 | 53 | include NullObjectTag 54 | include generation_mod 55 | include customization_mod 56 | end 57 | 58 | apply_operations(class_operations, null_class) 59 | 60 | null_class 61 | end 62 | 63 | ############################################################################ 64 | # Builder API 65 | # 66 | # See also the contents of lib/naught/null_class_builder/commands 67 | ############################################################################ 68 | 69 | def black_hole 70 | @stub_strategy = :stub_method_returning_self 71 | end 72 | 73 | def respond_to_any_message 74 | defer(:prepend => true) do |subject| 75 | subject.module_eval do 76 | def respond_to?(*) 77 | true 78 | end 79 | end 80 | stub_method(subject, :method_missing) 81 | end 82 | @interface_defined = true 83 | end 84 | 85 | def defer(options = {}, &deferred_operation) 86 | list = options[:class] ? class_operations : operations 87 | if options[:prepend] 88 | list.unshift(deferred_operation) 89 | else 90 | list << deferred_operation 91 | end 92 | end 93 | 94 | def stub_method(subject, name) 95 | send(@stub_strategy, subject, name) 96 | end 97 | 98 | def method_missing(method_name, *args, &block) 99 | command_name = command_name_for_method(method_name) 100 | if Commands.const_defined?(command_name) 101 | command_class = Commands.const_get(command_name) 102 | command_class.new(self, *args, &block).call 103 | else 104 | super 105 | end 106 | end 107 | 108 | if RUBY_VERSION >= '1.9' 109 | def respond_to_missing?(method_name, include_private = false) 110 | respond_to_definition(method_name, include_private, :respond_to_missing?) 111 | end 112 | else 113 | def respond_to?(method_name, include_private = false) 114 | respond_to_definition(method_name, include_private, :respond_to?) 115 | end 116 | end 117 | 118 | private 119 | 120 | def respond_to_definition(method_name, include_private, respond_to_method_name) 121 | command_name = command_name_for_method(method_name) 122 | Commands.const_defined?(command_name) || 123 | super_duper(respond_to_method_name, method_name, include_private) 124 | rescue NameError 125 | super_duper(respond_to_method_name, method_name, include_private) 126 | end 127 | 128 | def super_duper(method_name, *args) 129 | self.class.superclass.send(method_name, *args) 130 | end 131 | 132 | def define_basic_methods 133 | define_basic_instance_methods 134 | define_basic_class_methods 135 | end 136 | 137 | def apply_operations(operations, module_or_class) 138 | operations.each do |operation| 139 | operation.call(module_or_class) 140 | end 141 | end 142 | 143 | def define_basic_instance_methods 144 | defer do |subject| 145 | subject.module_exec(@inspect_proc) do |inspect_proc| 146 | define_method(:inspect, &inspect_proc) 147 | def initialize(*) 148 | end 149 | end 150 | end 151 | end 152 | 153 | def define_basic_class_methods 154 | defer(:class => true) do |subject| 155 | subject.module_eval do 156 | class << self 157 | alias_method :get, :new 158 | end 159 | klass = self 160 | define_method(:class) { klass } 161 | end 162 | end 163 | end 164 | 165 | def class_operations 166 | @class_operations ||= [] 167 | end 168 | 169 | def operations 170 | @operations ||= [] 171 | end 172 | 173 | def stub_method_returning_nil(subject, name) 174 | subject.module_eval do 175 | define_method(name) { |*| nil } 176 | end 177 | end 178 | 179 | def stub_method_returning_self(subject, name) 180 | subject.module_eval do 181 | define_method(name) { |*| self } 182 | end 183 | end 184 | 185 | def command_name_for_method(method_name) 186 | method_name.to_s.gsub(/(?:^|_)([a-z])/) { Regexp.last_match[1].upcase } 187 | end 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder/command.rb: -------------------------------------------------------------------------------- 1 | module Naught 2 | class NullClassBuilder 3 | class Command 4 | attr_reader :builder 5 | 6 | def initialize(builder) 7 | @builder = builder 8 | end 9 | 10 | def call 11 | fail(NotImplementedError.new('Method #call should be overriden in child classes')) 12 | end 13 | 14 | def defer(options = {}, &block) 15 | @builder.defer(options, &block) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder/commands.rb: -------------------------------------------------------------------------------- 1 | require 'naught/null_class_builder/commands/define_explicit_conversions' 2 | require 'naught/null_class_builder/commands/define_implicit_conversions' 3 | require 'naught/null_class_builder/commands/pebble' 4 | require 'naught/null_class_builder/commands/predicates_return' 5 | require 'naught/null_class_builder/commands/singleton' 6 | require 'naught/null_class_builder/commands/traceable' 7 | require 'naught/null_class_builder/commands/mimic' 8 | require 'naught/null_class_builder/commands/impersonate' 9 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder/commands/define_explicit_conversions.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | require 'naught/null_class_builder/command' 3 | 4 | module Naught 5 | class NullClassBuilder 6 | module Commands 7 | class DefineExplicitConversions < ::Naught::NullClassBuilder::Command 8 | def call 9 | defer do |subject| 10 | subject.module_eval do 11 | extend Forwardable 12 | def_delegators :nil, :to_a, :to_c, :to_f, :to_h, :to_i, :to_r, :to_s 13 | end 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder/commands/define_implicit_conversions.rb: -------------------------------------------------------------------------------- 1 | require 'naught/null_class_builder/command' 2 | 3 | module Naught 4 | class NullClassBuilder 5 | module Commands 6 | class DefineImplicitConversions < ::Naught::NullClassBuilder::Command 7 | def call 8 | defer do |subject| 9 | subject.module_eval do 10 | def to_ary 11 | [] 12 | end 13 | 14 | def to_hash 15 | {} 16 | end 17 | 18 | def to_int 19 | 0 20 | end 21 | 22 | def to_str 23 | '' 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder/commands/impersonate.rb: -------------------------------------------------------------------------------- 1 | module Naught 2 | class NullClassBuilder 3 | module Commands 4 | class Impersonate < Naught::NullClassBuilder::Commands::Mimic 5 | def initialize(builder, class_to_impersonate, options = {}) 6 | super 7 | builder.base_class = class_to_impersonate 8 | end 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder/commands/mimic.rb: -------------------------------------------------------------------------------- 1 | require 'naught/basic_object' 2 | require 'naught/null_class_builder/command' 3 | 4 | module Naught 5 | class NullClassBuilder 6 | module Commands 7 | class Mimic < Naught::NullClassBuilder::Command 8 | NULL_SINGLETON_CLASS = (class << Object.new; self; end) 9 | 10 | attr_reader :class_to_mimic, :include_super, :singleton_class 11 | 12 | def initialize(builder, class_to_mimic_or_options, options = {}) 13 | super(builder) 14 | 15 | if class_to_mimic_or_options.is_a?(Hash) 16 | options = class_to_mimic_or_options.merge(options) 17 | instance = options.fetch(:example) 18 | @singleton_class = (class << instance; self; end) 19 | @class_to_mimic = instance.class 20 | else 21 | @singleton_class = NULL_SINGLETON_CLASS 22 | @class_to_mimic = class_to_mimic_or_options 23 | end 24 | @include_super = options.fetch(:include_super) { true } 25 | 26 | builder.base_class = root_class_of(@class_to_mimic) 27 | class_to_mimic = @class_to_mimic 28 | builder.inspect_proc = lambda { "" } 29 | builder.interface_defined = true 30 | end 31 | 32 | def call 33 | defer do |subject| 34 | methods_to_stub.each do |method_name| 35 | builder.stub_method(subject, method_name) 36 | end 37 | end 38 | end 39 | 40 | private 41 | 42 | def root_class_of(klass) 43 | klass.ancestors.include?(Object) ? Object : Naught::BasicObject 44 | end 45 | 46 | def methods_to_stub 47 | methods_to_mimic = 48 | class_to_mimic.instance_methods(include_super) | 49 | singleton_class.instance_methods(false) 50 | methods_to_mimic - Object.instance_methods 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder/commands/pebble.rb: -------------------------------------------------------------------------------- 1 | require 'naught/null_class_builder/command' 2 | 3 | module Naught 4 | class NullClassBuilder 5 | module Commands 6 | class Pebble < ::Naught::NullClassBuilder::Command 7 | def initialize(builder, output = $stdout) 8 | @builder = builder 9 | @output = output 10 | end 11 | 12 | def call 13 | defer do |subject| 14 | subject.module_exec(@output) do |output| 15 | define_method(:method_missing) do |method_name, *args| 16 | pretty_args = args.collect(&:inspect).join(', ').tr("\"", "'") 17 | output.puts "#{method_name}(#{pretty_args}) from #{parse_caller}" 18 | self 19 | end 20 | 21 | def parse_caller 22 | caller = Kernel.caller(2).first 23 | method_name = caller.match(/\`([\w\s]+(\(\d+\s\w+\))?[\w\s]*)/) 24 | method_name ? method_name[1] : caller 25 | end 26 | private :parse_caller 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder/commands/predicates_return.rb: -------------------------------------------------------------------------------- 1 | require 'naught/null_class_builder/command' 2 | 3 | module Naught 4 | class NullClassBuilder 5 | module Commands 6 | class PredicatesReturn < Naught::NullClassBuilder::Command 7 | def initialize(builder, return_value) 8 | super(builder) 9 | @predicate_return_value = return_value 10 | end 11 | 12 | def call 13 | defer do |subject| 14 | define_method_missing(subject) 15 | define_predicate_methods(subject) 16 | end 17 | end 18 | 19 | private 20 | 21 | def define_method_missing(subject) 22 | subject.module_exec(@predicate_return_value) do |return_value| 23 | next unless subject.method_defined?(:method_missing) 24 | original_method_missing = instance_method(:method_missing) 25 | define_method(:method_missing) do |method_name, *args, &block| 26 | if method_name.to_s.end_with?('?') 27 | return_value 28 | else 29 | original_method_missing.bind(self).call(method_name, *args, &block) 30 | end 31 | end 32 | end 33 | end 34 | 35 | def define_predicate_methods(subject) 36 | subject.module_exec(@predicate_return_value) do |return_value| 37 | instance_methods.each do |method_name| 38 | next unless method_name.to_s.end_with?('?') 39 | define_method(method_name) do |*| 40 | return_value 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder/commands/singleton.rb: -------------------------------------------------------------------------------- 1 | require 'naught/null_class_builder/command' 2 | 3 | module Naught 4 | class NullClassBuilder 5 | module Commands 6 | class Singleton < Naught::NullClassBuilder::Command 7 | def call 8 | defer(:class => true) do |subject| 9 | require 'singleton' 10 | subject.module_eval do 11 | include ::Singleton 12 | 13 | def self.get(*) 14 | instance 15 | end 16 | 17 | %w(dup clone).each do |method_name| 18 | define_method method_name do 19 | self 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/naught/null_class_builder/commands/traceable.rb: -------------------------------------------------------------------------------- 1 | require 'naught/null_class_builder/command' 2 | 3 | module Naught 4 | class NullClassBuilder 5 | module Commands 6 | class Traceable < Naught::NullClassBuilder::Command 7 | def call 8 | defer do |subject| 9 | subject.module_eval do 10 | attr_reader :__file__, :__line__ 11 | 12 | def initialize(options = {}) 13 | range = (RUBY_VERSION.to_f == 1.9 && RUBY_PLATFORM != 'java') ? 4 : 3 14 | backtrace = options.fetch(:caller) { Kernel.caller(range) } 15 | @__file__, line = backtrace[0].split(':') 16 | @__line__ = line.to_i 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/naught/version.rb: -------------------------------------------------------------------------------- 1 | module Naught 2 | VERSION = '1.1.0' 3 | end 4 | -------------------------------------------------------------------------------- /naught.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'naught/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'naught' 8 | spec.version = Naught::VERSION 9 | spec.authors = ['Avdi Grimm'] 10 | spec.email = ['avdi@avdi.org'] 11 | spec.description = 'Naught is a toolkit for building Null Objects' 12 | spec.summary = spec.description 13 | spec.homepage = 'https://github.com/avdi/naught' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 1.3' 22 | end 23 | -------------------------------------------------------------------------------- /spec/base_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'null object with a custom base class' do 4 | subject(:null) { custom_base_null_class.new } 5 | 6 | let(:custom_base_null_class) do 7 | Naught.build do |b| 8 | b.base_class = Object 9 | end 10 | end 11 | 12 | it 'responds to base class methods' do 13 | expect(null.methods).to be_an Array 14 | end 15 | 16 | it 'responds to unknown methods' do 17 | expect(null.foo).to be_nil 18 | end 19 | 20 | it 'exposes the default base class choice, for the curious' do 21 | default_base_class = :not_set 22 | Naught.build do |b| 23 | default_base_class = b.base_class 24 | end 25 | expect(default_base_class).to eq(Naught::BasicObject) 26 | end 27 | 28 | describe 'singleton null object' do 29 | subject(:null_instance) { custom_base_singleton_null_class.instance } 30 | 31 | let(:custom_base_singleton_null_class) do 32 | Naught.build do |b| 33 | b.singleton 34 | b.base_class = Object 35 | end 36 | end 37 | 38 | it 'can be cloned' do 39 | expect(null_instance.clone).to be(null_instance) 40 | end 41 | 42 | it 'can be duplicated' do 43 | expect(null_instance.dup).to be(null_instance) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/basic_null_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'basic null object' do 4 | let(:null_class) { Naught.build } 5 | subject(:null) { null_class.new } 6 | 7 | it 'responds to arbitrary messages and returns nil' do 8 | expect(null.info).to be_nil 9 | expect(null.foobaz).to be_nil 10 | expect(null.to_s).to be_nil 11 | end 12 | 13 | it 'accepts any arguments for any messages' do 14 | null.foobaz(1, 2, 3) 15 | end 16 | 17 | it 'reports that it responds to any message' do 18 | expect(null).to respond_to(:info) 19 | expect(null).to respond_to(:foobaz) 20 | expect(null).to respond_to(:to_s) 21 | end 22 | 23 | it 'can be inspected' do 24 | expect(null.inspect).to eq('') 25 | end 26 | 27 | it 'knows its own class' do 28 | expect(null.class).to eq(null_class) 29 | end 30 | 31 | it 'aliases .new to .get' do 32 | expect(null_class.get.class).to be(null_class) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/blackhole_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'black hole null object' do 4 | subject(:null) { null_class.new } 5 | let(:null_class) do 6 | Naught.build(&:black_hole) 7 | end 8 | 9 | it 'returns self from arbitray method calls' do 10 | expect(null.info).to be(null) 11 | expect(null.foobaz).to be(null) 12 | expect(null << 'bar').to be(null) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/explicit_conversions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper.rb' 2 | 3 | describe 'explicitly convertable null object' do 4 | let(:null_class) do 5 | Naught.build(&:define_explicit_conversions) 6 | end 7 | subject(:null) { null_class.new } 8 | 9 | it 'defines common explicit conversions to return zero values' do 10 | expect(null.to_s).to eq('') 11 | expect(null.to_a).to eq([]) 12 | expect(null.to_i).to eq(0) 13 | expect(null.to_f).to eq(0.0) 14 | if RUBY_VERSION >= '2.0' 15 | expect(null.to_h).to eq({}) 16 | elsif RUBY_VERSION >= '1.9' 17 | expect(null.to_c).to eq(Complex(0)) 18 | expect(null.to_r).to eq(Rational(0)) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/functions/actual_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Actual()' do 4 | include ConvertableNull::Conversions 5 | 6 | specify 'given a null object, returns nil' do 7 | null = ConvertableNull.get 8 | expect(Actual(null)).to be_nil 9 | end 10 | 11 | specify 'given anything else, returns the input unchanged' do 12 | expect(Actual(false)).to be(false) 13 | str = 'hello' 14 | expect(Actual(str)).to be(str) 15 | expect(Actual(nil)).to be_nil 16 | end 17 | 18 | it 'also works with blocks' do 19 | expect(Actual { ConvertableNull.new }).to be_nil 20 | expect(Actual { 'foo' }).to eq('foo') 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/functions/just_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Just()' do 4 | include ConvertableNull::Conversions 5 | 6 | specify 'passes non-nullish values through' do 7 | expect(Just(false)).to be(false) 8 | str = 'hello' 9 | expect(Just(str)).to be(str) 10 | end 11 | 12 | specify 'rejects nullish values' do 13 | expect { Just(nil) }.to raise_error(ArgumentError) 14 | expect { Just('') }.to raise_error(ArgumentError) 15 | expect { Just(ConvertableNull.get) }.to raise_error(ArgumentError) 16 | end 17 | 18 | it 'also works with blocks' do 19 | expect { Just { nil }.class }.to raise_error(ArgumentError) 20 | expect(Just { 'foo' }).to eq('foo') 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/functions/maybe_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Maybe()' do 4 | include ConvertableNull::Conversions 5 | 6 | specify 'given nil, returns a null object' do 7 | expect(Maybe(nil).class).to be(ConvertableNull) 8 | end 9 | 10 | specify 'given a null object, returns the same null object' do 11 | null = ConvertableNull.get 12 | expect(Maybe(null)).to be(null) 13 | end 14 | 15 | specify 'given anything in null_equivalents, returns a null object' do 16 | expect(Maybe('').class).to be(ConvertableNull) 17 | end 18 | 19 | specify 'given anything else, returns the input unchanged' do 20 | expect(Maybe(false)).to be(false) 21 | str = 'hello' 22 | expect(Maybe(str)).to be(str) 23 | end 24 | 25 | it 'generates null objects with useful trace info' do 26 | null, line = Maybe(), __LINE__ # rubocop:disable ParallelAssignment 27 | expect(null.__file__).to eq(__FILE__) 28 | expect(null.__line__).to eq(line) 29 | end 30 | 31 | it 'also works with blocks' do 32 | expect(Maybe { nil }.class).to eq(ConvertableNull) 33 | expect(Maybe { 'foo' }).to eq('foo') 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/functions/null_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Null()' do 4 | include ConvertableNull::Conversions 5 | 6 | specify 'given no input, returns a null object' do 7 | expect(Null().class).to be(ConvertableNull) 8 | end 9 | 10 | specify 'given nil, returns a null object' do 11 | expect(Null(nil).class).to be(ConvertableNull) 12 | end 13 | 14 | specify 'given a null object, returns the same null object' do 15 | null = ConvertableNull.get 16 | expect(Null(null)).to be(null) 17 | end 18 | 19 | specify 'given anything in null_equivalents, returns a null object' do 20 | expect(Null('').class).to be(ConvertableNull) 21 | end 22 | 23 | specify 'given anything else, raises an ArgumentError' do 24 | expect { Null(false) }.to raise_error(ArgumentError) 25 | expect { Null('hello') }.to raise_error(ArgumentError) 26 | end 27 | 28 | it 'generates null objects with useful trace info' do 29 | null, line = Null(), __LINE__ # rubocop:disable ParallelAssignment 30 | expect(null.__file__).to eq(__FILE__) 31 | expect(null.__line__).to eq(line) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/implicit_conversions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'implicitly convertable null object' do 4 | subject(:null) { null_class.new } 5 | let(:null_class) do 6 | Naught.build(&:define_implicit_conversions) 7 | end 8 | it 'implicitly splats the same way an empty array does' do 9 | a, b = null 10 | expect(a).to be_nil 11 | expect(b).to be_nil 12 | end 13 | it 'is implicitly convertable to String' do 14 | expect(instance_eval(null)).to be_nil 15 | end 16 | it 'implicitly converts to an empty array' do 17 | expect(null.to_ary).to eq([]) 18 | end 19 | it 'implicitly converts to an empty hash' do 20 | expect(null.to_hash).to eq({}) 21 | end 22 | it 'implicitly converts to zero' do 23 | expect(null.to_int).to eq(0) 24 | end 25 | it 'implicitly converts to an empty string' do 26 | expect(null.to_str).to eq('') 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/mimic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'logger' 3 | 4 | describe 'null object mimicking a class' do 5 | class User 6 | attr_reader :login 7 | end 8 | 9 | module Authorizable 10 | def authorized_for?(_); end 11 | end 12 | 13 | class LibraryPatron < User 14 | include Authorizable 15 | attr_reader :name 16 | 17 | def member?; end 18 | 19 | def notify_of_overdue_books(_); end 20 | end 21 | 22 | subject(:null) { mimic_class.new } 23 | let(:mimic_class) do 24 | Naught.build do |b| 25 | b.mimic LibraryPatron 26 | end 27 | end 28 | it 'responds to all methods defined on the target class' do 29 | expect(null.member?).to be_nil 30 | expect(null.name).to be_nil 31 | expect(null.notify_of_overdue_books(['The Grapes of Wrath'])).to be_nil 32 | end 33 | 34 | it 'does not respond to methods not defined on the target class' do 35 | expect { null.foobar }.to raise_error(NoMethodError) 36 | end 37 | 38 | it 'reports which messages it does and does not respond to' do 39 | expect(null).to respond_to(:member?) 40 | expect(null).to respond_to(:name) 41 | expect(null).to respond_to(:notify_of_overdue_books) 42 | expect(null).not_to respond_to(:foobar) 43 | end 44 | it 'has an informative inspect string' do 45 | expect(null.inspect).to eq('') 46 | end 47 | 48 | it 'excludes Object methods from being mimicked' do 49 | expect(null.object_id).not_to be_nil 50 | expect(null.hash).not_to be_nil 51 | end 52 | 53 | it 'includes inherited methods' do 54 | expect(null.authorized_for?('something')).to be_nil 55 | expect(null.login).to be_nil 56 | end 57 | 58 | describe 'with include_super: false' do 59 | let(:mimic_class) do 60 | Naught.build do |b| 61 | b.mimic LibraryPatron, :include_super => false 62 | end 63 | end 64 | 65 | it 'excludes inherited methods' do 66 | expect(null).to_not respond_to(:authorized_for?) 67 | expect(null).to_not respond_to(:login) 68 | end 69 | end 70 | 71 | describe 'with an instance as example' do 72 | let(:mimic_class) do 73 | milton = LibraryPatron.new 74 | def milton.stapler; end 75 | Naught.build do |b| 76 | b.mimic :example => milton 77 | end 78 | end 79 | 80 | it 'responds to method defined only on the example instance' do 81 | expect(null).to respond_to(:stapler) 82 | end 83 | 84 | it 'responds to method defined on the class of the instance' do 85 | expect(null).to respond_to(:member?) 86 | end 87 | end 88 | end 89 | 90 | describe 'using mimic with black_hole' do 91 | subject(:null) { mimic_class.new } 92 | let(:mimic_class) do 93 | Naught.build do |b| 94 | b.mimic Logger 95 | b.black_hole 96 | end 97 | end 98 | 99 | shared_examples_for 'a black hole mimic' do 100 | it 'returns self from mimicked methods' do 101 | expect(null.info).to equal(null) 102 | expect(null.error).to equal(null) 103 | expect(null << 'test').to equal(null) 104 | end 105 | 106 | it 'does not respond to methods not defined on the target class' do 107 | expect { null.foobar }.to raise_error(NoMethodError) 108 | end 109 | end 110 | 111 | it_should_behave_like 'a black hole mimic' 112 | 113 | describe '(reverse order)' do 114 | let(:mimic_class) do 115 | Naught.build do |b| 116 | b.black_hole 117 | b.mimic Logger 118 | end 119 | end 120 | 121 | it_should_behave_like 'a black hole mimic' 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /spec/naught/null_object_builder/command_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Naught 4 | describe NullClassBuilder::Command do 5 | it 'is abstract' do 6 | command = NullClassBuilder::Command.new(nil) 7 | expect { command.call }.to raise_error(NotImplementedError) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/naught/null_object_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Naught 4 | class NullClassBuilder 5 | module Commands 6 | class TestCommand 7 | end 8 | end 9 | end 10 | 11 | describe NullClassBuilder do 12 | subject(:builder) { NullClassBuilder.new } 13 | it 'responds to commands defined in NullObjectBuilder::Commands' do 14 | expect(builder).to respond_to(:test_command) 15 | end 16 | 17 | it 'translates method calls into command invocations including arguments' do 18 | test_command = double 19 | expect(NullClassBuilder::Commands::TestCommand).to receive(:new). 20 | with(builder, 'foo', 42). 21 | and_return(test_command) 22 | expect(test_command).to receive(:call).and_return('COMMAND RESULT') 23 | expect(builder.test_command('foo', 42)).to eq('COMMAND RESULT') 24 | end 25 | 26 | it 'handles missing non-command missing methods normally' do 27 | expect(builder).not_to respond_to(:nonexistant_method) 28 | expect { builder.nonexistent_method }.to raise_error(NoMethodError) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/naught_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'null object impersonating another type' do 4 | class Point 5 | attr_reader :x, :y 6 | end 7 | 8 | subject(:null) { impersonation_class.new } 9 | let(:impersonation_class) do 10 | Naught.build do |b| 11 | b.impersonate Point 12 | end 13 | end 14 | 15 | it 'matches the impersonated type' do 16 | expect(null).to be_a Point 17 | end 18 | 19 | it 'responds to methods from the impersonated type' do 20 | expect(null.x).to be_nil 21 | expect(null.y).to be_nil 22 | end 23 | 24 | it 'does not respond to unknown methods' do 25 | expect { null.foo }.to raise_error(NoMethodError) 26 | end 27 | end 28 | 29 | describe 'traceable null object' do 30 | subject(:trace_null) do 31 | null_object_and_line.first 32 | end 33 | let(:null_object_and_line) do 34 | obj, line = trace_null_class.new, __LINE__ # rubocop:disable ParallelAssignment 35 | [obj, line] 36 | end 37 | let(:instantiation_line) { null_object_and_line.last } 38 | let(:trace_null_class) do 39 | Naught.build(&:traceable) 40 | end 41 | 42 | it 'remembers the file it was instantiated from' do 43 | expect(trace_null.__file__).to eq(__FILE__) 44 | end 45 | 46 | it 'remembers the line it was instantiated from' do 47 | expect(trace_null.__line__).to eq(instantiation_line) 48 | end 49 | 50 | def make_null 51 | trace_null_class.get(:caller => caller(1)) 52 | end 53 | 54 | it 'can accept custom backtrace info' do 55 | obj, line = make_null, __LINE__ # rubocop:disable ParallelAssignment 56 | expect(obj.__line__).to eq(line) 57 | end 58 | end 59 | 60 | describe 'customized null object' do 61 | subject(:custom_null) { custom_null_class.new } 62 | let(:custom_null_class) do 63 | Naught.build do |b| 64 | b.define_explicit_conversions 65 | def to_path 66 | '/dev/null' 67 | end 68 | 69 | def to_s 70 | 'NOTHING TO SEE HERE' 71 | end 72 | end 73 | end 74 | 75 | it 'responds to custom-defined methods' do 76 | expect(custom_null.to_path).to eq('/dev/null') 77 | end 78 | 79 | it 'allows generated methods to be overridden' do 80 | expect(custom_null.to_s).to eq('NOTHING TO SEE HERE') 81 | end 82 | end 83 | TestNull = Naught.build 84 | 85 | describe 'a named null object class' do 86 | it 'has named ancestor modules' do 87 | expect(TestNull.ancestors[0..2].collect(&:name)).to eq([ 88 | 'TestNull', 89 | 'TestNull::Customizations', 90 | 'TestNull::GeneratedMethods', 91 | ]) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/pebble_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'stringio' 3 | 4 | describe 'pebble null object' do 5 | class Caller 6 | def call_method(thing) 7 | thing.info 8 | end 9 | 10 | def call_method_inside_block(thing) 11 | 2.times.each { thing.info } 12 | end 13 | 14 | def call_method_inside_nested_block(thing) 15 | 2.times.each { 2.times.each { thing.info } } 16 | end 17 | end 18 | 19 | subject(:null) { null_class.new } 20 | let(:null_class) do 21 | output = test_output # getting local binding 22 | Naught.build do |b| 23 | b.pebble output 24 | end 25 | end 26 | 27 | let(:test_output) { StringIO.new } 28 | 29 | it 'prints the name of the method called' do 30 | expect(test_output).to receive(:puts).with(/^info\(\)/) 31 | null.info 32 | end 33 | 34 | it 'prints the arguments received' do 35 | expect(test_output).to receive(:puts).with(/^info\(\'foo\', 5, \:sym\)/) 36 | null.info('foo', 5, :sym) 37 | end 38 | 39 | it 'prints the name of the caller' do 40 | expect(test_output).to receive(:puts).with(/from call_method$/) 41 | Caller.new.call_method(null) 42 | end 43 | 44 | it 'returns self' do 45 | expect(null.info).to be(null) 46 | end 47 | 48 | context 'when is called from a block' do 49 | it 'prints the indication of a block', 50 | :pending => jruby? || rubinius? || ruby_18? do 51 | expect(test_output).to receive(:puts).twice. 52 | with(/from block/) 53 | Caller.new.call_method_inside_block(null) 54 | end 55 | 56 | it 'prints the name of the method that has the block' do 57 | expect(test_output).to receive(:puts).twice. 58 | with(/call_method_inside_block$/) 59 | Caller.new.call_method_inside_block(null) 60 | end 61 | end 62 | 63 | context 'when is called from many levels blocks' do 64 | it 'prints the indication of blocks and its levels', 65 | :pending => jruby? || rubinius? || ruby_18? do 66 | expect(test_output).to receive(:puts).exactly(4).times. 67 | with(/from block \(2 levels\)/) 68 | Caller.new.call_method_inside_nested_block(null) 69 | end 70 | 71 | it 'prints the name of the method that has the block' do 72 | expect(test_output).to receive(:puts).exactly(4).times. 73 | with(/call_method_inside_nested_block$/) 74 | Caller.new.call_method_inside_nested_block(null) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/predicate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'a null object with predicates_return(false)' do 4 | subject(:null) { null_class.new } 5 | let(:null_class) do 6 | Naught.build do |config| 7 | config.predicates_return false 8 | end 9 | end 10 | 11 | it 'responds to predicate-style methods with false' do 12 | expect(null.too_much_coffee?).to be(false) 13 | end 14 | 15 | it 'responds to other methods with nil' do 16 | expect(null.foobar).to be(nil) 17 | end 18 | 19 | describe '(black hole)' do 20 | let(:null_class) do 21 | Naught.build do |config| 22 | config.black_hole 23 | config.predicates_return false 24 | end 25 | end 26 | 27 | it 'responds to predicate-style methods with false' do 28 | expect(null.too_much_coffee?).to be(false) 29 | end 30 | 31 | it 'responds to other methods with self' do 32 | expect(null.foobar).to be(null) 33 | end 34 | end 35 | 36 | describe '(black hole, reverse order config)' do 37 | let(:null_class) do 38 | Naught.build do |config| 39 | config.predicates_return false 40 | config.black_hole 41 | end 42 | end 43 | 44 | it 'responds to predicate-style methods with false' do 45 | expect(null.too_much_coffee?).to be(false) 46 | end 47 | 48 | it 'responds to other methods with self' do 49 | expect(null.foobar).to be(null) 50 | end 51 | end 52 | 53 | class Coffee 54 | attr_reader :origin 55 | def black?; end 56 | end 57 | 58 | describe '(mimic)' do 59 | let(:null_class) do 60 | Naught.build do |config| 61 | config.mimic Coffee 62 | config.predicates_return false 63 | end 64 | end 65 | 66 | it 'responds to predicate-style methods with false' do 67 | expect(null.black?).to be(false) 68 | end 69 | 70 | it 'responds to other methods with nil' do 71 | expect(null.origin).to be(nil) 72 | end 73 | 74 | it 'does not respond to undefined methods' do 75 | expect(null).not_to respond_to(:leaf_variety) 76 | expect { null.leaf_variety }.to raise_error(NoMethodError) 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/singleton_null_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'singleton null object' do 4 | subject(:null_class) do 5 | Naught.build(&:singleton) 6 | end 7 | 8 | it 'does not respond to .new' do 9 | expect { null_class.new }.to raise_error(NoMethodError) 10 | end 11 | 12 | it 'has only one instance' do 13 | null1 = null_class.instance 14 | null2 = null_class.instance 15 | expect(null1).to be(null2) 16 | end 17 | 18 | it 'can be cloned' do 19 | null = null_class.instance 20 | expect(null.clone).to be(null) 21 | end 22 | 23 | it 'can be duplicated' do 24 | null = null_class.instance 25 | expect(null.dup).to be(null) 26 | end 27 | it 'aliases .instance to .get' do 28 | expect(null_class.get).to be null_class.instance 29 | end 30 | it 'permits arbitrary arguments to be passed to .get' do 31 | null_class.get(42, :foo => 'bar') 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | GEM_ROOT = File.expand_path('../../', __FILE__) 2 | $LOAD_PATH.unshift File.join(GEM_ROOT, 'lib') 3 | 4 | require 'simplecov' 5 | require 'coveralls' 6 | 7 | SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter] 8 | 9 | SimpleCov.start do 10 | add_filter '/spec/' 11 | minimum_coverage(97.7) 12 | end 13 | 14 | require 'naught' 15 | Dir[File.join(GEM_ROOT, 'spec', 'support', '**/*.rb')].each { |f| require f } 16 | -------------------------------------------------------------------------------- /spec/support/convertable_null.rb: -------------------------------------------------------------------------------- 1 | ConvertableNull = Naught.build do |b| 2 | b.null_equivalents << '' 3 | b.traceable 4 | end 5 | -------------------------------------------------------------------------------- /spec/support/jruby.rb: -------------------------------------------------------------------------------- 1 | def jruby? 2 | RUBY_PLATFORM == 'java' 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/rubinius.rb: -------------------------------------------------------------------------------- 1 | def rubinius? 2 | defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/ruby_18.rb: -------------------------------------------------------------------------------- 1 | def ruby_18? 2 | RUBY_VERSION.to_f == 1.8 3 | end 4 | --------------------------------------------------------------------------------