modis 1.4.1-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rubocop.yml +31 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +13 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +89 -0
- data/LICENSE.txt +22 -0
- data/README.md +45 -0
- data/Rakefile +17 -0
- data/benchmark/bench.rb +65 -0
- data/benchmark/find.rb +62 -0
- data/benchmark/persistence.rb +82 -0
- data/benchmark/redis/connection/fakedis.rb +90 -0
- data/lib/modis.rb +41 -0
- data/lib/modis/attribute.rb +103 -0
- data/lib/modis/configuration.rb +14 -0
- data/lib/modis/errors.rb +16 -0
- data/lib/modis/finder.rb +76 -0
- data/lib/modis/index.rb +84 -0
- data/lib/modis/model.rb +47 -0
- data/lib/modis/persistence.rb +233 -0
- data/lib/modis/transaction.rb +13 -0
- data/lib/modis/version.rb +3 -0
- data/lib/tasks/quality.rake +41 -0
- data/modis.gemspec +32 -0
- data/spec/attribute_spec.rb +172 -0
- data/spec/errors_spec.rb +18 -0
- data/spec/finder_spec.rb +109 -0
- data/spec/index_spec.rb +84 -0
- data/spec/persistence_spec.rb +319 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/simplecov_helper.rb +23 -0
- data/spec/support/simplecov_quality_formatter.rb +12 -0
- data/spec/transaction_spec.rb +16 -0
- data/spec/validations_spec.rb +49 -0
- metadata +173 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
# rubocop:disable all
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'redis/connection/hiredis'
|
5
|
+
|
6
|
+
class Redis
|
7
|
+
module Connection
|
8
|
+
class Fakedis < ::Redis::Connection::Hiredis
|
9
|
+
class << self
|
10
|
+
attr_accessor :reads, :read_indicies, :replaying, :recording
|
11
|
+
alias_method :replaying?, :replaying
|
12
|
+
alias_method :recording?, :recording
|
13
|
+
end
|
14
|
+
|
15
|
+
@reads = []
|
16
|
+
@read_indicies = []
|
17
|
+
|
18
|
+
def self.start_replay(name)
|
19
|
+
puts "Fakedis replaying."
|
20
|
+
self.replaying = true
|
21
|
+
|
22
|
+
@reads = Marshal.load(File.read(reads_path(name)))
|
23
|
+
@read_indicies = Marshal.load(File.read(read_indicies_path(name)))
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.start_recording
|
27
|
+
puts "Fakedis recording."
|
28
|
+
self.recording = true
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.stop_recording(name)
|
32
|
+
self.recording = false
|
33
|
+
|
34
|
+
puts "\nFakedis:"
|
35
|
+
puts " * #{reads.size} unique reads recorded"
|
36
|
+
|
37
|
+
FileUtils.mkdir_p("tmp/fakedis")
|
38
|
+
|
39
|
+
File.open(reads_path(name), 'w') { |fd| fd.write(Marshal.dump(reads)) }
|
40
|
+
File.open(read_indicies_path(name), 'w') { |fd| fd.write(Marshal.dump(read_indicies)) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.reads_path(name)
|
44
|
+
"tmp/fakedis/#{name}_reads.dump"
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.read_indicies_path(name)
|
48
|
+
"tmp/fakedis/#{name}_read_indicies.dump"
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(*args)
|
52
|
+
super
|
53
|
+
@reads_idx = -1
|
54
|
+
@read_depth = 0
|
55
|
+
end
|
56
|
+
|
57
|
+
def read
|
58
|
+
if self.class.recording?
|
59
|
+
@read_depth += 1
|
60
|
+
v = super
|
61
|
+
@read_depth -= 1
|
62
|
+
return v if @read_depth > 0
|
63
|
+
i = self.class.reads.index(v)
|
64
|
+
|
65
|
+
if i
|
66
|
+
self.class.read_indicies << i
|
67
|
+
else
|
68
|
+
self.class.reads << v
|
69
|
+
self.class.read_indicies << self.class.reads.size - 1
|
70
|
+
end
|
71
|
+
|
72
|
+
v
|
73
|
+
elsif self.class.replaying?
|
74
|
+
@reads_idx += 1
|
75
|
+
self.class.reads[self.class.read_indicies[@reads_idx]]
|
76
|
+
else
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def write(v)
|
82
|
+
if self.class.replaying?
|
83
|
+
# Do nothing.
|
84
|
+
else
|
85
|
+
super
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/modis.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'connection_pool'
|
3
|
+
require 'active_model'
|
4
|
+
require 'active_support/all'
|
5
|
+
require 'yaml'
|
6
|
+
require 'msgpack'
|
7
|
+
|
8
|
+
require 'modis/version'
|
9
|
+
require 'modis/configuration'
|
10
|
+
require 'modis/attribute'
|
11
|
+
require 'modis/errors'
|
12
|
+
require 'modis/persistence'
|
13
|
+
require 'modis/transaction'
|
14
|
+
require 'modis/finder'
|
15
|
+
require 'modis/index'
|
16
|
+
require 'modis/model'
|
17
|
+
|
18
|
+
module Modis
|
19
|
+
@mutex = Mutex.new
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_accessor :connection_pool, :redis_options, :connection_pool_size,
|
23
|
+
:connection_pool_timeout
|
24
|
+
end
|
25
|
+
|
26
|
+
self.redis_options = { driver: :hiredis }
|
27
|
+
self.connection_pool_size = 5
|
28
|
+
self.connection_pool_timeout = 5
|
29
|
+
|
30
|
+
def self.connection_pool
|
31
|
+
return @connection_pool if @connection_pool
|
32
|
+
@mutex.synchronize do
|
33
|
+
options = { size: connection_pool_size, timeout: connection_pool_timeout }
|
34
|
+
@connection_pool = ConnectionPool.new(options) { Redis.new(redis_options) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.with_connection
|
39
|
+
connection_pool.with { |connection| yield(connection) }
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Modis
|
2
|
+
module Attribute
|
3
|
+
TYPES = { string: [String],
|
4
|
+
integer: [Fixnum],
|
5
|
+
float: [Float],
|
6
|
+
timestamp: [Time],
|
7
|
+
hash: [Hash],
|
8
|
+
array: [Array],
|
9
|
+
boolean: [TrueClass, FalseClass] }.freeze
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.extend ClassMethods
|
13
|
+
base.instance_eval do
|
14
|
+
bootstrap_attributes
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def bootstrap_attributes(parent = nil)
|
20
|
+
attr_reader :attributes
|
21
|
+
|
22
|
+
class << self
|
23
|
+
attr_accessor :attributes, :attributes_with_defaults
|
24
|
+
end
|
25
|
+
|
26
|
+
self.attributes = parent ? parent.attributes.dup : {}
|
27
|
+
self.attributes_with_defaults = parent ? parent.attributes_with_defaults.dup : {}
|
28
|
+
|
29
|
+
attribute :id, :integer unless parent
|
30
|
+
end
|
31
|
+
|
32
|
+
def attribute(name, type, options = {})
|
33
|
+
name = name.to_s
|
34
|
+
raise AttributeError, "Attribute with name '#{name}' has already been specified." if attributes.key?(name)
|
35
|
+
|
36
|
+
type_classes = Array(type).map do |t|
|
37
|
+
raise UnsupportedAttributeType, t unless TYPES.key?(t)
|
38
|
+
TYPES[t]
|
39
|
+
end.flatten
|
40
|
+
|
41
|
+
attributes[name] = options.update(type: type)
|
42
|
+
attributes_with_defaults[name] = options[:default]
|
43
|
+
define_attribute_methods([name])
|
44
|
+
|
45
|
+
value_coercion = type == :timestamp ? 'value = Time.new(*value) if value && value.is_a?(Array) && value.count == 7' : nil
|
46
|
+
predicate = type_classes.map { |cls| "value.is_a?(#{cls.name})" }.join(' || ')
|
47
|
+
|
48
|
+
type_check = <<-RUBY
|
49
|
+
if value && !(#{predicate})
|
50
|
+
raise Modis::AttributeCoercionError, "Received value of type '\#{value.class}', expected '#{type_classes.join("', '")}' for attribute '#{name}'."
|
51
|
+
end
|
52
|
+
RUBY
|
53
|
+
|
54
|
+
class_eval <<-RUBY, __FILE__, __LINE__
|
55
|
+
def #{name}
|
56
|
+
attributes['#{name}']
|
57
|
+
end
|
58
|
+
|
59
|
+
def #{name}=(value)
|
60
|
+
#{value_coercion}
|
61
|
+
|
62
|
+
# ActiveSupport's Time#<=> does not perform well when comparing with NilClass.
|
63
|
+
if (value.nil? ^ attributes['#{name}'].nil?) || (value != attributes['#{name}'])
|
64
|
+
#{type_check}
|
65
|
+
#{name}_will_change!
|
66
|
+
attributes['#{name}'] = value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
RUBY
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def assign_attributes(hash)
|
74
|
+
hash.each do |k, v|
|
75
|
+
setter = "#{k}="
|
76
|
+
send(setter, v) if respond_to?(setter)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def write_attribute(key, value)
|
81
|
+
attributes[key.to_s] = value
|
82
|
+
end
|
83
|
+
|
84
|
+
def read_attribute(key)
|
85
|
+
attributes[key.to_s]
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def set_sti_type
|
91
|
+
return unless self.class.sti_child?
|
92
|
+
write_attribute(:type, self.class.name)
|
93
|
+
end
|
94
|
+
|
95
|
+
def reset_changes
|
96
|
+
@changed_attributes = nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def apply_defaults
|
100
|
+
@attributes = Hash[self.class.attributes_with_defaults]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/modis/errors.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Modis
|
2
|
+
class ModisError < StandardError; end
|
3
|
+
class RecordNotSaved < ModisError; end
|
4
|
+
class RecordNotFound < ModisError; end
|
5
|
+
class RecordInvalid < ModisError; end
|
6
|
+
class UnsupportedAttributeType < ModisError; end
|
7
|
+
class AttributeCoercionError < ModisError; end
|
8
|
+
class AttributeError < ModisError; end
|
9
|
+
class IndexError < ModisError; end
|
10
|
+
|
11
|
+
module Errors
|
12
|
+
def errors
|
13
|
+
@errors ||= ActiveModel::Errors.new(self)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/modis/finder.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
module Modis
|
2
|
+
module Finder
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def find(*ids)
|
9
|
+
models = find_all(ids)
|
10
|
+
ids.count == 1 ? models.first : models
|
11
|
+
end
|
12
|
+
|
13
|
+
def all
|
14
|
+
records = Modis.with_connection do |redis|
|
15
|
+
ids = redis.smembers(key_for(:all))
|
16
|
+
redis.pipelined do
|
17
|
+
ids.map { |id| record_for(redis, id) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
records_to_models(records)
|
22
|
+
end
|
23
|
+
|
24
|
+
def attributes_for(redis, id)
|
25
|
+
raise RecordNotFound, "Couldn't find #{name} without an ID" if id.nil?
|
26
|
+
|
27
|
+
attributes = deserialize(record_for(redis, id))
|
28
|
+
|
29
|
+
unless attributes['id'].present?
|
30
|
+
raise RecordNotFound, "Couldn't find #{name} with id=#{id}"
|
31
|
+
end
|
32
|
+
|
33
|
+
attributes
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_all(ids)
|
37
|
+
raise RecordNotFound, "Couldn't find #{name} without an ID" if ids.empty?
|
38
|
+
|
39
|
+
records = Modis.with_connection do |redis|
|
40
|
+
blk = proc { |id| record_for(redis, id) }
|
41
|
+
ids.count == 1 ? ids.map(&blk) : redis.pipelined { ids.map(&blk) }
|
42
|
+
end
|
43
|
+
|
44
|
+
models = records_to_models(records)
|
45
|
+
|
46
|
+
if models.count < ids.count
|
47
|
+
missing = ids - models.map(&:id)
|
48
|
+
raise RecordNotFound, "Couldn't find #{name} with id=#{missing.first}"
|
49
|
+
end
|
50
|
+
|
51
|
+
models
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def records_to_models(records)
|
57
|
+
records.map do |record|
|
58
|
+
model_for(deserialize(record)) unless record.blank?
|
59
|
+
end.compact
|
60
|
+
end
|
61
|
+
|
62
|
+
def model_for(attributes)
|
63
|
+
model_class(attributes).new(attributes, new_record: false)
|
64
|
+
end
|
65
|
+
|
66
|
+
def record_for(redis, id)
|
67
|
+
redis.hgetall(key_for(id))
|
68
|
+
end
|
69
|
+
|
70
|
+
def model_class(record)
|
71
|
+
return self if record["type"].blank?
|
72
|
+
record["type"].constantize
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/modis/index.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Modis
|
2
|
+
module Index
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.instance_eval do
|
6
|
+
bootstrap_indexes
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def bootstrap_indexes(parent = nil)
|
12
|
+
class << self
|
13
|
+
attr_accessor :indexed_attributes
|
14
|
+
end
|
15
|
+
|
16
|
+
self.indexed_attributes = parent ? parent.indexed_attributes.dup : []
|
17
|
+
end
|
18
|
+
|
19
|
+
def index(attribute)
|
20
|
+
attribute = attribute.to_s
|
21
|
+
raise IndexError, "No such attribute '#{attribute}'" unless attributes.key?(attribute)
|
22
|
+
indexed_attributes << attribute
|
23
|
+
end
|
24
|
+
|
25
|
+
def where(query)
|
26
|
+
raise IndexError, 'Queries using multiple indexes is not currently supported.' if query.keys.size > 1
|
27
|
+
attribute, value = query.first
|
28
|
+
ids = index_for(attribute, value)
|
29
|
+
return [] if ids.empty?
|
30
|
+
find_all(ids)
|
31
|
+
end
|
32
|
+
|
33
|
+
def index_for(attribute, value)
|
34
|
+
Modis.with_connection do |redis|
|
35
|
+
key = index_key(attribute, value)
|
36
|
+
redis.smembers(key).map(&:to_i)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def index_key(attribute, value)
|
41
|
+
"#{absolute_namespace}:index:#{attribute}:#{value.inspect}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def indexed_attributes
|
48
|
+
self.class.indexed_attributes
|
49
|
+
end
|
50
|
+
|
51
|
+
def index_key(attribute, value)
|
52
|
+
self.class.index_key(attribute, value)
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_to_indexes(redis)
|
56
|
+
return if indexed_attributes.empty?
|
57
|
+
|
58
|
+
indexed_attributes.each do |attribute|
|
59
|
+
key = index_key(attribute, read_attribute(attribute))
|
60
|
+
redis.sadd(key, id)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def remove_from_indexes(redis)
|
65
|
+
return if indexed_attributes.empty?
|
66
|
+
|
67
|
+
indexed_attributes.each do |attribute|
|
68
|
+
key = index_key(attribute, read_attribute(attribute))
|
69
|
+
redis.srem(key, id)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def update_indexes(redis)
|
74
|
+
return if indexed_attributes.empty?
|
75
|
+
|
76
|
+
(changes.keys & indexed_attributes).each do |attribute|
|
77
|
+
old_value, new_value = changes[attribute]
|
78
|
+
old_key = index_key(attribute, old_value)
|
79
|
+
new_key = index_key(attribute, new_value)
|
80
|
+
redis.smove(old_key, new_key, id)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/modis/model.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Modis
|
2
|
+
module Model
|
3
|
+
def self.included(base)
|
4
|
+
base.instance_eval do
|
5
|
+
include ActiveModel::Dirty
|
6
|
+
include ActiveModel::Validations
|
7
|
+
include ActiveModel::Serialization
|
8
|
+
|
9
|
+
extend ActiveModel::Naming
|
10
|
+
extend ActiveModel::Callbacks
|
11
|
+
|
12
|
+
define_model_callbacks :save, :create, :update, :destroy
|
13
|
+
|
14
|
+
include Modis::Errors
|
15
|
+
include Modis::Transaction
|
16
|
+
include Modis::Persistence
|
17
|
+
include Modis::Finder
|
18
|
+
include Modis::Attribute
|
19
|
+
include Modis::Index
|
20
|
+
|
21
|
+
base.extend(ClassMethods)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
def inherited(child)
|
27
|
+
super
|
28
|
+
bootstrap_sti(self, child)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(record = nil, options = {})
|
33
|
+
apply_defaults
|
34
|
+
set_sti_type
|
35
|
+
assign_attributes(record) if record
|
36
|
+
reset_changes
|
37
|
+
|
38
|
+
return unless options.key?(:new_record)
|
39
|
+
instance_variable_set('@new_record', options[:new_record])
|
40
|
+
end
|
41
|
+
|
42
|
+
def ==(other)
|
43
|
+
super || other.instance_of?(self.class) && id.present? && other.id == id
|
44
|
+
end
|
45
|
+
alias_method :eql?, :==
|
46
|
+
end
|
47
|
+
end
|