Skip to content
This repository has been archived by the owner on May 18, 2018. It is now read-only.

Commit

Permalink
Initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
dim committed Jul 7, 2011
0 parents commit 9a000bc
Show file tree
Hide file tree
Showing 17 changed files with 636 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.yardoc
.project
spec/tmp
doc
*.gem
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--colour
11 changes: 11 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source "http://rubygems.org"

gemspec

group :test do
gem "rspec"
gem "rspec-rails", :require => false
gem "sqlite3-ruby"
gem "shoulda-matchers"
gem "actionpack", :require => false
end
81 changes: 81 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
PATH
remote: .
specs:
preferable (0.1.0)
abstract
activerecord (~> 3.0.0)
activesupport (~> 3.0.0)

GEM
remote: http://rubygems.org/
specs:
abstract (1.0.0)
actionpack (3.0.9)
activemodel (= 3.0.9)
activesupport (= 3.0.9)
builder (~> 2.1.2)
erubis (~> 2.6.6)
i18n (~> 0.5.0)
rack (~> 1.2.1)
rack-mount (~> 0.6.14)
rack-test (~> 0.5.7)
tzinfo (~> 0.3.23)
activemodel (3.0.9)
activesupport (= 3.0.9)
builder (~> 2.1.2)
i18n (~> 0.5.0)
activerecord (3.0.9)
activemodel (= 3.0.9)
activesupport (= 3.0.9)
arel (~> 2.0.10)
tzinfo (~> 0.3.23)
activesupport (3.0.9)
arel (2.0.10)
builder (2.1.2)
diff-lcs (1.1.2)
erubis (2.6.6)
abstract (>= 1.0.0)
i18n (0.5.0)
rack (1.2.3)
rack-mount (0.6.14)
rack (>= 1.0.0)
rack-test (0.5.7)
rack (>= 1.0)
railties (3.0.9)
actionpack (= 3.0.9)
activesupport (= 3.0.9)
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (~> 0.14.4)
rake (0.9.2)
rdoc (3.8)
rspec (2.6.0)
rspec-core (~> 2.6.0)
rspec-expectations (~> 2.6.0)
rspec-mocks (~> 2.6.0)
rspec-core (2.6.4)
rspec-expectations (2.6.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0)
rspec-rails (2.6.1)
actionpack (~> 3.0)
activesupport (~> 3.0)
railties (~> 3.0)
rspec (~> 2.6.0)
shoulda-matchers (1.0.0.beta2)
sqlite3 (1.3.3)
sqlite3-ruby (1.3.3)
sqlite3 (>= 1.3.3)
thor (0.14.6)
tzinfo (0.3.29)

PLATFORMS
ruby

DEPENDENCIES
actionpack
preferable!
rspec
rspec-rails
shoulda-matchers
sqlite3-ruby
3 changes: 3 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Preferable

TODO
8 changes: 8 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'rake'
require 'rspec/mocks/version'
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec)

desc 'Default: run specs.'
task :default => :spec
13 changes: 13 additions & 0 deletions lib/preferable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require "active_support/core_ext"
require "active_record"

module Preferable
autoload :Model, "preferable/model"
autoload :Schema, "preferable/schema"
autoload :Field, "preferable/field"
autoload :Set, "preferable/set"
end

ActiveRecord::Base.class_eval do
include ::Preferable::Model
end
92 changes: 92 additions & 0 deletions lib/preferable/field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
class Preferable::Field
TYPES = [:string, :integer, :float, :boolean, :date, :datetime, :array].to_set.freeze
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set

attr_reader :name, :type, :options, :default

# Create a new Field.
#
# Params:
# name: The name symbol
# type: The type symbol, see Preferable::Field::TYPES
# options: The options hash (keys as symbols)
#
# Options:
# default: The default value.
# if: Value assignment proc. Value is only assigned if true is yielded.
# unless: Value assignment proc. Value is only assigned if false is yielded.
# cast: Only relevant for :array type. Specify the type of the array contents.
#
# Examples:
# Field.new :color, :string, :if => lambda {|v| v =~ /^[A-F0-9]{6}$/ }
# Field.new :holidays, :array, :cast => :date
# Field.new :newsletter, :boolean, :default => false
# Field.new :age, :integer, :unless => &:zero?
#
def initialize(name, type, options = {})
raise ArgumentError, "Unknown type '#{type}', available types are: #{TYPES.map(&:to_s).join(', ')}" unless TYPES.include?(type.to_sym)

@name = name.to_sym
@type = type.to_sym
@options = options.dup
@default = type_cast @options.delete(:default)
end

# Returns true if a value is assignable, else false. Assumes given value is already type-casted.
def valid?(value)
result = true
result = options[:if].call(value) if result && options[:if]
result = !options[:unless].call(value) if result && options[:unless]
result
end

# Opposite of #valid?
def invalid?(value)
!valid?(value)
end

# Is value equal to the default. . Assumes given value is already type-casted.
def default?(value)
value == default
end

# Converts a value.
def type_cast(value, to = self.type)
return nil unless value

case to
when :string
value.to_s
when :integer
value.to_i
when :boolean
TRUE_VALUES.include?(value)
when :datetime
to_time(value)
when :date
to_time(value).try(:to_date)
when :float
value.to_f
when :array
Array.wrap(value).tap do |wrap|
wrap.map! {|item| type_cast(item, options[:cast]) } if options[:cast]
end
else
value
end
end

private

def to_time(value)
case value
when Time
value.in_time_zone
when Date
value.beginning_of_day
else
Time.zone.parse(value) rescue nil
end
end

end
55 changes: 55 additions & 0 deletions lib/preferable/model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Includable module, for ActiveRecord::Base
module Preferable::Model
extend ActiveSupport::Concern

included do
class_attribute :_preferable
end

module ClassMethods

# Preferable definition for a model. Example:
#
# class User < ActiveRecord::Base
#
# preferable do
# integer :theme_id
# boolean :accessible, :default => false
# string :font_color, :default => "444444", :if => lambda {|value| value =~ /^[A-F0-9]{6}$/ }
# end
#
# end
#
def preferable(&block)
unless _preferable
self._preferable = Preferable::Schema.new
serialize :preferences, Preferable::Set
include PreferableMethods
end
self._preferable.instance_eval(&block) if block
self._preferable
end

end

module PreferableMethods

# Accessor to preferences. Examples:
#
# user = User.find(1)
# user.preferences[:theme_id] # => 8
# user.preferences[:theme_id] = 3
#
def preferences
value = read_attribute(:preferences)
value.is_a?(Preferable::Set) ? value : write_attribute(:preferences, Preferable::Set.new(self.class.name))
end

# Preferences writer. Updates existing preferences (doesn't replace them!)
def preferences=(hash)
preferences.update(hash) if hash.is_a?(Hash)
preferences
end

end
end
20 changes: 20 additions & 0 deletions lib/preferable/schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'set'

class Preferable::Schema < Hash

def initialize
super
end

def field(name, type, options = {})
item = Preferable::Field.new(name, type, options)
self[item.name] = item
end

Preferable::Field::TYPES.each do |sym|
define_method sym do |name, options|
field name, sym, options
end
end

end
46 changes: 46 additions & 0 deletions lib/preferable/set.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
class Preferable::Set < Hash
MODEL_KEY = "_".freeze
PRIVATE = [:rehash, :fetch, :store, :shift, :delete, :delete_if, :keep_if].to_set

# Make destructive methods private
(public_instance_methods - Hash.superclass.public_instance_methods).each do |m|
PRIVATE.include?(m.to_sym) || m.to_s.ends_with?('!')
end

def initialize(model_name)
super()
store MODEL_KEY, "::#{model_name}"
end

def model
@model ||= fetch(MODEL_KEY).constantize
end

def [](name)
field = find_field(name)
super(field.name) || field.default if field
end

def []=(name, value)
field = find_field(name) || return
value = field.type_cast(value)

if value.nil? || field.invalid?(value) || field.default?(value)
delete field.name
else
super field.name, value
end
end

def set(pairs)
pairs.each {|k, v| self[k] = v }
self
end

private

def find_field(name)
model.preferable[name.to_sym]
end

end
22 changes: 22 additions & 0 deletions preferable.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.required_ruby_version = '>= 1.8.7'
s.required_rubygems_version = ">= 1.3.6"

s.name = "preferable"
s.summary = "Simple preferences store for ActiveRecord"
s.description = "Great e.g. for storing user preferences"
s.version = '0.1.0'

s.authors = ["Dimitrij Denissenko"]
s.email = "dimitrij@blacksquaremedia.com"
s.homepage = "https://github.com/bsm/preferable"

s.require_path = 'lib'
s.files = Dir['LICENSE', 'README.markdown', 'lib/**/*']

s.add_dependency "abstract"
s.add_dependency "activerecord", "~> 3.0.0"
s.add_dependency "activesupport", "~> 3.0.0"
end
Loading

0 comments on commit 9a000bc

Please sign in to comment.