From 9a6f80cc538f0f5a6c8b555bf19f8da72fa85db0 Mon Sep 17 00:00:00 2001 From: mliem2k Date: Sun, 12 Jul 2026 20:32:53 +0800 Subject: [PATCH] Add Module#private_constants block for concise private constants Constants don't respect private/public regions the way methods do, so making one private normally means repeating its name in a separate private_constant call. private_constants { ... } marks every constant defined within the block as private automatically. --- README.md | 24 +++++++++++++ lib/type_toolkit/ext/module.rb | 26 +++++++++++++++ spec/private_constants_spec.rb | 61 ++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 spec/private_constants_spec.rb diff --git a/README.md b/README.md index 39f312b..72e07b8 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,30 @@ EmailNotifier.new.send_notification("Hello, world!") # ❌ TypeToolkit::Abstract # => Abstract method #send_notification was never implemented. ``` +### Private constants + +Unlike methods, constants don't respect `private`/`public` regions, so making one private normally means repeating its name in a separate `private_constant` call: + +```ruby +class Configuration + DEFAULT_TIMEOUT = 30 + private_constant :DEFAULT_TIMEOUT +end +``` + +Type Toolkit's `private_constants` marks every constant defined within its block as private, so you don't have to repeat yourself: + +```ruby +class Configuration + private_constants do + DEFAULT_TIMEOUT = 30 + end +end + +Configuration::DEFAULT_TIMEOUT # ❌ NameError +# => private constant Configuration::DEFAULT_TIMEOUT referenced +``` + ## Guiding Principles ### Blazingly fast™ diff --git a/lib/type_toolkit/ext/module.rb b/lib/type_toolkit/ext/module.rb index 1621a96..0300831 100644 --- a/lib/type_toolkit/ext/module.rb +++ b/lib/type_toolkit/ext/module.rb @@ -1,7 +1,33 @@ +# typed: true # frozen_string_literal: true class Module def interface! TypeToolkit.make_interface!(self) end + + # Marks every constant defined directly within the block as private. + # + # Constants don't respect `private`/`public` regions the way methods do, so making one private + # normally means repeating its name in a separate `private_constant` call. This bundles that up + # into a single block, so you don't have to. + # + # Example: + # + # class Configuration + # private_constants do + # DEFAULT_TIMEOUT = 30 + # end + # end + # + # Configuration::DEFAULT_TIMEOUT + # # => NameError: private constant Configuration::DEFAULT_TIMEOUT referenced + # + #: () { () -> void } -> void + def private_constants(&block) + constants_before = constants(false) + yield + new_constants = constants(false) - constants_before + new_constants.each { |constant_name| private_constant(constant_name) } + end end diff --git a/spec/private_constants_spec.rb b/spec/private_constants_spec.rb new file mode 100644 index 0000000..2032b13 --- /dev/null +++ b/spec/private_constants_spec.rb @@ -0,0 +1,61 @@ +# typed: ignore +# frozen_string_literal: true + +require "spec_helper" + +module TypeToolkit + class PrivateConstantsSpec < Minitest::Spec + class WithPrivateConstants + private_constants do + SECRET = 123 + ANOTHER_SECRET = 456 + end + + PUBLIC_CONSTANT = 789 + + class << self + def secret = SECRET + end + end + + class WithNestedPrivateConstant + private_constants do + class Inner + end + end + end + + describe "Module#private_constants" do + it "marks every constant defined within the block as private" do + assert_raises(NameError) { WithPrivateConstants::SECRET } + assert_raises(NameError) { WithPrivateConstants::ANOTHER_SECRET } + end + + it "still allows the defining scope to reference the constant unqualified" do + assert_equal 123, WithPrivateConstants.secret + end + + it "does not affect constants defined outside the block" do + assert_equal 789, WithPrivateConstants::PUBLIC_CONSTANT + end + + it "works for constants defined via the `class`/`module` keywords, not just `=`" do + assert_raises(NameError) { WithNestedPrivateConstant::Inner } + end + + it "does not raise when the block defines no new constants" do + WithPrivateConstants.private_constants {} + end + + it "propagates exceptions raised inside the block" do + assert_raises(RuntimeError) do + Module.new do + private_constants do + raise "boom" + end + end + end + end + end + end +end