├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── .travis.yml ├── Appraisals ├── Gemfile ├── MIT-LICENSE ├── README.rdoc ├── Rakefile ├── contributors.txt ├── gemfiles ├── rails_4.gemfile ├── rails_4.gemfile.lock ├── rails_5.gemfile ├── rails_5.gemfile.lock ├── rails_6_edge.gemfile └── rails_6_edge.gemfile.lock ├── lib ├── generators │ └── setler │ │ ├── setler_generator.rb │ │ └── templates │ │ ├── migration.rb │ │ └── model.rb ├── setler.rb └── setler │ ├── active_record.rb │ ├── exceptions.rb │ ├── scoped_settings.rb │ ├── settings.rb │ └── version.rb ├── setler.gemspec └── test ├── settings_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | coverage.data 3 | *.gem 4 | .bundle 5 | Gemfile.lock 6 | pkg/* 7 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | setler 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.5 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.8 4 | - 2.4.7 5 | - 2.5.7 6 | - 2.6.5 7 | before_install: "gem install bundler:1.17.2" 8 | cache: bundler 9 | script: "bundle exec rake test" 10 | notifications: 11 | email: false 12 | gemfile: 13 | - gemfiles/rails_4.gemfile 14 | - gemfiles/rails_5.gemfile 15 | - gemfiles/rails_6_edge.gemfile 16 | matrix: 17 | fast_finish: true 18 | exclude: 19 | - rvm: 2.3.8 20 | gemfile: gemfiles/rails_6_edge.gemfile 21 | - rvm: 2.4.7 22 | gemfile: gemfiles/rails_6_edge.gemfile 23 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails-4" do 2 | gem "rails", "4.2.11.1" 3 | gem 'sqlite3', '1.3.13' # 5/13/2019: LOCKED DOWN AGAIN. 4 | end 5 | 6 | appraise "rails-5" do 7 | gem "rails", "5.2.3" 8 | gem 'sqlite3' 9 | end 10 | 11 | appraise "rails-6-edge" do 12 | gem 'rails', git: 'https://github.com/rails/rails' 13 | gem 'sqlite3' 14 | end 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in setler.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Chris Kelly 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Setler 2 | 3 | Setler is a Gem that lets one easily implement the "Feature Flags" pattern, or add settings to individual models. This is a cleanroom implementation of what the 'rails-settings' gem does. It's been forked all over the place, and my favorite version of it doesn't have any tests and doesn't work with settings associated with models. 4 | 5 | {Build Status}[https://travis-ci.org/ckdake/setler] 6 | {Gem Version}[http://badge.fury.io/rb/setler] 7 | 8 | While Setler enables you to create both app-level and model-level settings, they are two separate things and don't mix. For example, if you create defaults for the app, they won't appear as defaults for individual models. 9 | 10 | == Setup 11 | 12 | Install the gem by adding this to your Gemfile: 13 | 14 | gem "setler" 15 | 16 | Generate the model: 17 | 18 | rails g setler 19 | 20 | Run the migration: 21 | 22 | rake db:migrate 23 | 24 | If you are using the `protected_attributes` gem you must add `attr_protected` to the top of you setler model. 25 | 26 | == Usage 27 | 28 | Create/Update settings: 29 | 30 | # Method calls and []= are synonymous 31 | Featureflags.bacon_dispenser_enabled = true 32 | Settings[:allowed_meats] = ['bacon', 'crunchy bacon'] 33 | 34 | Read settings: 35 | 36 | Featureflags.bacon_dispenser_enabled # true 37 | Settings[:allowed_meats].include?('bacon') # true 38 | 39 | Destroy them: 40 | 41 | Featureflags.destroy :bacon_dispenser_enabled 42 | Settings.destroy :allowed_meats 43 | 44 | List all settings: 45 | 46 | Featureflags.all_settings 47 | Settings.all_settings 48 | 49 | Set defaults in an initializer with something like: 50 | 51 | Featureflags.defaults[:bacon_dispenser_enabled] = false 52 | Settings.defaults[:allowed_meats] = ['itsnotbacon'] 53 | 54 | To revert to the default after changing a setting, destroy it. 55 | Note: Updating the setting to nil or false no longer makes it the default setting (> 0.0.6), but changes the setting to nil or false. 56 | 57 | Add them to any ActiveRecord object: 58 | 59 | class User < ActiveRecord::Base 60 | has_setler :settings 61 | end 62 | 63 | Then you get: 64 | 65 | user = User.first 66 | user.settings.favorite_meat = :bacon 67 | user.settings.favorite_meat # :bacon 68 | user.settings.all # { "favorite_meat" => :bacon } 69 | 70 | TODO: And look em up: 71 | 72 | User.with_settings_for('favorite_meat') # => scope of users with the favorite_meat setting 73 | 74 | == Gem Development 75 | 76 | Getting started is pretty straightforward: 77 | 78 | 1. Check out the code: `git clone git://github.com/ckdake/setler.git` 79 | 2. Bundle install: `bundle install` 80 | 3. Run: `appraisal install` to generate the appraisal's gemfiles. 81 | 4. Run the tests for all supported releases in Appraisals file and make sure they all pass and code coverage is still the same: `appraisal rake test` 82 | 83 | If you'd like to contribute code, make your changes and submit a pull request that includes appropriate tests 84 | 85 | For building the gem: `rake build` and to release a gem to github and Rubygems: `rake release` 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | require 'bundler/gem_tasks' 6 | 7 | require 'rake' 8 | require 'rake/testtask' 9 | require 'appraisal' 10 | 11 | Rake::TestTask.new(:test) do |test| 12 | test.libs << 'lib' << 'test' 13 | test.pattern = 'test/**/*_test.rb' 14 | test.verbose = true 15 | end 16 | 17 | require 'bundler/gem_tasks' 18 | 19 | require 'rake' 20 | require 'appraisal' 21 | 22 | desc 'Default' 23 | task :default do 24 | if ENV['BUNDLE_GEMFILE'] =~ /gemfiles/ 25 | task default: [:test] 26 | else 27 | Rake::Task['appraise'].invoke 28 | end 29 | end 30 | 31 | task :appraise do 32 | exec 'appraisal install && appraisal rake test' 33 | end 34 | -------------------------------------------------------------------------------- /contributors.txt: -------------------------------------------------------------------------------- 1 | Chris Kelly github.com/ckdake 2 | Andy Lindeman github.com/alindeman 3 | Phil Ostler github.com/philostler 4 | Rachel Heaton github.com/rheaton 5 | Corey Innis github.com/coreyti 6 | Steven Harman github.com/stevenharman 7 | Chris Stefano github.com/virtualstaticvoid 8 | Brian Stanwyck github.com/brianstanwyck 9 | Jay Hayes github.com/iamvery 10 | Al Snow github.com/jasnow 11 | Gal Tsubery github.com/tsubery 12 | -------------------------------------------------------------------------------- /gemfiles/rails_4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "4.2.11.1" 6 | gem "sqlite3", "1.3.13" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails_4.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | setler (0.0.13) 5 | activerecord (>= 3.0.0) 6 | rails (>= 3.0.0) 7 | sprockets (= 3.7.2) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | actionmailer (4.2.11.1) 13 | actionpack (= 4.2.11.1) 14 | actionview (= 4.2.11.1) 15 | activejob (= 4.2.11.1) 16 | mail (~> 2.5, >= 2.5.4) 17 | rails-dom-testing (~> 1.0, >= 1.0.5) 18 | actionpack (4.2.11.1) 19 | actionview (= 4.2.11.1) 20 | activesupport (= 4.2.11.1) 21 | rack (~> 1.6) 22 | rack-test (~> 0.6.2) 23 | rails-dom-testing (~> 1.0, >= 1.0.5) 24 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 25 | actionview (4.2.11.1) 26 | activesupport (= 4.2.11.1) 27 | builder (~> 3.1) 28 | erubis (~> 2.7.0) 29 | rails-dom-testing (~> 1.0, >= 1.0.5) 30 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 31 | activejob (4.2.11.1) 32 | activesupport (= 4.2.11.1) 33 | globalid (>= 0.3.0) 34 | activemodel (4.2.11.1) 35 | activesupport (= 4.2.11.1) 36 | builder (~> 3.1) 37 | activerecord (4.2.11.1) 38 | activemodel (= 4.2.11.1) 39 | activesupport (= 4.2.11.1) 40 | arel (~> 6.0) 41 | activesupport (4.2.11.1) 42 | i18n (~> 0.7) 43 | minitest (~> 5.1) 44 | thread_safe (~> 0.3, >= 0.3.4) 45 | tzinfo (~> 1.1) 46 | appraisal (2.2.0) 47 | bundler 48 | rake 49 | thor (>= 0.14.0) 50 | arel (6.0.4) 51 | builder (3.2.4) 52 | concurrent-ruby (1.1.5) 53 | crass (1.0.6) 54 | docile (1.3.2) 55 | erubis (2.7.0) 56 | globalid (0.4.2) 57 | activesupport (>= 4.2.0) 58 | i18n (0.9.5) 59 | concurrent-ruby (~> 1.0) 60 | loofah (2.4.0) 61 | crass (~> 1.0.2) 62 | nokogiri (>= 1.5.9) 63 | mail (2.7.1) 64 | mini_mime (>= 0.1.1) 65 | mini_mime (1.0.2) 66 | mini_portile2 (2.4.0) 67 | minitest (5.14.0) 68 | nokogiri (1.10.7) 69 | mini_portile2 (~> 2.4.0) 70 | rack (1.6.13) 71 | rack-test (0.6.3) 72 | rack (>= 1.0) 73 | rails (4.2.11.1) 74 | actionmailer (= 4.2.11.1) 75 | actionpack (= 4.2.11.1) 76 | actionview (= 4.2.11.1) 77 | activejob (= 4.2.11.1) 78 | activemodel (= 4.2.11.1) 79 | activerecord (= 4.2.11.1) 80 | activesupport (= 4.2.11.1) 81 | bundler (>= 1.3.0, < 2.0) 82 | railties (= 4.2.11.1) 83 | sprockets-rails 84 | rails-deprecated_sanitizer (1.0.3) 85 | activesupport (>= 4.2.0.alpha) 86 | rails-dom-testing (1.0.9) 87 | activesupport (>= 4.2.0, < 5.0) 88 | nokogiri (~> 1.6) 89 | rails-deprecated_sanitizer (>= 1.0.1) 90 | rails-html-sanitizer (1.3.0) 91 | loofah (~> 2.3) 92 | railties (4.2.11.1) 93 | actionpack (= 4.2.11.1) 94 | activesupport (= 4.2.11.1) 95 | rake (>= 0.8.7) 96 | thor (>= 0.18.1, < 2.0) 97 | rake (13.0.1) 98 | simplecov (0.18.1) 99 | docile (~> 1.1) 100 | simplecov-html (~> 0.11.0) 101 | simplecov-html (0.11.0) 102 | sprockets (3.7.2) 103 | concurrent-ruby (~> 1.0) 104 | rack (> 1, < 3) 105 | sprockets-rails (3.2.1) 106 | actionpack (>= 4.0) 107 | activesupport (>= 4.0) 108 | sprockets (>= 3.0.0) 109 | sqlite3 (1.3.13) 110 | thor (1.0.1) 111 | thread_safe (0.3.6) 112 | tzinfo (1.2.6) 113 | thread_safe (~> 0.1) 114 | 115 | PLATFORMS 116 | ruby 117 | 118 | DEPENDENCIES 119 | appraisal 120 | minitest 121 | rails (= 4.2.11.1) 122 | rake 123 | setler! 124 | simplecov 125 | sqlite3 (= 1.3.13) 126 | 127 | BUNDLED WITH 128 | 1.17.3 129 | -------------------------------------------------------------------------------- /gemfiles/rails_5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "5.2.3" 6 | gem "sqlite3" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails_5.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | setler (0.0.13) 5 | activerecord (>= 3.0.0) 6 | rails (>= 3.0.0) 7 | sprockets (= 3.7.2) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | actioncable (5.2.3) 13 | actionpack (= 5.2.3) 14 | nio4r (~> 2.0) 15 | websocket-driver (>= 0.6.1) 16 | actionmailer (5.2.3) 17 | actionpack (= 5.2.3) 18 | actionview (= 5.2.3) 19 | activejob (= 5.2.3) 20 | mail (~> 2.5, >= 2.5.4) 21 | rails-dom-testing (~> 2.0) 22 | actionpack (5.2.3) 23 | actionview (= 5.2.3) 24 | activesupport (= 5.2.3) 25 | rack (~> 2.0) 26 | rack-test (>= 0.6.3) 27 | rails-dom-testing (~> 2.0) 28 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 29 | actionview (5.2.3) 30 | activesupport (= 5.2.3) 31 | builder (~> 3.1) 32 | erubi (~> 1.4) 33 | rails-dom-testing (~> 2.0) 34 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 35 | activejob (5.2.3) 36 | activesupport (= 5.2.3) 37 | globalid (>= 0.3.6) 38 | activemodel (5.2.3) 39 | activesupport (= 5.2.3) 40 | activerecord (5.2.3) 41 | activemodel (= 5.2.3) 42 | activesupport (= 5.2.3) 43 | arel (>= 9.0) 44 | activestorage (5.2.3) 45 | actionpack (= 5.2.3) 46 | activerecord (= 5.2.3) 47 | marcel (~> 0.3.1) 48 | activesupport (5.2.3) 49 | concurrent-ruby (~> 1.0, >= 1.0.2) 50 | i18n (>= 0.7, < 2) 51 | minitest (~> 5.1) 52 | tzinfo (~> 1.1) 53 | appraisal (2.2.0) 54 | bundler 55 | rake 56 | thor (>= 0.14.0) 57 | arel (9.0.0) 58 | builder (3.2.4) 59 | concurrent-ruby (1.1.5) 60 | crass (1.0.6) 61 | docile (1.3.2) 62 | erubi (1.9.0) 63 | globalid (0.4.2) 64 | activesupport (>= 4.2.0) 65 | i18n (1.8.2) 66 | concurrent-ruby (~> 1.0) 67 | loofah (2.4.0) 68 | crass (~> 1.0.2) 69 | nokogiri (>= 1.5.9) 70 | mail (2.7.1) 71 | mini_mime (>= 0.1.1) 72 | marcel (0.3.3) 73 | mimemagic (~> 0.3.2) 74 | method_source (0.9.2) 75 | mimemagic (0.3.4) 76 | mini_mime (1.0.2) 77 | mini_portile2 (2.4.0) 78 | minitest (5.14.0) 79 | nio4r (2.5.2) 80 | nokogiri (1.10.7) 81 | mini_portile2 (~> 2.4.0) 82 | rack (2.2.1) 83 | rack-test (1.1.0) 84 | rack (>= 1.0, < 3) 85 | rails (5.2.3) 86 | actioncable (= 5.2.3) 87 | actionmailer (= 5.2.3) 88 | actionpack (= 5.2.3) 89 | actionview (= 5.2.3) 90 | activejob (= 5.2.3) 91 | activemodel (= 5.2.3) 92 | activerecord (= 5.2.3) 93 | activestorage (= 5.2.3) 94 | activesupport (= 5.2.3) 95 | bundler (>= 1.3.0) 96 | railties (= 5.2.3) 97 | sprockets-rails (>= 2.0.0) 98 | rails-dom-testing (2.0.3) 99 | activesupport (>= 4.2.0) 100 | nokogiri (>= 1.6) 101 | rails-html-sanitizer (1.3.0) 102 | loofah (~> 2.3) 103 | railties (5.2.3) 104 | actionpack (= 5.2.3) 105 | activesupport (= 5.2.3) 106 | method_source 107 | rake (>= 0.8.7) 108 | thor (>= 0.19.0, < 2.0) 109 | rake (13.0.1) 110 | simplecov (0.18.1) 111 | docile (~> 1.1) 112 | simplecov-html (~> 0.11.0) 113 | simplecov-html (0.11.0) 114 | sprockets (3.7.2) 115 | concurrent-ruby (~> 1.0) 116 | rack (> 1, < 3) 117 | sprockets-rails (3.2.1) 118 | actionpack (>= 4.0) 119 | activesupport (>= 4.0) 120 | sprockets (>= 3.0.0) 121 | sqlite3 (1.4.2) 122 | thor (1.0.1) 123 | thread_safe (0.3.6) 124 | tzinfo (1.2.6) 125 | thread_safe (~> 0.1) 126 | websocket-driver (0.7.1) 127 | websocket-extensions (>= 0.1.0) 128 | websocket-extensions (0.1.4) 129 | 130 | PLATFORMS 131 | ruby 132 | 133 | DEPENDENCIES 134 | appraisal 135 | minitest 136 | rails (= 5.2.3) 137 | rake 138 | setler! 139 | simplecov 140 | sqlite3 141 | 142 | BUNDLED WITH 143 | 1.17.3 144 | -------------------------------------------------------------------------------- /gemfiles/rails_6_edge.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", git: "https://github.com/rails/rails" 6 | gem "sqlite3" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails_6_edge.gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/rails/rails 3 | revision: c77a949299fcddd0f6f35d9bc21888b862ff665c 4 | specs: 5 | actioncable (6.1.0.alpha) 6 | actionpack (= 6.1.0.alpha) 7 | activesupport (= 6.1.0.alpha) 8 | nio4r (~> 2.0) 9 | websocket-driver (>= 0.6.1) 10 | actionmailbox (6.1.0.alpha) 11 | actionpack (= 6.1.0.alpha) 12 | activejob (= 6.1.0.alpha) 13 | activerecord (= 6.1.0.alpha) 14 | activestorage (= 6.1.0.alpha) 15 | activesupport (= 6.1.0.alpha) 16 | mail (>= 2.7.1) 17 | actionmailer (6.1.0.alpha) 18 | actionpack (= 6.1.0.alpha) 19 | actionview (= 6.1.0.alpha) 20 | activejob (= 6.1.0.alpha) 21 | activesupport (= 6.1.0.alpha) 22 | mail (~> 2.5, >= 2.5.4) 23 | rails-dom-testing (~> 2.0) 24 | actionpack (6.1.0.alpha) 25 | actionview (= 6.1.0.alpha) 26 | activesupport (= 6.1.0.alpha) 27 | rack (~> 2.0, >= 2.0.8) 28 | rack-test (>= 0.6.3) 29 | rails-dom-testing (~> 2.0) 30 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 31 | actiontext (6.1.0.alpha) 32 | actionpack (= 6.1.0.alpha) 33 | activerecord (= 6.1.0.alpha) 34 | activestorage (= 6.1.0.alpha) 35 | activesupport (= 6.1.0.alpha) 36 | nokogiri (>= 1.8.5) 37 | actionview (6.1.0.alpha) 38 | activesupport (= 6.1.0.alpha) 39 | builder (~> 3.1) 40 | erubi (~> 1.4) 41 | rails-dom-testing (~> 2.0) 42 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 43 | activejob (6.1.0.alpha) 44 | activesupport (= 6.1.0.alpha) 45 | globalid (>= 0.3.6) 46 | activemodel (6.1.0.alpha) 47 | activesupport (= 6.1.0.alpha) 48 | activerecord (6.1.0.alpha) 49 | activemodel (= 6.1.0.alpha) 50 | activesupport (= 6.1.0.alpha) 51 | activestorage (6.1.0.alpha) 52 | actionpack (= 6.1.0.alpha) 53 | activejob (= 6.1.0.alpha) 54 | activerecord (= 6.1.0.alpha) 55 | activesupport (= 6.1.0.alpha) 56 | marcel (~> 0.3.1) 57 | activesupport (6.1.0.alpha) 58 | concurrent-ruby (~> 1.0, >= 1.0.2) 59 | i18n (>= 1.6, < 2) 60 | minitest (~> 5.1) 61 | tzinfo (~> 1.1) 62 | zeitwerk (~> 2.2, >= 2.2.2) 63 | rails (6.1.0.alpha) 64 | actioncable (= 6.1.0.alpha) 65 | actionmailbox (= 6.1.0.alpha) 66 | actionmailer (= 6.1.0.alpha) 67 | actionpack (= 6.1.0.alpha) 68 | actiontext (= 6.1.0.alpha) 69 | actionview (= 6.1.0.alpha) 70 | activejob (= 6.1.0.alpha) 71 | activemodel (= 6.1.0.alpha) 72 | activerecord (= 6.1.0.alpha) 73 | activestorage (= 6.1.0.alpha) 74 | activesupport (= 6.1.0.alpha) 75 | bundler (>= 1.3.0) 76 | railties (= 6.1.0.alpha) 77 | sprockets-rails (>= 2.0.0) 78 | railties (6.1.0.alpha) 79 | actionpack (= 6.1.0.alpha) 80 | activesupport (= 6.1.0.alpha) 81 | method_source 82 | rake (>= 0.8.7) 83 | thor (~> 1.0) 84 | 85 | PATH 86 | remote: .. 87 | specs: 88 | setler (0.0.13) 89 | activerecord (>= 3.0.0) 90 | rails (>= 3.0.0) 91 | sprockets (= 3.7.2) 92 | 93 | GEM 94 | remote: https://rubygems.org/ 95 | specs: 96 | appraisal (2.2.0) 97 | bundler 98 | rake 99 | thor (>= 0.14.0) 100 | builder (3.2.4) 101 | concurrent-ruby (1.1.5) 102 | crass (1.0.6) 103 | docile (1.3.2) 104 | erubi (1.9.0) 105 | globalid (0.4.2) 106 | activesupport (>= 4.2.0) 107 | i18n (1.8.2) 108 | concurrent-ruby (~> 1.0) 109 | loofah (2.4.0) 110 | crass (~> 1.0.2) 111 | nokogiri (>= 1.5.9) 112 | mail (2.7.1) 113 | mini_mime (>= 0.1.1) 114 | marcel (0.3.3) 115 | mimemagic (~> 0.3.2) 116 | method_source (0.9.2) 117 | mimemagic (0.3.4) 118 | mini_mime (1.0.2) 119 | mini_portile2 (2.4.0) 120 | minitest (5.14.0) 121 | nio4r (2.5.2) 122 | nokogiri (1.10.7) 123 | mini_portile2 (~> 2.4.0) 124 | rack (2.2.1) 125 | rack-test (1.1.0) 126 | rack (>= 1.0, < 3) 127 | rails-dom-testing (2.0.3) 128 | activesupport (>= 4.2.0) 129 | nokogiri (>= 1.6) 130 | rails-html-sanitizer (1.3.0) 131 | loofah (~> 2.3) 132 | rake (13.0.1) 133 | simplecov (0.18.1) 134 | docile (~> 1.1) 135 | simplecov-html (~> 0.11.0) 136 | simplecov-html (0.11.0) 137 | sprockets (3.7.2) 138 | concurrent-ruby (~> 1.0) 139 | rack (> 1, < 3) 140 | sprockets-rails (3.2.1) 141 | actionpack (>= 4.0) 142 | activesupport (>= 4.0) 143 | sprockets (>= 3.0.0) 144 | sqlite3 (1.4.2) 145 | thor (1.0.1) 146 | thread_safe (0.3.6) 147 | tzinfo (1.2.6) 148 | thread_safe (~> 0.1) 149 | websocket-driver (0.7.1) 150 | websocket-extensions (>= 0.1.0) 151 | websocket-extensions (0.1.4) 152 | zeitwerk (2.2.2) 153 | 154 | PLATFORMS 155 | ruby 156 | 157 | DEPENDENCIES 158 | appraisal 159 | minitest 160 | rails! 161 | rake 162 | setler! 163 | simplecov 164 | sqlite3 165 | 166 | BUNDLED WITH 167 | 1.17.3 168 | -------------------------------------------------------------------------------- /lib/generators/setler/setler_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/migration' 2 | 3 | class SetlerGenerator < Rails::Generators::NamedBase 4 | include Rails::Generators::Migration 5 | 6 | argument :name, type: :string, default: "settings" 7 | 8 | source_root File.expand_path('../templates', __FILE__) 9 | 10 | @@migrations = false 11 | 12 | def self.next_migration_number(dirname) 13 | if ActiveRecord::Base.timestamped_migrations 14 | if @@migrations 15 | (current_migration_number(dirname) + 1) 16 | else 17 | @@migrations = true 18 | Time.now.utc.strftime("%Y%m%d%H%M%S") 19 | end 20 | else 21 | "%.3d" % (current_migration_number(dirname) + 1) 22 | end 23 | end 24 | 25 | def generate_model 26 | template "model.rb", File.join("app/models",class_path,"#{file_name}.rb"), force: true 27 | migration_template "migration.rb", "db/migrate/setler_create_#{table_name}.rb" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/generators/setler/templates/migration.rb: -------------------------------------------------------------------------------- 1 | class SetlerCreate<%= table_name.camelize %> < ActiveRecord::Migration 2 | def self.up 3 | create_table(:<%= table_name %>) do |t| 4 | t.string :var, null: false 5 | t.text :value, null: true 6 | t.integer :thing_id, null: true 7 | t.string :thing_type, limit: 30, null: true 8 | t.timestamps 9 | end 10 | 11 | add_index :<%= table_name %>, [ :thing_type, :thing_id, :var ], unique: true 12 | add_index :<%= table_name %>, :var, unique: true, where: "thing_id IS NULL AND thing_type is NULL" 13 | end 14 | 15 | def self.down 16 | drop_table :<%= table_name %> 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/generators/setler/templates/model.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %> < Setler::Settings 2 | end 3 | -------------------------------------------------------------------------------- /lib/setler.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | 3 | require_relative 'setler/version' 4 | require_relative 'setler/exceptions' 5 | require_relative 'setler/settings' 6 | require_relative 'setler/scoped_settings' 7 | require_relative 'setler/active_record' 8 | 9 | ::ActiveRecord::Base.extend Setler::ActiveRecord 10 | -------------------------------------------------------------------------------- /lib/setler/active_record.rb: -------------------------------------------------------------------------------- 1 | module Setler 2 | module ActiveRecord 3 | 4 | def has_setler(scopename = 'settings') 5 | define_method scopename do 6 | Setler::ScopedSettings.for_thing(self, scopename) 7 | end 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/setler/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Setler 2 | class SettingNotFound < StandardError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/setler/scoped_settings.rb: -------------------------------------------------------------------------------- 1 | module Setler 2 | class ScopedSettings < Settings 3 | def self.for_thing(object, scopename) 4 | self.table_name = scopename 5 | self.defaults = settings_constantize(scopename).defaults 6 | @object = object 7 | self 8 | end 9 | 10 | def self.thing_scoped 11 | self.base_class.where(thing_type: @object.class.base_class.to_s, thing_id: @object.id) 12 | end 13 | 14 | # do not use rails default to singularize because setler examples 15 | # user plural class names 16 | def self.settings_constantize(scopename) 17 | Object.const_get(scopename.to_s.camelize) 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/setler/settings.rb: -------------------------------------------------------------------------------- 1 | require 'rails/version' 2 | 3 | module Setler 4 | class Settings < ActiveRecord::Base 5 | serialize :value 6 | self.abstract_class = true 7 | 8 | def self.defaults 9 | @defaults ||= {}.with_indifferent_access 10 | end 11 | 12 | def self.defaults=(defaults) 13 | @defaults = defaults.with_indifferent_access 14 | end 15 | 16 | if Rails::VERSION::MAJOR == 3 17 | attr_accessible :var, :value 18 | 19 | def self.all 20 | warn '[DEPRECATED] Setler::Settings#all is deprecated. Please use #all_settings' 21 | all_settings 22 | end 23 | end 24 | 25 | # Get and Set variables when the calling method is the variable name 26 | def self.method_missing(method, *args, &block) 27 | if respond_to?(method) 28 | super(method, *args, &block) 29 | else 30 | method_name = method.to_s 31 | if method_name.ends_with?("=") 32 | self[method_name[0..-2]] = args.first 33 | elsif method_name.ends_with?("?") 34 | self[method_name[0..-2]].present? 35 | else 36 | self[method_name] 37 | end 38 | end 39 | end 40 | 41 | def self.[](var) 42 | the_setting = thing_scoped.find_by_var(var.to_s) 43 | the_setting.present? ? the_setting.value : defaults[var] 44 | end 45 | 46 | def self.[]=(var, value) 47 | # THIS IS BAD 48 | # thing_scoped.find_or_create_by_var(method_name[0..-2]) should work but doesnt for some reason 49 | # When @object is present, thing_scoped sets the where scope for the polymorphic association 50 | # but the find_or_create_by wasn't using the thing_type and thing_id 51 | if Rails::VERSION::MAJOR == 3 52 | thing_scoped.find_or_create_by_var_and_thing_type_and_thing_id( 53 | var.to_s, 54 | @object.try(:class).try(:base_class).try(:to_s), 55 | @object.try(:id) 56 | ).update({ :value => value }) 57 | else 58 | thing_scoped.find_or_create_by( 59 | var: var.to_s, 60 | thing_type: @object.try(:class).try(:base_class).try(:to_s), 61 | thing_id: @object.try(:id) 62 | ).update({ :value => value }) 63 | end 64 | end 65 | 66 | def self.destroy(var_name) 67 | var_name = var_name.to_s 68 | if setting = self.find_by_var(var_name) 69 | setting.destroy 70 | true 71 | else 72 | raise SettingNotFound, "Setting variable \"#{var_name}\" not found" 73 | end 74 | end 75 | 76 | def self.all_settings 77 | defaults.merge(Hash[thing_scoped.all.collect{ |s| [s.var, s.value] }]) 78 | end 79 | 80 | def self.thing_scoped 81 | self.where(thing_type: nil, thing_id: nil) 82 | end 83 | 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/setler/version.rb: -------------------------------------------------------------------------------- 1 | module Setler 2 | VERSION = "0.0.14" 3 | end 4 | -------------------------------------------------------------------------------- /setler.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "setler/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "setler" 7 | s.version = Setler::VERSION 8 | s.authors = ["Chris Kelly"] 9 | s.email = ["ckdake@ckdake.com"] 10 | s.license = 'MIT' 11 | s.homepage = "https://github.com/ckdake/setler" 12 | s.summary = %q{Settler lets you use the 'Feature Flags' pettern or add settings to models.} 13 | s.description = %q{Setler is a Gem that lets one easily implement the "Feature Flags" pattern, or add settings to individual models. This is a cleanroom implementation of what the 'rails-settings' gem does. It's been forked all over the place, and my favorite version of it doesn't have any tests and doesn't work with settings associated with models.} 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | 20 | s.add_dependency('activerecord', '>=3.0.0') 21 | s.add_dependency('rails', '>=3.0.0') 22 | s.add_dependency('sprockets', '3.7.2') # 10/16/2019: LOCKED DOWN 23 | 24 | s.add_development_dependency('rake') 25 | s.add_development_dependency('minitest') 26 | s.add_development_dependency('simplecov') 27 | s.add_development_dependency('appraisal') 28 | end 29 | -------------------------------------------------------------------------------- /test/settings_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ::SettingsTest < Minitest::Test 4 | setup_db 5 | 6 | def setup 7 | ::Settings.create(:var => 'test', :value => 'foo') 8 | ::Settings.create(:var => 'test2', :value => 'bar') 9 | end 10 | 11 | def teardown 12 | ::Settings.delete_all 13 | ::Settings.defaults = {}.with_indifferent_access 14 | 15 | ::Preferences.delete_all 16 | ::Preferences.defaults = {}.with_indifferent_access 17 | end 18 | 19 | def test_defaults 20 | ::Settings.defaults[:foo] = 'default foo' 21 | 22 | assert_equal 'default foo', ::Settings.foo 23 | 24 | ::Settings.foo = 'bar' 25 | assert_equal 'bar', ::Settings.foo 26 | end 27 | 28 | def tests_defaults_false 29 | ::Settings.defaults[:foo] = false 30 | assert_equal false, ::Settings.foo 31 | end 32 | 33 | def test_get 34 | assert_equal 'foo', ::Settings.test 35 | assert_equal 'bar', ::Settings.test2 36 | end 37 | 38 | def test_get_presence 39 | ::Settings.truthy = [1,2,3] 40 | ::Settings.falsy = [] 41 | assert_equal true, ::Settings.truthy? 42 | assert_equal false, ::Settings.falsy? 43 | end 44 | 45 | def test_get_with_array_syntax 46 | assert_equal 'foo', ::Settings["test"] 47 | assert_equal 'bar', ::Settings[:test2] 48 | end 49 | 50 | def test_update 51 | ::Settings.test = '321' 52 | assert_equal '321', ::Settings.test 53 | end 54 | 55 | def test_update_with_false 56 | ::Settings.test = false 57 | assert_equal false, ::Settings.test 58 | end 59 | 60 | def test_update_with_nil_and_default_not_nil 61 | ::Settings.defaults[:foo] = :test 62 | ::Settings.foo = nil 63 | assert_nil ::Settings.foo 64 | end 65 | 66 | def test_update_with_array_syntax 67 | ::Settings["test"] = '321' 68 | assert_equal '321', ::Settings.test 69 | 70 | ::Settings[:test] = '567' 71 | assert_equal '567', ::Settings.test 72 | end 73 | 74 | def test_create 75 | ::Settings.onetwothree = '123' 76 | assert_equal '123', ::Settings.onetwothree 77 | end 78 | 79 | def test_multithreaded_create 80 | s1 = Settings.new var: :conflict, value: 1 81 | s2 = Settings.new var: :conflict, value: 2 82 | s1.save! 83 | exc = assert_raises ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid do 84 | s2.save! 85 | end 86 | unless exc.is_a?(ActiveRecord::RecordNotUnique) 87 | assert exc.message.match(/UNIQUE/) 88 | end 89 | end 90 | 91 | def test_complex_serialization 92 | complex = [1, '2', {"three" => true}] 93 | ::Settings.complex = complex 94 | assert_equal complex, ::Settings.complex 95 | end 96 | 97 | def test_serialization_of_float 98 | ::Settings.float = 0.01 99 | ::Settings.reload 100 | assert_equal 0.01, ::Settings.float 101 | assert_equal 0.02, ::Settings.float * 2 102 | end 103 | 104 | def test_all_settings 105 | assert_equal({ "test2" => "bar", "test" => "foo" }, ::Settings.all_settings) 106 | end 107 | 108 | def test_destroy 109 | refute_nil ::Settings.test 110 | ::Settings.destroy :test 111 | assert_nil ::Settings.test 112 | end 113 | 114 | def test_destroy_reverts_to_default 115 | ::Settings.defaults[:foo] = :test 116 | ::Settings[:foo] = :bar 117 | 118 | ::Settings.destroy :foo 119 | assert_equal :test, ::Settings.foo 120 | end 121 | 122 | def test_multiple_settings_classes 123 | ::Settings.testing = '123' 124 | assert_nil ::Preferences.testing 125 | end 126 | 127 | def test_user_has_setler 128 | user = User.create name: 'user 1' 129 | assert_nil user.preferences.likes_bacon 130 | user.preferences.likes_bacon = true 131 | assert user.preferences.likes_bacon 132 | user.preferences.destroy :likes_bacon 133 | assert_nil user.preferences.likes_bacon 134 | end 135 | 136 | def test_user_settings_all 137 | ::Settings.destroy_all 138 | user = User.create name: 'user 1' 139 | assert_equal ::Preferences.all_settings, user.preferences.all_settings 140 | user.preferences.likes_bacon = true 141 | user.preferences.really_likes_bacon = true 142 | assert user.preferences.all_settings['likes_bacon'] 143 | assert !::Settings.all_settings['likes_bacon'] 144 | assert user.preferences.all_settings['really_likes_bacon'] 145 | assert !::Settings.all_settings['really_likes_bacon'] 146 | end 147 | 148 | def test_user_settings_override_defaults 149 | ::Settings.defaults[:foo] = false 150 | user = User.create name: 'user 1' 151 | assert !user.preferences.foo 152 | user.preferences.foo = true 153 | assert user.preferences.foo 154 | user.preferences.foo = false 155 | assert !user.preferences.foo 156 | end 157 | 158 | def test_user_preferences_has_defaults 159 | ::Preferences.defaults[:foo] = true 160 | user = User.create name: 'user 1' 161 | assert user.preferences.foo 162 | end 163 | 164 | # def test_user_has_settings_for 165 | # user1 = User.create name: 'awesome user' 166 | # user2 = User.create name: 'bad user' 167 | # user1.preferences.likes_bacon = true 168 | # assert_equal 1, User.with_settings_for('likes_bacon').count 169 | # assert_equal user1, User.with_settings_for('likes_bacon').first 170 | # end 171 | 172 | def test_destroy_when_setting_does_not_exist 173 | assert_raises Setler::SettingNotFound do 174 | ::Settings.destroy :not_a_setting 175 | end 176 | end 177 | 178 | def test_implementations_are_independent 179 | ::Preferences.create var: 'test', value: 'preferences foo' 180 | ::Preferences.create var: 'test2', value: 'preferences bar' 181 | 182 | refute_match ::Settings.all_settings, ::Preferences.all_settings 183 | 184 | assert_equal 'foo', ::Settings[:test] 185 | assert_equal 'bar', ::Settings[:test2] 186 | assert_equal 'preferences foo', ::Preferences[:test] 187 | assert_equal 'preferences bar', ::Preferences[:test2] 188 | end 189 | 190 | def test_defaults_are_independent 191 | ::Settings.defaults[:foo] = false 192 | 193 | refute_equal ::Settings.defaults, ::Preferences.defaults 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | require 'simplecov' 4 | SimpleCov.start 'rails' 5 | 6 | require 'bundler' 7 | begin 8 | Bundler.setup(:default, :development) 9 | rescue Bundler::BundlerError => e 10 | $stderr.puts e.message 11 | $stderr.puts "Run `bundle install` to install missing gems" 12 | exit e.status_code 13 | end 14 | 15 | require 'rails' 16 | require 'active_record' 17 | require 'minitest/autorun' 18 | 19 | require_relative '../lib/setler' 20 | 21 | ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:") 22 | 23 | class User < ActiveRecord::Base 24 | has_setler :preferences 25 | end 26 | 27 | class Settings < Setler::Settings 28 | end 29 | 30 | class Preferences < Setler::Settings 31 | end 32 | 33 | def setup_db 34 | ActiveRecord::Schema.define(:version => 1) do 35 | create_table :settings do |t| 36 | t.string :var, :null => false 37 | t.text :value, :null => true 38 | t.integer :thing_id, :null => true 39 | t.string :thing_type, :limit => 30, :null => true 40 | t.timestamps null: false 41 | end 42 | add_index :settings, [ :thing_type, :thing_id, :var ], :unique => true 43 | add_index :settings, :var, unique: true, where: "thing_id IS NULL AND thing_type is NULL" 44 | 45 | create_table :preferences do |t| 46 | t.string :var, :null => false 47 | t.text :value, :null => true 48 | t.integer :thing_id, :null => true 49 | t.string :thing_type, :limit => 30, :null => true 50 | t.timestamps null: false 51 | end 52 | add_index :preferences, [ :thing_type, :thing_id, :var ], :unique => true 53 | add_index :preferences, :var, unique: true, where: "thing_id IS NULL AND thing_type is NULL" 54 | 55 | create_table :users do |t| 56 | t.string :name 57 | end 58 | end 59 | end 60 | --------------------------------------------------------------------------------