├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── lib ├── redis-settings.rb └── redis │ ├── settings.rb │ └── settings │ ├── active_record.rb │ ├── railtie.rb │ └── version.rb ├── redis-settings.gemspec └── test ├── redis └── settings │ ├── active_record_test.rb │ ├── all_test.rb │ ├── clear_test.rb │ ├── configure_test.rb │ ├── get_test.rb │ ├── namespace_test.rb │ ├── remove_test.rb │ ├── root_namespace_test.rb │ └── set_test.rb ├── support └── models.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | cache: bundler 4 | rvm: 5 | - '2.3.0' 6 | - 2.2 7 | - 2.1 8 | - 2.0 9 | services: 10 | - redis-server 11 | notifications: 12 | email: false 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | redis-settings (0.2.3) 5 | redis 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (4.2.6) 11 | activesupport (= 4.2.6) 12 | builder (~> 3.1) 13 | activerecord (4.2.6) 14 | activemodel (= 4.2.6) 15 | activesupport (= 4.2.6) 16 | arel (~> 6.0) 17 | activesupport (4.2.6) 18 | i18n (~> 0.7) 19 | json (~> 1.7, >= 1.7.7) 20 | minitest (~> 5.1) 21 | thread_safe (~> 0.3, >= 0.3.4) 22 | tzinfo (~> 1.1) 23 | arel (6.0.3) 24 | awesome_print (1.6.1) 25 | builder (3.2.2) 26 | byebug (8.2.2) 27 | coderay (1.1.1) 28 | i18n (0.7.0) 29 | json (1.8.3) 30 | method_source (0.8.2) 31 | minitest (5.8.4) 32 | minitest-utils (0.3.0) 33 | minitest 34 | pry (0.10.3) 35 | coderay (~> 1.1.0) 36 | method_source (~> 0.8.1) 37 | slop (~> 3.4) 38 | pry-byebug (3.3.0) 39 | byebug (~> 8.0) 40 | pry (~> 0.10) 41 | pry-meta (0.0.10) 42 | awesome_print 43 | pry 44 | pry-byebug 45 | pry-remote 46 | pry-remote (0.1.8) 47 | pry (~> 0.9) 48 | slop (~> 3.0) 49 | rake (11.1.2) 50 | redis (3.2.2) 51 | redis-namespace (1.5.2) 52 | redis (~> 3.0, >= 3.0.4) 53 | slop (3.6.0) 54 | sqlite3 (1.3.11) 55 | thread_safe (0.3.5) 56 | tzinfo (1.2.2) 57 | thread_safe (~> 0.1) 58 | 59 | PLATFORMS 60 | ruby 61 | 62 | DEPENDENCIES 63 | activerecord 64 | minitest-utils 65 | pry-meta 66 | rake 67 | redis-namespace 68 | redis-settings! 69 | sqlite3 70 | 71 | BUNDLED WITH 72 | 1.11.2 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis::Settings 2 | 3 | [![Travis-CI](https://travis-ci.org/fnando/redis-settings.png)](https://travis-ci.org/fnando/redis-settings) 4 | [![CodeClimate](https://codeclimate.com/github/fnando/redis-settings.png)](https://codeclimate.com/github/fnando/redis-settings) 5 | [![Gem](https://img.shields.io/gem/v/redis-settings.svg)](https://rubygems.org/gems/redis-settings) 6 | [![Gem](https://img.shields.io/gem/dt/redis-settings.svg)](https://rubygems.org/gems/redis-settings) 7 | 8 | Store application and user settings on Redis. Comes with ActiveRecord support. 9 | 10 | ## Installation 11 | 12 | gem install redis-settings 13 | 14 | ## Usage 15 | 16 | ```ruby 17 | require "redis/settings" 18 | 19 | # Reuse an existing Redis connection. 20 | Redis::Settings.configure do |config| 21 | config.connection = Redis.new(:host => "localhost", :port => 6379) 22 | end 23 | 24 | # Create settings namespaced to app 25 | settings = Redis::Settings.new("app") 26 | settings.namespace 27 | #=> "settings/app" 28 | 29 | # Set values 30 | settings[:items_per_page] = 10 31 | settings.set(:items_per_page, 10) 32 | 33 | # Get values 34 | settings[:items_per_page] 35 | settings.get(:items_per_page) 36 | settings.get(:items_per_page, 20) #=> return 20 when items_per_page is not defined 37 | 38 | # Remove all settings on this namespace 39 | settings.clear 40 | 41 | # Retrieve a hash with defined settings 42 | settings.all 43 | ``` 44 | 45 | ### ActiveRecord 46 | 47 | ```ruby 48 | class User < ActiveRecord::Base 49 | end 50 | 51 | user = User.first 52 | user.settings[:role] = "admin" 53 | ``` 54 | 55 | **NOTE:** When record is destroyed, all settings are erased. 56 | 57 | ### Rails 58 | 59 | Rails set its own namespace like `settings/#{Rails.env}`. 60 | 61 | You can override the root namespace as 62 | 63 | ```ruby 64 | Redis::Settings.root_namespace = "custom" 65 | ``` 66 | 67 | ### JSON adapter 68 | 69 | Redis::Settings store all data in JSON format; [oj](https://github.com/ohler55/oj) will be used as the JSON adapter when available, defaulting to Ruby's JSON. 70 | 71 | You can set to anything by assigning the `Redis::Settings.json_parser` option. 72 | 73 | ```ruby 74 | Redis::Settings.json_parser = JSON 75 | ``` 76 | 77 | # Mantainer 78 | 79 | - Nando Vieira (http://nandovieira.com) 80 | 81 | # License 82 | 83 | (The MIT License) 84 | 85 | Permission is hereby granted, free of charge, to any person obtaining 86 | a copy of this software and associated documentation files (the 87 | 'Software'), to deal in the Software without restriction, including 88 | without limitation the rights to use, copy, modify, merge, publish, 89 | distribute, sublicense, and/or sell copies of the Software, and to 90 | permit persons to whom the Software is furnished to do so, subject to 91 | the following conditions: 92 | 93 | The above copyright notice and this permission notice shall be 94 | included in all copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 97 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 98 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 99 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 100 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 101 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 102 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 103 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.test_files = FileList["test/**/*_test.rb"] 7 | t.warning = false 8 | end 9 | 10 | task default: :test 11 | -------------------------------------------------------------------------------- /lib/redis-settings.rb: -------------------------------------------------------------------------------- 1 | require "redis/settings" 2 | -------------------------------------------------------------------------------- /lib/redis/settings.rb: -------------------------------------------------------------------------------- 1 | require "redis" 2 | 3 | class Redis 4 | class Settings 5 | require "redis/settings/active_record" if defined?(ActiveRecord) 6 | require "redis/settings/railtie" if defined?(Rails) 7 | 8 | class NewRecordError < StandardError 9 | def message; "You can't access settings on new records"; end 10 | end 11 | 12 | class << self 13 | attr_accessor :connection 14 | attr_accessor :root_namespace 15 | attr_accessor :json_parser 16 | end 17 | 18 | # Set the root namespace to "settings" by default. 19 | self.root_namespace = "settings" 20 | 21 | # Set the json parser. 22 | begin 23 | require "oj" 24 | self.json_parser = Oj 25 | rescue LoadError 26 | require "json" 27 | self.json_parser = JSON 28 | end 29 | 30 | # Return Redis::Settings. 31 | # 32 | # Redis::Settings.configure do |config| 33 | # config.connection = Redis.new(:host => "localhost", :port => 6379) 34 | # end 35 | # 36 | def self.configure(&block) 37 | yield self 38 | end 39 | 40 | # Initialize a new object that will be wrapped into the specified 41 | # namespace. 42 | # 43 | # s = Redis::Settings.new("app") 44 | # 45 | def initialize(namespace) 46 | @namespace = namespace 47 | end 48 | 49 | # Return instance's namespace concatenated with Redis::Settings.root_namespace. 50 | # 51 | # s = Redis::Settings.new("app") 52 | # s.namespace 53 | # #=> "settings/app" 54 | # 55 | def namespace 56 | "#{self.class.root_namespace}/#{@namespace}" 57 | end 58 | 59 | # Retrieve setting by its name. When nil, return the default value. 60 | # 61 | # s = Redis::Settings.new("app") 62 | # s.get(:items_per_page) 63 | # s.get(:items_per_page, 10) 64 | # 65 | def get(name, default = nil) 66 | value = redis.hget(namespace, name) 67 | 68 | if value 69 | payload = self.class.json_parser.load(value) 70 | value = payload["data"] 71 | end 72 | 73 | value.nil? ? default : value 74 | end 75 | 76 | # Define a value for the specified setting. 77 | # 78 | # s = Redis::Settings.new("app") 79 | # s.set(:items_per_page, 10) 80 | # 81 | def set(name, value) 82 | if value.nil? 83 | redis.hdel(namespace, name) 84 | else 85 | redis.hset(namespace, name, self.class.json_parser.dump(:data => value)) 86 | end 87 | end 88 | 89 | # Delete the specified option. Just a shortcut for settings.set(:name, nil). 90 | # 91 | def remove(name) 92 | set(name, nil) 93 | end 94 | 95 | alias_method :delete, :remove 96 | 97 | # Remove all settings from the current namespace 98 | def clear 99 | redis.del(namespace) 100 | end 101 | 102 | # Return a hash with all settings 103 | def all 104 | Hash[redis.hgetall(namespace).map {|k, v| [k.to_sym, self.class.json_parser.load(v)["data"]]}] 105 | end 106 | 107 | alias_method :[]=, :set 108 | alias_method :[], :get 109 | alias_method :fetch, :get 110 | 111 | private 112 | 113 | def redis # :nodoc: 114 | self.class.connection 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/redis/settings/active_record.rb: -------------------------------------------------------------------------------- 1 | class Redis 2 | class Settings 3 | module ActiveRecord 4 | def self.included(base) 5 | base.class_eval do 6 | include InstanceMethods 7 | after_destroy :clear_settings 8 | end 9 | end 10 | 11 | module InstanceMethods 12 | def settings 13 | raise Redis::Settings::NewRecordError if new_record? 14 | @settings ||= Settings.new("#{self.class.name.underscore}/#{id}") 15 | end 16 | 17 | def clear_settings 18 | settings.clear unless new_record? 19 | end 20 | end 21 | end 22 | end 23 | end 24 | 25 | ActiveRecord::Base.send :include, Redis::Settings::ActiveRecord 26 | -------------------------------------------------------------------------------- /lib/redis/settings/railtie.rb: -------------------------------------------------------------------------------- 1 | class Redis 2 | class Settings 3 | class Railtie < Rails::Railtie 4 | initializer :redis_settings do 5 | Settings.root_namespace = "settings/#{Rails.env}" 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/redis/settings/version.rb: -------------------------------------------------------------------------------- 1 | class Redis 2 | class Settings 3 | module Version 4 | MAJOR = 0 5 | MINOR = 2 6 | PATCH = 3 7 | STRING = "#{MAJOR}.#{MINOR}.#{PATCH}" 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /redis-settings.gemspec: -------------------------------------------------------------------------------- 1 | require "./lib/redis/settings/version" 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "redis-settings" 5 | s.version = Redis::Settings::Version::STRING 6 | s.platform = Gem::Platform::RUBY 7 | s.authors = ["Nando Vieira"] 8 | s.email = ["fnando.vieira@gmail.com"] 9 | s.homepage = "http://github.com/fnando/redis-settings" 10 | s.summary = %[Store application and user settings on Redis. Comes with ActiveRecord support.] 11 | s.description = s.summary 12 | 13 | s.files = `git ls-files`.split("\n") 14 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 15 | s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f) } 16 | s.require_paths = ["lib"] 17 | 18 | s.add_dependency "redis" 19 | s.add_development_dependency "rake" 20 | s.add_development_dependency "minitest-utils" 21 | s.add_development_dependency "activerecord" 22 | s.add_development_dependency "redis-namespace" 23 | s.add_development_dependency "sqlite3" 24 | s.add_development_dependency "pry-meta" 25 | end 26 | -------------------------------------------------------------------------------- /test/redis/settings/active_record_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ActiveRecordTest < Minitest::Test 4 | let(:user) { User.create! } 5 | let(:admin) { Admin::User.create! } 6 | let(:root) { Redis::Settings.root_namespace } 7 | 8 | setup do 9 | user 10 | admin 11 | root 12 | end 13 | 14 | test "injects settings method" do 15 | assert User.new.respond_to?(:settings) 16 | end 17 | 18 | test "raises when trying to access settings from a new record" do 19 | assert_raises(Redis::Settings::NewRecordError) { User.new.settings } 20 | end 21 | 22 | test "doesn't raise exception if new record was destroyed" do 23 | User.new { |u| u.id = 7 }.destroy 24 | end 25 | 26 | test "sets namespace accordingly" do 27 | assert_equal user.settings.namespace, "#{root}/user/#{user.id}" 28 | assert_equal admin.settings.namespace, "#{root}/admin/user/#{admin.id}" 29 | end 30 | 31 | test "defines setting" do 32 | admin.settings[:role] = "admin" 33 | user.settings[:role] = "support" 34 | 35 | assert_equal user.settings[:role], "support" 36 | assert_equal admin.settings[:role], "admin" 37 | end 38 | 39 | test "removes all settings when destroy a record" do 40 | user.settings[:role] = "customer" 41 | user.destroy 42 | settings = Redis::Settings.connection.hgetall("#{root}/user/#{user.id}") 43 | 44 | assert settings.empty? 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/redis/settings/all_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AllTest < Minitest::Test 4 | let(:settings) { Redis::Settings.new("app") } 5 | 6 | test "returns all settings" do 7 | settings[:items] = 10 8 | settings[:enabled] = true 9 | 10 | expected = {items: 10, enabled: true} 11 | 12 | assert_equal expected, settings.all 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/redis/settings/clear_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ClearTest < Minitest::Test 4 | let(:settings) { Redis::Settings.new("app") } 5 | let(:redis) { Redis::Settings.connection } 6 | 7 | test "removes all settings" do 8 | settings[:items] = 5 9 | 10 | refute redis.hgetall(settings.namespace).empty? 11 | settings.clear 12 | assert redis.hgetall(settings.namespace).empty? 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/redis/settings/configure_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ConfigureTest < Minitest::Test 4 | let(:settings) { Redis::Settings.new("app") } 5 | 6 | test "yields module" do 7 | config = nil 8 | 9 | Redis::Settings.configure {|c| config = c } 10 | 11 | assert_equal Redis::Settings, config 12 | end 13 | 14 | test "sets json parser" do 15 | assert_equal JSON, Redis::Settings.json_parser 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/redis/settings/get_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class GetTest < Minitest::Test 4 | let(:settings) { Redis::Settings.new("app") } 5 | 6 | test "gets value" do 7 | settings.set(:items, 5) 8 | assert_equal 5, settings.get(:items) 9 | end 10 | 11 | test "has #[] alias" do 12 | settings[:items] = 10 13 | assert_equal 10, settings[:items] 14 | end 15 | 16 | test "has #fetch alias" do 17 | settings[:items] = 10 18 | assert_equal 10, settings.fetch(:items) 19 | end 20 | 21 | test "returns default value" do 22 | settings[:items] = nil 23 | assert_equal 15, settings.get(:items, 15) 24 | end 25 | 26 | test "returns false as default value" do 27 | settings[:item] = false 28 | assert_equal false, settings.get(:item, true) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/redis/settings/namespace_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class NamespaceTest < Minitest::Test 4 | let(:settings) { Redis::Settings.new("app") } 5 | 6 | test "includes settings as namespace root" do 7 | assert_equal "settings/app", settings.namespace 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/redis/settings/remove_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RemoveTest < Minitest::Test 4 | let(:settings) { Redis::Settings.new("app") } 5 | let(:redis) { Redis::Settings.connection } 6 | 7 | test "removes option" do 8 | settings[:items] = 20 9 | settings.remove(:items) 10 | 11 | assert_nil redis.hget(settings.namespace, :items) 12 | end 13 | 14 | test "has alias" do 15 | settings[:items] = 20 16 | settings.delete(:items) 17 | 18 | assert_nil redis.hget(settings.namespace, :items) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/redis/settings/root_namespace_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RootNamespaceTest < Minitest::Test 4 | let(:settings) { Redis::Settings.new("app") } 5 | let(:redis) { Redis::Settings.connection } 6 | 7 | setup { Redis::Settings.root_namespace = "settings/development" } 8 | teardown { Redis::Settings.root_namespace = "settings" } 9 | 10 | test "uses custom namespace" do 11 | assert_equal "settings/development/app", settings.namespace 12 | end 13 | 14 | test "sets value using custom namespace" do 15 | settings[:items] = 10 16 | payload = JSON.parse( 17 | redis.hget("settings/development/app", :items) 18 | ) 19 | 20 | assert_equal Hash["data" => 10], payload 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/redis/settings/set_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SetTest < Minitest::Test 4 | let(:settings) { Redis::Settings.new("app") } 5 | let(:redis) { Redis::Settings.connection } 6 | 7 | test "sets value" do 8 | settings.set(:items, 5) 9 | payload = JSON.parse( 10 | redis.hget(settings.namespace, :items) 11 | ) 12 | 13 | assert_equal Hash["data" => 5], payload 14 | end 15 | 16 | test "has shortcut" do 17 | settings[:items] = 10 18 | payload = JSON.parse( 19 | redis.hget(settings.namespace, :items) 20 | ) 21 | 22 | assert_equal Hash["data" => 10], payload 23 | end 24 | 25 | test "removes setting when assigning nil" do 26 | settings[:items] = 20 27 | settings[:items] = nil 28 | 29 | assert_nil redis.hget(settings.namespace, :items) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/support/models.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | end 3 | 4 | module Admin 5 | class User < ActiveRecord::Base 6 | self.table_name = "users" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | require "active_record" 4 | require "redis/settings" 5 | require "redis/namespace" 6 | 7 | require "minitest/utils" 8 | require "minitest/autorun" 9 | 10 | Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|file| require file } 11 | 12 | ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:" 13 | 14 | $redis = Redis.new(host: "localhost", port: 6379) 15 | Redis::Settings.connection = Redis::Namespace.new("redis-settings", redis: $redis) 16 | 17 | ActiveRecord::Schema.define(version: 0) do 18 | create_table :users do |t| 19 | t.string :login 20 | end 21 | end 22 | 23 | module Minitest 24 | class Test 25 | setup do 26 | $redis.keys("redis-settings*").each {|key| $redis.del(key) } 27 | end 28 | end 29 | end 30 | --------------------------------------------------------------------------------