modis 1.1.0 → 1.2.0
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 +4 -4
- data/.gitignore +0 -1
- data/.rubocop.yml +24 -0
- data/.ruby-version +1 -1
- data/.travis.yml +2 -2
- data/Gemfile +1 -0
- data/Gemfile.lock +95 -0
- data/Rakefile +5 -3
- data/lib/modis/attribute.rb +82 -0
- data/lib/modis/errors.rb +2 -0
- data/lib/modis/finder.rb +64 -0
- data/lib/modis/index.rb +92 -0
- data/lib/modis/model.rb +9 -11
- data/lib/modis/persistence.rb +79 -41
- data/lib/modis/version.rb +1 -1
- data/lib/modis.rb +5 -4
- data/lib/tasks/quality.rake +34 -0
- data/spec/attribute_spec.rb +153 -0
- data/spec/errors_spec.rb +1 -1
- data/spec/{finders_spec.rb → finder_spec.rb} +18 -19
- data/spec/index_spec.rb +84 -0
- data/spec/persistence_spec.rb +37 -37
- data/spec/spec_helper.rb +1 -1
- data/spec/support/simplecov_quality_formatter.rb +9 -5
- data/spec/transaction_spec.rb +2 -2
- data/spec/validations_spec.rb +5 -5
- metadata +14 -9
- data/lib/modis/attributes.rb +0 -138
- data/lib/modis/finders.rb +0 -48
- data/lib/tasks/spec.rake +0 -18
- data/spec/attributes_spec.rb +0 -182
data/spec/validations_spec.rb
CHANGED
@@ -11,24 +11,24 @@ describe 'validations' do
|
|
11
11
|
|
12
12
|
it 'responds to valid?' do
|
13
13
|
model.name = nil
|
14
|
-
model.valid
|
14
|
+
expect(model.valid?).to be false
|
15
15
|
end
|
16
16
|
|
17
17
|
it 'sets errors on the model' do
|
18
18
|
model.name = nil
|
19
19
|
model.valid?
|
20
|
-
model.errors[:name].
|
20
|
+
expect(model.errors[:name]).to eq(["can't be blank"])
|
21
21
|
end
|
22
22
|
|
23
23
|
describe 'save' do
|
24
24
|
it 'returns true if the model is valid' do
|
25
25
|
model.name = "Ian"
|
26
|
-
model.save.
|
26
|
+
expect(model.save).to be true
|
27
27
|
end
|
28
28
|
|
29
29
|
it 'returns false if the model is invalid' do
|
30
30
|
model.name = nil
|
31
|
-
model.save.
|
31
|
+
expect(model.save).to be false
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -36,7 +36,7 @@ describe 'validations' do
|
|
36
36
|
it 'raises an error if the model is invalid' do
|
37
37
|
model.name = nil
|
38
38
|
expect do
|
39
|
-
model.save
|
39
|
+
expect(model.save!).to be false
|
40
40
|
end.to raise_error(Modis::RecordInvalid)
|
41
41
|
end
|
42
42
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: modis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ian Leitch
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-09-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -75,27 +75,31 @@ extra_rdoc_files: []
|
|
75
75
|
files:
|
76
76
|
- ".coveralls.yml"
|
77
77
|
- ".gitignore"
|
78
|
+
- ".rubocop.yml"
|
78
79
|
- ".ruby-gemset"
|
79
80
|
- ".ruby-version"
|
80
81
|
- ".travis.yml"
|
81
82
|
- Gemfile
|
83
|
+
- Gemfile.lock
|
82
84
|
- LICENSE.txt
|
83
85
|
- README.md
|
84
86
|
- Rakefile
|
85
87
|
- lib/modis.rb
|
86
|
-
- lib/modis/
|
88
|
+
- lib/modis/attribute.rb
|
87
89
|
- lib/modis/configuration.rb
|
88
90
|
- lib/modis/errors.rb
|
89
|
-
- lib/modis/
|
91
|
+
- lib/modis/finder.rb
|
92
|
+
- lib/modis/index.rb
|
90
93
|
- lib/modis/model.rb
|
91
94
|
- lib/modis/persistence.rb
|
92
95
|
- lib/modis/transaction.rb
|
93
96
|
- lib/modis/version.rb
|
94
|
-
- lib/tasks/
|
97
|
+
- lib/tasks/quality.rake
|
95
98
|
- modis.gemspec
|
96
|
-
- spec/
|
99
|
+
- spec/attribute_spec.rb
|
97
100
|
- spec/errors_spec.rb
|
98
|
-
- spec/
|
101
|
+
- spec/finder_spec.rb
|
102
|
+
- spec/index_spec.rb
|
99
103
|
- spec/persistence_spec.rb
|
100
104
|
- spec/spec_helper.rb
|
101
105
|
- spec/support/simplecov_helper.rb
|
@@ -126,9 +130,10 @@ signing_key:
|
|
126
130
|
specification_version: 4
|
127
131
|
summary: ActiveModel + Redis
|
128
132
|
test_files:
|
129
|
-
- spec/
|
133
|
+
- spec/attribute_spec.rb
|
130
134
|
- spec/errors_spec.rb
|
131
|
-
- spec/
|
135
|
+
- spec/finder_spec.rb
|
136
|
+
- spec/index_spec.rb
|
132
137
|
- spec/persistence_spec.rb
|
133
138
|
- spec/spec_helper.rb
|
134
139
|
- spec/support/simplecov_helper.rb
|
data/lib/modis/attributes.rb
DELETED
@@ -1,138 +0,0 @@
|
|
1
|
-
module Modis
|
2
|
-
module Attributes
|
3
|
-
TYPES = [:string, :integer, :float, :timestamp, :boolean, :array, :hash]
|
4
|
-
|
5
|
-
def self.included(base)
|
6
|
-
base.extend ClassMethods
|
7
|
-
|
8
|
-
base.instance_eval do
|
9
|
-
bootstrap_attributes
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
module ClassMethods
|
14
|
-
def bootstrap_attributes
|
15
|
-
class << self
|
16
|
-
attr_accessor :attributes
|
17
|
-
end
|
18
|
-
|
19
|
-
self.attributes = {}
|
20
|
-
|
21
|
-
attribute :id, :integer
|
22
|
-
end
|
23
|
-
|
24
|
-
def attribute(name, type = :string, options = {})
|
25
|
-
name = name.to_s
|
26
|
-
return if attributes.keys.include?(name)
|
27
|
-
raise UnsupportedAttributeType.new(type) unless TYPES.include?(type)
|
28
|
-
|
29
|
-
attributes[name] = options.update({ :type => type })
|
30
|
-
define_attribute_methods [name]
|
31
|
-
class_eval <<-EOS, __FILE__, __LINE__
|
32
|
-
def #{name}
|
33
|
-
attributes['#{name}']
|
34
|
-
end
|
35
|
-
|
36
|
-
def #{name}=(value)
|
37
|
-
value = coerce_to_type('#{name}', value)
|
38
|
-
#{name}_will_change! unless value == attributes['#{name}']
|
39
|
-
attributes['#{name}'] = value
|
40
|
-
end
|
41
|
-
EOS
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def attributes
|
46
|
-
@attributes ||= Hash[self.class.attributes.keys.zip]
|
47
|
-
end
|
48
|
-
|
49
|
-
def assign_attributes(hash)
|
50
|
-
hash.each do |k, v|
|
51
|
-
setter = "#{k}="
|
52
|
-
send(setter, v) if respond_to?(setter)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def write_attribute(key, value)
|
57
|
-
attributes[key.to_s] = value
|
58
|
-
end
|
59
|
-
|
60
|
-
protected
|
61
|
-
|
62
|
-
def set_sti_type
|
63
|
-
if self.class.sti_child?
|
64
|
-
assign_attributes(type: self.class.name)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def reset_changes
|
69
|
-
@changed_attributes.clear if @changed_attributes
|
70
|
-
end
|
71
|
-
|
72
|
-
def apply_defaults
|
73
|
-
defaults = {}
|
74
|
-
self.class.attributes.each do |attribute, options|
|
75
|
-
defaults[attribute] = options[:default] if options[:default]
|
76
|
-
end
|
77
|
-
assign_attributes(defaults)
|
78
|
-
end
|
79
|
-
|
80
|
-
def coerce_to_string(attribute, value)
|
81
|
-
attribute = attribute.to_s
|
82
|
-
return if value.blank?
|
83
|
-
type = self.class.attributes[attribute][:type]
|
84
|
-
if type == :array || type == :hash
|
85
|
-
MultiJson.encode(value) if value
|
86
|
-
elsif type == :timestamp
|
87
|
-
value.iso8601
|
88
|
-
else
|
89
|
-
value.to_s
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def coerce_to_type(attribute, value)
|
94
|
-
# TODO: allow an attribute to be set to nil
|
95
|
-
return if value.blank?
|
96
|
-
attribute = attribute.to_s
|
97
|
-
type = self.class.attributes[attribute][:type]
|
98
|
-
strict = self.class.attributes[attribute][:strict] != false
|
99
|
-
|
100
|
-
if type == :string
|
101
|
-
value.to_s
|
102
|
-
elsif type == :integer
|
103
|
-
value.to_i
|
104
|
-
elsif type == :float
|
105
|
-
value.to_f
|
106
|
-
elsif type == :timestamp
|
107
|
-
return value if value.kind_of?(Time)
|
108
|
-
Time.parse(value)
|
109
|
-
elsif type == :boolean
|
110
|
-
return true if [true, 'true'].include?(value)
|
111
|
-
return false if [false, 'false'].include?(value)
|
112
|
-
raise AttributeCoercionError.new("'#{value}' cannot be coerced to a :boolean.")
|
113
|
-
elsif type == :array
|
114
|
-
decode_json(value, Array, attribute, strict)
|
115
|
-
elsif type == :hash
|
116
|
-
decode_json(value, Hash, attribute, strict)
|
117
|
-
else
|
118
|
-
value
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def decode_json(value, type, attribute, strict)
|
123
|
-
return value if value.kind_of?(type)
|
124
|
-
begin
|
125
|
-
value = MultiJson.decode(value) if value.kind_of?(String)
|
126
|
-
rescue MultiJson::ParseError
|
127
|
-
raise if strict
|
128
|
-
else
|
129
|
-
value = value.to_s unless strict
|
130
|
-
end
|
131
|
-
if strict
|
132
|
-
return value if value.kind_of?(type)
|
133
|
-
raise AttributeCoercionError.new("Expected #{attribute} to be an #{type}, got #{value.class} instead.")
|
134
|
-
end
|
135
|
-
value
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
data/lib/modis/finders.rb
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
module Modis
|
2
|
-
module Finders
|
3
|
-
def self.included(base)
|
4
|
-
base.extend ClassMethods
|
5
|
-
end
|
6
|
-
|
7
|
-
module ClassMethods
|
8
|
-
def find(id)
|
9
|
-
record = attributes_for(id)
|
10
|
-
model_class(record).new(record, new_record: false)
|
11
|
-
end
|
12
|
-
|
13
|
-
def all
|
14
|
-
records = []
|
15
|
-
|
16
|
-
Modis.with_connection do |redis|
|
17
|
-
ids = redis.smembers(key_for(:all))
|
18
|
-
records = redis.pipelined do
|
19
|
-
ids.map { |id| redis.hgetall(key_for(id)) }
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
records.map do |record|
|
24
|
-
klass = model_class(record)
|
25
|
-
klass.new(record, new_record: false)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def attributes_for(id)
|
30
|
-
if id.nil?
|
31
|
-
raise RecordNotFound, "Couldn't find #{name} without an ID"
|
32
|
-
end
|
33
|
-
values = Modis.with_connection { |redis| redis.hgetall(key_for(id)) }
|
34
|
-
unless values['id'].present?
|
35
|
-
raise RecordNotFound, "Couldn't find #{name} with id=#{id}"
|
36
|
-
end
|
37
|
-
values
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def model_class(record)
|
43
|
-
return self if record["type"].blank?
|
44
|
-
return record["type"].constantize
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
data/lib/tasks/spec.rake
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
begin
|
2
|
-
require 'cane/rake_task'
|
3
|
-
|
4
|
-
desc "Run cane to check quality metrics"
|
5
|
-
Cane::RakeTask.new(:quality) do |cane|
|
6
|
-
cane.add_threshold 'coverage/covered_percent', :>=, 99
|
7
|
-
cane.no_style = false
|
8
|
-
cane.style_measure = 1000
|
9
|
-
cane.no_doc = true
|
10
|
-
cane.abc_max = 25
|
11
|
-
end
|
12
|
-
|
13
|
-
namespace :spec do
|
14
|
-
task :cane => ['spec', 'quality']
|
15
|
-
end
|
16
|
-
rescue LoadError
|
17
|
-
warn "cane not available, quality task not provided."
|
18
|
-
end
|
data/spec/attributes_spec.rb
DELETED
@@ -1,182 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module AttributesSpec
|
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 :hash_strict, :hash, strict: true
|
15
|
-
attribute :hash_not_strict, :hash, strict: false
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
describe Modis::Attributes do
|
20
|
-
let(:model) { AttributesSpec::MockModel.new }
|
21
|
-
|
22
|
-
it 'defines attributes' do
|
23
|
-
model.name = 'bar'
|
24
|
-
model.name.should == 'bar'
|
25
|
-
end
|
26
|
-
|
27
|
-
it 'applies an default value' do
|
28
|
-
model.name.should eq 'Janet'
|
29
|
-
model.age.should eq 60
|
30
|
-
end
|
31
|
-
|
32
|
-
it 'does not mark an attribute with a default as dirty' do
|
33
|
-
model.name_changed?.should be_false
|
34
|
-
end
|
35
|
-
|
36
|
-
it 'raises an error for an unsupported attribute type' do
|
37
|
-
expect do
|
38
|
-
class AttributesSpec::MockModel
|
39
|
-
attribute :unsupported, :symbol
|
40
|
-
end
|
41
|
-
end.to raise_error(Modis::UnsupportedAttributeType)
|
42
|
-
end
|
43
|
-
|
44
|
-
it 'assigns attributes' do
|
45
|
-
model.assign_attributes(name: 'bar')
|
46
|
-
model.name.should eq 'bar'
|
47
|
-
end
|
48
|
-
|
49
|
-
it 'does not attempt to assign attributes that are not defined on the model' do
|
50
|
-
model.assign_attributes(missing_attr: 'derp')
|
51
|
-
model.respond_to?(:missing_attr).should be_false
|
52
|
-
end
|
53
|
-
|
54
|
-
it 'allows an attribute to be nilled' do
|
55
|
-
model.name = nil
|
56
|
-
model.save!
|
57
|
-
model.class.find(model.id).name.should be_nil
|
58
|
-
end
|
59
|
-
|
60
|
-
describe ':string type' do
|
61
|
-
it 'is coerced' do
|
62
|
-
model.name = 'Ian'
|
63
|
-
model.save!
|
64
|
-
found = AttributesSpec::MockModel.find(model.id)
|
65
|
-
found.name.should eq 'Ian'
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
describe ':integer type' do
|
70
|
-
it 'is coerced' do
|
71
|
-
model.age = 18
|
72
|
-
model.save!
|
73
|
-
found = AttributesSpec::MockModel.find(model.id)
|
74
|
-
found.age.should eq 18
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
describe ':float type' do
|
79
|
-
it 'is coerced' do
|
80
|
-
model.percentage = 18.6
|
81
|
-
model.save!
|
82
|
-
found = AttributesSpec::MockModel.find(model.id)
|
83
|
-
found.percentage.should eq 18.6
|
84
|
-
end
|
85
|
-
|
86
|
-
it 'coerces a string representation to Float' do
|
87
|
-
model.percentage = '18.6'
|
88
|
-
model.save!
|
89
|
-
found = AttributesSpec::MockModel.find(model.id)
|
90
|
-
found.percentage.should eq 18.6
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
describe ':timestamp type' do
|
95
|
-
it 'is coerced' do
|
96
|
-
now = Time.now
|
97
|
-
model.created_at = now
|
98
|
-
model.save!
|
99
|
-
found = AttributesSpec::MockModel.find(model.id)
|
100
|
-
found.created_at.should be_kind_of(Time)
|
101
|
-
found.created_at.to_s.should eq now.to_s
|
102
|
-
end
|
103
|
-
|
104
|
-
it 'coerces a string representation to Time' do
|
105
|
-
now = Time.now
|
106
|
-
model.created_at = now.to_s
|
107
|
-
model.save!
|
108
|
-
found = AttributesSpec::MockModel.find(model.id)
|
109
|
-
found.created_at.should be_kind_of(Time)
|
110
|
-
found.created_at.to_s.should eq now.to_s
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
describe ':boolean type' do
|
115
|
-
it 'is coerced' do
|
116
|
-
model.flag = 'true'
|
117
|
-
model.save!
|
118
|
-
found = AttributesSpec::MockModel.find(model.id)
|
119
|
-
found.flag.should eq true
|
120
|
-
end
|
121
|
-
|
122
|
-
it 'raises an error if assigned a non-boolean value' do
|
123
|
-
expect { model.flag = 'unf!' }.to raise_error(Modis::AttributeCoercionError)
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
describe ':array type' do
|
128
|
-
it 'is coerced' do
|
129
|
-
model.array = [1, 2, 3]
|
130
|
-
model.save!
|
131
|
-
found = AttributesSpec::MockModel.find(model.id)
|
132
|
-
found.array.should eq [1, 2, 3]
|
133
|
-
end
|
134
|
-
|
135
|
-
it 'raises an error when assigned another type' do
|
136
|
-
expect { model.array = {foo: :bar} }.to raise_error(Modis::AttributeCoercionError)
|
137
|
-
end
|
138
|
-
|
139
|
-
it 'does not raise an error when assigned a JSON array string' do
|
140
|
-
expect { model.array = "[1,2,3]" }.to_not raise_error
|
141
|
-
end
|
142
|
-
|
143
|
-
it 'does not raise an error when a JSON string does not deserialize to an Array' do
|
144
|
-
expect { model.array = "{\"foo\":\"bar\"}" }.to raise_error(Modis::AttributeCoercionError)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
describe ':hash type' do
|
149
|
-
it 'is coerced' do
|
150
|
-
model.hash = {foo: :bar}
|
151
|
-
model.save!
|
152
|
-
found = AttributesSpec::MockModel.find(model.id)
|
153
|
-
found.hash.should eq({'foo' => 'bar'})
|
154
|
-
end
|
155
|
-
|
156
|
-
describe 'strict: true' do
|
157
|
-
it 'raises an error if the value cannot be decoded on assignment' do
|
158
|
-
expect { model.hash_strict = "test" }.to raise_error(MultiJson::ParseError)
|
159
|
-
end
|
160
|
-
|
161
|
-
it 'does not raise an error if the value can be decoded on assignment' do
|
162
|
-
expect { model.hash_strict = "{\"foo\":\"bar\"}" }.to_not raise_error
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
describe 'strict: false' do
|
167
|
-
it 'returns the value if it cannot be decoded' do
|
168
|
-
model.write_attribute(:hash_not_strict, "test")
|
169
|
-
model.save!
|
170
|
-
found = AttributesSpec::MockModel.find(model.id)
|
171
|
-
found.hash_not_strict.should eq "test"
|
172
|
-
end
|
173
|
-
|
174
|
-
it 'returns an integer as a string' do
|
175
|
-
model.hash_not_strict = 1
|
176
|
-
model.save!
|
177
|
-
found = AttributesSpec::MockModel.find(model.id)
|
178
|
-
found.hash_not_strict.should eq "1"
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|