Created
April 3, 2011 23:11
-
-
Save JEG2/900912 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "date" | |
require "digest/sha2" | |
require "pathname" | |
require "time" | |
require "uri" | |
require "rubygems" | |
require "rufus/tokyo" | |
class OklahomaMixer | |
class Database < Rufus::Tokyo::Table | |
def transaction(&block) | |
return block.call if defined?(@in_transaction) and @in_transaction | |
begin | |
@in_transaction = true | |
super | |
ensure | |
@in_transaction = false | |
end | |
end | |
end | |
def self.db_file | |
@db_file ||= "mixer.tct" | |
end | |
def self.db_file=(path) | |
@db_file = path | |
end | |
def self.db(mode = "wc") | |
return yield(@db) if defined?(@db) and @db | |
Database.open(db_file, :mode => mode) do |db| | |
begin | |
@db = db | |
yield @db | |
ensure | |
@db = nil | |
end | |
end | |
end | |
def self.validations | |
@validations ||= [ ] | |
end | |
def self.validate(validation, key = nil, &test) | |
validations << [key.nil? ? key : key.to_s, validation, test] | |
end | |
def self.apply_validation(key, name, options, &test) | |
if option = options.delete(name) | |
validate(name, key) { |object| | |
args = [ object, | |
object.respond_to?(key) ? object.send(key) : object[key] ] | |
args << option if test.arity > 1 | |
test[*args] | |
} | |
end | |
end | |
private_class_method :apply_validation | |
def self.apply_validations(key, kind, options) | |
options = options.inject({ }) { |all, option| | |
all.merge(option.is_a?(Hash) ? option : {option => true}) | |
} | |
apply_validation(key, :required, options) { |object, _| | |
keys = kind == :password ? %W[#{key}_encrypted #{key}_salt] : [key] | |
keys.all? { |k| | |
value = object[k] | |
not (value.nil? or (value.respond_to?(:empty?) and value.empty?)) | |
} | |
} | |
apply_validation(key, :confirm, options) { |object, value| | |
case kind | |
when :password | |
object.send("#{key}_matches?", object.send("#{key}_confirmation")) | |
else | |
value == object.send("#{key}_confirmation") | |
end | |
} and define_confirmation_accessor(key, kind) | |
apply_validation(key, :matches, options) { |_, value, regexp| | |
value =~ regexp | |
} | |
apply_validation(key, :numeric, options) { |object, _| | |
(Integer(object[key]) rescue Float(object[key])) rescue false | |
} | |
apply_validation(key, :integer, options) { |object, _| | |
Integer(object[key]) rescue false | |
} | |
apply_validation(key, :float, options) { |object, _| | |
Float(object[key]) rescue false | |
} | |
apply_validation(key, :<, options) { |_, value, limit| value < limit } | |
apply_validation(key, :<=, options) { |_, value, limit| value <= limit } | |
apply_validation(key, :>, options) { |_, value, limit| value > limit } | |
apply_validation(key, :>=, options) { |_, value, limit| value >= limit } | |
apply_validation(key, :==, options) { |_, value, limit| value == limit } | |
apply_validation(key, :===, options) { |_, value, test| test === value } | |
apply_validation(key, :in, options) { |_, value, allowed| | |
allowed.include? value | |
} | |
apply_validation(key, :not_in, options) { |_, value, not_allowed| | |
not not_allowed.include? value | |
} | |
apply_validation(key, :max_size, options) { |_, value, max| | |
value.size <= max | |
} | |
apply_validation(key, :min_size, options) { |_, value, min| | |
value.size >= min | |
} | |
apply_validation(key, :size, options) { |_, value, size| | |
size === value.size | |
} | |
end | |
private_class_method :apply_validations | |
def self.define_confirmation_accessor(key, kind) | |
var = "@#{key}_confirmation" | |
define_method(var[1..-1]) do | |
return unless instance_variable_defined? var | |
string_to_value(instance_variable_get(var), kind) | |
end | |
define_method("#{var[1..-1]}=") do |value| | |
instance_variable_set(var, value_to_string(value, kind)) | |
end | |
end | |
private_class_method :define_confirmation_accessor | |
def self.define_password_accessor(key) | |
define_method("#{key}_matches?") do |password| | |
self["#{key}_encrypted"] == | |
encrypt_password(password, self["#{key}_salt"]) | |
end | |
define_method("#{key}=") do |value| | |
if value.nil? | |
self["#{key}_salt"] = nil | |
self["#{key}_encrypted"] = nil | |
else | |
salt = generate_password_salt | |
self["#{key}_salt"] = salt | |
self["#{key}_encrypted"] = encrypt_password(value, salt) | |
end | |
end | |
end | |
private_class_method :define_password_accessor | |
def self.define_attribute_accessor(key, kind) | |
define_method("#{key}#{'?' if kind == :boolean}") do | |
string_to_value(self[key], kind) | |
end | |
define_method("#{key}=") do |value| | |
self[key] = value_to_string(value, kind) | |
end | |
end | |
private_class_method :define_attribute_accessor | |
def self.attribute(key, kind = :string, *options) | |
apply_validations(key, kind, options) | |
if kind == :password | |
define_password_accessor(key) | |
else | |
define_attribute_accessor(key, kind) | |
end | |
end | |
def self.string(key, *options) | |
attribute(key, :string, *options) | |
end | |
def self.password(key, *options) | |
attribute(key, :password, *options) | |
end | |
def self.integer(key, *options) | |
attribute(key, :integer, *options) | |
end | |
def self.float(key, *options) | |
attribute(key, :float, *options) | |
end | |
def self.boolean(key, *options) | |
attribute(key, :boolean, *options) | |
end | |
def self.timestamp(key, *options) | |
attribute(key, :timestamp, *options) | |
end | |
def self.path(key, *options) | |
attribute(key, :path, *options) | |
end | |
def self.url(key, *options) | |
attribute(key, :url, *options) | |
end | |
def self.find(*args) | |
if args.first == :all | |
OklahomaMixer.db do |db| | |
db.query do |query| | |
build_query(query, args.last.is_a?(Hash) ? args.last : { }) | |
end | |
end | |
end | |
end | |
def self.build_query(query, details) | |
query.add("", :starts_with, "#{self}:") | |
end | |
private_class_method :build_query | |
def initialize(*args) | |
@id = args.first.is_a?(Hash) ? nil : args.first | |
@new = @id.nil? | |
@attributes = { } | |
@errors = [ ] | |
update(args.last) if args.last.is_a? Hash | |
end | |
attr_accessor :id | |
attr_reader :errors | |
def primary_key | |
"#{self.class}:#{id}" | |
end | |
def new? | |
@new | |
end | |
def [](key) | |
@attributes[key.to_s] | |
end | |
def []=(key, value) | |
if value.nil? | |
@attributes.delete(key.to_s) | |
else | |
@attributes[key.to_s] = value.to_s | |
end | |
end | |
def update(attributes) | |
attributes.each do |key, value| | |
setter = "#{key}=" | |
if respond_to? setter | |
self.send(setter, value) | |
else | |
self[key] = value | |
end | |
end | |
end | |
def valid? | |
@errors.clear | |
self.class.validations.each do |key, validation, test| | |
@errors << [key, validation] unless test[self] | |
end | |
@errors.empty? | |
end | |
def invalid?(key) | |
[email protected](key.to_s) | |
end | |
def save | |
return false unless valid? | |
OklahomaMixer.db do |db| | |
self.id ||= db.generate_unique_id | |
db[primary_key] = @attributes | |
end | |
@new = false | |
true | |
rescue Rufus::Edo::EdoError | |
false | |
end | |
private | |
def string_to_value(string, kind) | |
return string if string.nil? | |
case kind | |
when :integer then Integer(string) rescue string.to_i | |
when :float then Float(string) rescue string.to_f | |
when :boolean then !!(string =~ /\A(?:t(?:rue)?|y(?:es)?|1)\z/i) | |
when :timestamp then Time.xmlschema(string).utc | |
when :path then Pathname.new(string) | |
when :url then URI.parse(string) rescue string | |
else string | |
end | |
end | |
def value_to_string(value, kind) | |
case kind | |
when :timestamp | |
case value | |
when Time then value.getutc.xmlschema | |
when DateTime then value.new_offset(0).strftime("%Y-%m-%dT%H:%M:%SZ") | |
when Date then value.strftime("%Y-%m-%dT%H:%M:%SZ") | |
else value | |
end | |
when :url | |
case value | |
when String then value.sub(/\A(?!\w+:)(?=.)/, "http://") | |
else value | |
end | |
else | |
value | |
end | |
end | |
def generate_password_salt | |
[Array.new(6) { rand(256).chr }.join].pack("m").chomp | |
end | |
def encrypt_password(password, salt) | |
Digest::SHA256.hexdigest("#{password}#{salt}") | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment