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,233 @@
|
|
1
|
+
module Modis
|
2
|
+
module Persistence
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.instance_eval do
|
6
|
+
class << self
|
7
|
+
attr_reader :sti_child
|
8
|
+
alias_method :sti_child?, :sti_child
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
# :nodoc:
|
15
|
+
def bootstrap_sti(parent, child)
|
16
|
+
child.instance_eval do
|
17
|
+
parent.instance_eval do
|
18
|
+
class << self
|
19
|
+
attr_accessor :sti_parent
|
20
|
+
end
|
21
|
+
attribute :type, :string unless attributes.key?('type')
|
22
|
+
end
|
23
|
+
|
24
|
+
@sti_child = true
|
25
|
+
@sti_parent = parent
|
26
|
+
|
27
|
+
bootstrap_attributes(parent)
|
28
|
+
bootstrap_indexes(parent)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def namespace
|
33
|
+
return sti_parent.namespace if sti_child?
|
34
|
+
@namespace ||= name.split('::').map(&:underscore).join(':')
|
35
|
+
end
|
36
|
+
|
37
|
+
def namespace=(value)
|
38
|
+
@namespace = value
|
39
|
+
@absolute_namespace = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def absolute_namespace
|
43
|
+
@absolute_namespace ||= [Modis.config.namespace, namespace].compact.join(':')
|
44
|
+
end
|
45
|
+
|
46
|
+
def key_for(id)
|
47
|
+
"#{absolute_namespace}:#{id}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def create(attrs)
|
51
|
+
model = new(attrs)
|
52
|
+
model.save
|
53
|
+
model
|
54
|
+
end
|
55
|
+
|
56
|
+
def create!(attrs)
|
57
|
+
model = new(attrs)
|
58
|
+
model.save!
|
59
|
+
model
|
60
|
+
end
|
61
|
+
|
62
|
+
YAML_MARKER = '---'.freeze
|
63
|
+
def deserialize(record)
|
64
|
+
values = record.values
|
65
|
+
values = MessagePack.unpack(msgpack_array_header(values.size) + values.join)
|
66
|
+
keys = record.keys
|
67
|
+
values.each_with_index { |v, i| record[keys[i]] = v }
|
68
|
+
record
|
69
|
+
rescue MessagePack::MalformedFormatError
|
70
|
+
found_yaml = false
|
71
|
+
|
72
|
+
record.each do |k, v|
|
73
|
+
if v.start_with?(YAML_MARKER)
|
74
|
+
found_yaml = true
|
75
|
+
record[k] = YAML.load(v)
|
76
|
+
else
|
77
|
+
record[k] = MessagePack.unpack(v)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if found_yaml
|
82
|
+
id = record['id']
|
83
|
+
STDERR.puts "#{self}(id: #{id}) contains attributes serialized as YAML. As of Modis 1.4.0, YAML is no longer used as the serialization format. To improve performance loading this record, you can force the record to new serialization format (MessagePack) with: #{self}.find(#{id}).save!(yaml_sucks: true)"
|
84
|
+
end
|
85
|
+
|
86
|
+
record
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def msgpack_array_header(n)
|
92
|
+
if n < 16
|
93
|
+
[0x90 | n].pack("C")
|
94
|
+
elsif n < 65536
|
95
|
+
[0xDC, n].pack("Cn")
|
96
|
+
else
|
97
|
+
[0xDD, n].pack("CN")
|
98
|
+
end.force_encoding(Encoding::UTF_8)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def persisted?
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def key
|
107
|
+
new_record? ? nil : self.class.key_for(id)
|
108
|
+
end
|
109
|
+
|
110
|
+
def new_record?
|
111
|
+
defined?(@new_record) ? @new_record : true
|
112
|
+
end
|
113
|
+
|
114
|
+
def save(args = {})
|
115
|
+
create_or_update(args)
|
116
|
+
rescue Modis::RecordInvalid
|
117
|
+
false
|
118
|
+
end
|
119
|
+
|
120
|
+
def save!(args = {})
|
121
|
+
create_or_update(args) || (raise RecordNotSaved)
|
122
|
+
end
|
123
|
+
|
124
|
+
def destroy
|
125
|
+
self.class.transaction do |redis|
|
126
|
+
run_callbacks :destroy do
|
127
|
+
redis.pipelined do
|
128
|
+
remove_from_indexes(redis)
|
129
|
+
redis.srem(self.class.key_for(:all), id)
|
130
|
+
redis.del(key)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def reload
|
137
|
+
new_attributes = Modis.with_connection { |redis| self.class.attributes_for(redis, id) }
|
138
|
+
initialize(new_attributes)
|
139
|
+
self
|
140
|
+
end
|
141
|
+
|
142
|
+
def update_attribute(name, value)
|
143
|
+
assign_attributes(name => value)
|
144
|
+
save(validate: false)
|
145
|
+
end
|
146
|
+
|
147
|
+
def update_attributes(attrs)
|
148
|
+
assign_attributes(attrs)
|
149
|
+
save
|
150
|
+
end
|
151
|
+
|
152
|
+
def update_attributes!(attrs)
|
153
|
+
assign_attributes(attrs)
|
154
|
+
save!
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def coerce_for_persistence(value)
|
160
|
+
value = [value.year, value.month, value.day, value.hour, value.min, value.sec, value.strftime("%:z")] if value.is_a?(Time)
|
161
|
+
MessagePack.pack(value)
|
162
|
+
end
|
163
|
+
|
164
|
+
def create_or_update(args = {})
|
165
|
+
validate(args)
|
166
|
+
future = persist(args[:yaml_sucks])
|
167
|
+
|
168
|
+
if future && (future == :unchanged || future.value == 'OK')
|
169
|
+
reset_changes
|
170
|
+
@new_record = false
|
171
|
+
true
|
172
|
+
else
|
173
|
+
false
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def validate(args)
|
178
|
+
skip_validate = args.key?(:validate) && args[:validate] == false
|
179
|
+
return if skip_validate || valid?
|
180
|
+
raise Modis::RecordInvalid, errors.full_messages.join(', ')
|
181
|
+
end
|
182
|
+
|
183
|
+
def persist(persist_all)
|
184
|
+
future = nil
|
185
|
+
set_id if new_record?
|
186
|
+
callback = new_record? ? :create : :update
|
187
|
+
|
188
|
+
self.class.transaction do |redis|
|
189
|
+
run_callbacks :save do
|
190
|
+
run_callbacks callback do
|
191
|
+
redis.pipelined do
|
192
|
+
attrs = coerced_attributes(persist_all)
|
193
|
+
future = attrs.any? ? redis.hmset(self.class.key_for(id), attrs) : :unchanged
|
194
|
+
|
195
|
+
if new_record?
|
196
|
+
redis.sadd(self.class.key_for(:all), id)
|
197
|
+
add_to_indexes(redis)
|
198
|
+
else
|
199
|
+
update_indexes(redis)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
future
|
207
|
+
end
|
208
|
+
|
209
|
+
def coerced_attributes(persist_all) # rubocop:disable Metrics/AbcSize
|
210
|
+
attrs = []
|
211
|
+
|
212
|
+
if new_record? || persist_all
|
213
|
+
attributes.each do |k, v|
|
214
|
+
if (self.class.attributes[k][:default] || nil) != v
|
215
|
+
attrs << k << coerce_for_persistence(v)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
else
|
219
|
+
changed_attributes.each do |k, _|
|
220
|
+
attrs << k << coerce_for_persistence(attributes[k])
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
attrs
|
225
|
+
end
|
226
|
+
|
227
|
+
def set_id
|
228
|
+
Modis.with_connection do |redis|
|
229
|
+
self.id = redis.incr("#{self.class.absolute_namespace}_id_seq")
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
begin
|
2
|
+
if ENV['TRAVIS']
|
3
|
+
namespace :spec do
|
4
|
+
task cane: ['spec']
|
5
|
+
end
|
6
|
+
else
|
7
|
+
require 'cane/rake_task'
|
8
|
+
|
9
|
+
desc 'Run cane to check quality metrics'
|
10
|
+
Cane::RakeTask.new(:cane_quality) do |cane|
|
11
|
+
cane.add_threshold 'coverage/covered_percent', :>=, 99
|
12
|
+
cane.no_style = false
|
13
|
+
cane.style_measure = 1000
|
14
|
+
cane.no_doc = true
|
15
|
+
cane.abc_max = 25
|
16
|
+
end
|
17
|
+
|
18
|
+
namespace :spec do
|
19
|
+
task cane: %w(spec cane_quality)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
rescue LoadError
|
23
|
+
warn "cane not available."
|
24
|
+
|
25
|
+
namespace :spec do
|
26
|
+
task cane: ['spec']
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
require 'rubocop/rake_task'
|
32
|
+
t = RuboCop::RakeTask.new
|
33
|
+
t.options << '-D'
|
34
|
+
rescue LoadError
|
35
|
+
warn 'rubocop not available.'
|
36
|
+
task rubocop: ['spec']
|
37
|
+
end
|
38
|
+
|
39
|
+
namespace :spec do
|
40
|
+
task quality: %w(cane rubocop)
|
41
|
+
end
|
data/modis.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'modis/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "modis"
|
8
|
+
gem.version = Modis::VERSION
|
9
|
+
gem.authors = ["Ian Leitch"]
|
10
|
+
gem.email = ["port001@gmail.com"]
|
11
|
+
gem.description = "ActiveModel + Redis"
|
12
|
+
gem.summary = "ActiveModel + Redis"
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_runtime_dependency 'activemodel', '>= 3.0'
|
21
|
+
gem.add_runtime_dependency 'activesupport', '>= 3.0'
|
22
|
+
gem.add_runtime_dependency 'redis', '>= 3.0'
|
23
|
+
gem.add_runtime_dependency 'hiredis', '>= 0.5'
|
24
|
+
gem.add_runtime_dependency 'connection_pool', '>= 2'
|
25
|
+
|
26
|
+
if defined? JRUBY_VERSION
|
27
|
+
gem.platform = 'java'
|
28
|
+
gem.add_runtime_dependency 'msgpack-jruby'
|
29
|
+
else
|
30
|
+
gem.add_runtime_dependency 'msgpack', '>= 0.5'
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module AttributeSpec
|
4
|
+
class MockModel
|
5
|
+
include Modis::Model
|
6
|
+
|
7
|
+
attribute :name, :string, default: 'Janet'
|
8
|
+
attribute :age, :integer, default: 60
|
9
|
+
attribute :percentage, :float
|
10
|
+
attribute :created_at, :timestamp
|
11
|
+
attribute :flag, :boolean
|
12
|
+
attribute :array, :array
|
13
|
+
attribute :hash, :hash
|
14
|
+
attribute :string_or_hash, [:string, :hash]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe Modis::Attribute do
|
19
|
+
let(:model) { AttributeSpec::MockModel.new }
|
20
|
+
|
21
|
+
it 'defines attributes' do
|
22
|
+
model.name = 'bar'
|
23
|
+
expect(model.name).to eq('bar')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'applies an default value' do
|
27
|
+
expect(model.name).to eq('Janet')
|
28
|
+
expect(model.age).to eq(60)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'does not mark an attribute with a default as dirty' do
|
32
|
+
expect(model.name_changed?).to be false
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'raises an error for an unsupported attribute type' do
|
36
|
+
expect do
|
37
|
+
module AttributeSpec
|
38
|
+
class MockModel
|
39
|
+
attribute :unsupported, :symbol
|
40
|
+
end
|
41
|
+
end.to raise_error(Modis::UnsupportedAttributeType)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'assigns attributes' do
|
46
|
+
model.assign_attributes(name: 'bar')
|
47
|
+
expect(model.name).to eq 'bar'
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'does not attempt to assign attributes that are not defined on the model' do
|
51
|
+
model.assign_attributes(missing_attr: 'derp')
|
52
|
+
expect(model.respond_to?(:missing_attrexpect)).to be false
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'allows an attribute to be nilled' do
|
56
|
+
model.name = nil
|
57
|
+
model.save!
|
58
|
+
expect(model.class.find(model.id).name).to be_nil
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'allows an attribute to be a blank string' do
|
62
|
+
model.name = ''
|
63
|
+
model.save!
|
64
|
+
expect(model.class.find(model.id).name).to eq('')
|
65
|
+
end
|
66
|
+
|
67
|
+
describe ':string type' do
|
68
|
+
it 'is coerced' do
|
69
|
+
model.name = 'Ian'
|
70
|
+
model.save!
|
71
|
+
found = AttributeSpec::MockModel.find(model.id)
|
72
|
+
expect(found.name).to eq('Ian')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe ':integer type' do
|
77
|
+
it 'is coerced' do
|
78
|
+
model.age = 18
|
79
|
+
model.save!
|
80
|
+
found = AttributeSpec::MockModel.find(model.id)
|
81
|
+
expect(found.age).to eq(18)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe ':float type' do
|
86
|
+
it 'is coerced' do
|
87
|
+
model.percentage = 18.6
|
88
|
+
model.save!
|
89
|
+
found = AttributeSpec::MockModel.find(model.id)
|
90
|
+
expect(found.percentage).to eq(18.6)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe ':timestamp type' do
|
95
|
+
it 'is coerced' do
|
96
|
+
time = Time.new(2014, 12, 11, 17, 31, 50, '-02:00')
|
97
|
+
model.created_at = time
|
98
|
+
model.save!
|
99
|
+
found = AttributeSpec::MockModel.find(model.id)
|
100
|
+
expect(found.created_at).to be_kind_of(Time)
|
101
|
+
expect(found.created_at.to_s).to eq(time.to_s)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe ':boolean type' do
|
106
|
+
describe true do
|
107
|
+
it 'is coerced' do
|
108
|
+
model.flag = true
|
109
|
+
model.save!
|
110
|
+
found = AttributeSpec::MockModel.find(model.id)
|
111
|
+
expect(found.flag).to be true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe false do
|
116
|
+
it 'is coerced' do
|
117
|
+
model.flag = false
|
118
|
+
model.save!
|
119
|
+
found = AttributeSpec::MockModel.find(model.id)
|
120
|
+
expect(found.flag).to be false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'raises an error if assigned a non-boolean value' do
|
125
|
+
expect { model.flag = 'unf!' }.to raise_error(Modis::AttributeCoercionError, "Received value of type 'String', expected 'TrueClass', 'FalseClass' for attribute 'flag'.")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe ':array type' do
|
130
|
+
it 'is coerced' do
|
131
|
+
model.array = [1, 2, 3]
|
132
|
+
model.save!
|
133
|
+
found = AttributeSpec::MockModel.find(model.id)
|
134
|
+
expect(found.array).to eq([1, 2, 3])
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'raises an error when assigned another type' do
|
138
|
+
expect { model.array = { foo: :bar } }.to raise_error(Modis::AttributeCoercionError, "Received value of type 'Hash', expected 'Array' for attribute 'array'.")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe ':hash type' do
|
143
|
+
it 'is coerced' do
|
144
|
+
model.hash = { foo: :bar }
|
145
|
+
model.save!
|
146
|
+
found = AttributeSpec::MockModel.find(model.id)
|
147
|
+
expect(found.hash).to eq('foo' => 'bar')
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'raises an error when assigned another type' do
|
151
|
+
expect { model.hash = [] }.to raise_error(Modis::AttributeCoercionError, "Received value of type 'Array', expected 'Hash' for attribute 'hash'.")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe 'variable type' do
|
156
|
+
it 'is coerced' do
|
157
|
+
model.string_or_hash = { foo: :bar }
|
158
|
+
model.save!
|
159
|
+
found = AttributeSpec::MockModel.find(model.id)
|
160
|
+
expect(found.string_or_hash).to eq('foo' => 'bar')
|
161
|
+
|
162
|
+
model.string_or_hash = 'test'
|
163
|
+
model.save!
|
164
|
+
found = AttributeSpec::MockModel.find(model.id)
|
165
|
+
expect(found.string_or_hash).to eq('test')
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'raises an error when assigned another type' do
|
169
|
+
expect { model.string_or_hash = [] }.to raise_error(Modis::AttributeCoercionError, "Received value of type 'Array', expected 'String', 'Hash' for attribute 'string_or_hash'.")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|