ricordami 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +13 -0
- data/ISSUES.md +0 -0
- data/MIT-LICENSE +21 -0
- data/README.md +454 -0
- data/TODO.md +21 -0
- data/examples/calls.rb +64 -0
- data/examples/singers.rb +42 -0
- data/lib/ricordami/attribute.rb +52 -0
- data/lib/ricordami/can_be_queried.rb +133 -0
- data/lib/ricordami/can_be_validated.rb +27 -0
- data/lib/ricordami/can_have_relationships.rb +152 -0
- data/lib/ricordami/configuration.rb +39 -0
- data/lib/ricordami/connection.rb +22 -0
- data/lib/ricordami/exceptions.rb +14 -0
- data/lib/ricordami/has_attributes.rb +159 -0
- data/lib/ricordami/has_indices.rb +105 -0
- data/lib/ricordami/is_lockable.rb +52 -0
- data/lib/ricordami/is_persisted.rb +123 -0
- data/lib/ricordami/is_retrievable.rb +35 -0
- data/lib/ricordami/key_namer.rb +63 -0
- data/lib/ricordami/model.rb +29 -0
- data/lib/ricordami/query.rb +68 -0
- data/lib/ricordami/relationship.rb +40 -0
- data/lib/ricordami/unique_index.rb +59 -0
- data/lib/ricordami/unique_validator.rb +21 -0
- data/lib/ricordami/value_index.rb +26 -0
- data/lib/ricordami/version.rb +3 -0
- data/lib/ricordami.rb +26 -0
- data/spec/acceptance/manage_relationships_spec.rb +42 -0
- data/spec/acceptance/model_with_validation_spec.rb +78 -0
- data/spec/acceptance/query_model_spec.rb +93 -0
- data/spec/acceptance_helper.rb +2 -0
- data/spec/ricordami/attribute_spec.rb +113 -0
- data/spec/ricordami/can_be_queried_spec.rb +254 -0
- data/spec/ricordami/can_be_validated_spec.rb +115 -0
- data/spec/ricordami/can_have_relationships_spec.rb +255 -0
- data/spec/ricordami/configuration_spec.rb +45 -0
- data/spec/ricordami/connection_spec.rb +25 -0
- data/spec/ricordami/exceptions_spec.rb +43 -0
- data/spec/ricordami/has_attributes_spec.rb +266 -0
- data/spec/ricordami/has_indices_spec.rb +73 -0
- data/spec/ricordami/is_lockable_spec.rb +45 -0
- data/spec/ricordami/is_persisted_spec.rb +186 -0
- data/spec/ricordami/is_retrievable_spec.rb +55 -0
- data/spec/ricordami/key_namer_spec.rb +56 -0
- data/spec/ricordami/model_spec.rb +65 -0
- data/spec/ricordami/query_spec.rb +156 -0
- data/spec/ricordami/relationship_spec.rb +123 -0
- data/spec/ricordami/unique_index_spec.rb +87 -0
- data/spec/ricordami/unique_validator_spec.rb +41 -0
- data/spec/ricordami/value_index_spec.rb +40 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/constants.rb +43 -0
- data/spec/support/db_manager.rb +18 -0
- data/test/bin/data_loader.rb +107 -0
- data/test/data/domains.txt +462 -0
- data/test/data/first_names.txt +1220 -0
- data/test/data/last_names.txt +1028 -0
- data/test/data/people_100_000.csv.bz2 +0 -0
- data/test/data/people_10_000.csv.bz2 +0 -0
- data/test/data/people_1_000_000.csv.bz2 +0 -0
- metadata +258 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
require "ricordami/key_namer"
|
2
|
+
require "ricordami/query.rb"
|
3
|
+
|
4
|
+
module Ricordami
|
5
|
+
module CanBeQueried
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
[:where, :and, :not, :any].each do |op|
|
10
|
+
define_method(op) do |*args|
|
11
|
+
options = args.first || {}
|
12
|
+
Query.new(self).send(op, options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def sort(*args)
|
17
|
+
Query.new(self).send(:sort, *args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def all(opts = {})
|
21
|
+
result_key = run_expressions(opts.delete(:expressions) || [])
|
22
|
+
get_result_ids(result_key, opts).map do |id|
|
23
|
+
self[id]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def paginate(opts = {})
|
28
|
+
result_key = run_expressions(opts.delete(:expressions) || [])
|
29
|
+
page = opts[:page] || 1
|
30
|
+
per_page = opts[:per_page] || 20
|
31
|
+
start = (page - 1) * per_page
|
32
|
+
opts[:limit] = [start, per_page]
|
33
|
+
get_result_ids(result_key, opts).map do |id|
|
34
|
+
self[id]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def first(opts = {})
|
39
|
+
result_key = run_expressions(opts.delete(:expressions) || [])
|
40
|
+
opts[:limit] = [0, 1]
|
41
|
+
ids = get_result_ids(result_key, opts)
|
42
|
+
self[ids.first]
|
43
|
+
end
|
44
|
+
|
45
|
+
def last(opts = {})
|
46
|
+
result_key = run_expressions(opts.delete(:expressions) || [])
|
47
|
+
size = redis.scard(result_key)
|
48
|
+
opts[:limit] = [size - 1, 1]
|
49
|
+
ids = get_result_ids(result_key, opts)
|
50
|
+
self[ids.first]
|
51
|
+
end
|
52
|
+
|
53
|
+
def rand(opts = {})
|
54
|
+
result_key = run_expressions(opts.delete(:expressions) || [])
|
55
|
+
size = redis.scard(result_key)
|
56
|
+
opts[:limit] = [Kernel.rand(size), 1]
|
57
|
+
ids = get_result_ids(result_key, opts)
|
58
|
+
self[ids.first]
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def run_expressions(expressions)
|
64
|
+
key_all_ids = indices[:id].uidx_key_name
|
65
|
+
result_key = expressions.reduce(key_all_ids) do |key, expression|
|
66
|
+
type, conditions = expression
|
67
|
+
condition_keys = get_keys_for_each_condition(conditions)
|
68
|
+
next key if condition_keys.empty?
|
69
|
+
target_key = key_name_for_expression(type, conditions, key)
|
70
|
+
send("run_#{type}", target_key, key, condition_keys)
|
71
|
+
end
|
72
|
+
result_key.empty?? [] : result_key
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_keys_for_each_condition(conditions)
|
76
|
+
conditions.map do |field, value|
|
77
|
+
index = indices[field]
|
78
|
+
raise MissingIndex.new("class: #{self}, attribute: #{field.inspect}") if index.nil?
|
79
|
+
index.key_name_for_value(value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def key_name_for_expression(type, conditions, previous_key)
|
84
|
+
KeyNamer.volatile_set(self, :key => previous_key,
|
85
|
+
:info => [type] + conditions.keys)
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_result_ids(key, opts)
|
89
|
+
return redis.smembers(key) unless opts[:sort_by] || opts[:limit]
|
90
|
+
sort_key = KeyNamer.sort(self, :sort_by => opts[:sort_by])
|
91
|
+
sort_options = opts.slice(:order, :limit)
|
92
|
+
redis.sort(key, sort_options.merge(:by => sort_key))
|
93
|
+
end
|
94
|
+
|
95
|
+
def run_and(key_name, start_key, keys)
|
96
|
+
# we get the intersection of the start key and the condition keys
|
97
|
+
redis.sinterstore(key_name, start_key, *keys)
|
98
|
+
key_name
|
99
|
+
end
|
100
|
+
|
101
|
+
alias :run_where :run_and
|
102
|
+
|
103
|
+
def run_any(key_name, start_key, keys)
|
104
|
+
tmp_key = KeyNamer.temporary(self)
|
105
|
+
keys.each_with_index do |key, i|
|
106
|
+
if i == 0
|
107
|
+
# if only one condition key, :any condition is same as :and condition
|
108
|
+
redis.sinterstore(key_name, start_key, keys.first)
|
109
|
+
else
|
110
|
+
# we get the intersection of the start key with each condition key
|
111
|
+
# and we make a union of all of those
|
112
|
+
end
|
113
|
+
redis.sinterstore(tmp_key, start_key, key)
|
114
|
+
redis.sunionstore(key_name, key_name, tmp_key)
|
115
|
+
end
|
116
|
+
redis.del(tmp_key)
|
117
|
+
key_name
|
118
|
+
end
|
119
|
+
|
120
|
+
def run_not(key_name, start_key, keys)
|
121
|
+
keys.each_with_index do |key, i|
|
122
|
+
redis.sdiffstore(key_name, i == 0 ? start_key : key_name, key)
|
123
|
+
end
|
124
|
+
key_name
|
125
|
+
end
|
126
|
+
|
127
|
+
def reverse_order(order)
|
128
|
+
return order.sub("DESC", "ASC") if order.index("DESC")
|
129
|
+
order.sub("ASC", "DESC")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "active_model/validations"
|
2
|
+
require "ricordami/unique_validator"
|
3
|
+
|
4
|
+
module Ricordami
|
5
|
+
module CanBeValidated
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include ActiveModel::Validations
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def validates_uniqueness_of(*attr_names)
|
11
|
+
validates_with UniqueValidator, _merge_attributes(attr_names)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
def valid?
|
17
|
+
raise ModelHasBeenDeleted.new("can't validate a deleted model") if deleted?
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def save(opts = {})
|
22
|
+
return false unless opts[:validate] == false || valid?
|
23
|
+
super(opts)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require "ricordami/relationship"
|
2
|
+
require "ricordami/can_be_queried"
|
3
|
+
|
4
|
+
module Ricordami
|
5
|
+
module CanHaveRelationships
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do |base|
|
9
|
+
base.send(:include, CanBeQueried)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def relationships
|
14
|
+
@relationships ||= {}
|
15
|
+
end
|
16
|
+
|
17
|
+
Relationship::SUPPORTED_TYPES.each do |type|
|
18
|
+
define_method(type) do |*args|
|
19
|
+
name = args.first
|
20
|
+
options = args[1] || {}
|
21
|
+
options.merge!(:other => name, :self => self.to_s.underscore.to_sym)
|
22
|
+
relationship = Relationship.new(type, options)
|
23
|
+
self.relationships[relationship.name] = relationship
|
24
|
+
setup_method = :"setup_#{type}"
|
25
|
+
send(setup_method, relationship) if respond_to?(setup_method, true)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def set_block_to_delete_dependents(relationship)
|
32
|
+
queue_deleting_operations do |obj, session|
|
33
|
+
ref_objs = obj.send(relationship.name)
|
34
|
+
ref_objs = [ref_objs] if relationship.type == :references_one
|
35
|
+
ref_objs.each { |ref_obj| ref_obj.prepare_delete(session) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_block_to_nullify_dependents(relationship)
|
40
|
+
queue_deleting_operations do |obj, session|
|
41
|
+
ref_objs = obj.send(relationship.name)
|
42
|
+
ref_objs = [ref_objs] if relationship.type == :references_one
|
43
|
+
ref_objs.each do |ref_obj|
|
44
|
+
ref_obj.update_attributes(relationship.referrer_id => nil)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def lazy_setup_references_many(relationship)
|
50
|
+
klass = relationship.object_class
|
51
|
+
referrer_id_sym = relationship.referrer_id.to_sym
|
52
|
+
define_method(relationship.name) do
|
53
|
+
return Query.new([], klass) unless persisted?
|
54
|
+
klass.where(referrer_id_sym => self.id)
|
55
|
+
end
|
56
|
+
case relationship.dependent
|
57
|
+
when :delete then set_block_to_delete_dependents(relationship)
|
58
|
+
when :nullify then set_block_to_nullify_dependents(relationship)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def define_builders(name, klass, referrer_id_sym)
|
63
|
+
# define reference build method
|
64
|
+
build_method = :"build_#{name}"
|
65
|
+
define_method(build_method) do |*args|
|
66
|
+
options = args.first || {}
|
67
|
+
klass.new(options.merge(referrer_id_sym => self.id))
|
68
|
+
end
|
69
|
+
# define reference create method
|
70
|
+
define_method(:"create_#{name}") do |*args|
|
71
|
+
send(build_method, *args).tap { |obj| obj.save }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def lazy_setup_references_one(relationship)
|
76
|
+
klass = relationship.object_class
|
77
|
+
referrer_id_sym = relationship.referrer_id.to_sym
|
78
|
+
define_builders(relationship.name, klass, referrer_id_sym)
|
79
|
+
# define reference method reader
|
80
|
+
define_method(relationship.name) do
|
81
|
+
return nil unless persisted?
|
82
|
+
klass.where(referrer_id_sym => self.id).first
|
83
|
+
end
|
84
|
+
case relationship.dependent
|
85
|
+
when :delete then set_block_to_delete_dependents(relationship)
|
86
|
+
when :nullify then set_block_to_nullify_dependents(relationship)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def setup_referenced_in(relationship)
|
91
|
+
attribute(relationship.referrer_id, :indexed => :value)
|
92
|
+
overide_referrer_id_reader(relationship)
|
93
|
+
end
|
94
|
+
|
95
|
+
def lazy_setup_referenced_in(relationship)
|
96
|
+
klass = relationship.object_class
|
97
|
+
name = relationship.name
|
98
|
+
define_builders(name, klass, relationship.referrer_id.to_sym)
|
99
|
+
referrer_var = :"@#{name}"
|
100
|
+
define_method(name) do
|
101
|
+
referrer = instance_variable_get(referrer_var)
|
102
|
+
return referrer unless referrer.nil?
|
103
|
+
referrer_id_val = send(relationship.referrer_id)
|
104
|
+
return nil if referrer_id_val.nil?
|
105
|
+
klass.get(referrer_id_val).tap do |referrer|
|
106
|
+
instance_variable_set(referrer_var, referrer)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def overide_referrer_id_reader(relationship)
|
112
|
+
referrer_var = :"@#{relationship.name}"
|
113
|
+
# overide referrer id to sweep cache
|
114
|
+
define_method(:"#{relationship.referrer_id}=") do |value|
|
115
|
+
instance_variable_set(referrer_var, nil)
|
116
|
+
super(value)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
module InstanceMethods
|
122
|
+
private
|
123
|
+
|
124
|
+
RE_METHOD = /^(build|create)_(.*)$/
|
125
|
+
|
126
|
+
def method_missing(meth, *args, &blk)
|
127
|
+
match = RE_METHOD.match(meth.to_s)
|
128
|
+
meth_root = match.nil?? meth : match[2].to_sym
|
129
|
+
if relationship = self.class.relationships[meth_root]
|
130
|
+
self.class.send(:"lazy_setup_#{relationship.type}", relationship)
|
131
|
+
send(meth, *args, &blk)
|
132
|
+
else
|
133
|
+
super
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
if Object.respond_to?(:respond_to_missing?)
|
138
|
+
def respond_to_missing?(meth, include_private)
|
139
|
+
self.class.relationships.has_key?(meth)
|
140
|
+
end
|
141
|
+
else
|
142
|
+
def respond_to?(meth)
|
143
|
+
match = RE_METHOD.match(meth.to_s)
|
144
|
+
meth_root = match.nil?? meth : match[2].to_sym
|
145
|
+
return true if self.class.relationships.has_key?(meth_root)
|
146
|
+
super
|
147
|
+
end
|
148
|
+
public :respond_to?
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Ricordami
|
2
|
+
module Configuration
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def configure(&block)
|
7
|
+
raise ArgumentError.new("block missing") unless block_given?
|
8
|
+
yield configuration
|
9
|
+
end
|
10
|
+
|
11
|
+
def configuration
|
12
|
+
@configuration ||= Config.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Config
|
18
|
+
ATTRIBUTE_NAMES = [:redis_host, :redis_port, :redis_db, :thread_safe]
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@options = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def from_hash(options)
|
25
|
+
@options = options.slice(*ATTRIBUTE_NAMES)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def method_missing(meth, *args, &blk)
|
31
|
+
return @options[meth] if ATTRIBUTE_NAMES.include?(meth)
|
32
|
+
if args.length == 1 && match = /^(.*)=$/.match(meth.to_s)
|
33
|
+
name = match[1].to_sym
|
34
|
+
return @options[name] = args.first if ATTRIBUTE_NAMES.include?(name)
|
35
|
+
end
|
36
|
+
raise AttributeNotSupported.new("attribute #{meth.to_s.inspect} is not supported")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Ricordami
|
2
|
+
module Connection
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def driver
|
7
|
+
@driver ||= create_driver
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def create_driver
|
13
|
+
c = self.configuration
|
14
|
+
Redis.new(:host => c.redis_host,
|
15
|
+
:port => c.redis_port,
|
16
|
+
:db => c.redis_db,
|
17
|
+
:thread_safe => c.thread_safe,
|
18
|
+
:timeout => 10)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Ricordami
|
2
|
+
Error = Class.new(StandardError)
|
3
|
+
NotFound = Class.new(Error)
|
4
|
+
AttributeNotSupported = Class.new(Error)
|
5
|
+
ReadOnlyAttribute = Class.new(Error)
|
6
|
+
InvalidIndexDefinition = Class.new(Error)
|
7
|
+
ModelHasBeenDeleted = Class.new(Error)
|
8
|
+
TypeNotSupported = Class.new(Error)
|
9
|
+
MissingIndex = Class.new(Error)
|
10
|
+
LockTimeout = Class.new(Error)
|
11
|
+
EventNotSupported = Class.new(Error)
|
12
|
+
OptionValueInvalid = Class.new(Error)
|
13
|
+
MissingMandatoryArgs = Class.new(Error)
|
14
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require "ricordami/key_namer"
|
2
|
+
require "ricordami/attribute"
|
3
|
+
|
4
|
+
module Ricordami
|
5
|
+
module HasAttributes
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include ActiveModel::AttributeMethods
|
8
|
+
include ActiveModel::Dirty
|
9
|
+
|
10
|
+
included do
|
11
|
+
attribute_method_suffix('', '=')
|
12
|
+
attribute :id, :read_only => true,
|
13
|
+
:initial => :sequence,
|
14
|
+
:type => :string
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def attributes
|
19
|
+
@attributes ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def attribute(name, options = {})
|
23
|
+
instance = Attribute.new(name, options)
|
24
|
+
options = OptionsExpander.new(self, options)
|
25
|
+
self.attributes[name.to_sym] = instance
|
26
|
+
index(instance.indexed => name.to_sym) if instance.indexed?
|
27
|
+
instance
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes_key_name_for(id)
|
31
|
+
KeyNamer.attributes(self.to_s, :id => id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module InstanceMethods
|
36
|
+
attr_reader :attributes
|
37
|
+
|
38
|
+
def initialize(attrs = {})
|
39
|
+
@attributes = {}.with_indifferent_access
|
40
|
+
@reloading = false
|
41
|
+
update_mem_attributes(attrs) unless attrs.empty?
|
42
|
+
set_default_attribute_values
|
43
|
+
end
|
44
|
+
|
45
|
+
# ActiveModel::AttributeMethods doesn't seem
|
46
|
+
# to generate a reader for id that uses
|
47
|
+
# #attributes["id"] in Ruby 1.8.7, so hard-coding
|
48
|
+
# it now
|
49
|
+
def id
|
50
|
+
@attributes["id"]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Replace attribute values with the hash attrs
|
54
|
+
# Note: attrs keys can be strings or symbols
|
55
|
+
def update_mem_attributes(attrs)
|
56
|
+
@reloading = true
|
57
|
+
update_mem_attributes!(attrs)
|
58
|
+
@reloading = false
|
59
|
+
end
|
60
|
+
|
61
|
+
def update_mem_attributes!(attrs)
|
62
|
+
valid_keys = self.class.attributes.keys
|
63
|
+
attrs.symbolize_keys.slice(*valid_keys).each do |name, value|
|
64
|
+
send(:"#{name}=", value)
|
65
|
+
end
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def update_mem_attributes(attrs)
|
70
|
+
valid_keys = self.class.attributes.keys
|
71
|
+
attrs.symbolize_keys.slice(*valid_keys).each do |name, value|
|
72
|
+
write_attribute(name, value)
|
73
|
+
end
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def write_attribute(name, value)
|
80
|
+
converted = value.send(self.class.attributes[name].converter) unless value.nil?
|
81
|
+
return converted if @attributes[name] == converted
|
82
|
+
if @persisted_attributes && @persisted_attributes[name] == converted
|
83
|
+
@changed_attributes.delete(name.to_s)
|
84
|
+
else
|
85
|
+
attribute_will_change!(name.to_s)
|
86
|
+
end
|
87
|
+
@attributes[name] = converted
|
88
|
+
end
|
89
|
+
|
90
|
+
def attribute(name)
|
91
|
+
@attributes[name]
|
92
|
+
end
|
93
|
+
|
94
|
+
def attribute=(name, value)
|
95
|
+
raise ModelHasBeenDeleted.new("can't update attribute #{name}") if deleted?
|
96
|
+
assert_can_update!(name) unless @reloading
|
97
|
+
write_attribute(name.to_sym, value)
|
98
|
+
end
|
99
|
+
|
100
|
+
def assert_can_update!(name)
|
101
|
+
definition = self.class.attributes[name.to_sym]
|
102
|
+
if definition.read_only? && @attributes[name].present?
|
103
|
+
raise ReadOnlyAttribute.new("can't change #{name}")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def set_default_attribute_values
|
108
|
+
self.class.attributes.each do |name, attribute|
|
109
|
+
unless @attributes.has_key?(name)
|
110
|
+
@attributes[name] = if attribute.default_value?
|
111
|
+
attribute_will_change!(name.to_s)
|
112
|
+
attribute.default_value
|
113
|
+
else
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def set_initial_attribute_values
|
121
|
+
self.class.attributes.each do |name, attribute|
|
122
|
+
unless @attributes[name].present?
|
123
|
+
@attributes[name] = if attribute.initial_value?
|
124
|
+
attribute_will_change!(name.to_s)
|
125
|
+
attribute.initial_value
|
126
|
+
else
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def attributes_key_name
|
134
|
+
@attributes_key_name ||= self.class.attributes_key_name_for(id)
|
135
|
+
end
|
136
|
+
|
137
|
+
def attributes_synced_with_db!
|
138
|
+
@persisted_attributes = @attributes.clone
|
139
|
+
@previously_changed = changes
|
140
|
+
@changed_attributes.clear if @changed_attributes
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class OptionsExpander
|
145
|
+
def initialize(model, opts = {})
|
146
|
+
opts.slice(:initial, :default).each do |name, value|
|
147
|
+
next unless value.is_a?(Symbol) && respond_to?(value)
|
148
|
+
opts[name] = send(value, model)
|
149
|
+
end
|
150
|
+
opts
|
151
|
+
end
|
152
|
+
|
153
|
+
def sequence(model)
|
154
|
+
key = KeyNamer.sequence(model, :type => "id")
|
155
|
+
Proc.new { model.redis.incr(key).to_s }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "ricordami/has_attributes"
|
2
|
+
require "ricordami/unique_index"
|
3
|
+
require "ricordami/value_index"
|
4
|
+
|
5
|
+
module Ricordami
|
6
|
+
module HasIndices
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def indices
|
11
|
+
@indices ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def index(options = {})
|
15
|
+
# for now we can only create unique indices
|
16
|
+
options.assert_valid_keys(:unique, :get_by, :value)
|
17
|
+
fields = options.delete(:unique)
|
18
|
+
return unique_index(fields, options) if fields.present?
|
19
|
+
field = options.delete(:value)
|
20
|
+
return value_index(field) if field.present?
|
21
|
+
raise InvalidIndexDefinition.new(self.class)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def add_index(index)
|
27
|
+
return false if self.indices.has_key?(index.name)
|
28
|
+
self.indices[index.name] = index
|
29
|
+
end
|
30
|
+
|
31
|
+
def value_index(field)
|
32
|
+
index = ValueIndex.new(self, field)
|
33
|
+
return nil unless add_index(index)
|
34
|
+
queue_saving_operations do |obj, session|
|
35
|
+
old_v = obj.send("#{field}_was")
|
36
|
+
new_v = obj.send(field)
|
37
|
+
next if old_v == new_v
|
38
|
+
if obj.persisted? && old_v.present?
|
39
|
+
indices[index.name].rem(obj.id, old_v)
|
40
|
+
end
|
41
|
+
indices[index.name].add(obj.id, new_v)
|
42
|
+
end
|
43
|
+
queue_deleting_operations do |obj, session|
|
44
|
+
if value = obj.send("#{field}_was")
|
45
|
+
indices[index.name].rem(obj.id, value, true).each do |command|
|
46
|
+
session.commands << command
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
index
|
51
|
+
end
|
52
|
+
|
53
|
+
def unique_index(fields, options = {})
|
54
|
+
create_unique_index(fields, options).tap do |index|
|
55
|
+
next if index.nil?
|
56
|
+
create_unique_get_method(index) if options[:get_by]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_unique_index(fields, options)
|
61
|
+
index = UniqueIndex.new(self, fields, options)
|
62
|
+
return nil unless add_index(index)
|
63
|
+
queue_saving_operations do |obj, session|
|
64
|
+
old_v = serialize_values(index.fields, obj, :previous => true)
|
65
|
+
new_v = serialize_values(index.fields, obj)
|
66
|
+
next if old_v == new_v
|
67
|
+
if obj.persisted? && old_v.present?
|
68
|
+
indices[index.name].rem(obj.id, old_v)
|
69
|
+
end
|
70
|
+
indices[index.name].add(obj.id, new_v)
|
71
|
+
end
|
72
|
+
queue_deleting_operations do |obj, session|
|
73
|
+
if value = serialize_values(index.fields, obj)
|
74
|
+
indices[index.name].rem(obj.id, value, true).each do |command|
|
75
|
+
session.commands << command
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
index
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_unique_get_method(index)
|
83
|
+
meth = :"get_by_#{index.fields.map(&:to_s).join("-")}"
|
84
|
+
define_singleton_method(meth) do |*args|
|
85
|
+
all = redis.hgetall(index.ref_key_name)
|
86
|
+
id = index.id_for_values(*args)
|
87
|
+
get(id)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def serialize_values(fields, obj, opts = {})
|
92
|
+
fields.map do |f|
|
93
|
+
attr = opts[:previous] ? "#{f}_was" : f
|
94
|
+
obj.send(attr)
|
95
|
+
end.join(UniqueIndex::SEPARATOR)
|
96
|
+
end
|
97
|
+
|
98
|
+
def define_singleton_method(*args, &block)
|
99
|
+
class << self
|
100
|
+
self
|
101
|
+
end.send(:define_method, *args, &block)
|
102
|
+
end unless method_defined? :define_singleton_method
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "ricordami/key_namer"
|
2
|
+
|
3
|
+
module Ricordami
|
4
|
+
module IsLockable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module InstanceMethods
|
8
|
+
# Pretty much stolen from redis objects
|
9
|
+
# http://github.com/nateware/redis-objects/blob/master/lib/redis/lock.rb
|
10
|
+
def lock!(options = {}, &block)
|
11
|
+
key = KeyNamer.lock(self.class, :id => id)
|
12
|
+
start = Time.now
|
13
|
+
acquired_lock = false
|
14
|
+
expiration = nil
|
15
|
+
expires_in = options.fetch(:expiration, 15)
|
16
|
+
timeout = options.fetch(:timeout, 1)
|
17
|
+
|
18
|
+
while (Time.now - start) < timeout
|
19
|
+
expiration = generate_expiration(expires_in)
|
20
|
+
acquired_lock = redis.setnx(key, expiration)
|
21
|
+
break if acquired_lock
|
22
|
+
|
23
|
+
old_expiration = redis.get(key).to_f
|
24
|
+
|
25
|
+
if old_expiration < Time.now.to_f
|
26
|
+
expiration = generate_expiration(expires_in)
|
27
|
+
old_expiration = redis.getset(key, expiration).to_f
|
28
|
+
|
29
|
+
if old_expiration < Time.now.to_f
|
30
|
+
acquired_lock = true
|
31
|
+
break
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
sleep 0.1
|
36
|
+
end
|
37
|
+
|
38
|
+
raise(LockTimeout.new(key, timeout)) unless acquired_lock
|
39
|
+
|
40
|
+
begin
|
41
|
+
yield
|
42
|
+
ensure
|
43
|
+
redis.del(key) if expiration > Time.now.to_f
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def generate_expiration(expiration)
|
48
|
+
(Time.now + expiration.to_f).to_f
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|