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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c6359a4c5d347150ae7cb19754caf5398fd42d0d
4
- data.tar.gz: 330ef4946885313394c85425599e4d1274cda2f8
3
+ metadata.gz: 9c1496e1e5c9b430c4aa6257d4ec122d3cb1ee37
4
+ data.tar.gz: 68c023c5b84245530772181c7a2bf564ebe0712f
5
5
  SHA512:
6
- metadata.gz: dd3275c3569475972681d14c3e1d877d12baf9a872367d4cf0700caf52bb587752414408aeb16afe4886e26a540b5fdcb86da64a0cc6dc3d07dc80b001c4c6e1
7
- data.tar.gz: 8527e4159f4b2d7c4ab0c2874518f3de5e24a854b646a26ad5a75be8e4e3d457d47878798167eb6fb1c071c25993ef9984431f9fd13cbb657d350127578f9481
6
+ metadata.gz: f77b3079fd2c210bb678b952710e188dda3eb1ae8d98d00f30b79b511671305df22a3d5863433c48c2a831bf795580afc8a6c1d6973530a0847e7a5f14cabfad
7
+ data.tar.gz: d22b018df72ac21e0bce75fba13816ecf7067b96602afae6f0d8d09321d2f24c8c194809d30bc221e543bcbc5bd4330157248eed20ac761c1560d0d647b99ddc
data/.gitignore CHANGED
@@ -3,7 +3,6 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
- Gemfile.lock
7
6
  InstalledFiles
8
7
  _yardoc
9
8
  coverage
data/.rubocop.yml ADDED
@@ -0,0 +1,24 @@
1
+ AllCops:
2
+ Exclude:
3
+ - modis.gemspec
4
+
5
+ LineLength:
6
+ Enabled: false
7
+
8
+ StringLiterals:
9
+ Enabled: false
10
+
11
+ Documentation:
12
+ Enabled: false
13
+
14
+ MethodLength:
15
+ Enabled: false
16
+
17
+ ClassLength:
18
+ Enabled: false
19
+
20
+ CyclomaticComplexity:
21
+ Enabled: false
22
+
23
+ Style/SignalException:
24
+ EnforcedStyle: only_raise
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-2.1.1
1
+ ruby-2.1.2
data/.travis.yml CHANGED
@@ -3,6 +3,6 @@ services:
3
3
  language: ruby
4
4
  rvm:
5
5
  - 2.0.0
6
- - 2.1.1
6
+ - 2.1.2
7
7
  - jruby
8
- - rbx
8
+ - rbx-2
data/Gemfile CHANGED
@@ -7,6 +7,7 @@ gem 'coveralls'
7
7
 
8
8
  platform :mri_19, :mri_20, :mri_21 do
9
9
  gem 'cane'
10
+ gem 'rubocop'
10
11
  end
11
12
 
12
13
  gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,95 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ modis (1.2.0)
5
+ activemodel (>= 3.0)
6
+ activesupport (>= 3.0)
7
+ connection_pool (>= 2)
8
+ redis (>= 3.0)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ activemodel (4.1.5)
14
+ activesupport (= 4.1.5)
15
+ builder (~> 3.1)
16
+ activesupport (4.1.5)
17
+ i18n (~> 0.6, >= 0.6.9)
18
+ json (~> 1.7, >= 1.7.7)
19
+ minitest (~> 5.1)
20
+ thread_safe (~> 0.1)
21
+ tzinfo (~> 1.1)
22
+ ast (2.0.0)
23
+ builder (3.2.2)
24
+ cane (2.6.2)
25
+ parallel
26
+ connection_pool (2.0.0)
27
+ coveralls (0.7.1)
28
+ multi_json (~> 1.3)
29
+ rest-client
30
+ simplecov (>= 0.7)
31
+ term-ansicolor
32
+ thor
33
+ diff-lcs (1.2.5)
34
+ docile (1.1.5)
35
+ i18n (0.6.11)
36
+ json (1.8.1)
37
+ mime-types (2.3)
38
+ minitest (5.4.1)
39
+ multi_json (1.10.1)
40
+ netrc (0.7.7)
41
+ parallel (1.3.0)
42
+ parser (2.2.0.pre.4)
43
+ ast (>= 1.1, < 3.0)
44
+ slop (~> 3.4, >= 3.4.5)
45
+ powerpack (0.0.9)
46
+ rainbow (2.0.0)
47
+ rake (10.3.2)
48
+ redis (3.1.0)
49
+ rest-client (1.7.2)
50
+ mime-types (>= 1.16, < 3.0)
51
+ netrc (~> 0.7)
52
+ rspec (3.0.0)
53
+ rspec-core (~> 3.0.0)
54
+ rspec-expectations (~> 3.0.0)
55
+ rspec-mocks (~> 3.0.0)
56
+ rspec-core (3.0.4)
57
+ rspec-support (~> 3.0.0)
58
+ rspec-expectations (3.0.4)
59
+ diff-lcs (>= 1.2.0, < 2.0)
60
+ rspec-support (~> 3.0.0)
61
+ rspec-mocks (3.0.4)
62
+ rspec-support (~> 3.0.0)
63
+ rspec-support (3.0.4)
64
+ rubocop (0.25.0)
65
+ parser (>= 2.2.0.pre.4, < 3.0)
66
+ powerpack (~> 0.0.6)
67
+ rainbow (>= 1.99.1, < 3.0)
68
+ ruby-progressbar (~> 1.4)
69
+ ruby-progressbar (1.5.1)
70
+ simplecov (0.9.0)
71
+ docile (~> 1.1.0)
72
+ multi_json
73
+ simplecov-html (~> 0.8.0)
74
+ simplecov-html (0.8.0)
75
+ slop (3.6.0)
76
+ term-ansicolor (1.3.0)
77
+ tins (~> 1.0)
78
+ thor (0.19.1)
79
+ thread_safe (0.3.4)
80
+ tins (1.3.2)
81
+ tzinfo (1.2.2)
82
+ thread_safe (~> 0.1)
83
+
84
+ PLATFORMS
85
+ java
86
+ ruby
87
+
88
+ DEPENDENCIES
89
+ cane
90
+ coveralls
91
+ modis!
92
+ rake
93
+ rspec
94
+ rubocop
95
+ simplecov
data/Rakefile CHANGED
@@ -8,8 +8,10 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
8
8
  spec.rspec_opts = ['--backtrace']
9
9
  end
10
10
 
11
- if RUBY_VERSION > '1.9' && defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
12
- task :default => 'spec:cane'
11
+ if ENV['TRAVIS'] && ENV['QUALITY'] == 'false'
12
+ task default: 'spec'
13
+ elsif RUBY_VERSION > '1.9' && defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
14
+ task default: 'spec:quality'
13
15
  else
14
- task :default => 'spec'
16
+ task default: 'spec'
15
17
  end
@@ -0,0 +1,82 @@
1
+ module Modis
2
+ module Attribute
3
+ TYPES = [:string, :integer, :float, :timestamp, :boolean, :array, :hash]
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.instance_eval do
8
+ bootstrap_attributes
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def bootstrap_attributes
14
+ class << self
15
+ attr_accessor :attributes
16
+ end
17
+
18
+ self.attributes = {}
19
+
20
+ attribute :id, :integer
21
+ end
22
+
23
+ def attribute(name, type = :string, options = {})
24
+ name = name.to_s
25
+ raise AttributeError, "Attribute with name '#{name}' has already been specified." if attributes.key?(name)
26
+ raise UnsupportedAttributeType, type unless TYPES.include?(type)
27
+
28
+ attributes[name] = options.update(type: type)
29
+ define_attribute_methods [name]
30
+ class_eval <<-EOS, __FILE__, __LINE__
31
+ def #{name}
32
+ attributes['#{name}']
33
+ end
34
+
35
+ def #{name}=(value)
36
+ ensure_type('#{name}', value)
37
+ #{name}_will_change! unless value == attributes['#{name}']
38
+ attributes['#{name}'] = value
39
+ end
40
+ EOS
41
+ end
42
+ end
43
+
44
+ def attributes
45
+ @attributes ||= Hash[self.class.attributes.keys.zip]
46
+ end
47
+
48
+ def assign_attributes(hash)
49
+ hash.each do |k, v|
50
+ setter = "#{k}="
51
+ send(setter, v) if respond_to?(setter)
52
+ end
53
+ end
54
+
55
+ def write_attribute(key, value)
56
+ attributes[key.to_s] = value
57
+ end
58
+
59
+ def read_attribute(key)
60
+ attributes[key.to_s]
61
+ end
62
+
63
+ protected
64
+
65
+ def set_sti_type
66
+ return unless self.class.sti_child?
67
+ assign_attributes(type: self.class.name)
68
+ end
69
+
70
+ def reset_changes
71
+ @changed_attributes.clear if @changed_attributes
72
+ end
73
+
74
+ def apply_defaults
75
+ defaults = {}
76
+ self.class.attributes.each do |attribute, options|
77
+ defaults[attribute] = options[:default] if options[:default]
78
+ end
79
+ assign_attributes(defaults)
80
+ end
81
+ end
82
+ end
data/lib/modis/errors.rb CHANGED
@@ -5,6 +5,8 @@ module Modis
5
5
  class RecordInvalid < ModisError; end
6
6
  class UnsupportedAttributeType < ModisError; end
7
7
  class AttributeCoercionError < ModisError; end
8
+ class AttributeError < ModisError; end
9
+ class IndexError < ModisError; end
8
10
 
9
11
  module Errors
10
12
  def errors
@@ -0,0 +1,64 @@
1
+ module Modis
2
+ module Finder
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def find(id)
9
+ Modis.with_connection do |redis|
10
+ attributes = attributes_for(redis, id)
11
+ model_for(attributes)
12
+ end
13
+ end
14
+
15
+ def all
16
+ records = []
17
+
18
+ Modis.with_connection do |redis|
19
+ ids = redis.smembers(key_for(:all))
20
+ records = redis.pipelined do
21
+ ids.map { |id| record_for(redis, id) }
22
+ end
23
+ end
24
+
25
+ records.map do |record|
26
+ attributes = record_to_attributes(record)
27
+ model_for(attributes)
28
+ end
29
+ end
30
+
31
+ def attributes_for(redis, id)
32
+ raise RecordNotFound, "Couldn't find #{name} without an ID" if id.nil?
33
+
34
+ attributes = record_to_attributes(record_for(redis, id))
35
+
36
+ unless attributes['id'].present?
37
+ raise RecordNotFound, "Couldn't find #{name} with id=#{id}"
38
+ end
39
+
40
+ attributes
41
+ end
42
+
43
+ private
44
+
45
+ def model_for(attributes)
46
+ model_class(attributes).new(attributes, new_record: false)
47
+ end
48
+
49
+ def record_to_attributes(record)
50
+ record.each { |k, v| record[k] = coerce_from_persistence(v) }
51
+ record
52
+ end
53
+
54
+ def record_for(redis, id)
55
+ redis.hgetall(key_for(id))
56
+ end
57
+
58
+ def model_class(record)
59
+ return self if record["type"].blank?
60
+ record["type"].constantize
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,92 @@
1
+ module Modis
2
+ module Index
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.instance_eval do
6
+ bootstrap_indexes
7
+ after__internal_create :add_to_index
8
+ after__internal_update :update_index
9
+ before__internal_destroy :remove_from_index
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def bootstrap_indexes
15
+ class << self
16
+ attr_accessor :indexed_attributes
17
+ end
18
+
19
+ self.indexed_attributes = []
20
+ end
21
+
22
+ def index(attribute)
23
+ attribute = attribute.to_s
24
+ raise IndexError, "No such attribute '#{attribute}'" unless attributes.key?(attribute)
25
+ indexed_attributes << attribute
26
+ end
27
+
28
+ def where(query)
29
+ raise IndexError, 'Queries using multiple indexes is not currently supported.' if query.keys.size > 1
30
+ attribute, value = query.first
31
+ index = index_for(attribute, value)
32
+ index.map { |id| find(id) }
33
+ end
34
+
35
+ def index_for(attribute, value)
36
+ Modis.with_connection do |redis|
37
+ key = index_key(attribute, value)
38
+ redis.smembers(key).map(&:to_i)
39
+ end
40
+ end
41
+
42
+ def index_key(attribute, value)
43
+ "#{absolute_namespace}:index:#{attribute}:#{value.inspect}"
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def indexed_attributes
50
+ self.class.indexed_attributes
51
+ end
52
+
53
+ def index_key(attribute, value)
54
+ self.class.index_key(attribute, value)
55
+ end
56
+
57
+ def add_to_index
58
+ return if indexed_attributes.empty?
59
+
60
+ Modis.with_connection do |redis|
61
+ indexed_attributes.each do |attribute|
62
+ key = index_key(attribute, read_attribute(attribute))
63
+ redis.sadd(key, id)
64
+ end
65
+ end
66
+ end
67
+
68
+ def remove_from_index
69
+ return if indexed_attributes.empty?
70
+
71
+ Modis.with_connection do |redis|
72
+ indexed_attributes.each do |attribute|
73
+ key = index_key(attribute, read_attribute(attribute))
74
+ redis.srem(key, id)
75
+ end
76
+ end
77
+ end
78
+
79
+ def update_index
80
+ return if indexed_attributes.empty?
81
+
82
+ Modis.with_connection do |redis|
83
+ (changes.keys & indexed_attributes).each do |attribute|
84
+ old_value, new_value = changes[attribute]
85
+ old_key = index_key(attribute, old_value)
86
+ new_key = index_key(attribute, new_value)
87
+ redis.smove(old_key, new_key, id)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
data/lib/modis/model.rb CHANGED
@@ -9,16 +9,15 @@ module Modis
9
9
  extend ActiveModel::Naming
10
10
  extend ActiveModel::Callbacks
11
11
 
12
- define_model_callbacks :save
13
- define_model_callbacks :create
14
- define_model_callbacks :update
15
- define_model_callbacks :destroy
12
+ define_model_callbacks :save, :create, :update, :destroy
13
+ define_model_callbacks :_internal_create, :_internal_update, :_internal_destroy
16
14
 
17
15
  include Modis::Errors
18
16
  include Modis::Transaction
19
17
  include Modis::Persistence
20
- include Modis::Finders
21
- include Modis::Attributes
18
+ include Modis::Finder
19
+ include Modis::Attribute
20
+ include Modis::Index
22
21
 
23
22
  base.extend(ClassMethods)
24
23
  end
@@ -31,20 +30,19 @@ module Modis
31
30
  end
32
31
  end
33
32
 
34
- def initialize(record=nil, options={})
33
+ def initialize(record = nil, options = {})
35
34
  set_sti_type
36
35
  apply_defaults
37
36
  assign_attributes(record.symbolize_keys) if record
38
37
  reset_changes
39
38
 
40
- if options.key?(:new_record)
41
- instance_variable_set('@new_record', options[:new_record])
42
- end
39
+ return unless options.key?(:new_record)
40
+ instance_variable_set('@new_record', options[:new_record])
43
41
  end
44
42
 
45
43
  def ==(other)
46
44
  super || other.instance_of?(self.class) && id.present? && other.id == id
47
45
  end
48
- alias :eql? :==
46
+ alias_method :eql?, :==
49
47
  end
50
48
  end
@@ -2,21 +2,25 @@ module Modis
2
2
  module Persistence
3
3
  def self.included(base)
4
4
  base.extend ClassMethods
5
+ base.instance_eval do
6
+ after__internal_create :track
7
+ before__internal_destroy :untrack
8
+ end
5
9
  end
6
10
 
7
11
  module ClassMethods
8
12
  # :nodoc:
9
13
  def bootstrap_sti(parent, child)
10
14
  child.instance_eval do
11
- parent.instance_eval do
12
- class << self
15
+ parent.instance_eval do
16
+ class << self
13
17
  attr_accessor :sti_parent
14
18
  end
15
- attribute :type, :string
19
+ attribute :type, :string unless attributes.key?('type')
16
20
  end
17
21
 
18
22
  class << self
19
- delegate :attributes, to: :sti_parent
23
+ delegate :attributes, :indexed_attributes, to: :sti_parent
20
24
  end
21
25
 
22
26
  @sti_child = true
@@ -35,9 +39,7 @@ module Modis
35
39
  @namespace = name.split('::').map(&:underscore).join(':')
36
40
  end
37
41
 
38
- def namespace=(value)
39
- @namespace = value
40
- end
42
+ attr_writer :namespace
41
43
 
42
44
  def absolute_namespace
43
45
  parts = [Modis.config.namespace, namespace]
@@ -49,9 +51,9 @@ module Modis
49
51
  end
50
52
 
51
53
  def create(attrs)
52
- model = new(attrs)
53
- model.save
54
- model
54
+ model = new(attrs)
55
+ model.save
56
+ model
55
57
  end
56
58
 
57
59
  def create!(attrs)
@@ -59,6 +61,10 @@ module Modis
59
61
  model.save!
60
62
  model
61
63
  end
64
+
65
+ def coerce_from_persistence(value)
66
+ YAML.load(value)
67
+ end
62
68
  end
63
69
 
64
70
  def persisted?
@@ -73,29 +79,28 @@ module Modis
73
79
  defined?(@new_record) ? @new_record : true
74
80
  end
75
81
 
76
- def save(args={})
77
- begin
78
- create_or_update(args)
79
- rescue Modis::RecordInvalid
80
- false
81
- end
82
+ def save(args = {})
83
+ create_or_update(args)
84
+ rescue Modis::RecordInvalid
85
+ false
82
86
  end
83
87
 
84
- def save!(args={})
88
+ def save!(args = {})
85
89
  create_or_update(args) || (raise RecordNotSaved)
86
90
  end
87
91
 
88
92
  def destroy
89
93
  self.class.transaction do |redis|
90
94
  run_callbacks :destroy do
91
- redis.del(key)
92
- untrack(id, redis)
95
+ run_callbacks :_internal_destroy do
96
+ redis.del(key)
97
+ end
93
98
  end
94
99
  end
95
100
  end
96
101
 
97
102
  def reload
98
- new_attributes = self.class.attributes_for(id)
103
+ new_attributes = Modis.with_connection { |redis| self.class.attributes_for(redis, id) }
99
104
  initialize(new_attributes)
100
105
  self
101
106
  end
@@ -115,36 +120,69 @@ module Modis
115
120
  save!
116
121
  end
117
122
 
118
- protected
123
+ private
119
124
 
120
- def create_or_update(args={})
121
- skip_validate = args.key?(:validate) && args[:validate] == false
122
- if !skip_validate && !valid?
123
- raise Modis::RecordInvalid, errors.full_messages.join(', ')
125
+ def coerce_for_persistence(attribute, value)
126
+ ensure_type(attribute, value)
127
+ YAML.dump(value)
128
+ end
129
+
130
+ def ensure_type(attribute, value)
131
+ type = self.class.attributes[attribute.to_s][:type]
132
+ type_classes = classes_for_type(type)
133
+
134
+ return unless value && !type_classes.include?(value.class)
135
+ raise Modis::AttributeCoercionError, "Received value of type '#{value.class}', expected '#{type}' for attribute '#{name}'."
136
+ end
137
+
138
+ def classes_for_type(type)
139
+ { string: [String],
140
+ integer: [Fixnum],
141
+ float: [Float],
142
+ timestamp: [Time],
143
+ hash: [Hash],
144
+ array: [Array],
145
+ boolean: [TrueClass, FalseClass] }[type]
146
+ end
147
+
148
+ def create_or_update(args = {})
149
+ validate(args)
150
+ future = persist
151
+
152
+ if future && future.value == 'OK'
153
+ reset_changes
154
+ @new_record = false
155
+ new_record? ? add_to_index : update_index
156
+ true
157
+ else
158
+ false
124
159
  end
160
+ end
161
+
162
+ def validate(args)
163
+ skip_validate = args.key?(:validate) && args[:validate] == false
164
+ return if skip_validate || valid?
165
+ raise Modis::RecordInvalid, errors.full_messages.join(', ')
166
+ end
125
167
 
168
+ def persist
126
169
  future = nil
127
170
  set_id if new_record?
171
+ callback = new_record? ? :create : :update
128
172
 
129
173
  self.class.transaction do |redis|
130
174
  run_callbacks :save do
131
- callback = new_record? ? :create : :update
132
175
  run_callbacks callback do
133
- attrs = []
134
- attributes.each { |k, v| attrs << k << coerce_to_string(k, v) }
135
- future = redis.hmset(self.class.key_for(id), attrs)
136
- track(id, redis) if new_record?
176
+ run_callbacks "_internal_#{callback}" do
177
+ attrs = []
178
+ attributes.each { |k, v| attrs << k << coerce_for_persistence(k, v) }
179
+ future = redis.hmset(self.class.key_for(id), attrs)
180
+ end
137
181
  end
138
182
  end
139
183
  end
140
184
 
141
- if future && future.value == 'OK'
142
- reset_changes
143
- @new_record = false
144
- true
145
- else
146
- false
147
- end
185
+ future
148
186
  end
149
187
 
150
188
  def set_id
@@ -153,12 +191,12 @@ module Modis
153
191
  end
154
192
  end
155
193
 
156
- def track(id, redis)
157
- redis.sadd(self.class.key_for(:all), id)
194
+ def track
195
+ Modis.with_connection { |redis| redis.sadd(self.class.key_for(:all), id) }
158
196
  end
159
197
 
160
- def untrack(id, redis)
161
- redis.srem(self.class.key_for(:all), id)
198
+ def untrack
199
+ Modis.with_connection { |redis| redis.srem(self.class.key_for(:all), id) }
162
200
  end
163
201
  end
164
202
  end
data/lib/modis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Modis
2
- VERSION = '1.1.0'
2
+ VERSION = '1.2.0'
3
3
  end