modis 1.4.1-java
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.
- 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
|