Skip to content

Instantly share code, notes, and snippets.

@donovan-duplessis
Created May 5, 2015 19:21
Show Gist options
  • Save donovan-duplessis/aba19427edeaf9794b55 to your computer and use it in GitHub Desktop.
Save donovan-duplessis/aba19427edeaf9794b55 to your computer and use it in GitHub Desktop.
=begin
* Name: setting.rb
* Description: Generic Settings Model (Rails 4.1+)
* Version: 0.3
* Author: Donovan du Plessis <donovan@binarytrooper.com>
* Date: 2014/07/31
=end
change_log = """
2014-07-23 DdP Initial version
2014-07-24 DdP - Add dump/reload/truncate maintenance tasks
- Add cache prefix (logical bucket) configuration
- Add documentation to setup, run and maintain
2014-07-31 DdP - Remove cache_prefix; use namespace on cache store config
- Add ask flag on reload method to allow bypass question
- Add account scope to reload, dump and truncate methods
- Add account name accessor and scope all queries with account
name, keeping cache keys unique by having account name
appended to namespace.
"""
=begin
Configuration
-------------
Generate Setting Model:
rails g model Setting key:string value:text account:string
Copy setting.rb into:
<rails-app>/app/models/
Migrate Database:
rake db:migrate
Add gem to Gemfile:
gem 'highline'
bundle install
Usage
-----
Set Account (defaults to 'settings'):
Setting.account = 'development'
Create Settings (cached for cache_expire time period):
Setting.application = 'Test Application'
Setting.site = { name: 'Test', date: DateTime.now }
Retrieve Settings:
Setting.application
Setting.site
Setting.site[:name]
Rereive Settings (bypass cache, retrieve from database, re-cache)
Setting.application!
Setting.site!
Maintenance
-----------
Dump
Setting.dump [print settings from database]
Setting.dump(scoped: false)
Reload [reload settings from database and reinitialize cache]
Setting.reload
Truncate [remove all settings from database and clear cache]
Setting.truncate
Setting.truncate(scoped: false)
Setup Options
-------------
cache_expire: Set cache expiry time period, example:
- Setting.cache_expire = 300.seconds | 60.minutes | 6.hours | 1.year
account: Set account name to scope queries and add account name to cache
namespace to keep buckets unique per account, example:
- Setting.account = "account_name"
Cache Store Implementations (*optional)
---------------------------------------
+ Redis
=======
Install Redis:
sudo apt-get install redis-server
Add gem to Gemfile:
gem 'redis-rails'
Setup cache store to use dalli:
[config/environments/[development|production].rb]
config.cache_store = :redis_store, {
host: 'localhost',
port: 6379,
db: 0,
namespace: Rails.application.class.parent_name.downcase
}
+ Memcache
==========
Install Memcached:
sudo apt-get install memcached
Add gem to Gemfile:
gem 'dalli'
Setup cache store to use dalli:
[config/environments/[development|production].rb]
config.cache_store = :dalli_store, %w( 127.0.0.1 ),
{ namespace: Rails.application.class.parent_name }
Cache Key Format:
-----------------
<namespace>:<account>:<keyname> = <value>
=end
require 'highline/import'
class Setting < ActiveRecord::Base
include Humanizeable
serialize :value
# Validations
validates :key, presence: true
validates :value, presence: true
validates_uniqueness_of :key, scope: [:account]
# Scopes
scope :scoped_to, ->(account) { where(account: account) }
scope :for_update, ->(account) { scoped_to(account).order(:key, :label) }
class << self
attr_accessor :cache_expire, :account
def cache_expire
@cache_expire ||= 1.month.to_i
end
def account
@account ||= name.downcase.pluralize
end
def method_missing(method, *args, &block)
query = method.to_s
case query.chars.last
when '='
set(key: query.chop, value: args.first)
when '!'
get(key: query.chop, cache: false)
when /^\w+$/
get(key: query)
else
nil
end
end
def dump(scoped: true)
_with_setting(scoped: scoped) do |setting|
puts "key=[#{setting.key}], value=[#{setting.value.to_s}]"
end
end
def dumpconfig(scoped: true)
values = []
_with_setting(ask: false, scoped: scoped) do |setting|
values << "#{setting.key} = #{setting.value.to_s}"
end
return values
end
def reload(ask=true)
_with_setting(ask: ask) do |setting|
get(key: setting.key, cache: false)
end
end
def truncate(scoped: true)
_with_setting(scoped: scoped) do |setting|
setting.destroy unless setting.value.nil?
Rails.cache.delete(cachekey(setting.key))
end
end
private
def _question(q)
ask "\033[1m\033[36m#{q}\033[0m \033[33m(y/n)\033[0m"
end
def _yesno?(prompt)
answer = _question(prompt)
case answer.downcase
when 'yes', 'y'
true
when 'no', 'n'
false
else
_yesno?(prompt)
end
end
def _with_setting(ask: true, scoped: true)
function = (caller[0][/`.*'/][1..-2]).titlecase
continue = (
ask ? _yesno?("#{function} settings from database and cache?") : true)
if continue
(scoped ? scoped_to(account) : all).each do |setting|
yield(setting)
end
end
end
def cachekey(key)
[account, key].join(':')
end
def get(key:, cache: true)
if cache && (setting = Rails.cache.read(cachekey(key)))
setting
else
setting = scoped_to(account).where(key: key).first
if setting
Rails.cache.write(
cachekey(key), setting.value, expires_in: cache_expire)
setting.value
end
end
end
def set(key:, value:)
entry = scoped_to(account).where(key: key).first || new(key: key)
if value.nil?
entry.destroy unless entry.value.nil?
Rails.cache.delete(cachekey(key))
else
entry.update_attributes(value: value, account: account)
Rails.cache.write(cachekey(key), value, expires_in: cache_expire)
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment