naught 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +1 -1
  3. data/lib/naught/basic_object.rb +4 -14
  4. data/lib/naught/call_location.rb +131 -0
  5. data/lib/naught/caller_info.rb +128 -0
  6. data/lib/naught/chain_proxy.rb +51 -0
  7. data/lib/naught/conversions.rb +108 -34
  8. data/lib/naught/null_class_builder/command.rb +42 -4
  9. data/lib/naught/null_class_builder/commands/callstack.rb +89 -0
  10. data/lib/naught/null_class_builder/commands/define_explicit_conversions.rb +21 -9
  11. data/lib/naught/null_class_builder/commands/define_implicit_conversions.rb +19 -21
  12. data/lib/naught/null_class_builder/commands/impersonate.rb +13 -1
  13. data/lib/naught/null_class_builder/commands/mimic.rb +75 -31
  14. data/lib/naught/null_class_builder/commands/null_safe_proxy.rb +92 -0
  15. data/lib/naught/null_class_builder/commands/pebble.rb +21 -17
  16. data/lib/naught/null_class_builder/commands/predicates_return.rb +42 -24
  17. data/lib/naught/null_class_builder/commands/singleton.rb +14 -17
  18. data/lib/naught/null_class_builder/commands/traceable.rb +16 -11
  19. data/lib/naught/null_class_builder/commands.rb +10 -8
  20. data/lib/naught/null_class_builder.rb +213 -119
  21. data/lib/naught/stub_strategy.rb +30 -0
  22. data/lib/naught/version.rb +3 -1
  23. data/lib/naught.rb +31 -7
  24. metadata +34 -66
  25. data/.gitignore +0 -23
  26. data/.rspec +0 -2
  27. data/.rubocop.yml +0 -65
  28. data/.travis.yml +0 -24
  29. data/Changelog.md +0 -18
  30. data/Gemfile +0 -25
  31. data/Guardfile +0 -15
  32. data/README.markdown +0 -474
  33. data/Rakefile +0 -15
  34. data/naught.gemspec +0 -22
  35. data/spec/base_object_spec.rb +0 -46
  36. data/spec/basic_null_object_spec.rb +0 -34
  37. data/spec/blackhole_spec.rb +0 -14
  38. data/spec/explicit_conversions_spec.rb +0 -21
  39. data/spec/functions/actual_spec.rb +0 -22
  40. data/spec/functions/just_spec.rb +0 -22
  41. data/spec/functions/maybe_spec.rb +0 -35
  42. data/spec/functions/null_spec.rb +0 -33
  43. data/spec/implicit_conversions_spec.rb +0 -28
  44. data/spec/mimic_spec.rb +0 -123
  45. data/spec/naught/null_object_builder/command_spec.rb +0 -10
  46. data/spec/naught/null_object_builder_spec.rb +0 -31
  47. data/spec/naught_spec.rb +0 -93
  48. data/spec/pebble_spec.rb +0 -77
  49. data/spec/predicate_spec.rb +0 -79
  50. data/spec/singleton_null_object_spec.rb +0 -33
  51. data/spec/spec_helper.rb +0 -15
  52. data/spec/support/convertable_null.rb +0 -4
  53. data/spec/support/jruby.rb +0 -3
  54. data/spec/support/rubinius.rb +0 -3
  55. data/spec/support/ruby_18.rb +0 -3
@@ -1,17 +1,29 @@
1
- require 'forwardable'
2
- require 'naught/null_class_builder/command'
1
+ require "naught/null_class_builder/command"
3
2
 
4
3
  module Naught
5
4
  class NullClassBuilder
6
5
  module Commands
7
- class DefineExplicitConversions < ::Naught::NullClassBuilder::Command
6
+ # Adds explicit conversion methods delegating to nil
7
+ #
8
+ # These methods return the same values that nil returns:
9
+ # - to_a => []
10
+ # - to_c => (0+0i)
11
+ # - to_f => 0.0
12
+ # - to_h => {}
13
+ # - to_i => 0
14
+ # - to_r => (0/1)
15
+ # - to_s => ""
16
+ #
17
+ # @api private
18
+ class DefineExplicitConversions < Command
19
+ METHODS = %i[to_a to_c to_f to_h to_i to_r to_s].freeze
20
+ private_constant :METHODS
21
+
22
+ # Install explicit conversion methods
23
+ # @return [void]
24
+ # @api private
8
25
  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
26
+ defer { |subject| METHODS.each { |name| subject.define_method(name) { nil.public_send(name) } } }
15
27
  end
16
28
  end
17
29
  end
@@ -1,29 +1,27 @@
1
- require 'naught/null_class_builder/command'
1
+ require "naught/null_class_builder/command"
2
2
 
3
3
  module Naught
4
4
  class NullClassBuilder
5
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
6
+ # Adds implicit conversion methods to the null class
7
+ #
8
+ # @api private
9
+ class DefineImplicitConversions < Command
10
+ EMPTY_ARRAY = [] #: Array[untyped]
11
+ EMPTY_HASH = {} #: Hash[untyped, untyped]
12
+ RETURN_VALUES = {
13
+ to_ary: EMPTY_ARRAY.freeze,
14
+ to_hash: EMPTY_HASH.freeze,
15
+ to_int: 0,
16
+ to_str: "".freeze
17
+ }.freeze
18
+ private_constant :EMPTY_ARRAY, :EMPTY_HASH, :RETURN_VALUES
17
19
 
18
- def to_int
19
- 0
20
- end
21
-
22
- def to_str
23
- ''
24
- end
25
- end
26
- end
20
+ # Install implicit conversion methods
21
+ # @return [void]
22
+ # @api private
23
+ def call
24
+ defer { |subject| RETURN_VALUES.each { |name, value| subject.define_method(name) { value } } }
27
25
  end
28
26
  end
29
27
  end
@@ -1,7 +1,19 @@
1
1
  module Naught
2
2
  class NullClassBuilder
3
3
  module Commands
4
- class Impersonate < Naught::NullClassBuilder::Commands::Mimic
4
+ # Build a null class that impersonates a given class
5
+ #
6
+ # Unlike Mimic, Impersonate makes the null class inherit from the target,
7
+ # so `is_a?` checks will pass.
8
+ #
9
+ # @api private
10
+ class Impersonate < Mimic
11
+ # Create an impersonate command for a class
12
+ #
13
+ # @param builder [NullClassBuilder]
14
+ # @param class_to_impersonate [Class]
15
+ # @param options [Hash]
16
+ # @api private
5
17
  def initialize(builder, class_to_impersonate, options = {})
6
18
  super
7
19
  builder.base_class = class_to_impersonate
@@ -1,53 +1,97 @@
1
- require 'naught/basic_object'
2
- require 'naught/null_class_builder/command'
1
+ require "naught/basic_object"
2
+ require "naught/null_class_builder/command"
3
3
 
4
4
  module Naught
5
5
  class NullClassBuilder
6
6
  module Commands
7
- class Mimic < Naught::NullClassBuilder::Command
8
- NULL_SINGLETON_CLASS = (class << Object.new; self; end)
7
+ # Build a null class that mimics an existing class or instance
8
+ #
9
+ # @api private
10
+ class Mimic < Command
11
+ # Methods that should never be mimicked as they interfere with
12
+ # other Naught features like predicates_return
13
+ # @see https://github.com/avdi/naught/issues/55
14
+ METHODS_TO_SKIP = (%i[method_missing respond_to? respond_to_missing?] + Object.instance_methods).freeze
15
+ private_constant :METHODS_TO_SKIP
9
16
 
10
- attr_reader :class_to_mimic, :include_super, :singleton_class
17
+ # Singleton class placeholder used when no instance is provided
18
+ NULL_SINGLETON_CLASS = Object.new.singleton_class.freeze
19
+ private_constant :NULL_SINGLETON_CLASS
11
20
 
21
+ # The class being mimicked by the null object
22
+ # @return [Class] class being mimicked
23
+ attr_reader :class_to_mimic
24
+
25
+ # Whether to include superclass methods when mimicking
26
+ # @return [Boolean] whether to include superclass methods
27
+ attr_reader :include_super
28
+
29
+ # The singleton class being mimicked (for instance-based mimicking)
30
+ # @return [Class] singleton class being mimicked
31
+ attr_reader :singleton_class
32
+
33
+ # Create a mimic command for a class or instance
34
+ #
35
+ # @param builder [NullClassBuilder]
36
+ # @param class_to_mimic_or_options [Class, Hash]
37
+ # @param options [Hash]
38
+ # @api private
12
39
  def initialize(builder, class_to_mimic_or_options, options = {})
13
40
  super(builder)
41
+ parse_arguments(class_to_mimic_or_options, options)
42
+ configure_builder
43
+ end
44
+
45
+ # Install stubbed methods from the target class or instance
46
+ #
47
+ # @return [void]
48
+ # @api private
49
+ def call
50
+ defer { |subject| methods_to_stub.each { |name| builder.stub_method(subject, name) } }
51
+ end
52
+
53
+ private
14
54
 
55
+ # Parse the arguments to determine what to mimic
56
+ #
57
+ # @param class_to_mimic_or_options [Class, Hash] class or options hash
58
+ # @param options [Hash] additional options
59
+ # @return [void]
60
+ def parse_arguments(class_to_mimic_or_options, options)
15
61
  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
62
+ options = class_to_mimic_or_options.merge(options)
63
+ instance = options.fetch(:example)
64
+ @singleton_class = instance.singleton_class
65
+ @class_to_mimic = instance.class
20
66
  else
21
67
  @singleton_class = NULL_SINGLETON_CLASS
22
- @class_to_mimic = class_to_mimic_or_options
68
+ @class_to_mimic = class_to_mimic_or_options
23
69
  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
70
+ @include_super = options.fetch(:include_super, true)
30
71
  end
31
72
 
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
73
+ # Configure the builder with the mimicked class's properties
74
+ #
75
+ # @return [void]
76
+ def configure_builder
77
+ builder.base_class = root_class_of(class_to_mimic)
78
+ klass = class_to_mimic
79
+ builder.inspect_proc = -> { "<null:#{klass}>" }
80
+ builder.interface_defined = true
38
81
  end
39
82
 
40
- private
41
-
42
- def root_class_of(klass)
43
- klass.ancestors.include?(Object) ? Object : Naught::BasicObject
44
- end
83
+ # Determine the root class to inherit from
84
+ #
85
+ # @param klass [Class] the class to analyze
86
+ # @return [Class] Object or Naught::BasicObject
87
+ def root_class_of(klass) = klass.ancestors.include?(Object) ? Object : Naught::BasicObject
45
88
 
89
+ # Compute the list of methods to stub on the null object
90
+ #
91
+ # @return [Array<Symbol>] methods to stub
46
92
  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
93
+ all_methods = class_to_mimic.instance_methods(include_super) | singleton_class.instance_methods(false)
94
+ all_methods - METHODS_TO_SKIP
51
95
  end
52
96
  end
53
97
  end
@@ -0,0 +1,92 @@
1
+ require "naught/null_class_builder/command"
2
+
3
+ module Naught
4
+ class NullClassBuilder
5
+ module Commands
6
+ # Enables null-safe proxy wrapping via the NullSafe() conversion function
7
+ #
8
+ # When enabled, the generated null class gains a NullSafe() function that
9
+ # wraps any value in a proxy. The proxy intercepts all method calls and
10
+ # wraps return values, replacing nil with the null object.
11
+ #
12
+ # @example Enable null-safe proxy
13
+ # NullObject = Naught.build do |config|
14
+ # config.null_safe_proxy
15
+ # end
16
+ #
17
+ # include NullObject::Conversions
18
+ #
19
+ # user = nil
20
+ # NullSafe(user).name.upcase # => <null>
21
+ #
22
+ # user = OpenStruct.new(name: nil)
23
+ # NullSafe(user).name.upcase # => <null>
24
+ #
25
+ # user = OpenStruct.new(name: "Bob")
26
+ # NullSafe(user).name.upcase # => "BOB"
27
+ #
28
+ # @api private
29
+ class NullSafeProxy < Command
30
+ # Install the NullSafe conversion function
31
+ # @return [void]
32
+ # @api private
33
+ def call
34
+ null_equivs = builder.null_equivalents
35
+ defer_class do |null_class|
36
+ proxy_class = build_proxy_class(null_class, null_equivs)
37
+ null_class.const_set(:NullSafeProxy, proxy_class)
38
+ install_null_safe_conversion(null_class, proxy_class, null_equivs)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # Build the proxy class that wraps objects for null-safe access
45
+ #
46
+ # @param null_class [Class] the null object class
47
+ # @param null_equivs [Array<Object>] values treated as null-equivalent
48
+ # @return [Class] the proxy class
49
+ # @api private
50
+ def build_proxy_class(null_class, null_equivs)
51
+ Class.new(::Naught::BasicObject) do
52
+ include ::Naught::NullSafeProxyTag
53
+
54
+ define_method(:initialize) { |target| @target = target }
55
+ define_method(:__target__) { @target }
56
+ define_method(:respond_to?) { |method_name, include_private = false| @target.respond_to?(method_name, include_private) }
57
+ define_method(:inspect) { "<null-safe-proxy(#{@target.inspect})>" }
58
+
59
+ define_method(:method_missing) do |method_name, *args, &block|
60
+ result = @target.__send__(method_name, *args, &block)
61
+ case result
62
+ when ::Naught::NullObjectTag then result
63
+ when *null_equivs then null_class.get
64
+ else self.class.new(result)
65
+ end
66
+ end
67
+
68
+ klass = self
69
+ define_method(:class) { klass }
70
+ end
71
+ end
72
+
73
+ # Install the NullSafe conversion method on the Conversions module
74
+ #
75
+ # @param null_class [Class] the null object class
76
+ # @param proxy_class [Class] the proxy class
77
+ # @param null_equivs [Array<Object>] values treated as null-equivalent
78
+ # @return [void]
79
+ # @api private
80
+ def install_null_safe_conversion(null_class, proxy_class, null_equivs)
81
+ null_class.const_get(:Conversions).define_method(:NullSafe) do |object|
82
+ case object
83
+ when ::Naught::NullObjectTag then object
84
+ when *null_equivs then null_class.get
85
+ else proxy_class.new(object)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,29 +1,33 @@
1
- require 'naught/null_class_builder/command'
1
+ require "naught/null_class_builder/command"
2
2
 
3
3
  module Naught
4
4
  class NullClassBuilder
5
5
  module Commands
6
- class Pebble < ::Naught::NullClassBuilder::Command
6
+ # Logs missing method calls and their call sites
7
+ #
8
+ # @api private
9
+ class Pebble < Command
10
+ # Create a pebble command with optional output stream
11
+ #
12
+ # @param builder [NullClassBuilder]
13
+ # @param output [#puts] output stream for log lines
14
+ # @api private
7
15
  def initialize(builder, output = $stdout)
8
- @builder = builder
16
+ super(builder)
9
17
  @output = output
10
18
  end
11
19
 
20
+ # Install the logging method_missing hook
21
+ # @return [void]
22
+ # @api private
12
23
  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
24
+ output = @output
25
+ defer_prepend_module do
26
+ define_method(:method_missing) do |method_name, *args|
27
+ pretty_args = args.map(&:inspect).join(", ").tr('"', "'")
28
+ caller_desc = Naught::CallerInfo.format_caller_for_pebble(Kernel.caller(1))
29
+ output.puts "#{method_name}(#{pretty_args}) from #{caller_desc}"
30
+ self
27
31
  end
28
32
  end
29
33
  end
@@ -1,45 +1,63 @@
1
- require 'naught/null_class_builder/command'
1
+ require "naught/null_class_builder/command"
2
2
 
3
3
  module Naught
4
4
  class NullClassBuilder
5
5
  module Commands
6
- class PredicatesReturn < Naught::NullClassBuilder::Command
6
+ # Overrides predicate methods to return a fixed value
7
+ #
8
+ # @api private
9
+ class PredicatesReturn < Command
10
+ # Create a predicates_return command with the given value
11
+ #
12
+ # @param builder [NullClassBuilder]
13
+ # @param return_value [Object] value to return for predicate methods
14
+ # @api private
7
15
  def initialize(builder, return_value)
8
16
  super(builder)
9
- @predicate_return_value = return_value
17
+ @return_value = return_value
10
18
  end
11
19
 
20
+ # Apply predicate overrides
21
+ # @return [void]
22
+ # @api private
12
23
  def call
13
- defer do |subject|
14
- define_method_missing(subject)
15
- define_predicate_methods(subject)
16
- end
24
+ install_method_missing_override
25
+ install_predicate_method_overrides
17
26
  end
18
27
 
19
- private
28
+ private
20
29
 
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)
30
+ # Install method_missing override for predicate methods
31
+ # @return [void]
32
+ # @api private
33
+ def install_method_missing_override
34
+ return_value = @return_value
35
+ defer_prepend_module do
25
36
  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
37
+ method_name.to_s.end_with?("?") ? return_value : super(method_name, *args, &block)
38
+ end
39
+
40
+ define_method(:respond_to?) do |method_name, include_private = false|
41
+ method_name.to_s.end_with?("?") || super(method_name, include_private)
31
42
  end
32
43
  end
33
44
  end
34
45
 
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
46
+ # Override existing predicate methods to return the configured value
47
+ # @return [void]
48
+ # @api private
49
+ def install_predicate_method_overrides
50
+ return_value = @return_value
51
+ defer do |subject|
52
+ predicate_methods = subject.instance_methods.select do |name|
53
+ name.to_s.end_with?("?") && name != :respond_to?
54
+ end
55
+ next if predicate_methods.empty?
56
+
57
+ predicate_mod = Module.new do
58
+ predicate_methods.each { |name| define_method(name) { |*| return_value } }
42
59
  end
60
+ subject.prepend(predicate_mod)
43
61
  end
44
62
  end
45
63
  end
@@ -1,25 +1,22 @@
1
- require 'naught/null_class_builder/command'
1
+ require "singleton"
2
+ require "naught/null_class_builder/command"
2
3
 
3
4
  module Naught
4
5
  class NullClassBuilder
5
6
  module Commands
6
- class Singleton < Naught::NullClassBuilder::Command
7
+ # Turns the null class into a Singleton
8
+ #
9
+ # @api private
10
+ class Singleton < Command
11
+ # Install Singleton behavior on the null class
12
+ # @return [void]
13
+ # @api private
7
14
  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
15
+ defer_class do |klass|
16
+ klass.include(::Singleton)
17
+ klass.singleton_class.undef_method(:get)
18
+ klass.define_singleton_method(:get) { |*| instance }
19
+ %i[dup clone].each { |name| klass.define_method(name) { self } }
23
20
  end
24
21
  end
25
22
  end
@@ -1,20 +1,25 @@
1
- require 'naught/null_class_builder/command'
1
+ require "naught/null_class_builder/command"
2
2
 
3
3
  module Naught
4
4
  class NullClassBuilder
5
5
  module Commands
6
- class Traceable < Naught::NullClassBuilder::Command
6
+ # Records the source location where a null object was created
7
+ #
8
+ # @api private
9
+ class Traceable < Command
10
+ # Install the traceable initializer
11
+ # @return [void]
12
+ # @api private
7
13
  def call
8
- defer do |subject|
9
- subject.module_eval do
10
- attr_reader :__file__, :__line__
14
+ defer_prepend_module do
15
+ attr_reader :__file__, :__line__
11
16
 
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
17
+ define_method(:initialize) do |options = {}|
18
+ backtrace = options.fetch(:caller) { Kernel.caller(3) }
19
+ caller_data = Naught::CallerInfo.parse(backtrace[0])
20
+ @__file__ = caller_data[:path]
21
+ @__line__ = caller_data[:lineno]
22
+ super(options)
18
23
  end
19
24
  end
20
25
  end
@@ -1,8 +1,10 @@
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'
1
+ require "naught/null_class_builder/commands/callstack"
2
+ require "naught/null_class_builder/commands/define_explicit_conversions"
3
+ require "naught/null_class_builder/commands/define_implicit_conversions"
4
+ require "naught/null_class_builder/commands/null_safe_proxy"
5
+ require "naught/null_class_builder/commands/pebble"
6
+ require "naught/null_class_builder/commands/predicates_return"
7
+ require "naught/null_class_builder/commands/singleton"
8
+ require "naught/null_class_builder/commands/traceable"
9
+ require "naught/null_class_builder/commands/mimic"
10
+ require "naught/null_class_builder/commands/impersonate"