├── .github ├── dependabot.yml └── workflows │ ├── push_gem.yml │ └── test.yml ├── .gitignore ├── BSDL ├── COPYING ├── Gemfile ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib └── singleton.rb ├── singleton.gemspec └── test └── test_singleton.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/singleton' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/singleton 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.MATZBOT_GITHUB_WORKFLOW_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | min_version: 2.4 10 | test: 11 | needs: ruby-versions 12 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 17 | os: [ ubuntu-latest, macos-latest, windows-latest ] 18 | exclude: 19 | - { os: macos-latest, ruby: 2.4 } 20 | - { os: macos-latest, ruby: 2.5 } 21 | - { os: windows-latest, ruby: truffleruby } 22 | - { os: windows-latest, ruby: truffleruby-head } 23 | runs-on: ${{ matrix.os }} 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up Ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby }} 30 | - name: Install dependencies 31 | run: bundle install 32 | - name: Run test 33 | run: rake test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "bundler" 7 | gem "rake" 8 | gem "test-unit" 9 | end 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Singleton 2 | 3 | The Singleton module implements the Singleton pattern. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'singleton' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install singleton 20 | 21 | ## Usage 22 | 23 | To use Singleton, include the module in your class. 24 | 25 | ```ruby 26 | class Klass 27 | include Singleton 28 | # ... 29 | end 30 | ``` 31 | 32 | This ensures that only one instance of Klass can be created. 33 | 34 | ```ruby 35 | a,b = Klass.instance, Klass.instance 36 | 37 | a == b 38 | # => true 39 | 40 | Klass.new 41 | # => NoMethodError - new is private ... 42 | ``` 43 | 44 | The instance is created at upon the first call of Klass.instance(). 45 | 46 | ```ruby 47 | class OtherKlass 48 | include Singleton 49 | # ... 50 | end 51 | 52 | ObjectSpace.each_object(OtherKlass){} 53 | # => 0 54 | 55 | OtherKlass.instance 56 | ObjectSpace.each_object(OtherKlass){} 57 | # => 1 58 | ``` 59 | 60 | This behavior is preserved under inheritance and cloning. 61 | 62 | ## Development 63 | 64 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 65 | 66 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 67 | 68 | ## Contributing 69 | 70 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/singleton. 71 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "singleton" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/singleton.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The Singleton module implements the Singleton pattern. 4 | # 5 | # == Usage 6 | # 7 | # To use Singleton, include the module in your class. 8 | # 9 | # class Klass 10 | # include Singleton 11 | # # ... 12 | # end 13 | # 14 | # This ensures that only one instance of Klass can be created. 15 | # 16 | # a,b = Klass.instance, Klass.instance 17 | # 18 | # a == b 19 | # # => true 20 | # 21 | # Klass.new 22 | # # => NoMethodError - new is private ... 23 | # 24 | # The instance is created at upon the first call of Klass.instance(). 25 | # 26 | # class OtherKlass 27 | # include Singleton 28 | # # ... 29 | # end 30 | # 31 | # ObjectSpace.each_object(OtherKlass){} 32 | # # => 0 33 | # 34 | # OtherKlass.instance 35 | # ObjectSpace.each_object(OtherKlass){} 36 | # # => 1 37 | # 38 | # 39 | # This behavior is preserved under inheritance and cloning. 40 | # 41 | # == Implementation 42 | # 43 | # This above is achieved by: 44 | # 45 | # * Making Klass.new and Klass.allocate private. 46 | # 47 | # * Overriding Klass.inherited(sub_klass) and Klass.clone() to ensure that the 48 | # Singleton properties are kept when inherited and cloned. 49 | # 50 | # * Providing the Klass.instance() method that returns the same object each 51 | # time it is called. 52 | # 53 | # * Overriding Klass._load(str) to call Klass.instance(). 54 | # 55 | # * Overriding Klass#clone and Klass#dup to raise TypeErrors to prevent 56 | # cloning or duping. 57 | # 58 | # == Singleton and Marshal 59 | # 60 | # By default Singleton's #_dump(depth) returns the empty string. Marshalling by 61 | # default will strip state information, e.g. instance variables from the instance. 62 | # Classes using Singleton can provide custom _load(str) and _dump(depth) methods 63 | # to retain some of the previous state of the instance. 64 | # 65 | # require 'singleton' 66 | # 67 | # class Example 68 | # include Singleton 69 | # attr_accessor :keep, :strip 70 | # def _dump(depth) 71 | # # this strips the @strip information from the instance 72 | # Marshal.dump(@keep, depth) 73 | # end 74 | # 75 | # def self._load(str) 76 | # instance.keep = Marshal.load(str) 77 | # instance 78 | # end 79 | # end 80 | # 81 | # a = Example.instance 82 | # a.keep = "keep this" 83 | # a.strip = "get rid of this" 84 | # 85 | # stored_state = Marshal.dump(a) 86 | # 87 | # a.keep = nil 88 | # a.strip = nil 89 | # b = Marshal.load(stored_state) 90 | # p a == b # => true 91 | # p a.keep # => "keep this" 92 | # p a.strip # => nil 93 | # 94 | module Singleton 95 | VERSION = "0.3.0" 96 | 97 | module SingletonInstanceMethods 98 | # Raises a TypeError to prevent cloning. 99 | def clone 100 | raise TypeError, "can't clone instance of singleton #{self.class}" 101 | end 102 | 103 | # Raises a TypeError to prevent duping. 104 | def dup 105 | raise TypeError, "can't dup instance of singleton #{self.class}" 106 | end 107 | 108 | # By default, do not retain any state when marshalling. 109 | def _dump(depth = -1) 110 | '' 111 | end 112 | end 113 | include SingletonInstanceMethods 114 | 115 | module SingletonClassMethods # :nodoc: 116 | 117 | def clone # :nodoc: 118 | Singleton.__init__(super) 119 | end 120 | 121 | # By default calls instance(). Override to retain singleton state. 122 | def _load(str) 123 | instance 124 | end 125 | 126 | def instance # :nodoc: 127 | @singleton__instance__ || @singleton__mutex__.synchronize { @singleton__instance__ ||= new } 128 | end 129 | 130 | private 131 | 132 | def inherited(sub_klass) 133 | super 134 | Singleton.__init__(sub_klass) 135 | end 136 | 137 | def set_instance(val) 138 | @singleton__instance__ = val 139 | end 140 | 141 | def set_mutex(val) 142 | @singleton__mutex__ = val 143 | end 144 | end 145 | 146 | def self.module_with_class_methods 147 | SingletonClassMethods 148 | end 149 | 150 | module SingletonClassProperties 151 | 152 | def self.included(c) 153 | # extending an object with Singleton is a bad idea 154 | c.undef_method :extend_object 155 | end 156 | 157 | def self.extended(c) 158 | # extending an object with Singleton is a bad idea 159 | c.singleton_class.send(:undef_method, :extend_object) 160 | end 161 | 162 | def __init__(klass) # :nodoc: 163 | klass.instance_eval { 164 | set_instance(nil) 165 | set_mutex(Thread::Mutex.new) 166 | } 167 | klass 168 | end 169 | 170 | private 171 | 172 | def append_features(mod) 173 | # help out people counting on transitive mixins 174 | unless mod.instance_of?(Class) 175 | raise TypeError, "Inclusion of the OO-Singleton module in module #{mod}" 176 | end 177 | super 178 | end 179 | 180 | def included(klass) 181 | super 182 | klass.private_class_method :new, :allocate 183 | klass.extend module_with_class_methods 184 | Singleton.__init__(klass) 185 | end 186 | end 187 | extend SingletonClassProperties 188 | 189 | ## 190 | # :singleton-method: _load 191 | # By default calls instance(). Override to retain singleton state. 192 | 193 | ## 194 | # :singleton-method: instance 195 | # Returns the singleton instance. 196 | end 197 | 198 | if defined?(Ractor) 199 | module RactorLocalSingleton 200 | include Singleton::SingletonInstanceMethods 201 | 202 | module RactorLocalSingletonClassMethods 203 | include Singleton::SingletonClassMethods 204 | def instance 205 | set_mutex(Thread::Mutex.new) if Ractor.current[mutex_key].nil? 206 | return Ractor.current[instance_key] if Ractor.current[instance_key] 207 | Ractor.current[mutex_key].synchronize { 208 | return Ractor.current[instance_key] if Ractor.current[instance_key] 209 | set_instance(new()) 210 | } 211 | Ractor.current[instance_key] 212 | end 213 | 214 | private 215 | 216 | def instance_key 217 | :"__RactorLocalSingleton_instance_with_class_id_#{object_id}__" 218 | end 219 | 220 | def mutex_key 221 | :"__RactorLocalSingleton_mutex_with_class_id_#{object_id}__" 222 | end 223 | 224 | def set_instance(val) 225 | Ractor.current[instance_key] = val 226 | end 227 | 228 | def set_mutex(val) 229 | Ractor.current[mutex_key] = val 230 | end 231 | end 232 | 233 | def self.module_with_class_methods 234 | RactorLocalSingletonClassMethods 235 | end 236 | 237 | extend Singleton::SingletonClassProperties 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /singleton.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-")+1, ".").join("/")].find do |dir| 5 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 6 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 7 | end rescue nil 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = name 12 | spec.version = version 13 | spec.authors = ["Yukihiro Matsumoto"] 14 | spec.email = ["matz@ruby-lang.org"] 15 | 16 | spec.summary = %q{The Singleton module implements the Singleton pattern.} 17 | spec.description = spec.summary 18 | spec.homepage = "https://github.com/ruby/singleton" 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | 24 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 25 | `git ls-files -z 2>#{IO::NULL}`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 26 | end 27 | spec.bindir = "exe" 28 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 29 | spec.require_paths = ["lib"] 30 | end 31 | -------------------------------------------------------------------------------- /test/test_singleton.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'singleton' 4 | 5 | class TestSingleton < Test::Unit::TestCase 6 | class SingletonTest 7 | include Singleton 8 | end 9 | 10 | def test_marshal 11 | o1 = SingletonTest.instance 12 | m = Marshal.dump(o1) 13 | o2 = Marshal.load(m) 14 | assert_same(o1, o2) 15 | end 16 | 17 | def test_instance_never_changes 18 | a = SingletonTest.instance 19 | b = SingletonTest.instance 20 | assert_same a, b 21 | end 22 | 23 | def test_initialize_raises_exception 24 | assert_raise NoMethodError do 25 | SingletonTest.new 26 | end 27 | end 28 | 29 | def test_allocate_raises_exception 30 | assert_raise NoMethodError do 31 | SingletonTest.allocate 32 | end 33 | end 34 | 35 | def test_clone_raises_exception 36 | exception = assert_raise TypeError do 37 | SingletonTest.instance.clone 38 | end 39 | 40 | expected = "can't clone instance of singleton TestSingleton::SingletonTest" 41 | 42 | assert_equal expected, exception.message 43 | end 44 | 45 | def test_dup_raises_exception 46 | exception = assert_raise TypeError do 47 | SingletonTest.instance.dup 48 | end 49 | 50 | expected = "can't dup instance of singleton TestSingleton::SingletonTest" 51 | 52 | assert_equal expected, exception.message 53 | end 54 | 55 | def test_include_in_module_raises_exception 56 | mod = Module.new 57 | 58 | exception = assert_raise TypeError do 59 | mod.class_eval do 60 | include Singleton 61 | end 62 | end 63 | 64 | expected = "Inclusion of the OO-Singleton module in module #{mod}" 65 | 66 | assert_equal expected, exception.message 67 | end 68 | 69 | def test_extending_singleton_raises_exception 70 | assert_raise NoMethodError do 71 | 'foo'.extend Singleton 72 | end 73 | end 74 | 75 | def test_inheritance_works_with_overridden_inherited_method 76 | super_super_called = false 77 | 78 | outer = Class.new do 79 | define_singleton_method :inherited do |sub| 80 | super_super_called = true 81 | end 82 | end 83 | 84 | inner = Class.new(outer) do 85 | include Singleton 86 | end 87 | 88 | tester = Class.new(inner) 89 | 90 | assert super_super_called 91 | 92 | a = tester.instance 93 | b = tester.instance 94 | assert_same a, b 95 | end 96 | 97 | def test_inheritance_creates_separate_singleton 98 | a = SingletonTest.instance 99 | b = Class.new(SingletonTest).instance 100 | 101 | assert_not_same a, b 102 | end 103 | 104 | def test_inheritance_instantiation 105 | klass = Class.new do 106 | include Singleton 107 | 108 | public_class_method :new 109 | end 110 | 111 | assert Class.new(klass).new 112 | end 113 | 114 | def test_class_level_cloning_preserves_singleton_behavior 115 | klass = SingletonTest.clone 116 | 117 | a = klass.instance 118 | b = klass.instance 119 | assert_same a, b 120 | end 121 | 122 | def test_class_level_cloning_creates_separate_singleton 123 | assert_not_same SingletonTest.instance, SingletonTest.clone.instance 124 | end 125 | end 126 | --------------------------------------------------------------------------------