modis 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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