naught-2.1.0/0000755000004100000410000000000015142672150013033 5ustar www-datawww-datanaught-2.1.0/naught.gemspec0000644000004100000410000000463315142672150015674 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: naught 2.1.0 ruby lib Gem::Specification.new do |s| s.name = "naught".freeze s.version = "2.1.0".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "changelog_uri" => "https://github.com/avdi/naught/blob/master/Changelog.md", "homepage_uri" => "https://github.com/avdi/naught", "rubygems_mfa_required" => "true", "source_code_uri" => "https://github.com/avdi/naught" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Avdi Grimm".freeze] s.date = "1980-01-02" s.description = "Naught is a toolkit for building Null Objects".freeze s.email = ["avdi@avdi.org".freeze] s.files = ["LICENSE.txt".freeze, "lib/naught.rb".freeze, "lib/naught/basic_object.rb".freeze, "lib/naught/call_location.rb".freeze, "lib/naught/caller_info.rb".freeze, "lib/naught/chain_proxy.rb".freeze, "lib/naught/conversions.rb".freeze, "lib/naught/null_class_builder.rb".freeze, "lib/naught/null_class_builder/command.rb".freeze, "lib/naught/null_class_builder/commands.rb".freeze, "lib/naught/null_class_builder/commands/callstack.rb".freeze, "lib/naught/null_class_builder/commands/define_explicit_conversions.rb".freeze, "lib/naught/null_class_builder/commands/define_implicit_conversions.rb".freeze, "lib/naught/null_class_builder/commands/impersonate.rb".freeze, "lib/naught/null_class_builder/commands/mimic.rb".freeze, "lib/naught/null_class_builder/commands/null_safe_proxy.rb".freeze, "lib/naught/null_class_builder/commands/pebble.rb".freeze, "lib/naught/null_class_builder/commands/predicates_return.rb".freeze, "lib/naught/null_class_builder/commands/singleton.rb".freeze, "lib/naught/null_class_builder/commands/traceable.rb".freeze, "lib/naught/stub_strategy.rb".freeze, "lib/naught/version.rb".freeze] s.homepage = "https://github.com/avdi/naught".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 3.2.0".freeze) s.rubygems_version = "4.0.6".freeze s.summary = "Naught is a toolkit for building Null Objects".freeze s.specification_version = 4 s.add_development_dependency(%q.freeze, [">= 2.0".freeze]) s.add_development_dependency(%q.freeze, [">= 12.0".freeze]) end naught-2.1.0/lib/0000755000004100000410000000000015142672150013601 5ustar www-datawww-datanaught-2.1.0/lib/naught.rb0000644000004100000410000000173515142672150015422 0ustar www-datawww-datarequire "naught/version" require "naught/caller_info" require "naught/null_class_builder" require "naught/null_class_builder/commands" # Top-level namespace for Naught null object helpers # # @example Create a basic null object class # NullObject = Naught.build # null = NullObject.new # null.foo #=> nil # # @example Create a black hole null object # BlackHole = Naught.build(&:black_hole) # BlackHole.new.foo.bar.baz #=> # # @api public module Naught # Build a null object class using the builder DSL # # @example # NullObject = Naught.build { |b| b.black_hole } # # @yieldparam builder [Naught::NullClassBuilder] builder DSL instance # @return [Class] generated null class def self.build(&) builder = NullClassBuilder.new builder.customize(&) builder.generate_class end # Marker module mixed into generated null objects module NullObjectTag; end # Marker module for null-safe proxy wrappers module NullSafeProxyTag; end end naught-2.1.0/lib/naught/0000755000004100000410000000000015142672150015067 5ustar www-datawww-datanaught-2.1.0/lib/naught/call_location.rb0000644000004100000410000001033215142672150020216 0ustar www-datawww-datamodule Naught # Represents a single method call in a null object's call trace # # This class provides an interface similar to Thread::Backtrace::Location, # capturing information about where a method was called on a null object. # # @api public class CallLocation # Create a CallLocation from a caller string # # @param method_name [Symbol, String] the method that was called # @param args [Array] arguments passed to the method # @param caller_string [String, nil] a single entry from Kernel.caller # @return [CallLocation] # @api private def self.from_caller(method_name, args, caller_string) data = CallerInfo.parse(caller_string || "") new( label: method_name, args: args, path: data[:path] || "", lineno: data[:lineno], base_label: data[:base_label] ) end # The name of the method that was called # # @return [String] the name of the method that was called # @example # location.label #=> "foo" attr_reader :label # Arguments passed to the method call # # @return [Array] arguments passed to the method call # @example # location.args #=> [1, 2, 3] attr_reader :args # The absolute path to the file where the call originated # # @return [String] the absolute path to the file where the call originated # @example # location.path #=> "/path/to/file.rb" attr_reader :path # @!method absolute_path # Returns the absolute path (alias for {#path}) # @return [String] the absolute path to the file # @example # location.absolute_path #=> "/path/to/file.rb" alias_method :absolute_path, :path # The line number where the call originated # # @return [Integer] the line number where the call originated # @example # location.lineno #=> 42 attr_reader :lineno # The name of the method that made the call # # @return [String, nil] the name of the method that made the call # @example # location.base_label #=> "some_method" attr_reader :base_label # Initialize a new CallLocation # # @param label [Symbol, String] the method that was called # @param args [Array] arguments passed to the method # @param path [String] path to the file where the call originated # @param lineno [Integer] line number where the call originated # @param base_label [String, nil] name of the method that made the call # @api private def initialize(label:, args:, path:, lineno:, base_label: nil) @label = label.to_s @args = args.dup.freeze @path = path @lineno = lineno @base_label = base_label end # Returns a human-readable string representation of the call # # @return [String] string representation # @example # location.to_s #=> "/path/to/file.rb:42:in `method' -> foo(1, 2)" def to_s pretty_args = args.map(&:inspect).join(", ") location = base_label ? "#{path}:#{lineno}:in `#{base_label}'" : "#{path}:#{lineno}" "#{location} -> #{label}(#{pretty_args})" end # Returns a detailed inspect representation # # @return [String] inspect representation # @example # location.inspect #=> "# foo(1)>" def inspect = "#<#{self.class} #{self}>" # Compare this CallLocation with another for equality # # @param other [CallLocation] the object to compare with # @return [Boolean] true if all attributes match # @example # location1 == location2 #=> true def ==(other) other.is_a?(CallLocation) && label == other.label && args == other.args && path == other.path && lineno == other.lineno && base_label == other.base_label end # @!method eql? # Compare for equality (alias for {#==}) # @return [Boolean] true if all attributes match # @example # location1.eql?(location2) #=> true alias_method :eql?, :== # Compute a hash value for this CallLocation # # @return [Integer] hash value based on all attributes # @example # location.hash #=> 123456789 def hash = [label, args, path, lineno, base_label].hash end end naught-2.1.0/lib/naught/null_class_builder.rb0000644000004100000410000002120115142672150021255 0ustar www-datawww-datarequire "naught/basic_object" require "naught/conversions" require "naught/stub_strategy" module Naught # Builds customized null object classes via a small DSL # # @api public class NullClassBuilder # Namespace for builder command classes # @api private module Commands; end # The base class for generated null objects # # @return [Class] base class for generated null objects # @example # builder.base_class #=> Naught::BasicObject attr_accessor :base_class # The inspect implementation for generated null objects # # @return [Proc] inspect implementation for generated null objects # @example # builder.inspect_proc.call #=> "" attr_accessor :inspect_proc # Whether a method-missing interface has been defined # # @return [Boolean] whether a method-missing interface has been defined # @example # builder.interface_defined #=> false attr_accessor :interface_defined # @!method interface_defined? # Check if a method-missing interface has been defined # @return [Boolean] true if interface is defined # @example # builder.interface_defined? #=> false alias_method :interface_defined?, :interface_defined # Create a new builder with default configuration # @api private def initialize @interface_defined = false @base_class = Naught::BasicObject @inspect_proc = -> { "" } @stub_strategy = StubStrategy::ReturnNil define_basic_methods end # Apply a customization block to this builder # # @yieldparam builder [NullClassBuilder] builder instance # @return [void] # @example # builder.customize { |b| b.black_hole } def customize(&) customization_module.module_exec(self, &) if block_given? end # Returns the module that holds customization methods # # @return [Module] module that holds customization methods # @example # builder.customization_module #=> # def customization_module = @customization_module ||= Module.new # Returns the list of values treated as null-equivalent # # @return [Array] values treated as null-equivalent # @example # builder.null_equivalents #=> [nil] def null_equivalents = @null_equivalents ||= [nil] # Generate the null object class based on queued operations # # @return [Class] generated null class # @example # NullClass = builder.generate_class def generate_class respond_to_any_message unless interface_defined? generation_mod = Module.new apply_operations(operations, generation_mod) null_class = build_null_class(generation_mod) apply_operations(class_operations, null_class) null_class end # Builder API - see also lib/naught/null_class_builder/commands # Configure method stubs to return self (black hole behavior) # # @see https://github.com/avdi/naught/issues/72 # @return [void] # @example # builder.black_hole def black_hole @stub_strategy = StubStrategy::ReturnSelf # Prepend marshal methods to avoid infinite recursion with method_missing defer_prepend_module do define_method(:marshal_dump) { nil } define_method(:marshal_load) { |*| nil } end end # Make null objects respond to any message # # @return [void] # @example # builder.respond_to_any_message def respond_to_any_message defer(prepend: true) do |subject| subject.define_method(:respond_to?) { |*, **| true } stub_method(subject, :method_missing) end @interface_defined = true end # Queue a deferred operation to be applied during class generation # # @param options [Hash] :class for class-level, :prepend to add at front # @yieldparam subject [Module, Class] target of the operation # @return [void] # @example # builder.defer { |subject| subject.define_method(:foo) { "bar" } } def defer(options = {}, &operation) target = options[:class] ? class_operations : operations options[:prepend] ? target.unshift(operation) : target.push(operation) end # Prepend a module generated from the given block # # @return [void] # @example # builder.defer_prepend_module { define_method(:foo) { "bar" } } def defer_prepend_module(&) prepend_modules << Module.new(&) end # Stub a method using the current stub strategy # # @param subject [Module, Class] target to define method on # @param name [Symbol] method name to stub # @return [void] # @example # builder.stub_method(some_module, :foo) def stub_method(subject, name) @stub_strategy.apply(subject, name) end # Dispatch builder DSL calls to command classes # @return [void] # @api private def method_missing(method_name, *args, &) command_class = lookup_command(method_name) command_class ? command_class.new(self, *args, &).call : super end # Check if builder responds to a DSL command # # @param method_name [Symbol] method name to check # @param include_private [Boolean] whether to include private methods # @return [Boolean] true if method_name maps to a known command # @api private def respond_to_missing?(method_name, include_private = false) !lookup_command(method_name).nil? || super rescue NameError super end private # Build the null object class with all configured modules # # @param generation_mod [Module] module containing generated methods # @return [Class] the null object class # @api private def build_null_class(generation_mod) customization_mod = customization_module null_equivs = null_equivalents modules_to_prepend = prepend_modules Class.new(@base_class) do const_set :GeneratedMethods, generation_mod const_set :Customizations, customization_mod conversions_mod = Module.new { include Conversions } Conversions.configure(conversions_mod, null_class: self, null_equivs: null_equivs) const_set :Conversions, conversions_mod include NullObjectTag include generation_mod include customization_mod modules_to_prepend.each { |mod| prepend mod } end end # Define the basic methods required by all null objects # # @return [void] # @api private def define_basic_methods define_basic_instance_methods define_basic_class_methods end # Apply deferred operations to the target module or class # # @param ops [Array] operations to apply # @param target [Module, Class] target for the operations # @return [void] # @api private def apply_operations(ops, target) ops.each { |op| op.call(target) } end # Define the basic instance methods for null objects # # @return [void] # @api private def define_basic_instance_methods builder = self defer do |subject| subject.define_method(:inspect, &builder.inspect_proc) subject.define_method(:initialize) { |*, **, &| } end end # Define the basic class methods for null objects # # @return [void] # @api private def define_basic_class_methods defer(class: true) do |klass| klass.define_singleton_method(:get) do |*args, **kwargs, &block| kw = kwargs #: Hash[Symbol, untyped] new(*args, **kw, &block) end klass.define_method(:class) { klass } end end # Returns the list of class-level operations # # @return [Array] class-level operations # @api private def class_operations = @class_operations ||= [] # Returns the list of instance-level operations # # @return [Array] instance-level operations # @api private def operations = @operations ||= [] # Returns the list of modules to prepend # # @return [Array] modules to prepend to the null class # @api private def prepend_modules = @prepend_modules ||= [] # Look up a command class by method name # # @param method_name [Symbol] method name to look up # @return [Class, nil] command class if found, nil otherwise # @api private def lookup_command(method_name) command_name = camelize(method_name) Commands.const_get(command_name) if Commands.const_defined?(command_name) end # Convert a snake_case method name to CamelCase # # @param name [Symbol, String] the name to convert # @return [String] the CamelCase version # @api private def camelize(name) = name.to_s.gsub(/(?:^|_)([a-z])/) { ::Regexp.last_match(1).upcase } end end naught-2.1.0/lib/naught/chain_proxy.rb0000644000004100000410000000323515142672150017742 0ustar www-datawww-datarequire "naught/basic_object" module Naught # Lightweight proxy for tracking chained method calls # # Used by the callstack feature to group chained method calls # (e.g., `null.foo.bar.baz`) into a single trace while keeping # separate calls (e.g., `null.foo; null.bar`) in separate traces. # # @api private class ChainProxy < BasicObject # Create a new ChainProxy # # @param root [Object] the original null object being tracked # @param current_trace [Array] the trace to append calls to def initialize(root, current_trace) @root = root @current_trace = current_trace end # Handle method calls by recording them and returning self for chaining # # @param method_name [Symbol] the method being called # @param args [Array] arguments passed to the method # @return [ChainProxy] self for method chaining # rubocop:disable Style/MissingRespondToMissing -- BasicObject doesn't use respond_to_missing? def method_missing(method_name, *args) location = ::Naught::CallLocation.from_caller( method_name, args, ::Kernel.caller(1, 1).first ) @current_trace << location self end # rubocop:enable Style/MissingRespondToMissing # Check if the proxy responds to a method # # @return [true] chain proxies respond to any method def respond_to?(*, **) = true # Return a string representation of the proxy # # @return [String] a simple representation of the proxy def inspect = "" # Return the class of the root null object # # @return [Class] the class of the root null object def class = @root.class end end naught-2.1.0/lib/naught/null_class_builder/0000755000004100000410000000000015142672150020734 5ustar www-datawww-datanaught-2.1.0/lib/naught/null_class_builder/command.rb0000644000004100000410000000270615142672150022704 0ustar www-datawww-datamodule Naught class NullClassBuilder # Base class for builder command implementations # # @api private class Command # Builder instance for this command # @return [NullClassBuilder] # @api private attr_reader :builder # Create a command bound to a builder # # @param builder [NullClassBuilder] # @return [void] # @api private def initialize(builder) @builder = builder end # Execute the command # # @raise [NotImplementedError] when not overridden # @return [void] # @api private def call raise NotImplementedError, "Method #call should be overridden in child classes" end private # Delegate a deferred operation to the builder # # @param options [Hash] operation options # @yieldparam subject [Module, Class] # @yieldreturn [void] # @return [void] # @api private def defer(options = {}, &) = builder.defer(options, &) # Delegate a deferred class operation to the builder # # @yieldparam subject [Class] # @yieldreturn [void] # @return [void] # @api private def defer_class(&) = builder.defer(class: true, &) # Delegate a prepend module operation to the builder # # @yieldreturn [void] # @return [void] # @api private def defer_prepend_module(&) = builder.defer_prepend_module(&) end end end naught-2.1.0/lib/naught/null_class_builder/commands/0000755000004100000410000000000015142672150022535 5ustar www-datawww-datanaught-2.1.0/lib/naught/null_class_builder/commands/null_safe_proxy.rb0000644000004100000410000000644415142672150026303 0ustar www-datawww-datarequire "naught/null_class_builder/command" module Naught class NullClassBuilder module Commands # Enables null-safe proxy wrapping via the NullSafe() conversion function # # When enabled, the generated null class gains a NullSafe() function that # wraps any value in a proxy. The proxy intercepts all method calls and # wraps return values, replacing nil with the null object. # # @example Enable null-safe proxy # NullObject = Naught.build do |config| # config.null_safe_proxy # end # # include NullObject::Conversions # # user = nil # NullSafe(user).name.upcase # => # # user = OpenStruct.new(name: nil) # NullSafe(user).name.upcase # => # # user = OpenStruct.new(name: "Bob") # NullSafe(user).name.upcase # => "BOB" # # @api private class NullSafeProxy < Command # Install the NullSafe conversion function # @return [void] # @api private def call null_equivs = builder.null_equivalents defer_class do |null_class| proxy_class = build_proxy_class(null_class, null_equivs) null_class.const_set(:NullSafeProxy, proxy_class) install_null_safe_conversion(null_class, proxy_class, null_equivs) end end private # Build the proxy class that wraps objects for null-safe access # # @param null_class [Class] the null object class # @param null_equivs [Array] values treated as null-equivalent # @return [Class] the proxy class # @api private def build_proxy_class(null_class, null_equivs) Class.new(::Naught::BasicObject) do include ::Naught::NullSafeProxyTag define_method(:initialize) { |target| @target = target } define_method(:__target__) { @target } define_method(:respond_to?) { |method_name, include_private = false| @target.respond_to?(method_name, include_private) } define_method(:inspect) { "" } define_method(:method_missing) do |method_name, *args, &block| result = @target.__send__(method_name, *args, &block) case result when ::Naught::NullObjectTag then result when *null_equivs then null_class.get else self.class.new(result) end end klass = self define_method(:class) { klass } end end # Install the NullSafe conversion method on the Conversions module # # @param null_class [Class] the null object class # @param proxy_class [Class] the proxy class # @param null_equivs [Array] values treated as null-equivalent # @return [void] # @api private def install_null_safe_conversion(null_class, proxy_class, null_equivs) null_class.const_get(:Conversions).define_method(:NullSafe) do |object| case object when ::Naught::NullObjectTag then object when *null_equivs then null_class.get else proxy_class.new(object) end end end end end end end naught-2.1.0/lib/naught/null_class_builder/commands/singleton.rb0000644000004100000410000000124015142672150025061 0ustar www-datawww-datarequire "singleton" require "naught/null_class_builder/command" module Naught class NullClassBuilder module Commands # Turns the null class into a Singleton # # @api private class Singleton < Command # Install Singleton behavior on the null class # @return [void] # @api private def call defer_class do |klass| klass.include(::Singleton) klass.singleton_class.undef_method(:get) klass.define_singleton_method(:get) { |*| instance } %i[dup clone].each { |name| klass.define_method(name) { self } } end end end end end end naught-2.1.0/lib/naught/null_class_builder/commands/define_explicit_conversions.rb0000644000004100000410000000146715142672150030655 0ustar www-datawww-datarequire "naught/null_class_builder/command" module Naught class NullClassBuilder module Commands # Adds explicit conversion methods delegating to nil # # These methods return the same values that nil returns: # - to_a => [] # - to_c => (0+0i) # - to_f => 0.0 # - to_h => {} # - to_i => 0 # - to_r => (0/1) # - to_s => "" # # @api private class DefineExplicitConversions < Command METHODS = %i[to_a to_c to_f to_h to_i to_r to_s].freeze private_constant :METHODS # Install explicit conversion methods # @return [void] # @api private def call defer { |subject| METHODS.each { |name| subject.define_method(name) { nil.public_send(name) } } } end end end end end naught-2.1.0/lib/naught/null_class_builder/commands/pebble.rb0000644000004100000410000000212015142672150024306 0ustar www-datawww-datarequire "naught/null_class_builder/command" module Naught class NullClassBuilder module Commands # Logs missing method calls and their call sites # # @api private class Pebble < Command # Create a pebble command with optional output stream # # @param builder [NullClassBuilder] # @param output [#puts] output stream for log lines # @api private def initialize(builder, output = $stdout) super(builder) @output = output end # Install the logging method_missing hook # @return [void] # @api private def call output = @output defer_prepend_module do define_method(:method_missing) do |method_name, *args| pretty_args = args.map(&:inspect).join(", ").tr('"', "'") caller_desc = Naught::CallerInfo.format_caller_for_pebble(Kernel.caller(1)) output.puts "#{method_name}(#{pretty_args}) from #{caller_desc}" self end end end end end end end naught-2.1.0/lib/naught/null_class_builder/commands/mimic.rb0000644000004100000410000001352615142672150024167 0ustar www-datawww-datarequire "naught/basic_object" require "naught/null_class_builder/command" module Naught class NullClassBuilder module Commands # Build a null class that mimics an existing class or instance # # @api private class Mimic < Command # Methods that should never be mimicked as they interfere with # other Naught features like predicates_return # @see https://github.com/avdi/naught/issues/55 METHODS_TO_SKIP = (%i[method_missing respond_to? respond_to_missing?] + Object.instance_methods).freeze private_constant :METHODS_TO_SKIP # Singleton class placeholder used when no instance is provided NULL_SINGLETON_CLASS = Object.new.singleton_class.freeze private_constant :NULL_SINGLETON_CLASS # The class being mimicked by the null object # @return [Class] class being mimicked attr_reader :class_to_mimic # Whether to include superclass methods when mimicking # @return [Boolean] whether to include superclass methods attr_reader :include_super # The singleton class being mimicked (for instance-based mimicking) # @return [Class] singleton class being mimicked attr_reader :singleton_class # The example instance for dynamic method discovery # @return [Object, nil] example instance or nil attr_reader :example_instance # Whether to include dynamically-defined methods # @return [Boolean] whether to include dynamic methods attr_reader :include_dynamic # Create a mimic command for a class or instance # # @param builder [NullClassBuilder] # @param class_to_mimic_or_options [Class, Hash] # @param options [Hash] # @api private def initialize(builder, class_to_mimic_or_options, options = {}) super(builder) parse_arguments(class_to_mimic_or_options, options) configure_builder end # Install stubbed methods from the target class or instance # # @return [void] # @api private def call defer { |subject| methods_to_stub.each { |name| builder.stub_method(subject, name) } } end private # Parse the arguments to determine what to mimic # # @param class_to_mimic_or_options [Class, Hash] class or options hash # @param options [Hash] additional options # @return [void] def parse_arguments(class_to_mimic_or_options, options) if class_to_mimic_or_options.is_a?(Hash) options = class_to_mimic_or_options.merge(options) @example_instance = options.fetch(:example) @singleton_class = @example_instance.singleton_class @class_to_mimic = @example_instance.class else @example_instance = nil @singleton_class = NULL_SINGLETON_CLASS @class_to_mimic = class_to_mimic_or_options end @include_super = options.fetch(:include_super, true) @include_dynamic = options.fetch(:include_dynamic, !@example_instance.nil?) end # Configure the builder with the mimicked class's properties # # @return [void] def configure_builder builder.base_class = root_class_of(class_to_mimic) klass = class_to_mimic builder.inspect_proc = -> { "" } builder.interface_defined = true end # Determine the root class to inherit from # # @param klass [Class] the class to analyze # @return [Class] Object or Naught::BasicObject def root_class_of(klass) = klass.ancestors.include?(Object) ? Object : Naught::BasicObject # Compute the list of methods to stub on the null object # # @return [Array] methods to stub def methods_to_stub all_methods = class_to_mimic.instance_methods(include_super) | singleton_class.instance_methods(false) all_methods |= dynamic_methods if include_dynamic all_methods - METHODS_TO_SKIP end # Discover dynamically-defined methods from the example instance # # This handles classes like Stripe that use method_missing and # respond_to_missing? to define methods based on instance data. # # @return [Array] dynamic method names def dynamic_methods return [] unless example_instance candidates = discover_method_candidates candidates.select { |name| example_instance.respond_to?(name) } end # Discover candidate method names from the example instance # # Tries multiple approaches to find method names: # 1. If the instance responds to :keys (like Stripe objects), use those # 2. If the instance responds to :attributes, use those # 3. If the instance responds to :to_h or :to_hash, use the hash keys # # @return [Array] candidate method names def discover_method_candidates candidates = [] #: Array[Symbol] # Stripe-style objects expose keys candidates |= example_instance.keys.map(&:to_sym) if example_instance.respond_to?(:keys) # ActiveRecord-style objects expose attribute_names if example_instance.respond_to?(:attribute_names) candidates |= example_instance.attribute_names.map(&:to_sym) end # OpenStruct-style objects can be converted to hash if example_instance.respond_to?(:to_h) && !example_instance.is_a?(Object.const_get(:Hash)) begin hash = example_instance.to_h candidates |= hash.keys.map(&:to_sym) if hash.is_a?(Hash) rescue # Ignore errors from to_h end end candidates end end end end end naught-2.1.0/lib/naught/null_class_builder/commands/callstack.rb0000644000004100000410000000602615142672150025027 0ustar www-datawww-datarequire "naught/null_class_builder/command" require "naught/call_location" require "naught/chain_proxy" module Naught class NullClassBuilder module Commands # Records method calls made on null objects for debugging # # When enabled, each null object instance tracks all method calls made to it, # including the method name, arguments, and source location. Calls are grouped # into "traces" - each time a method is called directly on the original null # object (rather than on a chained result), a new trace begins. # # This uses lightweight proxy objects for chaining so that we can distinguish # between `null.foo.bar` (one trace with two calls) and `null.foo; null.bar` # (two traces with one call each). # # @example Basic usage # NullObject = Naught.build do |config| # config.callstack # end # # null = NullObject.new # null.foo(1, 2).bar # null.baz # # null.__call_trace__ # # => [ # # [#, # # #], # # [#] # # ] # # @api private class Callstack < Command # Install the callstack tracking mechanism # @return [void] # @api private def call install_call_trace_accessor install_method_missing_tracking install_chain_proxy_class end private # Install the __call_trace__ accessor on null objects # @return [void] # @api private def install_call_trace_accessor defer_prepend_module do attr_reader :__call_trace__ define_method(:initialize) do |*args, **kwargs| super(*args, **kwargs) @__call_trace__ = [] #: Array[Array[Naught::CallLocation]] end end end # Install method_missing override that records calls # @return [void] # @api private def install_method_missing_tracking defer_prepend_module do define_method(:respond_to?) do |method_name, include_private = false| method_name == :__call_trace__ || super(method_name, include_private) end define_method(:method_missing) do |method_name, *args, &block| location = Naught::CallLocation.from_caller(method_name, args, Kernel.caller(1, 1).first) @__call_trace__ ||= [] #: Array[Array[Naught::CallLocation]] @__call_trace__ << [location] Naught::ChainProxy.new(self, @__call_trace__.last) end end end # Install the ChainProxy class constant for backwards compatibility # @return [void] # @api private def install_chain_proxy_class defer_class { |null_class| null_class.const_set(:ChainProxy, Naught::ChainProxy) } end end end end end naught-2.1.0/lib/naught/null_class_builder/commands/impersonate.rb0000644000004100000410000000125715142672150025415 0ustar www-datawww-datamodule Naught class NullClassBuilder module Commands # Build a null class that impersonates a given class # # Unlike Mimic, Impersonate makes the null class inherit from the target, # so `is_a?` checks will pass. # # @api private class Impersonate < Mimic # Create an impersonate command for a class # # @param builder [NullClassBuilder] # @param class_to_impersonate [Class] # @param options [Hash] # @api private def initialize(builder, class_to_impersonate, options = {}) super builder.base_class = class_to_impersonate end end end end end naught-2.1.0/lib/naught/null_class_builder/commands/traceable.rb0000644000004100000410000000144715142672150025012 0ustar www-datawww-datarequire "naught/null_class_builder/command" module Naught class NullClassBuilder module Commands # Records the source location where a null object was created # # @api private class Traceable < Command # Install the traceable initializer # @return [void] # @api private def call defer_prepend_module do attr_reader :__file__, :__line__ define_method(:initialize) do |options = {}| backtrace = options.fetch(:caller) { Kernel.caller(3) } caller_data = Naught::CallerInfo.parse(backtrace[0]) @__file__ = caller_data[:path] @__line__ = caller_data[:lineno] super(options) end end end end end end end naught-2.1.0/lib/naught/null_class_builder/commands/predicates_return.rb0000644000004100000410000000407015142672150026605 0ustar www-datawww-datarequire "naught/null_class_builder/command" module Naught class NullClassBuilder module Commands # Overrides predicate methods to return a fixed value # # @api private class PredicatesReturn < Command # Create a predicates_return command with the given value # # @param builder [NullClassBuilder] # @param return_value [Object] value to return for predicate methods # @api private def initialize(builder, return_value) super(builder) @return_value = return_value end # Apply predicate overrides # @return [void] # @api private def call install_method_missing_override install_predicate_method_overrides end private # Install method_missing override for predicate methods # @return [void] # @api private def install_method_missing_override return_value = @return_value defer_prepend_module do define_method(:method_missing) do |method_name, *args, &block| method_name.to_s.end_with?("?") ? return_value : super(method_name, *args, &block) end define_method(:respond_to?) do |method_name, include_private = false| method_name.to_s.end_with?("?") || super(method_name, include_private) end end end # Override existing predicate methods to return the configured value # @return [void] # @api private def install_predicate_method_overrides return_value = @return_value defer do |subject| predicate_methods = subject.instance_methods.select do |name| name.to_s.end_with?("?") && name != :respond_to? end next if predicate_methods.empty? predicate_mod = Module.new do predicate_methods.each { |name| define_method(name) { |*| return_value } } end subject.prepend(predicate_mod) end end end end end end naught-2.1.0/lib/naught/null_class_builder/commands/define_implicit_conversions.rb0000644000004100000410000000147115142672150030641 0ustar www-datawww-datarequire "naught/null_class_builder/command" module Naught class NullClassBuilder module Commands # Adds implicit conversion methods to the null class # # @api private class DefineImplicitConversions < Command EMPTY_ARRAY = [] #: Array[untyped] EMPTY_HASH = {} #: Hash[untyped, untyped] RETURN_VALUES = { to_ary: EMPTY_ARRAY.freeze, to_hash: EMPTY_HASH.freeze, to_int: 0, to_str: "".freeze }.freeze private_constant :EMPTY_ARRAY, :EMPTY_HASH, :RETURN_VALUES # Install implicit conversion methods # @return [void] # @api private def call defer { |subject| RETURN_VALUES.each { |name, value| subject.define_method(name) { value } } } end end end end end naught-2.1.0/lib/naught/null_class_builder/commands.rb0000644000004100000410000000112315142672150023057 0ustar www-datawww-datarequire "naught/null_class_builder/commands/callstack" require "naught/null_class_builder/commands/define_explicit_conversions" require "naught/null_class_builder/commands/define_implicit_conversions" require "naught/null_class_builder/commands/null_safe_proxy" require "naught/null_class_builder/commands/pebble" require "naught/null_class_builder/commands/predicates_return" require "naught/null_class_builder/commands/singleton" require "naught/null_class_builder/commands/traceable" require "naught/null_class_builder/commands/mimic" require "naught/null_class_builder/commands/impersonate" naught-2.1.0/lib/naught/version.rb0000644000004100000410000000012715142672150017101 0ustar www-datawww-data# Top-level namespace for Naught module Naught # Gem version VERSION = "2.1.0" end naught-2.1.0/lib/naught/basic_object.rb0000644000004100000410000000022215142672150020017 0ustar www-datawww-datamodule Naught # BasicObject subclass used as a minimal base for null objects # # @api private class BasicObject < ::BasicObject end end naught-2.1.0/lib/naught/stub_strategy.rb0000644000004100000410000000153715142672150020321 0ustar www-datawww-datamodule Naught # Strategies for stubbing methods on null objects # # @api private module StubStrategy # Stub that returns nil from any method module ReturnNil # Define a method that returns nil # # @param subject [Module, Class] target to define method on # @param name [Symbol] method name to define # @return [void] def self.apply(subject, name) subject.define_method(name) { |*, **, &| nil } end end # Stub that returns self from any method (black hole) module ReturnSelf # Define a method that returns self # # @param subject [Module, Class] target to define method on # @param name [Symbol] method name to define # @return [void] def self.apply(subject, name) subject.define_method(name) { |*, **, &| self } end end end end naught-2.1.0/lib/naught/conversions.rb0000644000004100000410000001016515142672150017767 0ustar www-datawww-datamodule Naught # Helper conversion API available on generated null classes # # This module is designed to be configured per null class via # {Conversions.configure}. Each generated null class gets its # own configured version of these conversion functions. # # @api public module Conversions # Sentinel value for no argument passed NOTHING_PASSED = Object.new.freeze private_constant :NOTHING_PASSED class << self # Configure a Conversions module for a specific null class # # @param mod [Module] module to configure # @param null_class [Class] the generated null class # @param null_equivs [Array] values treated as null-equivalent # @return [void] # @api private def configure(mod, null_class:, null_equivs:) mod.define_method(:__null_class__) { null_class } mod.define_method(:__null_equivs__) { null_equivs } mod.send(:private, :__null_class__, :__null_equivs__) end end # Return a null object for +object+ if it is null-equivalent # # @example # include MyNullObject::Conversions # Null() #=> # Null(nil) #=> # # @param object [Object] candidate object # @return [Object] a null object # @raise [ArgumentError] if +object+ is not null-equivalent def Null(object = NOTHING_PASSED) return object if null_object?(object) return make_null(1) if null_equivalent?(object, include_nothing: true) raise ArgumentError, "Null() requires a null-equivalent value, " \ "got #{object.class}: #{object.inspect}" end # Return a null object for null-equivalent values, otherwise the value # # @example # Maybe(nil) #=> # Maybe("hello") #=> "hello" # # @param object [Object] candidate object # @yieldreturn [Object] optional lazy value # @return [Object] null object or original value def Maybe(object = nil) object = yield if block_given? return object if null_object?(object) return make_null(1) if null_equivalent?(object) object end # Return the value if not null-equivalent, otherwise raise # # @example # Just("hello") #=> "hello" # Just(nil) # raises ArgumentError # # @param object [Object] candidate object # @yieldreturn [Object] optional lazy value # @return [Object] original value # @raise [ArgumentError] if value is null-equivalent def Just(object = nil) object = yield if block_given? if null_object?(object) || null_equivalent?(object) raise ArgumentError, "Just() requires a non-null value, got: #{object.inspect}" end object end # Return +nil+ for null objects, otherwise return the value # # @example # Actual(null) #=> nil # Actual("hello") #=> "hello" # # @param object [Object] candidate object # @yieldreturn [Object] optional lazy value # @return [Object, nil] actual value or nil def Actual(object = nil) object = yield if block_given? null_object?(object) ? nil : object end private # Check if an object is a null object # # @param object [Object] the object to check # @return [Boolean] true if the object is a null object # @api private def null_object?(object) NullObjectTag === object end # Check if an object is null-equivalent (nil or custom null equivalents) # # @param object [Object] the object to check # @param include_nothing [Boolean] whether to treat NOTHING_PASSED as null-equivalent # @return [Boolean] true if the object is null-equivalent # @api private def null_equivalent?(object, include_nothing: false) return true if include_nothing && object == NOTHING_PASSED __null_equivs__.any? { |equiv| equiv === object } end # Create a new null object instance # # @param caller_offset [Integer] additional stack frames to skip # @return [Object] a new null object # @api private def make_null(caller_offset) __null_class__.get(caller: caller(caller_offset + 1)) end end end naught-2.1.0/lib/naught/caller_info.rb0000644000004100000410000001103215142672150017666 0ustar www-datawww-datamodule Naught # Utility for parsing Ruby caller/backtrace information # # Extracts structured information from caller strings like: # "/path/to/file.rb:42:in `method_name'" # "/path/to/file.rb:42:in `block in method_name'" # "/path/to/file.rb:42:in `block (2 levels) in method_name'" # # @api private module CallerInfo # Pattern matching quoted method signature in caller strings # Matches both backticks and single quotes for cross-Ruby compatibility SIGNATURE_PATTERN = /['`](?[^'`]+)['`]$/ private_constant :SIGNATURE_PATTERN module_function # Parse a caller string into structured components # # @param caller_string [String] a single entry from Kernel.caller # @return [Hash] parsed components with keys :path, :lineno, :base_label def parse(caller_string) path, lineno, method_part = caller_string.to_s.split(":", 3) { path: path, lineno: lineno.to_i, base_label: extract_base_label(method_part) } end # Format caller information for display in pebble output # # Handles nested block detection by examining the call stack. # # @param stack [Array] the call stack from Kernel.caller # @return [String] formatted caller description def format_caller_for_pebble(stack) caller_line = stack.first signature = extract_signature(caller_line.split(":", 3)[2]) return caller_line unless signature block_info, method_name = parse_signature(signature) block_info = adjusted_block_info(block_info, stack, method_name) block_info ? "#{block_info} #{method_name}" : method_name end # Extract the base method name from the method part of a caller string # # @param method_part [String, nil] the third component after splitting on ":" # @return [String, nil] the extracted method name def extract_base_label(method_part) signature = extract_signature(method_part) return nil unless signature _block_info, method_name = parse_signature(signature) method_name end # Extract the full method signature including block info # # @param method_part [String, nil] the third component after splitting on ":" # @return [String, nil] the full signature def extract_signature(method_part) method_part&.match(SIGNATURE_PATTERN)&.[](:signature) end # Split a signature into block info and base method name # # @param signature [String] the method signature # @return [Array(String, String), Array(nil, String)] [block_info, method_name] def split_signature(signature) signature.include?(" in ") ? signature.split(" in ", 2) : [nil, signature] end # Count nested block levels in the call stack # # @param stack [Array] the call stack # @param target_method [String] the method name to look for # @return [Integer] the number of nested block levels def count_block_levels(stack, target_method) stack.reduce(0) do |levels, entry| signature = extract_signature(entry.split(":", 3)[2]) break levels unless signature block_info, method_name = parse_signature(signature) if method_name == target_method block_info&.start_with?("block") ? levels + 1 : (break levels) else levels end end end # Parse a signature into block info and clean method name # # @param signature [String] the method signature # @return [Array(String, String), Array(nil, String)] [block_info, method_name] def parse_signature(signature) block_info, method_part = split_signature(signature) method_name = method_part.split(/[#.]/).last [block_info, method_name] end # Adjust block info to show nested levels if applicable # # @param block_info [String, nil] current block info # @param stack [Array] the call stack # @param method_name [String] the method name to look for # @return [String, nil] adjusted block info def adjusted_block_info(block_info, stack, method_name) return block_info unless simple_block?(block_info) levels = count_block_levels(stack, method_name) (levels > 1) ? "block (#{levels} levels)" : block_info end # Check if block_info is a simple "block" without level info # # @param block_info [String, nil] # @return [Boolean] def simple_block?(block_info) block_info&.start_with?("block") && !block_info.include?("levels") end private :parse_signature, :adjusted_block_info, :simple_block? end end naught-2.1.0/LICENSE.txt0000644000004100000410000000207515142672150014662 0ustar www-datawww-dataCopyright (c) 2013-2026 Avdi Grimm, Erik Berlin MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.