active_remote-cached 1.1.1 → 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
  SHA256:
3
- metadata.gz: b6ac1efd0991c3ee528e08b55099bf6f05a6c1eb243f290e4197be290b725dca
4
- data.tar.gz: 34229f667ea990196cf2593fe31207532ddbc60fdd72904c8ff618a5e9b3c5c1
3
+ metadata.gz: db04ead0732ea2b7c372a7c7f674a8d3450c483a05688a56ee3684f2dc7c2688
4
+ data.tar.gz: 6d0823e6ff844c6effb2da7dddc15353da1240a694cc88324a1ed9c1d273189c
5
5
  SHA512:
6
- metadata.gz: e38d49aedda27d13809af64e7f939bd2578dc8f45a3f0f657f0dabf4bae5e3f376078ae6b16bf8765a3e8e7d198f93fb7105e292e7555a1e86927d879c4992ad
7
- data.tar.gz: 1c4f37e645e020daf1e4523e6788aa397a1d23a7bda1f59e4335bc17d1501eaaf5343ce7d94a72134d1f56a581099021456f391051dd3f841a432cb627c0d20b
6
+ metadata.gz: 760fe52b8cc4b6b1e3f5d7bb61af29567f0a8deb99ea0d4b29941bed11f293c1ef43667dcbefb9bf7a18423680871b7dbcd19ef314a9a7e58976385ae0305d59
7
+ data.tar.gz: 544d6d1e7d17afb3bb125af5d2e66efd41eae5676cd6984491fa9c611d0a0c258e8e8a69020adb13b9ad240dd4f4ab46e548defcd6b06194eee4789ee4402868
@@ -0,0 +1,85 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+ workflow_dispatch:
9
+
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ test:
16
+ name: ${{ matrix.ruby }} / ${{ matrix.appraisal }}
17
+ runs-on: ubuntu-latest
18
+ env:
19
+ BUNDLE_GEMFILE: ${{ github.workspace }}/Gemfile
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ ruby:
24
+ - '3.1'
25
+ - '3.4'
26
+ - jruby-9.4
27
+ - jruby-10.0
28
+ appraisal:
29
+ - active_remote-6.1
30
+ - active_remote-7.0
31
+ - active_remote-7.1
32
+ - active_remote-7.2
33
+ - active_remote-8.0
34
+ exclude:
35
+ # active_remote 8.0 requires ruby 3.2 or later.
36
+ # jruby-9.4 is ruby 3.1 compatible.
37
+ - ruby: '3.1'
38
+ appraisal: active_remote-8.0
39
+ - ruby: jruby-9.4
40
+ appraisal: active_remote-8.0
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+
44
+ - name: Set up Ruby
45
+ uses: ruby/setup-ruby@v1
46
+ with:
47
+ ruby-version: ${{ matrix.ruby }}
48
+ bundler-cache: true
49
+
50
+ - name: Generate the appraisal gemfiles
51
+ run: bundle exec appraisal generate
52
+
53
+ # bundler-cache above only caches the root Gemfile. The appraisal
54
+ # gemfiles do not exist until the step above runs, so they need their own
55
+ # cache. Without this every job installs active_remote from cold.
56
+ - name: Cache the appraisal dependencies
57
+ uses: actions/cache@v4
58
+ with:
59
+ path: gemfiles/vendor/bundle
60
+ key: ${{ runner.os }}-${{ matrix.ruby }}-${{ matrix.appraisal }}-${{ hashFiles('Appraisals', '*.gemspec') }}
61
+ restore-keys: |
62
+ ${{ runner.os }}-${{ matrix.ruby }}-${{ matrix.appraisal }}-
63
+
64
+ - name: Install the appraisal dependencies
65
+ run: |
66
+ bundle exec appraisal ${{ matrix.appraisal }} bundle config set --local path "$GITHUB_WORKSPACE/gemfiles/vendor/bundle"
67
+ bundle exec appraisal ${{ matrix.appraisal }} bundle install --jobs 4 --retry 3
68
+
69
+ - name: Run the specs
70
+ run: bundle exec appraisal ${{ matrix.appraisal }} rspec
71
+
72
+ rubocop:
73
+ name: RuboCop
74
+ runs-on: ubuntu-latest
75
+ steps:
76
+ - uses: actions/checkout@v4
77
+
78
+ - name: Set up Ruby
79
+ uses: ruby/setup-ruby@v1
80
+ with:
81
+ ruby-version: '3.4'
82
+ bundler-cache: true
83
+
84
+ - name: Run RuboCop
85
+ run: bundle exec rubocop --parallel
data/.gitignore CHANGED
@@ -17,3 +17,4 @@ spec/reports
17
17
  test/tmp
18
18
  test/version_tmp
19
19
  tmp
20
+ gemfiles/
data/.rubocop.yml CHANGED
@@ -1,24 +1,30 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 3.1
3
+ # Do not adopt new cops automatically. Without this, every run prints the
4
+ # full pending cop list as a warning.
5
+ NewCops: disable
6
+ # Keep the default Exclude list. Without this, the Exclude below replaces the
7
+ # defaults, and RuboCop walks into vendor/bundle and reads the .rubocop.yml of
8
+ # every installed gem. Some of those name cops that no longer exist.
9
+ inherit_mode:
10
+ merge:
11
+ - Exclude
3
12
  Exclude:
4
13
  - 'gemfiles/**/*'
5
14
 
6
- Layout/AccessModifierIndentation:
7
- Enabled: false
8
- Layout/CommentIndentation:
9
- Enabled: false
10
- Layout/IndentationConsistency:
11
- Enabled: false
12
-
13
- Style/AccessModifierDeclarations:
14
- Enabled: false
15
-
16
15
  Naming/MethodParameterName:
17
16
  Enabled: false
18
17
 
19
18
  Metrics/BlockLength:
20
19
  Exclude:
21
20
  - 'spec/**/*'
21
+ - '*.gemspec'
22
+
23
+ # ActiveSupport cache #fetch only calls the block on a miss, so the block form
24
+ # is not the same as the Hash#fetch default value form.
25
+ Style/RedundantFetchBlock:
26
+ Exclude:
27
+ - 'spec/active_remote/cached/cache_spec.rb'
22
28
 
23
29
  Naming/FileName:
24
30
  Exclude:
@@ -32,11 +38,11 @@ Metrics/ModuleLength:
32
38
  Style/Documentation:
33
39
  Enabled: false
34
40
 
41
+ # The generated finder methods in this file are long heredocs.
35
42
  Metrics/MethodLength:
36
- Enabled: false
37
-
38
- Naming/VariableNumber:
39
- Enabled: false
43
+ Exclude:
44
+ - 'lib/active_remote/cached.rb'
45
+ - 'spec/**/*'
40
46
 
41
47
  Style/HashSyntax:
42
48
  Description: >-
@@ -50,6 +56,3 @@ Style/HashSyntax:
50
56
  # Use lambdas instead of stabbys
51
57
  Style/Lambda:
52
58
  EnforcedStyle: lambda
53
-
54
- Style/MissingRespondToMissing:
55
- Enabled: false
data/Appraisals ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise 'active_remote-6.1' do
4
+ gem 'active_remote', '~> 6.1.0'
5
+ # activesupport 6.1 is not compatible with concurrent-ruby 1.3.5 and later
6
+ gem 'concurrent-ruby', '< 1.3.5'
7
+ # ruby 3.4 removed these from the default gems, activesupport 6.1 still needs them
8
+ gem 'base64'
9
+ gem 'bigdecimal'
10
+ gem 'mutex_m'
11
+ end
12
+
13
+ appraise 'active_remote-7.0' do
14
+ gem 'active_remote', '~> 7.0.0'
15
+ end
16
+
17
+ appraise 'active_remote-7.1' do
18
+ gem 'active_remote', '~> 7.1.0'
19
+ end
20
+
21
+ appraise 'active_remote-7.2' do
22
+ gem 'active_remote', '~> 7.2.0'
23
+ end
24
+
25
+ appraise 'active_remote-8.0' do
26
+ gem 'active_remote', '~> 8.0.0'
27
+ end
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # ActiveRemote::Cached
2
2
 
3
+ [![CI](https://github.com/skunkworker/active_remote-cached/actions/workflows/ci.yml/badge.svg)](https://github.com/skunkworker/active_remote-cached/actions/workflows/ci.yml)
4
+
3
5
  Provides cached finders for ActiveRemote models that allow a caching provider to cache the result of a query.
4
6
 
5
7
  ## Installation
@@ -88,6 +90,125 @@ Each finder as takes an optional options hash that will override the options pas
88
90
  customer = ::Customer.cached_find_by_id(1, :expires_in => 15.minutes)
89
91
  ```
90
92
 
93
+ ## Development
94
+
95
+ Install the dependencies:
96
+
97
+ ```shell
98
+ bundle install
99
+ ```
100
+
101
+ Run the specs against the default gemfile:
102
+
103
+ ```shell
104
+ bundle exec rspec
105
+ ```
106
+
107
+ Run RuboCop:
108
+
109
+ ```shell
110
+ bundle exec rubocop
111
+ ```
112
+
113
+ ### Test matrix
114
+
115
+ This gem uses [appraisal](https://github.com/thoughtbot/appraisal) to test against
116
+ several `active_remote` versions. The `Appraisals` file defines each version.
117
+
118
+ Generate the gemfiles. They are not committed:
119
+
120
+ ```shell
121
+ bundle exec appraisal generate
122
+ ```
123
+
124
+ Install every appraisal:
125
+
126
+ ```shell
127
+ bundle exec appraisal install
128
+ ```
129
+
130
+ Run the specs against every appraisal:
131
+
132
+ ```shell
133
+ bundle exec appraisal rspec
134
+ ```
135
+
136
+ Run the specs against one appraisal:
137
+
138
+ ```shell
139
+ bundle exec appraisal active_remote-8.0 rspec
140
+ ```
141
+
142
+ Remove the generated gemfiles:
143
+
144
+ ```shell
145
+ bundle exec appraisal clean
146
+ ```
147
+
148
+ CI runs this matrix on Ruby 3.1, Ruby 3.4, JRuby 9.4, and JRuby 10.0.
149
+ `active_remote` 8.0 requires Ruby 3.2 or later. CI does not run that
150
+ version on Ruby 3.1 or JRuby 9.4.
151
+
152
+ ## Upgrading to 1.2.0
153
+
154
+ ### Every cache key changes
155
+
156
+ Before 1.2.0 the cache key held only the argument values, joined with no
157
+ separator. Three different finders shared one cache entry:
158
+
159
+ ```ruby
160
+ Customer.cached_find_by_name_and_email("x", "y") # key: "xy"
161
+ Customer.cached_find_by_city_and_state("x", "y") # key: "xy" same entry
162
+ Customer.cached_find_by_id("xy") # key: "xy" same entry
163
+ ```
164
+
165
+ The key now names each field, so each finder gets its own entry:
166
+
167
+ ```ruby
168
+ Customer.cached_find_by_name_and_email("x", "y") # key: "email.y/name.x"
169
+ ```
170
+
171
+ Every existing cache entry becomes a miss after the upgrade. Expect one cold
172
+ period. The gem already causes this on an ActiveSupport upgrade, through
173
+ `RUBY_AND_ACTIVE_SUPPORT_VERSION`.
174
+
175
+ ### A bad call now raises
176
+
177
+ A dynamic finder called with too few arguments used to pass `nil` for the
178
+ missing field and cache the result. It now raises `ArgumentError`:
179
+
180
+ ```ruby
181
+ Customer.cached_find_by_email_and_name("only_one") # => ArgumentError
182
+ ```
183
+
184
+ ### The cache provider validator raises a new class
185
+
186
+ `ActiveRemote::Cached::Cache::InvalidCacheProvider` replaces the bare
187
+ `RuntimeError` that `ActiveRemote::Cached.cache` raised for a provider that is
188
+ missing a method.
189
+
190
+ ## Known behavior
191
+
192
+ Two behaviors are recorded in the specs. Neither is fixed. Read
193
+ `spec/active_remote/cached_spec.rb` for the specs that describe them.
194
+
195
+ ### Finder name matching is not anchored
196
+
197
+ `_method_missing_name` matches a finder name inside a longer method name. A
198
+ method named `not_cached_find_by_guid` resolves to `cached_find_by_guid`.
199
+
200
+ ### A subclass has its own empty cached_methods list
201
+
202
+ A subclass inherits the finder methods its parent defined, and the options
203
+ those finders were declared with. It does not inherit the `cached_methods`
204
+ list. The parent accepts the finder arguments in any order. The subclass
205
+ accepts them only in the order the method was defined.
206
+
207
+ ```ruby
208
+ Parent.cached_find_by_beta_and_alpha('B', 'A') # works
209
+ Child.cached_find_by_beta_and_alpha('B', 'A') # raises NoMethodError
210
+ ```
211
+
91
212
  ## Contributing
92
213
 
93
214
  1. Fork it
data/Rakefile CHANGED
@@ -1,14 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/gem_tasks'
4
- require 'rake/testtask'
4
+ require 'rspec/core/rake_task'
5
+ require 'appraisal'
5
6
 
6
- Rake::TestTask.new do |t|
7
- t.libs.push 'lib'
8
- t.libs.push 'spec'
9
- t.pattern = 'spec/**/*_spec.rb'
10
- t.verbose = true
11
- end
7
+ ::RSpec::Core::RakeTask.new(:spec)
12
8
 
13
- task :spec => :test
14
9
  task :default => :spec
@@ -6,6 +6,21 @@ lib = File.expand_path('lib', __dir__)
6
6
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
7
7
  require 'active_remote/cached/version'
8
8
 
9
+ HOMEPAGE = 'https://github.com/mxenabled/active_remote-cached'
10
+
11
+ # git ls-files returns nothing outside a checkout, so a build from a released
12
+ # tarball needs the glob.
13
+ def gem_files
14
+ files = if File.directory?(File.join(__dir__, '.git'))
15
+ `git ls-files`.split($INPUT_RECORD_SEPARATOR)
16
+ else
17
+ Dir.glob('{lib,spec}/**/*', File::FNM_DOTMATCH) +
18
+ %w[LICENSE.txt README.md Rakefile Appraisals active_remote-cached.gemspec]
19
+ end
20
+
21
+ files.reject { |file| File.directory?(file) }
22
+ end
23
+
9
24
  Gem::Specification.new do |gem|
10
25
  gem.name = 'active_remote-cached'
11
26
  gem.version = ActiveRemote::Cached::VERSION
@@ -13,21 +28,31 @@ Gem::Specification.new do |gem|
13
28
  gem.email = ['brandonsdewitt@gmail.com', 'devexperience@mx.com']
14
29
  gem.description = ' Provides "cached" finders and a DSL to enumerate which finders should have cached versions '
15
30
  gem.summary = ' Provides a configuration for caching mechanisms and finders on ActiveRemote models'
16
- gem.homepage = ''
31
+ gem.homepage = HOMEPAGE
32
+ gem.license = 'MIT'
33
+
34
+ gem.metadata = {
35
+ 'homepage_uri' => HOMEPAGE,
36
+ 'source_code_uri' => HOMEPAGE,
37
+ 'rubygems_mfa_required' => 'true'
38
+ }
17
39
 
18
- gem.required_ruby_version = '>= 2.6'
19
- gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
40
+ gem.required_ruby_version = '>= 3.1'
41
+ gem.files = gem_files
20
42
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
21
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
43
  gem.require_paths = ['lib']
23
44
 
24
45
  gem.add_dependency 'active_remote', '>= 6.1'
25
- gem.add_dependency 'activesupport'
46
+ # NullStore and ActiveSupport::VERSION::STRING. Matches the active_remote floor.
47
+ gem.add_dependency 'activesupport', '>= 6.1'
26
48
 
49
+ gem.add_development_dependency 'appraisal'
27
50
  gem.add_development_dependency 'bundler'
28
51
  gem.add_development_dependency 'mocha'
52
+ # ostruct leaves the default gems in ruby 4.0
53
+ gem.add_development_dependency 'ostruct'
29
54
  gem.add_development_dependency 'pry'
30
55
  gem.add_development_dependency 'rake'
31
56
  gem.add_development_dependency 'rspec', '>= 3.0'
32
- gem.add_development_dependency 'rubocop'
57
+ gem.add_development_dependency 'rubocop', '~> 1.80'
33
58
  end
@@ -1,45 +1,79 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRemote
2
4
  module Cached
3
5
  class ArgumentKeys
4
6
  attr_reader :arguments, :argument_string, :options
5
7
 
6
8
  REMOVE_CHARACTERS = /[[:space:]+=><{}\[\];:\-,]/
7
- REPLACE_MAP = [
8
- [' ', 'SP'],
9
- ['+', 'PL'],
10
- ['=', 'EQ'],
11
- ['>', 'GT'],
12
- ['<', 'LT'],
13
- ['{', 'LB'],
14
- ['}', 'RB'],
15
- ['[', 'LB2'],
16
- [']', 'RB2'],
17
- [';', 'SC'],
18
- [':', 'CO'],
19
- ['-', 'DA'],
20
- [',', 'COM']
21
- ].freeze
9
+ # Covers the same characters as REMOVE_CHARACTERS. A tab or a newline
10
+ # left in a key breaks the Memcached protocol.
11
+ REPLACE_MAP = {
12
+ ' ' => 'SP',
13
+ "\t" => 'TB',
14
+ "\n" => 'NL',
15
+ "\r" => 'CR',
16
+ "\f" => 'FF',
17
+ "\v" => 'VT',
18
+ '+' => 'PL',
19
+ '=' => 'EQ',
20
+ '>' => 'GT',
21
+ '<' => 'LT',
22
+ '{' => 'LB',
23
+ '}' => 'RB',
24
+ '[' => 'LB2',
25
+ ']' => 'RB2',
26
+ ';' => 'SC',
27
+ ':' => 'CO',
28
+ '-' => 'DA',
29
+ ',' => 'COM'
30
+ }.freeze
31
+ REPLACE_CHARACTERS = ::Regexp.union(REPLACE_MAP.keys)
32
+
33
+ # The separators below are absent from both REMOVE_CHARACTERS and
34
+ # REPLACE_MAP, so they survive either option. escape_value/1 escapes them
35
+ # inside a value, which makes the key one-to-one with the arguments.
36
+ FIELD_SEPARATOR = '.'
37
+ PAIR_SEPARATOR = '/'
38
+ ESCAPE_MAP = { '%' => '%25', FIELD_SEPARATOR => '%2E', PAIR_SEPARATOR => '%2F' }.freeze
39
+ ESCAPE_CHARACTERS = %r{[%./]}
40
+
41
+ # Build a key that names each field, so that two finders with the same
42
+ # values do not share one cache entry.
43
+ #
44
+ # for_fields([:alpha, :beta], ['x', 'y'], {}).cache_key
45
+ # # => "alpha.x/beta.y"
46
+ #
47
+ def self.for_fields(field_names, values, options)
48
+ pairs = field_names.each_with_index.map do |field_name, index|
49
+ "#{field_name}#{FIELD_SEPARATOR}#{normalize_value(values[index])}"
50
+ end
51
+
52
+ new(pairs.join(PAIR_SEPARATOR), options)
53
+ end
54
+
55
+ def self.normalize_value(value)
56
+ [value].flatten.compact.map { |element| escape_value(element) }.join(FIELD_SEPARATOR)
57
+ end
58
+ private_class_method :normalize_value
59
+
60
+ def self.escape_value(value)
61
+ value.to_s.gsub(ESCAPE_CHARACTERS, ESCAPE_MAP)
62
+ end
63
+ private_class_method :escape_value
22
64
 
23
65
  def initialize(*arguments, options)
24
66
  @options = options
25
67
  @arguments = arguments.flatten.compact
26
- @argument_string = ''
27
-
28
- @arguments.each do |argument|
29
- @argument_string << argument.to_s
30
- end
68
+ @argument_string = @arguments.join
31
69
  end
32
70
 
33
71
  def cache_key
34
72
  return @argument_string.gsub(REMOVE_CHARACTERS, '') if remove_characters?
73
+ return @argument_string unless replace_characters?
35
74
 
36
- if replace_characters?
37
- REPLACE_MAP.each do |character, replacement|
38
- @argument_string.gsub!(character, replacement)
39
- end
40
- end
41
-
42
- @argument_string
75
+ # One pass, rather than one gsub for each entry in the map.
76
+ @argument_string.gsub(REPLACE_CHARACTERS, REPLACE_MAP)
43
77
  end
44
78
 
45
79
  def to_s
@@ -5,6 +5,10 @@ require 'delegate'
5
5
  module ActiveRemote
6
6
  module Cached
7
7
  class Cache < ::SimpleDelegator
8
+ # Raised when the given cache provider is missing a method the library
9
+ # calls on it.
10
+ class InvalidCacheProvider < ::StandardError; end
11
+
8
12
  attr_reader :cache_provider
9
13
 
10
14
  def initialize(new_cache_provider)
@@ -29,14 +33,19 @@ module ActiveRemote
29
33
  @nested_cache_provider = ::ActiveSupport::Cache::MemoryStore.new
30
34
  end
31
35
 
36
+ def nested_caching?
37
+ !nested_cache_provider.is_a?(::ActiveSupport::Cache::NullStore)
38
+ end
39
+
32
40
  def exist?(*args)
33
41
  nested_cache_provider.exist?(*args) || super
34
42
  end
35
43
 
36
44
  def fetch(name, options = {})
37
- fetch_value = nested_cache_provider.fetch(name, options) { super }
45
+ provider_options = provider_fetch_options(options)
46
+ fetch_value = nested_cache_provider.fetch(name, provider_options) { super(name, provider_options) }
38
47
 
39
- delete(name) unless valid_fetched_value?(fetch_value, options)
48
+ delete(name) if delete_after_fetch?(fetch_value, options, provider_options)
40
49
 
41
50
  fetch_value
42
51
  end
@@ -54,6 +63,24 @@ module ActiveRemote
54
63
 
55
64
  attr_reader :nested_cache_provider
56
65
 
66
+ # :skip_nil tells the provider not to write a nil at all, which saves a
67
+ # write and the delete that follows it. Only an ActiveSupport store is
68
+ # known to honor the option.
69
+ def provider_fetch_options(options)
70
+ return options if options.fetch(:allow_nil, false)
71
+ return options unless cache_provider.is_a?(::ActiveSupport::Cache::Store)
72
+
73
+ options.merge(:skip_nil => true)
74
+ end
75
+
76
+ def delete_after_fetch?(value, options, provider_options)
77
+ return false if valid_fetched_value?(value, options)
78
+ # The provider already skipped the write.
79
+ return false if value.nil? && provider_options[:skip_nil]
80
+
81
+ true
82
+ end
83
+
57
84
  def valid_fetched_value?(value, options = {})
58
85
  return false if value.nil? && !options.fetch(:allow_nil, false)
59
86
  return false if !options.fetch(:allow_empty, false) && value.respond_to?(:empty?) && value.empty?
@@ -64,10 +91,9 @@ module ActiveRemote
64
91
  def validate_provider_method_present(method_name)
65
92
  return if cache_provider.respond_to?(method_name)
66
93
 
67
- raise <<-CACHE_METHOD
68
- ActiveRemote::Cached::Cache must respond_to? #{method_name}
69
- in order to be used as a caching interface for ActiveRemote
70
- CACHE_METHOD
94
+ raise InvalidCacheProvider,
95
+ "ActiveRemote::Cached::Cache must respond_to? #{method_name} " \
96
+ 'in order to be used as a caching interface for ActiveRemote'
71
97
  end
72
98
  end
73
99
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRemote
4
4
  module Cached
5
- VERSION = '1.1.1'
5
+ VERSION = '1.2.0'
6
6
  end
7
7
  end