pax_global_header00006660000000000000000000000064151517204340014514gustar00rootroot0000000000000052 comment=542d258ff485ce252b4aa8b3d05bc0b61c7584f9 avdi-naught-dea6146/000077500000000000000000000000001515172043400143375ustar00rootroot00000000000000avdi-naught-dea6146/.github/000077500000000000000000000000001515172043400156775ustar00rootroot00000000000000avdi-naught-dea6146/.github/dependabot.yml000066400000000000000000000001661515172043400205320ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" avdi-naught-dea6146/.github/workflows/000077500000000000000000000000001515172043400177345ustar00rootroot00000000000000avdi-naught-dea6146/.github/workflows/docs.yml000066400000000000000000000007301515172043400214070ustar00rootroot00000000000000name: Docs on: push: branches: [master] pull_request: branches: [master] jobs: yard: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' bundler-cache: true - name: Generate YARD documentation run: bundle exec rake yard - name: Verify YARD documentation coverage run: bundle exec rake yardstick avdi-naught-dea6146/.github/workflows/lint.yml000066400000000000000000000006641515172043400214330ustar00rootroot00000000000000name: Lint on: push: branches: [master] pull_request: branches: [master] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' bundler-cache: true - name: Run RuboCop run: bundle exec rake rubocop - name: Run Standard run: bundle exec rake standard avdi-naught-dea6146/.github/workflows/push.yml000066400000000000000000000017021515172043400214360ustar00rootroot00000000000000name: Push gem to RubyGems on: push: tags: - "v*" permissions: contents: read jobs: push: if: github.repository == 'avdi/naught' runs-on: ubuntu-latest environment: name: rubygems.org url: https://rubygems.org/gems/naught permissions: contents: write id-token: write steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ruby bundler-cache: true - uses: rubygems/configure-rubygems-credentials@v1.0.0 - name: Update RubyGems run: gem update --system - name: Build gem run: bundle exec rake build - name: Sign gem with Sigstore run: gem exec sigstore-cli sign pkg/*.gem --bundle pkg/naught.gem.sigstore.json - name: Push gem run: gem push pkg/*.gem --attestation pkg/naught.gem.sigstore.json - name: Wait for release run: gem exec rubygems-await pkg/*.gem avdi-naught-dea6146/.github/workflows/test.yml000066400000000000000000000012201515172043400214310ustar00rootroot00000000000000name: Test on: push: branches: [master] pull_request: branches: [master] jobs: test: runs-on: ubuntu-latest env: BUNDLE_WITHOUT: docs:typecheck strategy: fail-fast: false matrix: ruby-version: - '3.2' - '3.3' - '3.4' - '4.0' - 'jruby-10.0' steps: - uses: actions/checkout@v6 - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true cache-version: 1 - name: Run tests run: bundle exec rake test avdi-naught-dea6146/.github/workflows/typecheck.yml000066400000000000000000000006031515172043400224350ustar00rootroot00000000000000name: Typecheck on: push: branches: [master] pull_request: branches: [master] jobs: typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' bundler-cache: true - name: Run Steep type check run: bundle exec rake steep avdi-naught-dea6146/.gitignore000066400000000000000000000003171515172043400163300ustar00rootroot00000000000000*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp /naught.org /naught.html /bin /TAGS /gems.tags /tags avdi-naught-dea6146/.rspec000066400000000000000000000000271515172043400154530ustar00rootroot00000000000000--color --order random avdi-naught-dea6146/.rubocop.yml000066400000000000000000000007611515172043400166150ustar00rootroot00000000000000inherit_gem: standard: config/base.yml standard-performance: config/base.yml plugins: - rubocop-minitest - rubocop-performance AllCops: TargetRubyVersion: 3.2 NewCops: enable SuggestExtensions: false # Standard Ruby compatibility Style/StringLiterals: EnforcedStyle: double_quotes Style/StringLiteralsInInterpolation: EnforcedStyle: double_quotes # Naught generates classes dynamically, so class identity checks need assert_equal Minitest/AssertInstanceOf: Enabled: false avdi-naught-dea6146/.yardstick.yml000066400000000000000000000000231515172043400171300ustar00rootroot00000000000000--- threshold: 100 avdi-naught-dea6146/CHANGELOG.md000066400000000000000000000056051515172043400161560ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [2.3.0] - 2026-03-04 ### Added - RBS signatures for all NullClassBuilder DSL methods dispatched via `method_missing` (`define_explicit_conversions`, `define_implicit_conversions`, `predicates_return`, `mimic`, `impersonate`, `pebble`, `singleton`, `traceable`, `callstack`, `null_safe_proxy`). ## [2.2.0] - 2026-03-04 ### Fixed - Include RBS signature file in gem package. ## [2.1.0] - 2025-02-06 ### Added - Dynamic method discovery for `mimic example:` ([#78](https://github.com/avdi/naught/issues/78)). When using `mimic example:`, Naught now automatically discovers and stubs dynamically-defined methods (via `method_missing`/`respond_to_missing?`). This fixes compatibility with libraries like Stripe that define methods based on API response data. Can be disabled with `include_dynamic: false`. ## [2.0.0] - 2025-02-01 ### Added - Callstack tracking for null objects ([5660cd3](https://github.com/avdi/naught/commit/5660cd3b9ae3ebfc1af40cb9c8f797635727e585)). New `config.callstack` option records all method calls made on a null object, including arguments and source location. Use `__call_trace__` to inspect the recorded calls. - Null-safe proxy for chained method calls ([51b5ae4](https://github.com/avdi/naught/commit/51b5ae4040b0128e62159166bc40d476d876a6c2)). New `config.null_safe_proxy` enables the `NullSafe()` conversion function that wraps values in a proxy, replacing nil returns with null objects for safe method chaining. ### Fixed - Marshal.dump compatibility with black_hole null objects ([bd9b135](https://github.com/avdi/naught/commit/bd9b135c59a428aa63338851d5f8e378ebc92e1f)). - Composing mimic with predicates_return for classes with method_missing ([37991c2](https://github.com/avdi/naught/commit/37991c216a605600452d52c76380cfc11d18ca4b)). ## [1.1.0] ### Added - Support for supplying an example object to mimic ([df2b62c](https://github.com/avdi/naught/commit/df2b62c027812760ce200177ce056929b5aea339)). - Implicit conversion for to_hash ([e20dc47](https://github.com/avdi/naught/commit/e20dc472d3bc71ba927d6ddb0fb0032e1646df77)). - Implicit conversion for to_int ([d32d4ea](https://github.com/avdi/naught/commit/d32d4ea32a9a847bffd6cf18f480bdfaaf7a3641)). ## [1.0.0] ### Changed - Replace `::BasicObject` with `Naught::BasicObject` ([8defad0](https://github.com/avdi/naught/commit/8defad0bf9eb65e33054bf0a6e9c625c87c3e6df)). - Delegate explicit conversions to nil instead of defining them explicitly ([85c195d](https://github.com/avdi/naught/commit/85c195de80ed56993b88f47e09112c903a92a167)). - Add support for (and run tests on) Ruby 1.8, 1.9, 2.0, 2.1, JRuby, and Rubinius. ## [0.0.3] ### Added - New "pebble" mode (Guilherme Carvalho). avdi-naught-dea6146/Gemfile000066400000000000000000000006421515172043400156340ustar00rootroot00000000000000source "https://rubygems.org" gemspec group :test do gem "logger" gem "minitest", ">= 6" gem "minitest-mock" gem "rake" gem "rdoc" gem "rubocop-minitest" gem "rubocop-performance" gem "simplecov" gem "standard" gem "standard-performance" end group :docs do gem "irb" gem "redcarpet", platforms: :mri gem "yard" gem "yardstick" end group :typecheck do gem "steep", platforms: :mri end avdi-naught-dea6146/LICENSE.txt000066400000000000000000000020751515172043400161660ustar00rootroot00000000000000Copyright (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. avdi-naught-dea6146/README.md000066400000000000000000000343021515172043400156200ustar00rootroot00000000000000[![Gem Version](https://badge.fury.io/rb/naught.svg)][gem] [![Test](https://github.com/avdi/naught/actions/workflows/test.yml/badge.svg)][test] [![Lint](https://github.com/avdi/naught/actions/workflows/lint.yml/badge.svg)][lint] [![Docs](https://github.com/avdi/naught/actions/workflows/docs.yml/badge.svg)][docs] [![Typecheck](https://github.com/avdi/naught/actions/workflows/typecheck.yml/badge.svg)][typecheck] [gem]: https://rubygems.org/gems/naught [test]: https://github.com/avdi/naught/actions/workflows/test.yml [lint]: https://github.com/avdi/naught/actions/workflows/lint.yml [docs]: https://github.com/avdi/naught/actions/workflows/docs.yml [typecheck]: https://github.com/avdi/naught/actions/workflows/typecheck.yml A quick intro to Naught ----------------------- #### What's all this now then? Naught is a toolkit for building [Null Objects](http://en.wikipedia.org/wiki/Null_Object_pattern) in Ruby. #### What's that supposed to mean? Null Objects can make your code more [confident](http://confidentruby.com). Here's a method that's not very sure of itself. ```ruby class Geordi def make_it_so(logger = nil) logger && logger.info("Reversing the flux phase capacitance!") logger && logger.info("Bounding a tachyon particle beam off of Data's cat!") logger && logger.warn("Warning, bogon levels are rising!") end end ``` Now, observe as we give it a dash of confidence with the Null Object pattern! ```ruby class NullLogger def debug(*); end def info(*); end def warn(*); end def error(*); end def fatal(*); end end class Geordi def make_it_so(logger = NullLogger.new) logger.info "Reversing the flux phase capacitance!" logger.info "Bounding a tachyon particle beam off of Data's cat!" logger.warn "Warning, bogon levels are rising!" end end ``` By providing a `NullLogger` which implements [some of] the `Logger` interface as no-op methods, we've gotten rid of those unsightly `&&` operators. #### That was simple enough. Why do I need a library for it? You don't! The Null Object pattern is a very simple one at its core. #### And yet here we are… Yes. While you don't *need* a Null Object library, this one offers some conveniences you probably won't find elsewhere. But there's an even more important reason I wrote this library. In the immortal last words of James T. Kirk: "It was… *fun!*" #### OK, so how do I use this thing? Well, what would you like to do? #### I dunno, gimme an object that responds to any message with nil Sure thing! ```ruby require "naught" NullObject = Naught.build null = NullObject.new null.foo # => nil null.bar # => nil ``` #### That was… weird. What's with this "build" business? Naught is a *toolkit* for building null object classes. It is not a one-size-fits-all solution. What else can I make for you? #### How about a "black hole" null object that supports infinite chaining of methods? OK. ```ruby require "naught" BlackHole = Naught.build do |config| config.black_hole end null = BlackHole.new null.foo # => null.foo.bar.baz # => null << "hello" << "world" # => ``` #### What's that "config" thing? That's what you use to customize the generated class to your liking. Internally, Naught uses the [Builder Pattern](http://en.wikipedia.org/wiki/Builder_pattern) to make this work.. #### Whatever. What if I want a null object that has conversions to Integer, String, etc. using sensible conversions to "zero values"? We can do that. ```ruby require "naught" NullObject = Naught.build do |config| config.define_explicit_conversions end null = NullObject.new null.to_s # => "" null.to_i # => 0 null.to_f # => 0.0 null.to_a # => [] null.to_h # => {} null.to_c # => (0+0i) null.to_r # => (0/1) ``` #### Ah, but what about implicit conversions such as `#to_str`? Like what if I want a null object that implicitly splats the same way as an empty array? Gotcha covered. ```ruby require "naught" NullObject = Naught.build do |config| config.define_implicit_conversions end null = NullObject.new null.to_str # => "" null.to_ary # => [] a, b, c = [] a # => nil b # => nil c # => nil x, y, z = null x # => nil y # => nil z # => nil ``` #### How about a null object that only stubs out the methods from a specific class? That's what `mimic` is for. ```ruby require "naught" NullIO = Naught.build do |config| config.mimic IO end null_io = NullIO.new null_io << "foo" # => nil null_io.readline # => nil null_io.foobar # => # ~> -:11:in `
': undefined method `foobar' for # :NullIO (NoMethodError) ``` There is also `impersonate` which takes `mimic` one step further. The generated null class will be derived from the impersonated class. This is handy when refitting legacy code that contains type checks. ```ruby require "naught" NullIO = Naught.build do |config| config.impersonate IO end null_io = NullIO.new IO === null_io # => true case null_io when IO puts "Yep, checks out!" null_io << "some output" else raise "Hey, I expected an IO!" end # >> Yep, checks out! ``` #### My objects are unique and special snowflakes, with new methods added to them at runtime. How are you gonna mimic *that*, hotshot? 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). ```ruby require "naught" require "logging" log = Logging.logger["test"] log.info NullLog = Naught.build do |config| config.mimic example: log end null_log = NullLog.new null_log.info # => nil ``` #### What about objects that define methods dynamically, like Stripe API resources? When you use `mimic example:`, Naught automatically discovers dynamically-defined methods too. This works with libraries like Stripe that use `method_missing` to define methods based on API response data. ```ruby require "naught" require "stripe" invoice = Stripe::Invoice.retrieve("inv_123") NullInvoice = Naught.build do |config| config.mimic example: invoice end null_invoice = NullInvoice.new null_invoice.period_end # => nil null_invoice.amount_due # => nil null_invoice.customer # => nil ``` Naught discovers dynamic methods by checking if the example object responds to `keys`, `attribute_names`, or `to_h`. If you want to disable this behavior, pass `include_dynamic: false`: ```ruby NullInvoice = Naught.build do |config| config.mimic example: invoice, include_dynamic: false end ``` #### What about predicate methods? You know, the ones that end with question marks? Shouldn't they return `false` instead of `nil`? Sure, if you'd like. ```ruby require "naught" NullObject = Naught.build do |config| config.predicates_return false end null = NullObject.new null.foo # => nil null.bar? # => false null.nil? # => false ``` #### Alright smartypants. What if I want to add my own methods? Not a problem, just define them in the `.build` block. ```ruby require "naught" NullObject = Naught.build do |config| config.define_explicit_conversions config.predicates_return false def to_path "/dev/null" end # You can override methods generated by Naught def to_s "NOTHING TO SEE HERE MOVE ALONG" end def nil? true end end null = NullObject.new null.to_path # => "/dev/null" null.to_s # => "NOTHING TO SEE HERE MOVE ALONG" null.nil? # => true ``` #### Got anything else up your sleeve? Well, we can make the null class a singleton, since null objects generally have no state. ```ruby require "naught" NullObject = Naught.build do |config| config.singleton end null = NullObject.instance null.__id__ # => 17844080 NullObject.instance.__id__ # => 17844080 NullObject.new # => # ~> -:11:in `
': private method `new' called for # NullObject:Class (NoMethodError) ``` Speaking of null objects with state, we can also enable tracing. This is handy for playing "where'd that null come from?!" Try doing *that* with `nil`! ```ruby require "naught" NullObject = Naught.build do |config| config.traceable end null = NullObject.new # line 7 null.__file__ # => "example.rb" null.__line__ # => 7 ``` We can even conditionally enable either singleton mode (for production) or tracing (for development). Here's an example of using the `$DEBUG` global variable (set with the `-d` option to ruby) to choose which one. ```ruby require "naught" NullObject = Naught.build do |config| if $DEBUG config.traceable else config.singleton end end ``` The only caveat is that when swapping between singleton and non-singleton implementations, you should be careful to always instantiate your null objects with `NullObject.get`, not `.new` or `.instance`. `.get` will work whether the class is implemented as a singleton or not. ```ruby NullObject.get # => ``` #### What if I want to track every method call made on the null object? Use the `callstack` configuration to record all method calls, including arguments and source location. This is helpful for debugging when you need to understand exactly how a null object is being used. ```ruby require "naught" NullObject = Naught.build do |config| config.callstack end null = NullObject.new null.foo(1, 2).bar null.baz null.__call_trace__ # => [ # [#, # #], # [#] # ] ``` Each trace represents a chain of method calls. The `CallLocation` objects provide access to the method name (`label`), arguments (`args`), file path (`path`), and line number (`lineno`). #### What about safely chaining methods that might return nil? The `null_safe_proxy` configuration adds a `NullSafe()` conversion function that wraps values in a proxy. If any method in a chain returns nil, it gets replaced with a null object, allowing the chain to continue safely. ```ruby require "naught" 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" ``` #### And if I want to know legacy code better? Naught can make a null object behave as a pebble object. ```ruby require "naught" NullObject = Naught.build do |config| if $DEBUG config.pebble else config.black_hole end end ``` Now you can pass the pebble object to your code and see which messages are sent to the pebble. ```ruby null = NullObject.new class MyConsumer < Struct.new(:producer) def consume producer.produce end end MyConsumer.new(null).consume # >> produce() from consume # => ``` #### Are you done yet? Just one more thing. For maximum convenience, Naught-generated null classes also come with a full suite of conversion functions which can be included into your classes. ```ruby require "naught" NullObject = Naught.build include NullObject::Conversions # Convert nil to null objects. Everything else passes through. Maybe(42) # => 42 Maybe(nil) # => Maybe(NullObject.get) # => Maybe { 42 } # => 42 # Insist on a non-null (or nil) value Just(42) # => 42 Just(nil) rescue $! # => # Just(NullObject.get) rescue $! # => #> # nils and nulls become nulls. Everything else is rejected. Null() # => Null(42) rescue $! # => # Null(nil) # => Null(NullObject.get) # => # Convert nulls back to nils. Everything else passes through. Useful # for preventing null objects from "leaking" into public API return # values. Actual(42) # => 42 Actual(nil) # => nil Actual(NullObject.get) # => nil Actual { 42 } # => 42 ``` Installation -------------- ``` {.example} gem install naught ``` Requirements -------------- - Ruby Contributing -------------- - Fork, branch, submit PR, blah blah blah. Don't forget tests. Who's responsible ----------------- Naught is by [Avdi Grimm](http://devblog.avdi.org/) and maintained by Erik Berlin. Prior Art --------- This isn't the first Ruby Null Object library. Others to check out include: - [NullAndVoid](https://github.com/jfelchner/null_and_void) - [BlankSlate](https://github.com/saturnflyer/blank_slate) The Book -------- If you've read this far, you might be interested in the short ebook, *Much Ado About Naught*, 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/). Further reading --------------- - [Null Object: Something for Nothing](http://www.two-sdg.demon.co.uk/curbralan/papers/europlop/NullObject.pdf) (PDF) by Kevlin Henney - [The Null Object Pattern](http://www.cs.oberlin.edu/~jwalker/refs/woolf.ps) (PS) by Bobby Woolf - [NullObject](http://www.c2.com/cgi/wiki?NullObject) on WikiWiki - [Null Object pattern](http://en.wikipedia.org/wiki/Null_Object_pattern) on Wikipedia - [Null Objects and Falsiness](http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/), by Avdi Grimm Libraries Using Naught ---------------------- See [reverse dependencies on RubyGems](https://rubygems.org/gems/naught/reverse_dependencies). avdi-naught-dea6146/Rakefile000066400000000000000000000034161515172043400160100ustar00rootroot00000000000000require "bundler/gem_tasks" # Override release task to skip gem push (handled by GitHub Actions with attestations) Rake::Task["release"].clear desc "Build gem and create tag (gem push handled by CI)" task release: %w[build release:guard_clean release:source_control_push] require "rake/testtask" Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" t.test_files = FileList["test/**/*_test.rb"] end begin require "rubocop/rake_task" RuboCop::RakeTask.new rescue LoadError task :rubocop do warn "RuboCop is disabled" end end begin require "standard/rake" rescue LoadError task :standard do warn "Standard is disabled" end task "standard:fix" do warn "Standard is disabled" end end begin require "yaml" require "yard/rake/yardoc_task" require "yardstick/rake/verify" desc "Generate YARD documentation" YARD::Rake::YardocTask.new(:yard) do |t| t.files = ["lib/**/*.rb"] t.options = ["--no-private"] end desc "Verify YARD documentation coverage" options = YAML.load_file(".yardstick.yml", permitted_classes: [Symbol]) Yardstick::Rake::Verify.new(:yardstick, options) task yardstick: :yard rescue LoadError task :yard do warn "YARD is disabled" end task :yardstick do warn "Yardstick is disabled" end end begin require "steep" desc "Type check with Steep" task :steep do # Use --log-level=fatal to suppress internal Steep worker debug messages sh "bundle exec steep check --log-level=fatal" end rescue LoadError task :steep do warn "Steep is disabled" end end desc "Run all linters (RuboCop and Standard)" task lint: %i[rubocop standard] desc "Fix all auto-correctable lint issues" task "lint:fix": %i[rubocop:autocorrect_all standard:fix] task default: %i[test lint yardstick steep] avdi-naught-dea6146/Steepfile000066400000000000000000000037711515172043400162120ustar00rootroot00000000000000# frozen_string_literal: true target :lib do signature "sig" check "lib" library "singleton" library "forwardable" # Start with strict diagnostics configure_code_diagnostics(Steep::Diagnostic::Ruby.strict) # Naught is a heavily metaprogrammed library that dynamically generates # null object classes. Many patterns used (Module.new with blocks, # define_method within blocks, super in dynamic methods, Class.new blocks) # are beyond what Steep can statically analyze because the block bodies # are evaluated with a different `self` context at runtime. # # These diagnostics are demoted to hints for metaprogramming patterns: configure_code_diagnostics do |hash| # define_method/attr_reader called inside Module.new/Class.new blocks hash[Steep::Diagnostic::Ruby::NoMethod] = :hint # @ivar references inside dynamically defined methods hash[Steep::Diagnostic::Ruby::UnknownInstanceVariable] = :hint # Constants like ::Singleton, ::Forwardable accessed in blocks hash[Steep::Diagnostic::Ruby::UnknownConstant] = :hint # Proc/block type mismatches for metaprogramming patterns hash[Steep::Diagnostic::Ruby::BlockTypeMismatch] = :hint # super calls inside dynamically defined methods hash[Steep::Diagnostic::Ruby::UnexpectedSuper] = :hint # Arguments to super in dynamic methods hash[Steep::Diagnostic::Ruby::UnexpectedPositionalArgument] = :hint # Splat args in dynamic method definitions hash[Steep::Diagnostic::Ruby::FallbackAny] = :hint # Method parameter mismatches in dynamically defined methods hash[Steep::Diagnostic::Ruby::MethodParameterMismatch] = :hint hash[Steep::Diagnostic::Ruby::MethodArityMismatch] = :hint hash[Steep::Diagnostic::Ruby::DifferentMethodParameterKind] = :hint # Type mismatches when passing self or singleton classes hash[Steep::Diagnostic::Ruby::ArgumentTypeMismatch] = :hint # Singleton class assignment type mismatches hash[Steep::Diagnostic::Ruby::IncompatibleAssignment] = :hint end end avdi-naught-dea6146/lib/000077500000000000000000000000001515172043400151055ustar00rootroot00000000000000avdi-naught-dea6146/lib/naught.rb000066400000000000000000000017351515172043400167260ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/000077500000000000000000000000001515172043400163735ustar00rootroot00000000000000avdi-naught-dea6146/lib/naught/basic_object.rb000066400000000000000000000002221515172043400213230ustar00rootroot00000000000000module Naught # BasicObject subclass used as a minimal base for null objects # # @api private class BasicObject < ::BasicObject end end avdi-naught-dea6146/lib/naught/call_location.rb000066400000000000000000000103321515172043400215220ustar00rootroot00000000000000module 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 avdi-naught-dea6146/lib/naught/caller_info.rb000066400000000000000000000110321515172043400211720ustar00rootroot00000000000000module 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 avdi-naught-dea6146/lib/naught/chain_proxy.rb000066400000000000000000000032351515172043400212460ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/conversions.rb000066400000000000000000000101651515172043400212730ustar00rootroot00000000000000module 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 avdi-naught-dea6146/lib/naught/null_class_builder.rb000066400000000000000000000212011515172043400225610ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/null_class_builder/000077500000000000000000000000001515172043400222405ustar00rootroot00000000000000avdi-naught-dea6146/lib/naught/null_class_builder/command.rb000066400000000000000000000027061515172043400242100ustar00rootroot00000000000000module 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 avdi-naught-dea6146/lib/naught/null_class_builder/commands.rb000066400000000000000000000011231515172043400243630ustar00rootroot00000000000000require "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" avdi-naught-dea6146/lib/naught/null_class_builder/commands/000077500000000000000000000000001515172043400240415ustar00rootroot00000000000000avdi-naught-dea6146/lib/naught/null_class_builder/commands/callstack.rb000066400000000000000000000060261515172043400263330ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/null_class_builder/commands/define_explicit_conversions.rb000066400000000000000000000014671515172043400321610ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/null_class_builder/commands/define_implicit_conversions.rb000066400000000000000000000014711515172043400321450ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/null_class_builder/commands/impersonate.rb000066400000000000000000000012571515172043400267210ustar00rootroot00000000000000module 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 avdi-naught-dea6146/lib/naught/null_class_builder/commands/mimic.rb000066400000000000000000000135261515172043400254730ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/null_class_builder/commands/null_safe_proxy.rb000066400000000000000000000064441515172043400276070ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/null_class_builder/commands/pebble.rb000066400000000000000000000021201515172043400256120ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/null_class_builder/commands/predicates_return.rb000066400000000000000000000040701515172043400301110ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/null_class_builder/commands/singleton.rb000066400000000000000000000012401515172043400263650ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/null_class_builder/commands/traceable.rb000066400000000000000000000014471515172043400263160ustar00rootroot00000000000000require "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 avdi-naught-dea6146/lib/naught/stub_strategy.rb000066400000000000000000000015371515172043400216250ustar00rootroot00000000000000module 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 avdi-naught-dea6146/lib/naught/version.rb000066400000000000000000000001271515172043400204050ustar00rootroot00000000000000# Top-level namespace for Naught module Naught # Gem version VERSION = "2.3.0" end avdi-naught-dea6146/naught.gemspec000066400000000000000000000015661515172043400172020ustar00rootroot00000000000000lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "naught/version" Gem::Specification.new do |spec| spec.name = "naught" spec.version = Naught::VERSION spec.authors = ["Avdi Grimm"] spec.email = ["avdi@avdi.org"] spec.description = "Naught is a toolkit for building Null Objects" spec.summary = spec.description spec.homepage = "https://github.com/avdi/naught" spec.license = "MIT" spec.required_ruby_version = ">= 3.2.0" spec.files = Dir["lib/**/*", "sig/**/*", "LICENSE.txt", "README.markdown", "Changelog.md"] spec.require_paths = ["lib"] spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "https://github.com/avdi/naught" spec.metadata["changelog_uri"] = "https://github.com/avdi/naught/blob/master/Changelog.md" spec.metadata["rubygems_mfa_required"] = "true" end avdi-naught-dea6146/sig/000077500000000000000000000000001515172043400151215ustar00rootroot00000000000000avdi-naught-dea6146/sig/naught.rbs000066400000000000000000000254421515172043400171260ustar00rootroot00000000000000# Type signature for Naught - a toolkit for building Null Objects in Ruby module Naught VERSION: String # Lightweight proxy for tracking chained method calls class ChainProxy < BasicObject def initialize: (untyped root, Array[CallLocation] current_trace) -> void def method_missing: (Symbol method_name, *untyped args) ?{ () -> untyped } -> ChainProxy def respond_to?: (*untyped, **untyped) -> true def inspect: () -> String def class: () -> Class end # Utility for parsing Ruby caller/backtrace information module CallerInfo # module_function methods are both instance and singleton methods def self?.parse: (String caller_string) -> { path: String?, lineno: Integer, base_label: String? } def self?.extract_base_label: (String? method_part) -> String? def self?.extract_signature: (String? method_part) -> String? def self?.split_signature: (String signature) -> Array[String?] def self?.format_caller_for_pebble: (Array[String] stack) -> String? def self?.count_block_levels: (Array[String] stack, String target_method) -> Integer private def self?.parse_signature: (String signature) -> Array[String?] def self?.adjusted_block_info: (String? block_info, Array[String] stack, String method_name) -> String? def self?.simple_block?: (String? block_info) -> bool end # Represents a single method call in a null object's call trace class CallLocation def self.from_caller: (Symbol | String method_name, Array[untyped] args, String? caller_string) -> CallLocation attr_reader label: String attr_reader args: Array[untyped] attr_reader path: String attr_reader lineno: Integer attr_reader base_label: String? def absolute_path: () -> String def initialize: (label: Symbol | String, args: Array[untyped], path: String, lineno: Integer, ?base_label: String?) -> void def to_s: () -> String def inspect: () -> String def ==: (untyped other) -> bool def eql?: (untyped other) -> bool def hash: () -> Integer end # Build a null object class using the builder DSL def self.build: () { (NullClassBuilder) -> void } -> Class | () -> Class # Marker module mixed into generated null objects module NullObjectTag end # Marker module for null-safe proxy wrappers module NullSafeProxyTag end # BasicObject subclass used as a minimal base for null objects class BasicObject < ::BasicObject end # Helper conversion API available on generated null classes module Conversions # Configure a Conversions module for a specific null class def self.configure: (Module mod, null_class: Class, null_equivs: Array[untyped]) -> void # Return a null object for object if it is null-equivalent def Null: (?untyped object) -> untyped # Return a null object for null-equivalent values, otherwise the value def Maybe: (?untyped object) -> untyped | [T] () { () -> T } -> (T | untyped) # Return the value if not null-equivalent, otherwise raise def Just: (?untyped object) -> untyped | [T] () { () -> T } -> T # Return nil for null objects, otherwise return the value def Actual: (?untyped object) -> untyped | [T] () { () -> T } -> T? private def __null_class__: () -> Class def __null_equivs__: () -> Array[untyped] def null_object?: (untyped object) -> bool def null_equivalent?: (untyped object, ?include_nothing: bool) -> bool def make_null: (Integer caller_offset) -> untyped end # Strategies for stubbing methods on null objects module StubStrategy # Stub that returns nil from any method module ReturnNil def self.apply: (Module subject, Symbol name) -> void end # Stub that returns self from any method (black hole) module ReturnSelf def self.apply: (Module subject, Symbol name) -> void end end # Builds customized null object classes via a small DSL class NullClassBuilder # Namespace for builder command classes module Commands end type stub_strategy = singleton(StubStrategy::ReturnNil) | singleton(StubStrategy::ReturnSelf) type deferred_operation = ^(untyped) -> void attr_accessor base_class: Class attr_accessor inspect_proc: ^() -> String attr_accessor interface_defined: bool def interface_defined?: () -> bool def initialize: () -> void # Apply a customization block to this builder def customize: () { (NullClassBuilder) -> void } -> void | () -> void # Module that holds customization methods def customization_module: () -> Module # Values treated as null-equivalent def null_equivalents: () -> Array[untyped] # Generate the null object class based on queued operations def generate_class: () -> Class # Configure method stubs to return self (black hole behavior) def black_hole: () -> void # DSL methods dispatched via method_missing to command classes def define_explicit_conversions: () -> void def define_implicit_conversions: () -> void def predicates_return: (untyped value) -> void def mimic: (Class class_to_mimic, ?{ ?include_super: bool, ?include_dynamic: bool } options) -> void | ({ example: untyped, ?include_super: bool, ?include_dynamic: bool } options) -> void def impersonate: (Class class_to_impersonate, ?{ ?include_super: bool, ?include_dynamic: bool } options) -> void def pebble: (?Naught::NullClassBuilder::Commands::Pebble::_Output output) -> void def singleton: () -> void def traceable: () -> void def callstack: () -> void def null_safe_proxy: () -> void # Make null objects respond to any message and stub method_missing def respond_to_any_message: () -> void # Queue a deferred operation to be applied during class generation # Block is evaluated in module context via module_eval def defer: (?Hash[Symbol, bool] options) { (untyped) -> untyped } -> void # Prepend a module generated from the given block # Block is evaluated in module context via Module.new def defer_prepend_module: () { () -> untyped } -> void # Stub a method using the current stub strategy def stub_method: (untyped subject, Symbol name) -> void private def build_null_class: (Module generation_mod) -> Class def define_basic_methods: () -> void def apply_operations: (Array[deferred_operation] operations, untyped module_or_class) -> void def define_basic_instance_methods: () -> void def define_basic_class_methods: () -> void def class_operations: () -> Array[deferred_operation] def operations: () -> Array[deferred_operation] def prepend_modules: () -> Array[Module] def lookup_command: (Symbol method_name) -> singleton(Command)? def camelize: (Symbol | String name) -> String # Base class for builder command implementations class Command attr_reader builder: NullClassBuilder def initialize: (NullClassBuilder builder, *untyped, **untyped) ?{ () -> untyped } -> void def call: () -> void private def defer: (?Hash[Symbol, bool] options) { (untyped) -> untyped } -> void def defer_class: () { (untyped) -> untyped } -> void def defer_prepend_module: () { () -> untyped } -> void end end end # Command classes under the Commands namespace module Naught class NullClassBuilder module Commands class DefineExplicitConversions < Naught::NullClassBuilder::Command def initialize: (NullClassBuilder builder, *untyped, **untyped) ?{ () -> untyped } -> void def call: () -> void end class DefineImplicitConversions < Naught::NullClassBuilder::Command EMPTY_ARRAY: Array[untyped] EMPTY_HASH: Hash[untyped, untyped] RETURN_VALUES: Hash[Symbol, untyped] def initialize: (NullClassBuilder builder, *untyped, **untyped) ?{ () -> untyped } -> void def call: () -> void end class Mimic < Naught::NullClassBuilder::Command METHODS_TO_SKIP: Array[Symbol] NULL_SINGLETON_CLASS: Class attr_reader class_to_mimic: Class attr_reader include_super: bool attr_reader singleton_class: Class attr_reader example_instance: untyped attr_reader include_dynamic: bool def initialize: (NullClassBuilder builder, Class | Hash[Symbol, untyped] class_to_mimic_or_options, ?Hash[Symbol, untyped] options) ?{ () -> untyped } -> void def call: () -> void private def parse_arguments: (Class | Hash[Symbol, untyped] class_to_mimic_or_options, Hash[Symbol, untyped] options) -> void def configure_builder: () -> void def root_class_of: (Class klass) -> Class def methods_to_stub: () -> Array[Symbol] def dynamic_methods: () -> Array[Symbol] def discover_method_candidates: () -> Array[Symbol] end class Impersonate < Naught::NullClassBuilder::Commands::Mimic def initialize: (NullClassBuilder builder, Class class_to_impersonate, ?Hash[Symbol, untyped] options) ?{ () -> untyped } -> void end class Pebble < Naught::NullClassBuilder::Command interface _Output def puts: (String) -> void end @output: _Output def initialize: (NullClassBuilder builder, ?_Output output) ?{ () -> untyped } -> void def call: () -> void end class PredicatesReturn < Naught::NullClassBuilder::Command @return_value: untyped def initialize: (NullClassBuilder builder, untyped return_value) ?{ () -> untyped } -> void def call: () -> void private def install_method_missing_override: () -> void def install_predicate_method_overrides: () -> void end class Singleton < Naught::NullClassBuilder::Command def initialize: (NullClassBuilder builder, *untyped, **untyped) ?{ () -> untyped } -> void def call: () -> void end class Traceable < Naught::NullClassBuilder::Command def initialize: (NullClassBuilder builder, *untyped, **untyped) ?{ () -> untyped } -> void def call: () -> void end class NullSafeProxy < Naught::NullClassBuilder::Command def initialize: (NullClassBuilder builder, *untyped, **untyped) ?{ () -> untyped } -> void def call: () -> void private def build_proxy_class: (Class null_class, Array[untyped] null_equivs) -> Class def install_null_safe_conversion: (Class null_class, Class proxy_class, Array[untyped] null_equivs) -> void end class Callstack < Naught::NullClassBuilder::Command def initialize: (NullClassBuilder builder, *untyped, **untyped) ?{ () -> untyped } -> void def call: () -> void private def install_call_trace_accessor: () -> void def install_method_missing_tracking: () -> void def install_chain_proxy_class: () -> void end end end end avdi-naught-dea6146/test/000077500000000000000000000000001515172043400153165ustar00rootroot00000000000000avdi-naught-dea6146/test/base_object_test.rb000066400000000000000000000017611515172043400211470ustar00rootroot00000000000000require "test_helper" class BaseObjectTest < Minitest::Test def setup @custom_base_null_class = Naught.build do |b| b.base_class = Object end @null = @custom_base_null_class.new end def test_responds_to_base_class_methods assert_kind_of Array, @null.methods end def test_responds_to_unknown_methods assert_nil @null.foo end def test_exposes_the_default_base_class_choice default_base_class = :not_set Naught.build do |b| default_base_class = b.base_class end assert_equal Naught::BasicObject, default_base_class end end class BaseObjectSingletonTest < Minitest::Test def setup @custom_base_singleton_null_class = Naught.build do |b| b.singleton b.base_class = Object end @null_instance = @custom_base_singleton_null_class.instance end def test_can_be_cloned assert_same @null_instance, @null_instance.clone end def test_can_be_duplicated assert_same @null_instance, @null_instance.dup end end avdi-naught-dea6146/test/basic_null_object_test.rb000066400000000000000000000012351515172043400223440ustar00rootroot00000000000000require "test_helper" class BasicNullObjectTest < NaughtTestCase def setup @null_class, @null = build_null end def test_returns_nil_from_any_method assert_returns_nil @null, :info assert_returns_nil @null, :foobaz assert_returns_nil @null, :to_s end def test_accepts_any_arguments @null.foobaz(1, 2, 3) end def test_responds_to_any_method assert_responds_to_anything @null end def test_inspects_as_null assert_equal "", @null.inspect end def test_knows_its_own_class assert_equal @null_class, @null.class end def test_aliases_new_to_get assert_same @null_class, @null_class.get.class end end avdi-naught-dea6146/test/blackhole_test.rb000066400000000000000000000017301515172043400206270ustar00rootroot00000000000000require "test_helper" class BlackholeTest < NaughtTestCase def setup @null_class, @null = build_null(&:black_hole) end def test_returns_self_from_any_method assert_returns_self @null, :info assert_returns_self @null, :foobaz assert_same @null, @null << "bar" end end # Test for GitHub issue #72: black_hole and Marshal.dump don't work together # https://github.com/avdi/naught/issues/72 # Note: Marshal requires a named constant, so we define one within the test class class BlackholeMarshalTest < NaughtTestCase MarshalableBlackHole = Naught.build(&:black_hole) def setup @null = MarshalableBlackHole.new end def test_can_be_marshaled_and_unmarshaled loaded = Marshal.load(Marshal.dump(@null)) assert_kind_of MarshalableBlackHole, loaded end def test_marshaled_object_retains_black_hole_behavior loaded = Marshal.load(Marshal.dump(@null)) assert_same loaded, loaded.foo assert_same loaded, loaded.bar.baz end end avdi-naught-dea6146/test/caller_info_test.rb000066400000000000000000000104601515172043400211600ustar00rootroot00000000000000require "test_helper" class CallerInfoParseTest < NaughtTestCase def test_parses_simple_caller_string result = Naught::CallerInfo.parse("/path/to/file.rb:42:in `method_name'") assert_equal "/path/to/file.rb", result[:path] assert_equal 42, result[:lineno] assert_equal "method_name", result[:base_label] end def test_parses_caller_without_method_info result = Naught::CallerInfo.parse("/path/to/file.rb:10") assert_equal "/path/to/file.rb", result[:path] assert_equal 10, result[:lineno] assert_nil result[:base_label] end def test_parses_block_in_method result = Naught::CallerInfo.parse("/path/to/file.rb:5:in `block in my_method'") assert_equal "my_method", result[:base_label] end def test_parses_class_method result = Naught::CallerInfo.parse("/path/to/file.rb:5:in `MyClass#instance_method'") assert_equal "instance_method", result[:base_label] end def test_parses_nested_block result = Naught::CallerInfo.parse("/path/to/file.rb:5:in `block (2 levels) in my_method'") assert_equal "my_method", result[:base_label] end end class CallerInfoExtractBaseLabelTest < NaughtTestCase def test_returns_nil_for_nil_input assert_nil Naught::CallerInfo.extract_base_label(nil) end def test_returns_nil_when_no_quotes_present assert_nil Naught::CallerInfo.extract_base_label("no quotes here") end def test_extracts_simple_method_name assert_equal "foo", Naught::CallerInfo.extract_base_label("in `foo'") end end class CallerInfoExtractSignatureTest < NaughtTestCase def test_returns_nil_for_nil_input assert_nil Naught::CallerInfo.extract_signature(nil) end def test_returns_nil_when_no_quotes_present assert_nil Naught::CallerInfo.extract_signature("no quotes here") end def test_extracts_quoted_signature assert_equal "my_method", Naught::CallerInfo.extract_signature("in `my_method'") end def test_extracts_block_signature assert_equal "block in my_method", Naught::CallerInfo.extract_signature("in `block in my_method'") end end class CallerInfoSplitSignatureTest < NaughtTestCase def test_splits_block_signature block_info, method_name = Naught::CallerInfo.split_signature("block in my_method") assert_equal "block", block_info assert_equal "my_method", method_name end def test_returns_nil_block_info_for_simple_method block_info, method_name = Naught::CallerInfo.split_signature("my_method") assert_nil block_info assert_equal "my_method", method_name end end class CallerInfoFormatCallerForPebbleTest < NaughtTestCase def test_formats_simple_method_call stack = ["/path/to/file.rb:10:in `my_method'"] assert_equal "my_method", Naught::CallerInfo.format_caller_for_pebble(stack) end def test_formats_block_call stack = ["/path/to/file.rb:10:in `block in my_method'"] assert_equal "block my_method", Naught::CallerInfo.format_caller_for_pebble(stack) end def test_returns_raw_caller_when_unrecognized_format stack = ["unusual format without method info"] assert_equal "unusual format without method info", Naught::CallerInfo.format_caller_for_pebble(stack) end end class CallerInfoCountBlockLevelsTest < NaughtTestCase def test_counts_single_block_level stack = [ "/path/to/file.rb:10:in `block in my_method'", "/path/to/file.rb:5:in `my_method'" ] assert_equal 1, Naught::CallerInfo.count_block_levels(stack, "my_method") end def test_counts_multiple_block_levels stack = [ "/path/to/file.rb:10:in `block in my_method'", "/path/to/file.rb:8:in `block in each'", "/path/to/file.rb:7:in `block in my_method'", "/path/to/file.rb:5:in `my_method'" ] assert_equal 2, Naught::CallerInfo.count_block_levels(stack, "my_method") end def test_stops_at_non_matching_entry stack = [ "/path/to/file.rb:10:in `block in my_method'", "/path/to/file.rb:8:in `other_method'", "/path/to/file.rb:5:in `my_method'" ] assert_equal 1, Naught::CallerInfo.count_block_levels(stack, "my_method") end def test_stops_when_no_signature_found stack = [ "/path/to/file.rb:10:in `block in my_method'", "unusual format without method info", "/path/to/file.rb:5:in `my_method'" ] assert_equal 1, Naught::CallerInfo.count_block_levels(stack, "my_method") end end avdi-naught-dea6146/test/callstack_test.rb000066400000000000000000000231511515172043400206450ustar00rootroot00000000000000require "test_helper" module CallstackTestHelper def build_callstack_null build_null(&:callstack) end end class CallLocationAttributesTest < NaughtTestCase include CallLocationHelper def test_stores_method_name_as_label assert_equal "foo", make_location.label end def test_converts_symbol_label_to_string assert_equal "foo", make_location(label: :foo).label end def test_stores_arguments assert_equal [1, "hello", :sym], make_location(args: [1, "hello", :sym]).args end def test_freezes_arguments args = [1, 2, 3] location = make_location(args: args) assert_predicate location.args, :frozen? args << 4 assert_equal [1, 2, 3], location.args end def test_stores_path assert_equal "/path/to/file.rb", make_location.path end def test_absolute_path_is_alias_for_path location = make_location assert_equal location.path, location.absolute_path end def test_stores_lineno assert_equal 42, make_location.lineno end def test_stores_base_label assert_equal "method", make_location.base_label end end class CallLocationFormattingTest < NaughtTestCase include CallLocationHelper def test_to_s_with_base_label location = make_location(args: [1, "bar"], base_label: "some_method") assert_equal "/path/to/file.rb:42:in `some_method' -> foo(1, \"bar\")", location.to_s end def test_to_s_without_base_label location = make_location(args: [], base_label: nil) assert_equal "/path/to/file.rb:42 -> foo()", location.to_s end def test_inspect_includes_class_name assert_match(/^#", chain.inspect end def test_chain_proxy_class_returns_null_class chain = @null.foo assert_equal @null_class, chain.class end end class CallstackUnusualCallerTest < NaughtTestCase include CallstackTestHelper def setup @null_class, @null = build_callstack_null end def test_handles_caller_without_method_signature # Stub Kernel.caller to return a format without method info fake_caller = ["unusual_format.rb:10"] Kernel.stub(:caller, ->(*) { [fake_caller.first] }) do @null.foo end location = @null.__call_trace__[0][0] assert_equal "unusual_format.rb", location.path assert_equal 10, location.lineno assert_nil location.base_label end def test_chain_proxy_handles_caller_without_method_signature # First call to get the chain proxy chain = @null.foo # Stub Kernel.caller for the chained call fake_caller = ["unusual_format.rb:20"] Kernel.stub(:caller, ->(*) { [fake_caller.first] }) do chain.bar end location = @null.__call_trace__[0][1] assert_equal "unusual_format.rb", location.path assert_equal 20, location.lineno assert_nil location.base_label end def test_handles_caller_with_method_part_but_no_quotes # Stub Kernel.caller to return a format with method part but no quoted method name fake_caller = ["file.rb:10:no quotes here"] Kernel.stub(:caller, ->(*) { [fake_caller.first] }) do @null.foo end location = @null.__call_trace__[0][0] assert_nil location.base_label end def test_chain_proxy_handles_caller_with_method_part_but_no_quotes chain = @null.foo fake_caller = ["file.rb:20:no quotes here"] Kernel.stub(:caller, ->(*) { [fake_caller.first] }) do chain.bar end location = @null.__call_trace__[0][1] assert_nil location.base_label end end class CallstackRespondToTest < NaughtTestCase include CallstackTestHelper def setup @null_class, @null = build_callstack_null end def test_respond_to_call_trace assert_respond_to @null, :__call_trace__ end def test_respond_to_delegates_to_super_for_other_methods # respond_to? should delegate to super for methods other than __call_trace__ # Since this null object responds to any message, this should return true assert_respond_to @null, :some_random_method end end class CallstackWithTraceableTest < NaughtTestCase def setup @null_class = build_null_class { |b| b.callstack b.traceable } @null = @null_class.new @line = __LINE__ - 1 end def test_records_file @null.foo.bar assert_equal __FILE__, @null.__file__ end def test_records_line assert_equal @line, @null.__line__ end def test_callstack_still_works @null.foo.bar assert_equal 1, @null.__call_trace__.size assert_equal %w[foo bar], @null.__call_trace__[0].map(&:label) end end class CallstackWithPredicatesReturnTest < NaughtTestCase def setup @null_class, @null = build_null { |b| b.callstack b.predicates_return false } end def test_predicate_calls_are_not_recorded # predicates_return's method_missing intercepts these and returns # without calling super, so predicate calls are NOT recorded refute_predicate @null, :valid? @null.foo.bar assert_equal 1, @null.__call_trace__.size assert_equal %w[foo bar], @null.__call_trace__[0].map(&:label) end end avdi-naught-dea6146/test/explicit_conversions_test.rb000066400000000000000000000010141515172043400231470ustar00rootroot00000000000000require "test_helper" class ExplicitConversionsTest < NaughtTestCase def setup @null_class, @null = build_null(&:define_explicit_conversions) end def test_converts_to_empty_string assert_equal "", @null.to_s end def test_converts_to_empty_array assert_empty @null.to_a end def test_converts_to_zero_integer assert_equal 0, @null.to_i end def test_converts_to_zero_float assert_in_delta 0.0, @null.to_f end def test_converts_to_empty_hash assert_empty @null.to_h end end avdi-naught-dea6146/test/functions/000077500000000000000000000000001515172043400173265ustar00rootroot00000000000000avdi-naught-dea6146/test/functions/actual_test.rb000066400000000000000000000011561515172043400221660ustar00rootroot00000000000000require "test_helper" class ActualTest < NaughtTestCase include ConvertableNull::Conversions def test_given_null_object_returns_nil assert_nil Actual(ConvertableNull.get) end def test_given_false_returns_false refute Actual(false) end def test_given_string_returns_string str = "hello" assert_same str, Actual(str) end def test_given_nil_returns_nil assert_nil Actual(nil) end def test_block_yielding_null_object_returns_nil assert_nil Actual { ConvertableNull.new } end def test_block_yielding_value_returns_value assert_equal "foo", Actual { "foo" } end end avdi-naught-dea6146/test/functions/just_test.rb000066400000000000000000000013171515172043400217010ustar00rootroot00000000000000require "test_helper" class JustTest < NaughtTestCase include ConvertableNull::Conversions def test_passes_false_through refute Just(false) end def test_passes_strings_through str = "hello" assert_same str, Just(str) end def test_rejects_nil assert_raises(ArgumentError) { Just(nil) } end def test_rejects_null_equivalent assert_raises(ArgumentError) { Just("") } end def test_rejects_null_objects assert_raises(ArgumentError) { Just(ConvertableNull.get) } end def test_block_yielding_nil_raises_argument_error assert_raises(ArgumentError) { Just { nil } } end def test_block_yielding_value_returns_value assert_equal "foo", Just { "foo" } end end avdi-naught-dea6146/test/functions/maybe_test.rb000066400000000000000000000017511515172043400220130ustar00rootroot00000000000000require "test_helper" class MaybeTest < NaughtTestCase include ConvertableNull::Conversions def test_given_nil_returns_null_object assert_same ConvertableNull, Maybe(nil).class end def test_given_null_object_returns_same_object null = ConvertableNull.get assert_same null, Maybe(null) end def test_given_null_equivalent_returns_null_object assert_same ConvertableNull, Maybe("").class end def test_given_false_returns_false refute Maybe(false) end def test_given_string_returns_string str = "hello" assert_same str, Maybe(str) end def test_tracks_file_location null = Maybe() assert_equal __FILE__, null.__file__ end def test_tracks_line_location null = Maybe() assert_equal __LINE__ - 2, null.__line__ end def test_block_yielding_nil_returns_null_object assert_equal ConvertableNull, Maybe { nil }.class end def test_block_yielding_value_returns_value assert_equal "foo", Maybe { "foo" } end end avdi-naught-dea6146/test/functions/null_test.rb000066400000000000000000000016631515172043400216720ustar00rootroot00000000000000require "test_helper" class NullFunctionTest < NaughtTestCase include ConvertableNull::Conversions def test_given_no_input_returns_null_object assert_same ConvertableNull, Null().class end def test_given_nil_returns_null_object assert_same ConvertableNull, Null(nil).class end def test_given_null_object_returns_same_object null = ConvertableNull.get assert_same null, Null(null) end def test_given_null_equivalent_returns_null_object assert_same ConvertableNull, Null("").class end def test_given_false_raises_argument_error assert_raises(ArgumentError) { Null(false) } end def test_given_non_null_string_raises_argument_error assert_raises(ArgumentError) { Null("hello") } end def test_tracks_file_location null = Null() assert_equal __FILE__, null.__file__ end def test_tracks_line_location null = Null() assert_equal __LINE__ - 2, null.__line__ end end avdi-naught-dea6146/test/implicit_conversions_test.rb000066400000000000000000000014061515172043400231450ustar00rootroot00000000000000require "test_helper" class ImplicitConversionsTest < NaughtTestCase def setup @null_class, @null = build_null(&:define_implicit_conversions) end def test_implicitly_splats_first_element_as_nil a, = @null assert_nil a end def test_implicitly_splats_second_element_as_nil _, b = @null assert_nil b end def test_is_implicitly_convertable_to_string assert_nil instance_eval(@null) end def test_implicitly_converts_to_an_empty_array assert_empty @null.to_ary end def test_implicitly_converts_to_an_empty_hash assert_empty @null.to_hash end def test_implicitly_converts_to_zero assert_equal 0, @null.to_int end def test_implicitly_converts_to_an_empty_string assert_equal "", @null.to_str end end avdi-naught-dea6146/test/mimic_test.rb000066400000000000000000000271011515172043400200010ustar00rootroot00000000000000require "test_helper" require "logger" # Shared assertions for mimic + black_hole combinations module MimicBlackHoleAssertions def test_returns_self_from_info_method assert_returns_self @null, :info end def test_returns_self_from_error_method assert_returns_self @null, :error end def test_returns_self_from_shift_method assert_same @null, @null << "test" end def test_does_not_respond_to_undefined_methods assert_raises(NoMethodError) { @null.foobar } end end class MimicTest < NaughtTestCase def setup @mimic_class = build_null_class { |b| b.mimic NaughtTestFixtures::LibraryPatron } @null = @mimic_class.new end def test_responds_to_member_method assert_nil @null.member? end def test_responds_to_name_method assert_nil @null.name end def test_responds_to_notify_of_overdue_books_method assert_nil @null.notify_of_overdue_books(["The Grapes of Wrath"]) end def test_does_not_respond_to_methods_not_defined_on_the_target_class assert_raises(NoMethodError) { @null.foobar } end def test_reports_it_responds_to_member assert_respond_to @null, :member? end def test_reports_it_responds_to_name assert_respond_to @null, :name end def test_reports_it_responds_to_notify_of_overdue_books assert_respond_to @null, :notify_of_overdue_books end def test_reports_it_does_not_respond_to_foobar refute_respond_to @null, :foobar end def test_has_an_informative_inspect_string assert_equal "", @null.inspect end def test_does_not_mimic_object_id_from_object refute_nil @null.object_id end def test_does_not_mimic_hash_from_object refute_nil @null.hash end def test_includes_inherited_method_authorized_for assert_nil @null.authorized_for?("something") end def test_includes_inherited_method_login assert_nil @null.login end end class MimicWithoutSuperTest < NaughtTestCase def setup @mimic_class = build_null_class { |b| b.mimic NaughtTestFixtures::LibraryPatron, include_super: false } @null = @mimic_class.new end def test_excludes_inherited_method_authorized_for refute_respond_to @null, :authorized_for? end def test_excludes_inherited_method_login refute_respond_to @null, :login end end class MimicWithExampleTest < NaughtTestCase def setup milton = NaughtTestFixtures::LibraryPatron.new def milton.stapler end @mimic_class = build_null_class { |b| b.mimic example: milton } @null = @mimic_class.new end def test_responds_to_method_defined_only_on_the_example_instance assert_respond_to @null, :stapler end def test_responds_to_method_defined_on_the_class_of_the_instance assert_respond_to @null, :member? end end class MimicBasicObjectSubclassTest < NaughtTestCase def setup @mimic_class = build_null_class { |b| b.mimic NaughtTestFixtures::BasicWidget } @null = @mimic_class.new end def test_uses_basic_object_as_base_class_and_responds_to_mimicked_methods assert_nil @null.widget_method end end class MimicWithBlackHoleTest < NaughtTestCase include MimicBlackHoleAssertions def setup _, @null = build_null { |b| b.mimic Logger b.black_hole } end end class MimicWithBlackHoleReverseOrderTest < NaughtTestCase include MimicBlackHoleAssertions def setup _, @null = build_null { |b| b.black_hole b.mimic Logger } end end # Test for GitHub issue #55: Composing black_hole, predicates_return and impersonate # https://github.com/avdi/naught/issues/55 class MimicDoesNotStubMethodMissingTest < NaughtTestCase def setup @mimic_class = build_null_class { |b| b.mimic NaughtTestFixtures::DynamicClass } @null = @mimic_class.new end def test_stubs_regular_methods assert_nil @null.regular_method end def test_stubs_predicate_methods assert_nil @null.active? end def test_does_not_respond_to_dynamic_methods # The null class should not mimic the dynamic method behavior refute_respond_to @null, :dynamic_foo end def test_raises_for_undefined_methods assert_raises(NoMethodError) { @null.undefined_method } end end class MimicWithPredicatesReturnAndMethodMissingTest < NaughtTestCase def setup @mimic_class = build_null_class { |b| b.mimic NaughtTestFixtures::DynamicClass b.predicates_return false } @null = @mimic_class.new end def test_predicates_return_false_for_mimicked_predicates refute_predicate @null, :active? end def test_predicates_return_false_for_any_predicate refute_predicate @null, :something? end def test_regular_methods_return_nil assert_nil @null.regular_method end end class MimicWithBlackHoleAndPredicatesReturnTest < NaughtTestCase def setup @mimic_class = build_null_class { |b| b.mimic NaughtTestFixtures::DynamicClass b.black_hole b.predicates_return false } @null = @mimic_class.new end def test_predicates_return_false refute_predicate @null, :active? end def test_regular_mimicked_methods_return_self assert_same @null, @null.regular_method end def test_undefined_methods_raise_error assert_raises(NoMethodError) { @null.foobar } end end class ImpersonateWithBlackHoleAndPredicatesReturnTest < NaughtTestCase def setup @impersonate_class = build_null_class { |b| b.impersonate NaughtTestFixtures::DynamicClass b.black_hole b.predicates_return false } @null = @impersonate_class.new end def test_predicates_return_false refute_predicate @null, :active? end def test_regular_mimicked_methods_return_self assert_same @null, @null.regular_method end def test_is_a_dynamic_class assert_kind_of NaughtTestFixtures::DynamicClass, @null end def test_undefined_methods_raise_error assert_raises(NoMethodError) { @null.foobar } end end # Test for GitHub issue #78: mimic() with include_dynamic option # https://github.com/avdi/naught/issues/78 class MimicWithIncludeDynamicTest < NaughtTestCase def setup example = NaughtTestFixtures::StripeStyleObject.new( id: "inv_123", amount: 1000, period_end: Time.now.to_i ) @mimic_class = build_null_class { |b| b.mimic example: example, include_dynamic: true } @null = @mimic_class.new end def test_responds_to_dynamic_method assert_respond_to @null, :period_end end def test_dynamic_method_returns_nil assert_nil @null.period_end end def test_responds_to_all_dynamic_methods assert_respond_to @null, :id assert_respond_to @null, :amount assert_respond_to @null, :period_end end def test_responds_to_regular_method assert_respond_to @null, :regular_method end def test_does_not_respond_to_undefined_methods refute_respond_to @null, :undefined_attribute end def test_raises_for_undefined_methods assert_raises(NoMethodError) { @null.undefined_attribute } end end class MimicWithIncludeDynamicAndBlackHoleTest < NaughtTestCase def setup example = NaughtTestFixtures::StripeStyleObject.new( id: "inv_123", customer: "cus_456" ) @mimic_class = build_null_class { |b| b.mimic example: example, include_dynamic: true b.black_hole } @null = @mimic_class.new end def test_dynamic_method_returns_self assert_same @null, @null.id end def test_regular_method_returns_self assert_same @null, @null.regular_method end def test_undefined_methods_raise_error assert_raises(NoMethodError) { @null.undefined_attribute } end end class MimicWithExampleIncludesDynamicByDefaultTest < NaughtTestCase def setup example = NaughtTestFixtures::StripeStyleObject.new( id: "inv_123", period_end: Time.now.to_i ) @mimic_class = build_null_class { |b| b.mimic example: example } @null = @mimic_class.new end def test_responds_to_dynamic_method_by_default_with_example assert_respond_to @null, :period_end end def test_responds_to_regular_method assert_respond_to @null, :regular_method end end class MimicWithExampleExplicitlyDisabledDynamicTest < NaughtTestCase def setup example = NaughtTestFixtures::StripeStyleObject.new( id: "inv_123", period_end: Time.now.to_i ) @mimic_class = build_null_class { |b| b.mimic example: example, include_dynamic: false } @null = @mimic_class.new end def test_does_not_respond_to_dynamic_method_when_explicitly_disabled refute_respond_to @null, :period_end end def test_responds_to_regular_method assert_respond_to @null, :regular_method end end class MimicWithIncludeDynamicActiveRecordStyleTest < NaughtTestCase def setup example = NaughtTestFixtures::ActiveRecordStyleObject.new( name: "John", email: "john@example.com" ) @mimic_class = build_null_class { |b| b.mimic example: example, include_dynamic: true } @null = @mimic_class.new end def test_responds_to_dynamic_methods_via_attribute_names assert_respond_to @null, :name assert_respond_to @null, :email end def test_dynamic_methods_return_nil assert_nil @null.name assert_nil @null.email end def test_responds_to_regular_method assert_respond_to @null, :regular_method end end class MimicWithIncludeDynamicOpenStructStyleTest < NaughtTestCase def setup example = NaughtTestFixtures::OpenStructStyleObject.new( foo: "bar", baz: 123 ) @mimic_class = build_null_class { |b| b.mimic example: example, include_dynamic: true } @null = @mimic_class.new end def test_responds_to_dynamic_methods_via_to_h assert_respond_to @null, :foo assert_respond_to @null, :baz end def test_dynamic_methods_return_nil assert_nil @null.foo assert_nil @null.baz end def test_responds_to_regular_method assert_respond_to @null, :regular_method end end class MimicWithIncludeDynamicBrokenToHTest < NaughtTestCase def setup example = NaughtTestFixtures::BrokenToHObject.new( foo: "bar" ) @mimic_class = build_null_class { |b| b.mimic example: example, include_dynamic: true } @null = @mimic_class.new end def test_handles_broken_to_h_gracefully # Should not raise an error when building assert_respond_to @null, :regular_method end def test_does_not_respond_to_dynamic_methods_when_to_h_fails # Since to_h raises and there's no keys/attribute_names, no dynamic methods found refute_respond_to @null, :foo end end class MimicWithIncludeDynamicClassBasedTest < NaughtTestCase def setup # Test include_dynamic with class-based mimic (no example instance) @mimic_class = build_null_class { |b| b.mimic NaughtTestFixtures::StripeStyleObject, include_dynamic: true } @null = @mimic_class.new end def test_responds_to_regular_methods assert_respond_to @null, :regular_method end def test_does_not_have_dynamic_methods_without_example # Without an example instance, we can't discover dynamic methods assert_nil @null.regular_method end end class MimicWithIncludeDynamicNonHashToHTest < NaughtTestCase def setup example = NaughtTestFixtures::NonHashToHObject.new( foo: "bar" ) @mimic_class = build_null_class { |b| b.mimic example: example, include_dynamic: true } @null = @mimic_class.new end def test_handles_non_hash_to_h_gracefully # Should not raise an error when building assert_respond_to @null, :regular_method end def test_does_not_respond_to_dynamic_methods_when_to_h_returns_non_hash # Since to_h returns an Array and there's no keys/attribute_names, no dynamic methods found refute_respond_to @null, :foo end end avdi-naught-dea6146/test/naught/000077500000000000000000000000001515172043400166045ustar00rootroot00000000000000avdi-naught-dea6146/test/naught/null_class_builder/000077500000000000000000000000001515172043400224515ustar00rootroot00000000000000avdi-naught-dea6146/test/naught/null_class_builder/command_test.rb000066400000000000000000000003341515172043400254530ustar00rootroot00000000000000require "test_helper" class NullClassBuilderCommandTest < Minitest::Test def test_is_abstract command = Naught::NullClassBuilder::Command.new(nil) assert_raises(NotImplementedError) { command.call } end end avdi-naught-dea6146/test/naught/null_class_builder_test.rb000066400000000000000000000023051515172043400240350ustar00rootroot00000000000000require "test_helper" require "minitest/mock" module Naught class NullClassBuilder module Commands class TestCommand def initialize(_builder, *_args) end def call end end end end end class NullClassBuilderTest < Minitest::Test def setup @builder = Naught::NullClassBuilder.new end def test_responds_to_commands_defined_in_null_object_builder_commands assert_respond_to @builder, :test_command end def test_returns_the_result_of_the_command_call test_command = Minitest::Mock.new test_command.expect(:call, "COMMAND RESULT") mock_new = ->(_builder, *_args) { test_command } Naught::NullClassBuilder::Commands::TestCommand.stub(:new, mock_new) do assert_equal "COMMAND RESULT", @builder.test_command("foo", 42) end test_command.verify end def test_does_not_respond_to_nonexistent_methods refute_respond_to @builder, :nonexistant_method end def test_raises_no_method_error_for_nonexistent_methods assert_raises(NoMethodError) { @builder.nonexistent_method } end def test_handles_method_names_that_would_create_invalid_constant_names refute_respond_to @builder, :"123invalid" end end avdi-naught-dea6146/test/naught_test.rb000066400000000000000000000035061515172043400201740ustar00rootroot00000000000000require "test_helper" class ImpersonationTest < NaughtTestCase def setup @null_class = Naught.build { |b| b.impersonate NaughtTestFixtures::Point } @null = @null_class.new end def test_matches_the_impersonated_type assert_kind_of NaughtTestFixtures::Point, @null end def test_responds_to_methods_from_impersonated_type assert_nil @null.x assert_nil @null.y end def test_does_not_respond_to_unknown_methods assert_raises(NoMethodError) { @null.foo } end end class TraceableTest < NaughtTestCase def setup @null_class = Naught.build(&:traceable) @null = @null_class.new @line = __LINE__ - 1 end def test_remembers_file assert_equal __FILE__, @null.__file__ end def test_remembers_line assert_equal @line, @null.__line__ end def test_accepts_custom_backtrace obj = @null_class.get(caller: caller(0)) assert_equal __LINE__ - 2, obj.__line__ end end class CustomizedNullObjectTest < NaughtTestCase def setup @null_class = Naught.build do |b| b.define_explicit_conversions define_method(:to_path) { File::NULL } define_method(:to_s) { "NOTHING TO SEE HERE" } end @null = @null_class.new end def test_responds_to_custom_methods assert_equal File::NULL, @null.to_path end def test_can_override_generated_methods assert_equal "NOTHING TO SEE HERE", @null.to_s end end class NamedNullObjectClassTest < NaughtTestCase # Tests that assigning a null class to a constant gives it a proper name NamedNull = Naught.build def test_has_named_ancestor_modules expected = [ "NamedNullObjectClassTest::NamedNull", "NamedNullObjectClassTest::NamedNull::Customizations", "NamedNullObjectClassTest::NamedNull::GeneratedMethods" ] assert_equal expected, NamedNull.ancestors[0..2].map(&:name) end end avdi-naught-dea6146/test/null_safe_proxy_test.rb000066400000000000000000000101271515172043400221140ustar00rootroot00000000000000require "test_helper" class NullSafeProxyTest < NaughtTestCase def setup @null_class = Naught.build do |b| b.null_safe_proxy end end def null_safe(obj) @null_class::Conversions.instance_method(:NullSafe).bind_call(self, obj) end def test_null_safe_wraps_regular_object obj = Object.new proxy = null_safe(obj) assert_equal "", proxy.inspect end def test_null_safe_passes_through_null_object null = @null_class.new result = null_safe(null) assert_same null, result end def test_null_safe_converts_nil_to_null result = null_safe(nil) assert_null_object result assert_equal @null_class, result.class end def test_proxy_returns_null_when_method_returns_nil obj = Object.new def obj.foo nil end proxy = null_safe(obj) result = proxy.foo assert_null_object result end def test_proxy_wraps_non_nil_return_values obj = Object.new def obj.foo "hello" end proxy = null_safe(obj) result = proxy.foo assert_includes result.inspect, "null-safe-proxy" assert_includes result.inspect, "hello" end def test_chained_calls_propagate_proxy obj = Object.new def obj.foo inner = Object.new def inner.bar "result" end inner end proxy = null_safe(obj) result = proxy.foo.bar assert_includes result.inspect, "null-safe-proxy" assert_includes result.inspect, "result" end def test_nil_in_chain_returns_null_object obj = Object.new def obj.foo inner = Object.new def inner.bar nil end inner end proxy = null_safe(obj) result = proxy.foo.bar assert_null_object result end def test_can_chain_after_nil_with_black_hole null_class = Naught.build do |b| b.black_hole b.null_safe_proxy end null_safe_method = null_class::Conversions.instance_method(:NullSafe) obj = Object.new def obj.foo nil end result = null_safe_method.bind_call(self, obj).foo.bar.baz assert_null_object result end def test_proxy_responds_to_target_methods obj = Object.new def obj.custom_method 42 end proxy = null_safe(obj) assert_respond_to proxy, :custom_method end def test_proxy_target_accessible obj = Object.new proxy = null_safe(obj) assert_same obj, proxy.__target__ end def test_proxy_is_null_safe_proxy obj = Object.new proxy = null_safe(obj) assert_operator Naught::NullSafeProxyTag, :===, proxy end def test_unwrapping_with_actual obj = "hello" proxy = null_safe(obj) inner_proxy = proxy.upcase actual_value = inner_proxy.__target__ assert_equal "HELLO", actual_value end def test_with_custom_null_equivalents null_class = Naught.build do |b| b.null_equivalents << false b.null_safe_proxy end null_safe_method = null_class::Conversions.instance_method(:NullSafe) obj = Object.new def obj.falsey false end proxy = null_safe_method.bind_call(self, obj) result = proxy.falsey assert_null_object result end def test_null_safe_with_false_default_not_null obj = Object.new def obj.falsey false end proxy = null_safe(obj) result = proxy.falsey assert_includes result.inspect, "null-safe-proxy" assert_includes result.inspect, "false" end def test_proxy_passes_block_to_method obj = [1, 2, 3] proxy = null_safe(obj) result = proxy.map { |x| x * 2 } assert_includes result.inspect, "null-safe-proxy" assert_equal [2, 4, 6], result.__target__ end def test_proxy_passes_arguments_to_method obj = "hello world" proxy = null_safe(obj) result = proxy.split(" ") assert_includes result.inspect, "null-safe-proxy" assert_equal ["hello", "world"], result.__target__ end def test_proxy_passes_through_null_object_from_method null = @null_class.new obj = Object.new obj.define_singleton_method(:get_null) { null } proxy = null_safe(obj) result = proxy.get_null assert_same null, result end end avdi-naught-dea6146/test/pebble_test.rb000066400000000000000000000037441515172043400201430ustar00rootroot00000000000000require "test_helper" require "stringio" module PebbleTestSetup def setup @output = StringIO.new output = @output _, @null = build_null { |b| b.pebble output } @caller = NaughtTestFixtures::Caller.new end end class PebbleBasicTest < NaughtTestCase include PebbleTestSetup def test_prints_method_name @null.info assert_match(/^info\(\)/, @output.string) end def test_prints_arguments @null.info("foo", 5, :sym) assert_match(/^info\('foo', 5, :sym\)/, @output.string) end def test_prints_caller_name @caller.call_method(@null) assert_match(/call_method$/, @output.string) end def test_returns_self assert_same @null, @null.info end end class PebbleBlockTest < NaughtTestCase include PebbleTestSetup def test_prints_block_and_method_info @caller.call_method_inside_block(@null) assert_match(/block/, @output.string) assert_match(/call_method_inside_block$/, @output.string) end def test_prints_nested_block_levels @caller.call_method_inside_nested_block(@null) assert_match(/block \(2 levels\)/, @output.string) assert_match(/call_method_inside_nested_block$/, @output.string) end end class PebbleEdgeCasesTest < NaughtTestCase include PebbleTestSetup def test_prints_full_caller_when_format_unrecognized Kernel.stub(:caller, ->(*) { ["unusual format without method info"] }) do @null.info end assert_match(/from unusual format without method info/, @output.string) end def test_calculates_block_levels_from_jruby_style_stack fake_stack = [ "fake.rb:1:in `block in call_method_inside_nested_block'", "fake.rb:2:in `block in each'", "fake.rb:3:in `block in call_method_inside_nested_block'", "unusual format without method info", "fake.rb:4:in `call_method_inside_nested_block'" ] Kernel.stub(:caller, ->(*) { fake_stack }) { @null.info } assert_match(/block \(2 levels\) call_method_inside_nested_block$/, @output.string) end end avdi-naught-dea6146/test/predicate_test.rb000066400000000000000000000050451515172043400206460ustar00rootroot00000000000000require "test_helper" # Shared assertions for predicate + black_hole combinations module PredicateBlackHoleAssertions def test_predicates_return_false refute_predicate @null, :too_much_coffee? end def test_other_methods_return_self assert_returns_self @null, :foobar end end class PredicateTest < NaughtTestCase def setup _, @null = build_null { |b| b.predicates_return false } end def test_responds_to_predicate_style_methods_with_false refute_predicate @null, :too_much_coffee? end def test_responds_to_other_methods_with_nil assert_nil @null.foobar end def test_reports_responding_to_predicate_methods assert_respond_to @null, :too_much_coffee? end end class PredicateBlackHoleTest < NaughtTestCase include PredicateBlackHoleAssertions def setup _, @null = build_null { |b| b.black_hole b.predicates_return false } end end class PredicateBlackHoleReverseOrderTest < NaughtTestCase include PredicateBlackHoleAssertions def setup _, @null = build_null { |b| b.predicates_return false b.black_hole } end end class PredicateMimicTest < NaughtTestCase def setup @null_class = build_null_class { |b| b.mimic NaughtTestFixtures::Coffee b.predicates_return false } @null = @null_class.new end def test_mimicked_predicate_returns_false refute_predicate @null, :black? end def test_other_mimicked_methods_return_nil assert_nil @null.origin end def test_does_not_respond_to_undefined_methods refute_respond_to @null, :leaf_variety end def test_raises_for_undefined_methods assert_raises(NoMethodError) { @null.leaf_variety } end end # Test for GitHub issue #60: predicates_return(false) breaks NullObject coercion # https://github.com/avdi/naught/issues/60 class PredicateCoercionTest < NaughtTestCase def setup @null_class = Naught.build { |b| b.predicates_return false } @null_class.define_method(:coerce) { |other| [other, 1] } @null = @null_class.new end def test_coerce_works_with_predicates_return assert_equal [5, 1], @null.coerce(5) end def test_responds_to_coerce assert_respond_to @null, :coerce end def test_division_coercion_works assert_equal 1, 1 / @null end def test_multiplication_coercion_works assert_equal 5, 5 * @null end def test_addition_coercion_works assert_equal 4, 3 + @null end def test_subtraction_coercion_works assert_equal 6, 7 - @null end def test_predicates_still_return_false refute_predicate @null, :valid? end end avdi-naught-dea6146/test/singleton_null_object_test.rb000066400000000000000000000012221515172043400232610ustar00rootroot00000000000000require "test_helper" class SingletonNullObjectTest < NaughtTestCase def setup @null_class = build_null_class(&:singleton) end def test_does_not_respond_to_new assert_raises(NoMethodError) { @null_class.new } end def test_has_only_one_instance assert_same @null_class.instance, @null_class.instance end def test_clone_and_dup_return_self null = @null_class.instance assert_same null, null.clone assert_same null, null.dup end def test_get_returns_the_singleton_instance assert_same @null_class.instance, @null_class.get end def test_get_ignores_arguments @null_class.get(42, foo: "bar") end end avdi-naught-dea6146/test/support/000077500000000000000000000000001515172043400170325ustar00rootroot00000000000000avdi-naught-dea6146/test/support/call_location_helper.rb000066400000000000000000000010321515172043400235150ustar00rootroot00000000000000# Helper for creating CallLocation objects in tests module CallLocationHelper # Default values for test CallLocation instances DEFAULT_LOCATION = { label: :foo, args: [1], path: "/path/to/file.rb", lineno: 42, base_label: "method" }.freeze # Create a CallLocation with default values that can be overridden # # @param overrides [Hash] attributes to override # @return [Naught::CallLocation] def make_location(overrides = {}) Naught::CallLocation.new(**DEFAULT_LOCATION.merge(overrides)) end end avdi-naught-dea6146/test/support/convertable_null.rb000066400000000000000000000001231515172043400227110ustar00rootroot00000000000000ConvertableNull = Naught.build do |b| b.null_equivalents << "" b.traceable end avdi-naught-dea6146/test/support/fixtures.rb000066400000000000000000000100271515172043400212300ustar00rootroot00000000000000# Shared test fixtures for Naught tests # # This module provides reusable fixture classes for testing # null object behavior across different test files. module NaughtTestFixtures # Simple 2D point with x and y coordinates class Point attr_reader :x, :y end # A user with login credentials class User attr_reader :login end # Authorization capability mixin module Authorizable def authorized_for?(_) end end # A library patron with borrowed book tracking class LibraryPatron < User include Authorizable attr_reader :name def member? end def notify_of_overdue_books(_) end end # BasicObject subclass for testing mimic with BasicObject class BasicWidget < BasicObject def widget_method end end # A class with method_missing, similar to ActiveRecord class DynamicClass def regular_method "regular" end def active? true end def method_missing(method_name, *) if method_name.to_s.start_with?("dynamic_") "dynamic: #{method_name}" else super end end def respond_to_missing?(method_name, include_private = false) method_name.to_s.start_with?("dynamic_") || super end end # Coffee class for predicate testing class Coffee attr_reader :origin def black? = nil end # Caller class for pebble testing class Caller def call_method(thing) = thing.info def call_method_inside_block(thing) = 2.times { thing.info } def call_method_inside_nested_block(thing) = 2.times { 2.times { thing.info } } end # Simulates Stripe-style objects that define methods dynamically based on data # These objects expose a `keys` method to discover available attributes class StripeStyleObject def initialize(values = {}) @values = values end def keys @values.keys end def regular_method "regular" end def method_missing(name, *) if @values.key?(name) @values[name] else super end end def respond_to_missing?(name, include_private = false) @values.key?(name) || super end end # Simulates ActiveRecord-style objects with attribute_names class ActiveRecordStyleObject def initialize(attributes = {}) @attributes = attributes end def attribute_names @attributes.keys.map(&:to_s) end def regular_method "regular" end def method_missing(name, *) if @attributes.key?(name) @attributes[name] else super end end def respond_to_missing?(name, include_private = false) @attributes.key?(name) || super end end # Simulates OpenStruct-style objects with to_h class OpenStructStyleObject def initialize(values = {}) @values = values end def to_h @values.dup end def regular_method "regular" end def method_missing(name, *) if @values.key?(name) @values[name] else super end end def respond_to_missing?(name, include_private = false) @values.key?(name) || super end end # Object whose to_h raises an error class BrokenToHObject def initialize(values = {}) @values = values end def to_h raise "broken!" end def regular_method "regular" end def method_missing(name, *) if @values.key?(name) @values[name] else super end end def respond_to_missing?(name, include_private = false) @values.key?(name) || super end end # Object whose to_h returns a non-Hash value class NonHashToHObject def initialize(values = {}) @values = values end def to_h # Returns an Array instead of a Hash @values.to_a end def regular_method "regular" end def method_missing(name, *) if @values.key?(name) @values[name] else super end end def respond_to_missing?(name, include_private = false) @values.key?(name) || super end end end avdi-naught-dea6146/test/support/naught_test_case.rb000066400000000000000000000033221515172043400226770ustar00rootroot00000000000000# Shared test base class for Naught tests # # Provides common setup patterns and helper methods for testing # null object configurations. class NaughtTestCase < Minitest::Test # Build a null class with the given configuration block # # @yield [builder] configuration block # @return [Class] the generated null class def build_null_class(&) Naught.build(&) end # Build a null class and create an instance # # @yield [builder] configuration block # @return [Array(Class, Object)] the null class and instance def build_null(&) null_class = build_null_class(&) [null_class, null_class.new] end # Assert that an object responds to any method name # # @param obj [Object] the object to test def assert_responds_to_anything(obj) assert_respond_to obj, :foo assert_respond_to obj, :bar_baz assert_respond_to obj, :any_method_at_all end # Assert that a method returns nil # # @param obj [Object] the object to call the method on # @param method_name [Symbol] the method to call def assert_returns_nil(obj, method_name) assert_nil obj.public_send(method_name) end # Assert that a method returns self # # @param obj [Object] the object to call the method on # @param method_name [Symbol] the method to call def assert_returns_self(obj, method_name) assert_same obj, obj.public_send(method_name) end # Assert that an object is a null object # # @param obj [Object] the object to test def assert_null_object(obj) assert_operator Naught::NullObjectTag, :===, obj end # Refute that an object is a null object # # @param obj [Object] the object to test def refute_null_object(obj) refute_operator Naught::NullObjectTag, :===, obj end end avdi-naught-dea6146/test/test_helper.rb000066400000000000000000000006501515172043400201620ustar00rootroot00000000000000GEM_ROOT = File.expand_path("..", __dir__) $LOAD_PATH.unshift File.join(GEM_ROOT, "lib") require "simplecov" SimpleCov.start do add_filter "/test/" if RUBY_ENGINE != "jruby" enable_coverage :branch minimum_coverage line: 100, branch: 100 else minimum_coverage line: 99.5 end end require "minitest/autorun" require "naught" Dir[File.join(GEM_ROOT, "test", "support", "**/*.rb")].each { |f| require f }