├── Gemfile ├── init.rb ├── CONTRIBUTORS ├── .gitignore ├── Manifest.txt ├── Rakefile ├── History.rdoc ├── Gemfile.lock ├── acts_as_singleton.gemspec ├── lib └── acts_as_singleton.rb ├── README.rdoc └── test └── acts_as_singleton_test.rb /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require "acts_as_singleton" 2 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | 13 Stephen Celis 2 | 2 logicaltext 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.gem 3 | bin 4 | doc 5 | pkg 6 | rdoc 7 | vendor 8 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | History.txt 2 | Manifest.txt 3 | README.txt 4 | Rakefile 5 | init.rb 6 | lib/acts_as_singleton.rb 7 | test/acts_as_singleton_test.rb 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | $: << File.expand_path('../lib', __FILE__) 2 | require 'active_record' 3 | require 'acts_as_singleton' 4 | 5 | require 'rake' 6 | require 'rake/testtask' 7 | 8 | desc 'Default: run unit tests.' 9 | task :default => :test 10 | 11 | desc 'Test the acts_as_singleton plugin.' 12 | Rake::TestTask.new do |t| 13 | t.libs << 'lib' 14 | t.libs << 'test' 15 | t.pattern = 'test/**/*_test.rb' 16 | t.verbose = true 17 | end 18 | -------------------------------------------------------------------------------- /History.rdoc: -------------------------------------------------------------------------------- 1 | === 0.0.4 / 2010-01-21 2 | 3 | * 1 bugfix 4 | 5 | * Allow 'find' calls from ActiveRecord. 6 | 7 | 8 | === 0.0.3 / 2009-11-29 9 | 10 | * 1 bugfix 11 | 12 | * Don't make ActiveRecord::Base.allocate private. 13 | 14 | 15 | === 0.0.2 / 2009-04-13 16 | 17 | * 1 minor enhancement 18 | 19 | * Update regular expression to allow for a needed method. 20 | 21 | 22 | === 0.0.1 / 2009-03-30 23 | 24 | * 1 major enhancement 25 | 26 | * Birthday! 27 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | acts_as_singleton (0.0.7) 5 | activerecord (~> 3.1.0) 6 | 7 | GEM 8 | remote: http://rubygems.org/ 9 | specs: 10 | activemodel (3.1.0) 11 | activesupport (= 3.1.0) 12 | bcrypt-ruby (~> 3.0.0) 13 | builder (~> 3.0.0) 14 | i18n (~> 0.6) 15 | activerecord (3.1.0) 16 | activemodel (= 3.1.0) 17 | activesupport (= 3.1.0) 18 | arel (~> 2.2.1) 19 | tzinfo (~> 0.3.29) 20 | activesupport (3.1.0) 21 | multi_json (~> 1.0) 22 | arel (2.2.1) 23 | bcrypt-ruby (3.0.0) 24 | builder (3.0.0) 25 | i18n (0.6.0) 26 | multi_json (1.0.3) 27 | rake (0.9.2) 28 | sqlite3 (1.3.4) 29 | sqlite3-ruby (1.3.3) 30 | sqlite3 (>= 1.3.3) 31 | tzinfo (0.3.29) 32 | 33 | PLATFORMS 34 | ruby 35 | 36 | DEPENDENCIES 37 | acts_as_singleton! 38 | rake 39 | sqlite3-ruby 40 | -------------------------------------------------------------------------------- /acts_as_singleton.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "acts_as_singleton" 3 | s.version = "0.0.8" 4 | s.summary = "A lightweight singleton library for your Active Record models." 5 | s.description = "It just makes sense to store mutable, site-wide, admin-level settings in the database. Right? A key-value table may be more flexible, but maybe we don't want to be flexible! If you truly want that flexibility: http://github.com/stephencelis/kvc" 6 | 7 | s.files = ["History.rdoc", "Manifest.txt", "README.rdoc", "Rakefile", "init.rb", "lib/acts_as_singleton.rb", "test/acts_as_singleton_test.rb"] 8 | 9 | s.add_dependency "activerecord", ">= 3.1.0" 10 | s.add_development_dependency "rake" 11 | s.add_development_dependency "sqlite3-ruby" 12 | 13 | s.has_rdoc = true 14 | s.extra_rdoc_files = %w(History.rdoc Manifest.txt README.rdoc) 15 | s.rdoc_options = %w(--main README.rdoc) 16 | 17 | s.author = "Stephen Celis" 18 | s.email = "stephen@stephencelis.com" 19 | s.homepage = "http://github.com/stephencelis/acts_as_singleton" 20 | end 21 | -------------------------------------------------------------------------------- /lib/acts_as_singleton.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | # A lightweight singleton library for your Active Record models. 3 | # 4 | # class HomepageSettings < ActiveRecord::Base 5 | # include ActiveRecord::Singleton 6 | # end 7 | # 8 | # HomepageSettings.instance # => # 9 | # 10 | # Like Ruby's built-in module, it will not prevent you from instantiating 11 | # more than one object, but it will make it more difficult. 12 | # 13 | # HomepageSettings.__send__(:new).new_record? # => true 14 | # 15 | # Most methods assuming more than one database record will be made private 16 | # upon including this module. Make sure your public class methods do not 17 | # clash with the ActiveRecord::Singleton::PRIVATE pattern, or 18 | # define them after including the module. 19 | module Singleton 20 | VERSION = "0.0.8" 21 | 22 | # This pattern matches methods that should be made private because they 23 | # should not be used in singleton classes. 24 | PRIVATE = \ 25 | /^all$|^create(?!_reflection|_callback)|^find(?!er|_callback|_generated)|^first|^minimum$|^maximum$|^new$|d_sco|^update/ 26 | 27 | def self.included(model) 28 | model.class_eval do 29 | private_class_method *methods.grep(PRIVATE) # Deny existent others. 30 | 31 | class << self 32 | def exists? # Refuse arguments. 33 | super 34 | end 35 | 36 | # Returns the first (presumably only) record in the database, or 37 | # creates one. 38 | # 39 | # If validation fails on creation, it will return an unsaved object. 40 | # 41 | # HomepageSettings.instance.update_attributes :welcome => "Hello!" 42 | def instance 43 | first || create 44 | end 45 | 46 | def find(*) 47 | unless caller.first.include?("lib/active_record") 48 | raise NoMethodError, 49 | "private method `find' called for #{inspect}" 50 | end 51 | super 52 | end 53 | 54 | def find_by_sql(*) 55 | unless caller.first.include?("lib/active_record") 56 | raise NoMethodError, 57 | "private method `find_by_sql' called for #{inspect}" 58 | end 59 | super 60 | end 61 | end 62 | 63 | def clone 64 | raise TypeError, "can't clone instance of singleton #{self.class}" 65 | end 66 | 67 | def dup 68 | raise TypeError, "can't dup instance of singleton #{self.class}" 69 | end 70 | 71 | def inspect 72 | super.sub(/id: .+?, /) {} # Irrelevant. 73 | end 74 | end 75 | end 76 | end 77 | 78 | class << Base 79 | # Class method to provide a more Rails-like experience. Merely includes 80 | # the ActiveRecord::Singleton module. 81 | # 82 | # class HomepageSettings < ActiveRecord::Base 83 | # acts_as_singleton # Equivalent to "include ActiveRecord::Singleton". 84 | # end 85 | def acts_as_singleton 86 | include Singleton 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = acts_as_singleton 2 | 3 | http://github.com/stephencelis/acts_as_singleton 4 | 5 | 6 | == DESCRIPTION 7 | 8 | A lightweight singleton library for your Active Record models. 9 | 10 | It just makes sense to store mutable, site-wide, admin-level settings in the 11 | database. Right? A key-value table may be more flexible, but maybe we don't 12 | want to be flexible! 13 | 14 | If you truly want that flexibility: http://github.com/stephencelis/kvc 15 | 16 | 17 | == FEATURES/PROBLEMS 18 | 19 | * Lightning-fast queries! Doesn't get any faster than this! (Guaranteed.) 20 | * Follows the ultra-cool "singleton" pattern! (Tell your friends.) 21 | * What? That's not the "cool" singleton in Ruby? (Don't tell your friends?) 22 | 23 | 24 | == SYNOPSIS 25 | 26 | class HomepageSettings < ActiveRecord::Base 27 | include ActiveRecord::Singleton 28 | end 29 | 30 | 31 | How Rubyish! Oh, you want it Railsish? Very well... 32 | 33 | class HomepageSettings < ActiveRecord::Base 34 | acts_as_singleton 35 | end 36 | 37 | 38 | Have your cake and eat your silly idiom, too! Just try to create a row! 39 | 40 | HomepageSettings.instance # => # 41 | 42 | 43 | Don't even try to access it otherwise. It won't work! 44 | 45 | 46 | == REQUIREMENTS 47 | 48 | * Active Record 2.3.2 or greater. 49 | * A delicate palate. 50 | 51 | 52 | == INSTALL 53 | 54 | === As a gem 55 | 56 | Configure: 57 | 58 | # config/environment.rb 59 | config.gem "acts_as_singleton" 60 | 61 | 62 | And install: 63 | 64 | % [sudo] rake gems:install 65 | 66 | 67 | === As a plugin 68 | 69 | Traditional: 70 | 71 | % script/plugin install git://github.com/stephencelis/acts_as_singleton.git 72 | 73 | 74 | Or, as a submodule: 75 | 76 | % git submodule add git://github.com/stephencelis/acts_as_singleton.git \ 77 | vendor/plugins/acts_as_singleton 78 | 79 | 80 | == LICENSE 81 | 82 | (The MIT License) 83 | 84 | (c) 2009-2012 Stephen Celis, stephen@stephencelis.com. 85 | 86 | Permission is hereby granted, free of charge, to any person obtaining a copy 87 | of this software and associated documentation files (the "Software"), to deal 88 | in the Software without restriction, including without limitation the rights 89 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 90 | copies of the Software, and to permit persons to whom the Software is 91 | furnished to do so, subject to the following conditions: 92 | 93 | The above copyright notice and this permission notice shall be included in all 94 | copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 97 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 98 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 99 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 100 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 101 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 102 | SOFTWARE. 103 | -------------------------------------------------------------------------------- /test/acts_as_singleton_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'active_record' 3 | require "#{File.dirname(__FILE__)}/../init" 4 | require 'active_support' 5 | require 'active_support/test_case' 6 | ActiveRecord::Migration.verbose = false 7 | 8 | def setup_db 9 | ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:') 10 | ActiveRecord::Schema.define(:version => 1) do 11 | create_table :homepage_settings do |t| 12 | t.string :type 13 | t.text :welcome_message 14 | t.timestamp :published_at, :expired_at 15 | end 16 | 17 | create_table :cottages do |t| 18 | t.belongs_to :home_away_from_home, :polymorphic => true 19 | end 20 | end 21 | end 22 | 23 | def teardown_db 24 | ActiveRecord::Base.connection.tables.each do |table| 25 | ActiveRecord::Base.connection.drop_table(table) 26 | end 27 | end 28 | 29 | setup_db 30 | class HomepageSettings < ActiveRecord::Base 31 | acts_as_singleton 32 | has_many :cottages, :as => :home_away_from_home 33 | end 34 | 35 | class LoggedInSettings < HomepageSettings 36 | end 37 | 38 | class Cottage < ActiveRecord::Base 39 | belongs_to :home_away_from_home, :polymorphic => true 40 | validates_associated :home_away_from_home 41 | end 42 | 43 | class ActsAsSingletonTest < ActiveSupport::TestCase 44 | setup do 45 | setup_db 46 | end 47 | 48 | teardown do 49 | teardown_db 50 | 51 | HomepageSettings.instance_eval do 52 | remove_instance_variable :@instance if defined? @instance 53 | end 54 | end 55 | 56 | test "should be provocative" do 57 | assert_raise(NoMethodError) { HomepageSettings.new } 58 | assert_raise(NoMethodError) { HomepageSettings.create } 59 | assert_raise(NoMethodError) { HomepageSettings.update } 60 | assert_raise(NoMethodError) { HomepageSettings.alloc } 61 | assert_raise(NoMethodError) { HomepageSettings.all } 62 | assert_raise(NoMethodError) { HomepageSettings.first } 63 | assert_raise(NoMethodError) { HomepageSettings.find } 64 | assert_raise(NoMethodError) { HomepageSettings.find_by_sql("") } 65 | assert_raise(NoMethodError) { HomepageSettings.named_scope } 66 | assert_raise(NoMethodError) { HomepageSettings.minimum } 67 | assert_raise(NoMethodError) { HomepageSettings.maximum } 68 | assert_raise(ArgumentError) { HomepageSettings.exists? 1 } 69 | assert_raise(TypeError) { HomepageSettings.instance.clone } 70 | assert_raise(TypeError) { HomepageSettings.instance.dup } 71 | end 72 | 73 | test "should be lazy" do 74 | assert !HomepageSettings.exists? 75 | assert_instance_of HomepageSettings, HomepageSettings.instance 76 | assert HomepageSettings.exists? 77 | end 78 | 79 | test "should be equal" do 80 | assert_equal HomepageSettings.instance, HomepageSettings.instance 81 | end 82 | 83 | test "should be unidentifiable" do 84 | assert_no_match(/id: .+?,/, HomepageSettings.inspect) 85 | assert_no_match(/id: .+?,/, HomepageSettings.instance.inspect) 86 | end 87 | 88 | test "should be mutable" do 89 | assert_nothing_raised do 90 | HomepageSettings.instance.update_attributes! :welcome_message => "OH HAI" 91 | end 92 | end 93 | 94 | test "should honor STI" do 95 | ActiveRecord::Base.clear_active_connections! 96 | HomepageSettings.instance # Fetch parent. 97 | LoggedInSettings.instance # Fetch child. 98 | assert_nothing_raised ActiveRecord::SubclassNotFound do 99 | LoggedInSettings.instance # Fetch again. 100 | end 101 | end 102 | 103 | test "should honor polymorphism" do 104 | HomepageSettings.instance.cottages.create 105 | end 106 | end 107 | --------------------------------------------------------------------------------