naught 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +41 -50
- data/.travis.yml +12 -8
- data/Changelog.md +6 -0
- data/Gemfile +3 -9
- data/Guardfile +5 -5
- data/README.markdown +43 -5
- data/Rakefile +1 -1
- data/lib/naught/basic_object.rb +2 -2
- data/lib/naught/conversions.rb +2 -2
- data/lib/naught/null_class_builder.rb +36 -33
- data/lib/naught/null_class_builder/command.rb +1 -2
- data/lib/naught/null_class_builder/commands/define_explicit_conversions.rb +11 -7
- data/lib/naught/null_class_builder/commands/define_implicit_conversions.rb +22 -10
- data/lib/naught/null_class_builder/commands/impersonate.rb +9 -5
- data/lib/naught/null_class_builder/commands/mimic.rb +43 -25
- data/lib/naught/null_class_builder/commands/pebble.rb +2 -3
- data/lib/naught/null_class_builder/commands/predicates_return.rb +32 -30
- data/lib/naught/null_class_builder/commands/singleton.rb +17 -13
- data/lib/naught/null_class_builder/commands/traceable.rb +15 -11
- data/lib/naught/version.rb +1 -1
- data/naught.gemspec +3 -3
- data/spec/base_object_spec.rb +2 -3
- data/spec/basic_null_object_spec.rb +0 -1
- data/spec/blackhole_spec.rb +1 -3
- data/spec/explicit_conversions_spec.rb +1 -3
- data/spec/functions/maybe_spec.rb +2 -2
- data/spec/functions/null_spec.rb +2 -3
- data/spec/implicit_conversions_spec.rb +7 -4
- data/spec/mimic_spec.rb +26 -20
- data/spec/naught_spec.rb +9 -17
- data/spec/predicate_spec.rb +7 -12
- data/spec/singleton_null_object_spec.rb +2 -4
- data/spec/spec_helper.rb +8 -6
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 429858c6dc05004904b5c95c8e536587996175fb
|
4
|
+
data.tar.gz: 06ac8cf2ac0868e8b9d12d9cc15ee2c93c30428d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53cc9f5fc1b17d678163f02c7d61e967895a21afb01a93bcddc68963654e036051cfd303eb492f11da7d1b6e67508ccaf8ebe40ee2b45ec0d3884cec11ce123f
|
7
|
+
data.tar.gz: 1bb2ba430a1be17b50394c2ea2f3e035fec669d4b014c61726cfad653f55c1f42217534d1f920bd9d9cb62096ebb574abe8da0f8017e86d6739a5accb1579277
|
data/.rubocop.yml
CHANGED
@@ -1,74 +1,65 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
- 'Gemfile'
|
4
|
-
- 'Rakefile'
|
5
|
-
- 'naught.gemspec'
|
6
|
-
|
7
|
-
# Avoid long parameter lists
|
8
|
-
ParameterLists:
|
9
|
-
Max: 4
|
10
|
-
CountKeywordArgs: true
|
1
|
+
Lint/NestedMethodDefinition:
|
2
|
+
Enabled: false
|
11
3
|
|
12
|
-
|
13
|
-
Max:
|
4
|
+
Metrics/BlockNesting:
|
5
|
+
Max: 2
|
14
6
|
|
15
|
-
|
7
|
+
Metrics/LineLength:
|
8
|
+
AllowURI: true
|
9
|
+
Max: 93 # TODO: Lower to 80
|
10
|
+
|
11
|
+
Metrics/MethodLength:
|
16
12
|
CountComments: false
|
17
|
-
Max: 21
|
13
|
+
Max: 21 # TODO: Lower to 15
|
18
14
|
|
19
|
-
|
20
|
-
|
21
|
-
|
15
|
+
Metrics/ParameterLists:
|
16
|
+
Max: 4
|
17
|
+
CountKeywordArgs: true
|
18
|
+
|
19
|
+
Style/AccessModifierIndentation:
|
20
|
+
EnforcedStyle: outdent
|
21
|
+
|
22
|
+
Style/ClassVars:
|
23
|
+
Enabled: false
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
+
Style/CollectionMethods:
|
26
|
+
Enabled: true
|
25
27
|
PreferredMethods:
|
26
28
|
map: 'collect'
|
29
|
+
map!: 'collect!'
|
27
30
|
reduce: 'inject'
|
28
31
|
find: 'detect'
|
29
32
|
find_all: 'select'
|
30
33
|
|
31
|
-
|
32
|
-
LineLength:
|
34
|
+
Style/Documentation:
|
33
35
|
Enabled: false
|
34
36
|
|
35
|
-
|
36
|
-
|
37
|
-
Enabled: false
|
37
|
+
Style/DotPosition:
|
38
|
+
EnforcedStyle: trailing
|
38
39
|
|
39
|
-
|
40
|
-
HashSyntax:
|
41
|
-
EnforcedStyle: hash_rockets
|
42
|
-
|
43
|
-
# No spaces inside hash literals
|
44
|
-
SpaceInsideHashLiteralBraces:
|
45
|
-
EnforcedStyle: no_space
|
46
|
-
|
47
|
-
# Allow dots at the end of lines
|
48
|
-
DotPosition:
|
40
|
+
Style/DoubleNegation:
|
49
41
|
Enabled: false
|
50
42
|
|
51
|
-
|
52
|
-
Encoding:
|
43
|
+
Style/EachWithObject:
|
53
44
|
Enabled: false
|
54
45
|
|
55
|
-
|
56
|
-
Enabled:
|
57
|
-
|
58
|
-
# Align ends correctly
|
59
|
-
EndAlignment:
|
60
|
-
AlignWith: variable
|
46
|
+
Style/Encoding:
|
47
|
+
Enabled: false
|
61
48
|
|
62
|
-
|
63
|
-
|
64
|
-
IndentWhenRelativeTo: end
|
65
|
-
IndentOneStep: false
|
49
|
+
Style/HashSyntax:
|
50
|
+
EnforcedStyle: hash_rockets
|
66
51
|
|
67
|
-
Lambda:
|
52
|
+
Style/Lambda:
|
68
53
|
Enabled: false
|
69
54
|
|
70
|
-
MethodName:
|
55
|
+
Style/MethodName:
|
71
56
|
Enabled: false
|
72
57
|
|
73
|
-
|
74
|
-
|
58
|
+
Style/RaiseArgs:
|
59
|
+
EnforcedStyle: compact
|
60
|
+
|
61
|
+
Style/SpaceInsideHashLiteralBraces:
|
62
|
+
EnforcedStyle: no_space
|
63
|
+
|
64
|
+
Style/TrailingComma:
|
65
|
+
EnforcedStyleForMultiline: 'comma'
|
data/.travis.yml
CHANGED
@@ -1,20 +1,24 @@
|
|
1
|
-
before_install:
|
2
|
-
|
3
|
-
|
4
|
-
|
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"
|
5
7
|
language: ruby
|
6
8
|
rvm:
|
7
9
|
- 1.8.7
|
8
|
-
- 1.9.2
|
9
10
|
- 1.9.3
|
10
11
|
- 2.0.0
|
11
|
-
- 2.1
|
12
|
-
-
|
12
|
+
- 2.1
|
13
|
+
- 2.2
|
14
|
+
- jruby-9000
|
13
15
|
- jruby-head
|
14
|
-
- rbx
|
16
|
+
- rbx-2
|
15
17
|
- ruby-head
|
16
18
|
matrix:
|
17
19
|
allow_failures:
|
18
20
|
- rvm: jruby-head
|
21
|
+
- rvm: rbx-2
|
19
22
|
- rvm: ruby-head
|
20
23
|
fast_finish: true
|
24
|
+
sudo: false
|
data/Changelog.md
CHANGED
@@ -1,3 +1,9 @@
|
|
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
|
+
|
1
7
|
## 1.0.0
|
2
8
|
|
3
9
|
- [Replace `::BasicObject` with `Naught::BasicObject`](https://github.com/avdi/naught/commit/8defad0bf9eb65e33054bf0a6e9c625c87c3e6df)
|
data/Gemfile
CHANGED
@@ -6,13 +6,12 @@ gemspec
|
|
6
6
|
gem 'rake'
|
7
7
|
|
8
8
|
group :development do
|
9
|
-
platforms :ruby_19, :ruby_20, :ruby_21 do
|
9
|
+
platforms :ruby_19, :ruby_20, :ruby_21, :ruby_22 do
|
10
10
|
gem 'guard'
|
11
11
|
gem 'guard-bundler'
|
12
12
|
gem 'guard-rspec'
|
13
13
|
end
|
14
14
|
gem 'pry'
|
15
|
-
gem 'pry-rescue'
|
16
15
|
end
|
17
16
|
|
18
17
|
group :test do
|
@@ -20,12 +19,7 @@ group :test do
|
|
20
19
|
gem 'json', :platforms => [:jruby, :rbx, :ruby_18, :ruby_19]
|
21
20
|
gem 'libnotify'
|
22
21
|
gem 'mime-types', '~> 1.25', :platforms => [:jruby, :ruby_18]
|
22
|
+
gem 'rest-client', '~> 1.6.0', :platforms => [:jruby, :ruby_18]
|
23
23
|
gem 'rspec', '>= 2.14'
|
24
|
-
gem 'rubocop', '
|
25
|
-
end
|
26
|
-
|
27
|
-
platforms :rbx do
|
28
|
-
gem 'racc'
|
29
|
-
gem 'rubinius-coverage', '~> 2.0'
|
30
|
-
gem 'rubysl', '~> 2.0'
|
24
|
+
gem 'rubocop', '~> 0.34.0', :platforms => [:ruby_19, :ruby_20, :ruby_21, :ruby_22]
|
31
25
|
end
|
data/Guardfile
CHANGED
@@ -3,13 +3,13 @@ guard 'bundler' do
|
|
3
3
|
watch(/^.+\.gemspec/)
|
4
4
|
end
|
5
5
|
|
6
|
-
guard :rspec, cli
|
6
|
+
guard :rspec, :cli => '-fs --color --order rand' do
|
7
7
|
watch(%r{^spec/.+_spec\.rb$})
|
8
|
-
watch(%r{^lib/(.+)\.rb$})
|
9
|
-
watch('spec/spec_helper.rb')
|
8
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
9
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
10
10
|
end
|
11
11
|
|
12
|
-
guard 'ctags-bundler', emacs
|
13
|
-
watch(
|
12
|
+
guard 'ctags-bundler', :emacs => true, :src_path => ['lib', 'spec/support'] do
|
13
|
+
watch(%r{^(lib|spec/support)/.*\.rb$})
|
14
14
|
watch('Gemfile.lock')
|
15
15
|
end
|
data/README.markdown
CHANGED
@@ -1,7 +1,16 @@
|
|
1
|
-
[][gem]
|
2
|
+
[][travis]
|
3
|
+
[][gemnasium]
|
4
|
+
[][codeclimate]
|
5
|
+
[][coveralls]
|
6
|
+
[][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
|
5
14
|
|
6
15
|
A quick intro to Naught
|
7
16
|
-------------------------
|
@@ -205,6 +214,25 @@ end
|
|
205
214
|
# >> Yep, checks out!
|
206
215
|
```
|
207
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
|
+
|
208
236
|
#### What about predicate methods? You know, the ones that end with question marks? Shouldn't they return `false` instead of `nil`?
|
209
237
|
|
210
238
|
Sure, if you'd like.
|
@@ -397,7 +425,7 @@ gem install naught
|
|
397
425
|
Requirements
|
398
426
|
--------------
|
399
427
|
|
400
|
-
- Ruby
|
428
|
+
- Ruby
|
401
429
|
|
402
430
|
Contributing
|
403
431
|
--------------
|
@@ -417,6 +445,10 @@ This isn't the first Ruby Null Object library. Others to check out include:
|
|
417
445
|
- [NullAndVoid](https://github.com/jfelchner/null_and_void)
|
418
446
|
- [BlankSlate](https://github.com/saturnflyer/blank_slate)
|
419
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/).
|
420
452
|
|
421
453
|
Further reading
|
422
454
|
-----------------
|
@@ -434,3 +466,9 @@ Further reading
|
|
434
466
|
- [Null Objects and
|
435
467
|
Falsiness](http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/),
|
436
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.
|
data/Rakefile
CHANGED
data/lib/naught/basic_object.rb
CHANGED
@@ -4,11 +4,11 @@ module Naught
|
|
4
4
|
end
|
5
5
|
else
|
6
6
|
class BasicObject #:nodoc:
|
7
|
-
keep = %w
|
7
|
+
keep = %w(
|
8
8
|
! != == __id__ __send__ equal? instance_eval instance_exec
|
9
9
|
method_missing singleton_method_added singleton_method_removed
|
10
10
|
singleton_method_undefined
|
11
|
-
|
11
|
+
)
|
12
12
|
instance_methods.each do |method_name|
|
13
13
|
undef_method(method_name) unless keep.include?(method_name)
|
14
14
|
end
|
data/lib/naught/conversions.rb
CHANGED
@@ -16,7 +16,7 @@ module Naught
|
|
16
16
|
when :nothing_passed, *@@null_equivs
|
17
17
|
@@null_class.get(:caller => caller(1))
|
18
18
|
else
|
19
|
-
fail
|
19
|
+
fail(ArgumentError.new("#{object.inspect} is not null!"))
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
@@ -36,7 +36,7 @@ module Naught
|
|
36
36
|
object = yield if block_given?
|
37
37
|
case object
|
38
38
|
when NullObjectTag, *@@null_equivs
|
39
|
-
fail
|
39
|
+
fail(ArgumentError.new("Null value: #{object.inspect}"))
|
40
40
|
else
|
41
41
|
object
|
42
42
|
end
|
@@ -2,12 +2,13 @@ require 'naught/basic_object'
|
|
2
2
|
require 'naught/conversions'
|
3
3
|
|
4
4
|
module Naught
|
5
|
-
class NullClassBuilder
|
5
|
+
class NullClassBuilder # rubocop:disable ClassLength
|
6
6
|
# make sure this module exists
|
7
7
|
module Commands
|
8
8
|
end
|
9
9
|
|
10
10
|
attr_accessor :base_class, :inspect_proc, :interface_defined
|
11
|
+
alias_method :interface_defined?, :interface_defined
|
11
12
|
|
12
13
|
def initialize
|
13
14
|
@interface_defined = false
|
@@ -17,10 +18,6 @@ module Naught
|
|
17
18
|
define_basic_methods
|
18
19
|
end
|
19
20
|
|
20
|
-
def interface_defined?
|
21
|
-
!!@interface_defined
|
22
|
-
end
|
23
|
-
|
24
21
|
def customize(&customization_block)
|
25
22
|
return unless customization_block
|
26
23
|
customization_module.module_exec(self, &customization_block)
|
@@ -34,7 +31,7 @@ module Naught
|
|
34
31
|
@null_equivalents ||= [nil]
|
35
32
|
end
|
36
33
|
|
37
|
-
def generate_class
|
34
|
+
def generate_class # rubocop:disable AbcSize
|
38
35
|
respond_to_any_message unless interface_defined?
|
39
36
|
generation_mod = Module.new
|
40
37
|
customization_mod = customization_module # get a local binding
|
@@ -63,32 +60,6 @@ module Naught
|
|
63
60
|
null_class
|
64
61
|
end
|
65
62
|
|
66
|
-
def method_missing(method_name, *args, &block)
|
67
|
-
command_name = command_name_for_method(method_name)
|
68
|
-
if Commands.const_defined?(command_name)
|
69
|
-
command_class = Commands.const_get(command_name)
|
70
|
-
command_class.new(self, *args, &block).call
|
71
|
-
else
|
72
|
-
super
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
if RUBY_VERSION >= '1.9'
|
77
|
-
def respond_to_missing?(method_name, include_private = false)
|
78
|
-
command_name = command_name_for_method(method_name)
|
79
|
-
Commands.const_defined?(command_name) || super
|
80
|
-
rescue NameError
|
81
|
-
super
|
82
|
-
end
|
83
|
-
else
|
84
|
-
def respond_to?(method_name, include_private = false)
|
85
|
-
command_name = command_name_for_method(method_name)
|
86
|
-
Commands.const_defined?(command_name) || super
|
87
|
-
rescue NameError
|
88
|
-
super
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
63
|
############################################################################
|
93
64
|
# Builder API
|
94
65
|
#
|
@@ -124,7 +95,39 @@ module Naught
|
|
124
95
|
send(@stub_strategy, subject, name)
|
125
96
|
end
|
126
97
|
|
127
|
-
|
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
|
128
131
|
|
129
132
|
def define_basic_methods
|
130
133
|
define_basic_instance_methods
|
@@ -1,13 +1,17 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
require 'naught/null_class_builder/command'
|
3
3
|
|
4
|
-
module Naught
|
5
|
-
class
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
11
15
|
end
|
12
16
|
end
|
13
17
|
end
|
@@ -1,16 +1,28 @@
|
|
1
1
|
require 'naught/null_class_builder/command'
|
2
2
|
|
3
|
-
module Naught
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
11
21
|
|
12
|
-
|
13
|
-
|
22
|
+
def to_str
|
23
|
+
''
|
24
|
+
end
|
25
|
+
end
|
14
26
|
end
|
15
27
|
end
|
16
28
|
end
|
@@ -1,8 +1,12 @@
|
|
1
|
-
module Naught
|
2
|
-
class
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
6
10
|
end
|
7
11
|
end
|
8
12
|
end
|
@@ -1,37 +1,55 @@
|
|
1
1
|
require 'naught/basic_object'
|
2
2
|
require 'naught/null_class_builder/command'
|
3
3
|
|
4
|
-
module Naught
|
5
|
-
class
|
6
|
-
|
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 { "<null:#{class_to_mimic}>" }
|
29
|
+
builder.interface_defined = true
|
30
|
+
end
|
7
31
|
|
8
|
-
|
9
|
-
|
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
|
10
39
|
|
11
|
-
|
12
|
-
@include_super = options.fetch(:include_super) { true }
|
40
|
+
private
|
13
41
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
42
|
+
def root_class_of(klass)
|
43
|
+
klass.ancestors.include?(Object) ? Object : Naught::BasicObject
|
44
|
+
end
|
18
45
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
23
51
|
end
|
24
52
|
end
|
25
53
|
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def root_class_of(klass)
|
30
|
-
klass.ancestors.include?(Object) ? Object : Naught::BasicObject
|
31
|
-
end
|
32
|
-
|
33
|
-
def methods_to_stub
|
34
|
-
class_to_mimic.instance_methods(include_super) - Object.instance_methods
|
35
|
-
end
|
36
54
|
end
|
37
55
|
end
|
@@ -12,9 +12,8 @@ module Naught
|
|
12
12
|
def call
|
13
13
|
defer do |subject|
|
14
14
|
subject.module_exec(@output) do |output|
|
15
|
-
|
16
|
-
|
17
|
-
pretty_args = args.collect(&:inspect).join(', ').gsub("\"", "'")
|
15
|
+
define_method(:method_missing) do |method_name, *args|
|
16
|
+
pretty_args = args.collect(&:inspect).join(', ').tr("\"", "'")
|
18
17
|
output.puts "#{method_name}(#{pretty_args}) from #{parse_caller}"
|
19
18
|
self
|
20
19
|
end
|
@@ -1,42 +1,44 @@
|
|
1
1
|
require 'naught/null_class_builder/command'
|
2
2
|
|
3
|
-
module Naught
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
def call
|
13
|
+
defer do |subject|
|
14
|
+
define_method_missing(subject)
|
15
|
+
define_predicate_methods(subject)
|
16
|
+
end
|
17
|
+
end
|
16
18
|
|
17
|
-
|
19
|
+
private
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
28
31
|
end
|
29
32
|
end
|
30
33
|
end
|
31
|
-
end
|
32
|
-
end
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
40
42
|
end
|
41
43
|
end
|
42
44
|
end
|
@@ -1,20 +1,24 @@
|
|
1
1
|
require 'naught/null_class_builder/command'
|
2
2
|
|
3
|
-
module Naught
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
13
|
+
def self.get(*)
|
14
|
+
instance
|
15
|
+
end
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
%w(dup clone).each do |method_name|
|
18
|
+
define_method method_name do
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
18
22
|
end
|
19
23
|
end
|
20
24
|
end
|
@@ -1,17 +1,21 @@
|
|
1
1
|
require 'naught/null_class_builder/command'
|
2
2
|
|
3
|
-
module Naught
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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__
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
15
19
|
end
|
16
20
|
end
|
17
21
|
end
|
data/lib/naught/version.rb
CHANGED
data/naught.gemspec
CHANGED
@@ -8,14 +8,14 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = Naught::VERSION
|
9
9
|
spec.authors = ['Avdi Grimm']
|
10
10
|
spec.email = ['avdi@avdi.org']
|
11
|
-
spec.description =
|
11
|
+
spec.description = 'Naught is a toolkit for building Null Objects'
|
12
12
|
spec.summary = spec.description
|
13
13
|
spec.homepage = 'https://github.com/avdi/naught'
|
14
14
|
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
|
-
spec.executables = spec.files.grep(
|
18
|
-
spec.test_files = spec.files.grep(
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
|
21
21
|
spec.add_development_dependency 'bundler', '~> 1.3'
|
data/spec/base_object_spec.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe 'null object with a custom base class' do
|
4
|
-
|
5
4
|
subject(:null) { custom_base_null_class.new }
|
6
5
|
|
7
6
|
let(:custom_base_null_class) do
|
@@ -10,11 +9,11 @@ describe 'null object with a custom base class' do
|
|
10
9
|
end
|
11
10
|
end
|
12
11
|
|
13
|
-
it '
|
12
|
+
it 'responds to base class methods' do
|
14
13
|
expect(null.methods).to be_an Array
|
15
14
|
end
|
16
15
|
|
17
|
-
it '
|
16
|
+
it 'responds to unknown methods' do
|
18
17
|
expect(null.foo).to be_nil
|
19
18
|
end
|
20
19
|
|
data/spec/blackhole_spec.rb
CHANGED
@@ -12,7 +12,7 @@ describe 'Maybe()' do
|
|
12
12
|
expect(Maybe(null)).to be(null)
|
13
13
|
end
|
14
14
|
|
15
|
-
specify 'given anything in null_equivalents,
|
15
|
+
specify 'given anything in null_equivalents, returns a null object' do
|
16
16
|
expect(Maybe('').class).to be(ConvertableNull)
|
17
17
|
end
|
18
18
|
|
@@ -23,7 +23,7 @@ describe 'Maybe()' do
|
|
23
23
|
end
|
24
24
|
|
25
25
|
it 'generates null objects with useful trace info' do
|
26
|
-
null, line = Maybe(), __LINE__
|
26
|
+
null, line = Maybe(), __LINE__ # rubocop:disable ParallelAssignment
|
27
27
|
expect(null.__file__).to eq(__FILE__)
|
28
28
|
expect(null.__line__).to eq(line)
|
29
29
|
end
|
data/spec/functions/null_spec.rb
CHANGED
@@ -16,7 +16,7 @@ describe 'Null()' do
|
|
16
16
|
expect(Null(null)).to be(null)
|
17
17
|
end
|
18
18
|
|
19
|
-
specify 'given anything in null_equivalents,
|
19
|
+
specify 'given anything in null_equivalents, returns a null object' do
|
20
20
|
expect(Null('').class).to be(ConvertableNull)
|
21
21
|
end
|
22
22
|
|
@@ -26,9 +26,8 @@ describe 'Null()' do
|
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'generates null objects with useful trace info' do
|
29
|
-
null, line = Null(), __LINE__
|
29
|
+
null, line = Null(), __LINE__ # rubocop:disable ParallelAssignment
|
30
30
|
expect(null.__file__).to eq(__FILE__)
|
31
31
|
expect(null.__line__).to eq(line)
|
32
32
|
end
|
33
|
-
|
34
33
|
end
|
@@ -3,9 +3,7 @@ require 'spec_helper'
|
|
3
3
|
describe 'implicitly convertable null object' do
|
4
4
|
subject(:null) { null_class.new }
|
5
5
|
let(:null_class) do
|
6
|
-
Naught.build
|
7
|
-
b.define_implicit_conversions
|
8
|
-
end
|
6
|
+
Naught.build(&:define_implicit_conversions)
|
9
7
|
end
|
10
8
|
it 'implicitly splats the same way an empty array does' do
|
11
9
|
a, b = null
|
@@ -18,8 +16,13 @@ describe 'implicitly convertable null object' do
|
|
18
16
|
it 'implicitly converts to an empty array' do
|
19
17
|
expect(null.to_ary).to eq([])
|
20
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
|
21
25
|
it 'implicitly converts to an empty string' do
|
22
26
|
expect(null.to_str).to eq('')
|
23
27
|
end
|
24
|
-
|
25
28
|
end
|
data/spec/mimic_spec.rb
CHANGED
@@ -3,31 +3,20 @@ require 'logger'
|
|
3
3
|
|
4
4
|
describe 'null object mimicking a class' do
|
5
5
|
class User
|
6
|
-
|
7
|
-
'bob'
|
8
|
-
end
|
6
|
+
attr_reader :login
|
9
7
|
end
|
10
8
|
|
11
9
|
module Authorizable
|
12
|
-
def authorized_for?(
|
13
|
-
true
|
14
|
-
end
|
10
|
+
def authorized_for?(_); end
|
15
11
|
end
|
16
12
|
|
17
13
|
class LibraryPatron < User
|
18
14
|
include Authorizable
|
15
|
+
attr_reader :name
|
19
16
|
|
20
|
-
def member
|
21
|
-
true
|
22
|
-
end
|
23
|
-
|
24
|
-
def name
|
25
|
-
'Bob'
|
26
|
-
end
|
17
|
+
def member?; end
|
27
18
|
|
28
|
-
def notify_of_overdue_books(
|
29
|
-
puts 'Notifying...'
|
30
|
-
end
|
19
|
+
def notify_of_overdue_books(_); end
|
31
20
|
end
|
32
21
|
|
33
22
|
subject(:null) { mimic_class.new }
|
@@ -78,6 +67,24 @@ describe 'null object mimicking a class' do
|
|
78
67
|
expect(null).to_not respond_to(:login)
|
79
68
|
end
|
80
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
|
81
88
|
end
|
82
89
|
|
83
90
|
describe 'using mimic with black_hole' do
|
@@ -89,7 +96,7 @@ describe 'using mimic with black_hole' do
|
|
89
96
|
end
|
90
97
|
end
|
91
98
|
|
92
|
-
|
99
|
+
shared_examples_for 'a black hole mimic' do
|
93
100
|
it 'returns self from mimicked methods' do
|
94
101
|
expect(null.info).to equal(null)
|
95
102
|
expect(null.error).to equal(null)
|
@@ -101,7 +108,7 @@ describe 'using mimic with black_hole' do
|
|
101
108
|
end
|
102
109
|
end
|
103
110
|
|
104
|
-
|
111
|
+
it_should_behave_like 'a black hole mimic'
|
105
112
|
|
106
113
|
describe '(reverse order)' do
|
107
114
|
let(:mimic_class) do
|
@@ -111,7 +118,6 @@ describe 'using mimic with black_hole' do
|
|
111
118
|
end
|
112
119
|
end
|
113
120
|
|
114
|
-
|
121
|
+
it_should_behave_like 'a black hole mimic'
|
115
122
|
end
|
116
|
-
|
117
123
|
end
|
data/spec/naught_spec.rb
CHANGED
@@ -2,13 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe 'null object impersonating another type' do
|
4
4
|
class Point
|
5
|
-
|
6
|
-
23
|
7
|
-
end
|
8
|
-
|
9
|
-
def y
|
10
|
-
42
|
11
|
-
end
|
5
|
+
attr_reader :x, :y
|
12
6
|
end
|
13
7
|
|
14
8
|
subject(:null) { impersonation_class.new }
|
@@ -37,14 +31,12 @@ describe 'traceable null object' do
|
|
37
31
|
null_object_and_line.first
|
38
32
|
end
|
39
33
|
let(:null_object_and_line) do
|
40
|
-
obj, line = trace_null_class.new, __LINE__
|
34
|
+
obj, line = trace_null_class.new, __LINE__ # rubocop:disable ParallelAssignment
|
41
35
|
[obj, line]
|
42
36
|
end
|
43
37
|
let(:instantiation_line) { null_object_and_line.last }
|
44
38
|
let(:trace_null_class) do
|
45
|
-
Naught.build
|
46
|
-
b.traceable
|
47
|
-
end
|
39
|
+
Naught.build(&:traceable)
|
48
40
|
end
|
49
41
|
|
50
42
|
it 'remembers the file it was instantiated from' do
|
@@ -60,7 +52,7 @@ describe 'traceable null object' do
|
|
60
52
|
end
|
61
53
|
|
62
54
|
it 'can accept custom backtrace info' do
|
63
|
-
obj, line = make_null, __LINE__
|
55
|
+
obj, line = make_null, __LINE__ # rubocop:disable ParallelAssignment
|
64
56
|
expect(obj.__line__).to eq(line)
|
65
57
|
end
|
66
58
|
end
|
@@ -91,11 +83,11 @@ end
|
|
91
83
|
TestNull = Naught.build
|
92
84
|
|
93
85
|
describe 'a named null object class' do
|
94
|
-
it 'has named ancestor modules'
|
86
|
+
it 'has named ancestor modules' do
|
95
87
|
expect(TestNull.ancestors[0..2].collect(&:name)).to eq([
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
88
|
+
'TestNull',
|
89
|
+
'TestNull::Customizations',
|
90
|
+
'TestNull::GeneratedMethods',
|
91
|
+
])
|
100
92
|
end
|
101
93
|
end
|
data/spec/predicate_spec.rb
CHANGED
@@ -9,11 +9,11 @@ describe 'a null object with predicates_return(false)' do
|
|
9
9
|
end
|
10
10
|
|
11
11
|
it 'responds to predicate-style methods with false' do
|
12
|
-
expect(null.too_much_coffee?).to
|
12
|
+
expect(null.too_much_coffee?).to be(false)
|
13
13
|
end
|
14
14
|
|
15
15
|
it 'responds to other methods with nil' do
|
16
|
-
expect(null.foobar).to
|
16
|
+
expect(null.foobar).to be(nil)
|
17
17
|
end
|
18
18
|
|
19
19
|
describe '(black hole)' do
|
@@ -25,7 +25,7 @@ describe 'a null object with predicates_return(false)' do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'responds to predicate-style methods with false' do
|
28
|
-
expect(null.too_much_coffee?).to
|
28
|
+
expect(null.too_much_coffee?).to be(false)
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'responds to other methods with self' do
|
@@ -42,7 +42,7 @@ describe 'a null object with predicates_return(false)' do
|
|
42
42
|
end
|
43
43
|
|
44
44
|
it 'responds to predicate-style methods with false' do
|
45
|
-
expect(null.too_much_coffee?).to
|
45
|
+
expect(null.too_much_coffee?).to be(false)
|
46
46
|
end
|
47
47
|
|
48
48
|
it 'responds to other methods with self' do
|
@@ -51,13 +51,8 @@ describe 'a null object with predicates_return(false)' do
|
|
51
51
|
end
|
52
52
|
|
53
53
|
class Coffee
|
54
|
-
|
55
|
-
|
56
|
-
end
|
57
|
-
|
58
|
-
def origin
|
59
|
-
'Ethiopia'
|
60
|
-
end
|
54
|
+
attr_reader :origin
|
55
|
+
def black?; end
|
61
56
|
end
|
62
57
|
|
63
58
|
describe '(mimic)' do
|
@@ -69,7 +64,7 @@ describe 'a null object with predicates_return(false)' do
|
|
69
64
|
end
|
70
65
|
|
71
66
|
it 'responds to predicate-style methods with false' do
|
72
|
-
expect(null.black?).to
|
67
|
+
expect(null.black?).to be(false)
|
73
68
|
end
|
74
69
|
|
75
70
|
it 'responds to other methods with nil' do
|
@@ -2,13 +2,11 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe 'singleton null object' do
|
4
4
|
subject(:null_class) do
|
5
|
-
Naught.build
|
6
|
-
b.singleton
|
7
|
-
end
|
5
|
+
Naught.build(&:singleton)
|
8
6
|
end
|
9
7
|
|
10
8
|
it 'does not respond to .new' do
|
11
|
-
expect { null_class.new }.to raise_error
|
9
|
+
expect { null_class.new }.to raise_error(NoMethodError)
|
12
10
|
end
|
13
11
|
|
14
12
|
it 'has only one instance' do
|
data/spec/spec_helper.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
GEM_ROOT = File.expand_path('../../', __FILE__)
|
2
2
|
$LOAD_PATH.unshift File.join(GEM_ROOT, 'lib')
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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)
|
10
12
|
end
|
11
13
|
|
12
14
|
require 'naught'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: naught
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Avdi Grimm
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -98,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
98
|
version: '0'
|
99
99
|
requirements: []
|
100
100
|
rubyforge_project:
|
101
|
-
rubygems_version: 2.
|
101
|
+
rubygems_version: 2.4.5.1
|
102
102
|
signing_key:
|
103
103
|
specification_version: 4
|
104
104
|
summary: Naught is a toolkit for building Null Objects
|