composite_cache_store 0.0.3 → 0.0.4

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
  SHA256:
3
- metadata.gz: 689876c5fc7b2bd00343817eac73b522a0687482aaaeb1058a471c7fcac275e7
4
- data.tar.gz: dba46b9f25d1302377dd0fb64c9765a8c6016545f78fdb57444282e6b4d94cd8
3
+ metadata.gz: 8e8b3140f8d6d37fd4b5c876b550863f13a4dc134142e1cbc7115c63d167a3d4
4
+ data.tar.gz: b16ced0803e2be6075d8fe80a6e532285a0f9aed1fef63a6d8cd0092563a1c9d
5
5
  SHA512:
6
- metadata.gz: a04ad705343eea18cdd44d399cbd2264b11fa7eb91a6be8c0d98960b8937510abd77a780a95c816217bf2752cf7fde885dbf423b04aaee11024280876e19f994
7
- data.tar.gz: 72bcee27ef6647b444582c481dfa011fedbb587d540537d193a3f1042965bf16e7cb740ed059f6483b7926afdd99dd2b84fe80715665ea8908dcf2c3efc1ca0f
6
+ metadata.gz: cdf5f5918114b2f7f148be7518131ed141df10ee1d8d6f0c5096859e3185b0f0698c9df1b47e9dd0525aeb9a8feb4113c28dff0538d281b417621711cc48f1a3
7
+ data.tar.gz: 2ed7a6b022cedcb8fa079e9d8d70a02b42c934b1d1b2d15bcc208c3480e422cc79e6ecdd039420f445103e08e4ca1f98b43f821054f6cd9624b88ac9766c5386
data/README.md CHANGED
@@ -1,39 +1,53 @@
1
- # CompositeCacheStore
2
-
3
- ### A composite cache store comprised of layered ActiveSupport::Cache::Store instances
1
+ <p align="center">
2
+ <h1 align="center">CompositeCacheStore 🚀</h1>
3
+ <p align="center">
4
+ <a href="http://blog.codinghorror.com/the-best-code-is-no-code-at-all/">
5
+ <img alt="Lines of Code" src="https://img.shields.io/badge/loc-137-47d299.svg" />
6
+ </a>
7
+ <a href="https://codeclimate.com/github/hopsoft/composite_cache_store/maintainability">
8
+ <img src="https://api.codeclimate.com/v1/badges/80bcd3acced072534a3a/maintainability" />
9
+ </a>
10
+ <a href="https://rubygems.org/gems/composite_cache_store">
11
+ <img alt="GEM Version" src="https://img.shields.io/gem/v/composite_cache_store?color=168AFE&include_prereleases&logo=ruby&logoColor=FE1616">
12
+ </a>
13
+ <a href="https://rubygems.org/gems/composite_cache_store">
14
+ <img alt="GEM Downloads" src="https://img.shields.io/gem/dt/composite_cache_store?color=168AFE&logo=ruby&logoColor=FE1616">
15
+ </a>
16
+ <a href="https://github.com/testdouble/standard">
17
+ <img alt="Ruby Style" src="https://img.shields.io/badge/style-standard-168AFE?logo=ruby&logoColor=FE1616" />
18
+ </a>
19
+ <a href="https://github.com/hopsoft/composite_cache_store/actions/workflows/tests.yml">
20
+ <img alt="Tests" src="https://github.com/hopsoft/composite_cache_store/actions/workflows/tests.yml/badge.svg" />
21
+ </a>
22
+ <a href="https://github.com/sponsors/hopsoft">
23
+ <img alt="Sponsors" src="https://img.shields.io/github/sponsors/hopsoft?color=eb4aaa&logo=GitHub%20Sponsors" />
24
+ </a>
25
+ <br>
26
+ <a href="https://ruby.social/@hopsoft">
27
+ <img alt="Ruby.Social Follow" src="https://img.shields.io/mastodon/follow/000008274?domain=https%3A%2F%2Fruby.social&label=%40hopsoft&style=social">
28
+ </a>
29
+ <a href="https://twitter.com/hopsoft">
30
+ <img alt="Twitter Follow" src="https://img.shields.io/twitter/url?label=%40hopsoft&style=social&url=https%3A%2F%2Ftwitter.com%2Fhopsoft">
31
+ </a>
32
+ </p>
33
+ <h2 align="center">Boost application speed and maximize user satisfaction with layered caching</h2>
34
+ </p>
4
35
 
5
36
  <!-- Tocer[start]: Auto-generated, don't remove. -->
6
37
 
7
38
  ## Table of Contents
8
39
 
9
- - [Why a composite cache?](#why-a-composite-cache)
10
40
  - [Sponsors](#sponsors)
41
+ - [Why a composite cache?](#why-a-composite-cache)
42
+ - [Eventual consistentency](#eventual-consistentency)
11
43
  - [Dependencies](#dependencies)
12
44
  - [Installation](#installation)
13
45
  - [Setup](#setup)
14
- - [Ruby on Rails](#ruby-on-rails)
15
46
  - [Usage](#usage)
16
47
  - [License](#license)
17
48
 
18
49
  <!-- Tocer[finish]: Auto-generated, don't remove. -->
19
50
 
20
- ## Why a composite cache?
21
-
22
- Most web applications implement some form of caching mechanics to improve performance.
23
- Sufficiently large applications often employ a persistence service to back the cache.
24
- _(Redis, Memcache, etc.)_ These services make it possible to use a shared cache between multiple machines/processes.
25
-
26
- While these services are robust and performant, they can also be a source of latency and are potential bottlenecks.
27
- __A composite (or layered) cache can mitigate these risks__
28
- by reducing traffic and backpressure on the persistence service.
29
-
30
- Consider a composite cache that wraps a remote Redis-backed "layer 2 cache" with a local in-memory "layer 1 cache".
31
- When both caches are warm, a read hit on the local in-memory "layer 1 cache" returns instantly and avoids the overhead of
32
- inter-process communication (IPC) and/or network traffic _(with its attendant data marshaling and socket/wire noise)_
33
- associated with accessing the remote Redis-backed "layer 2 cache".
34
-
35
- To summarize: __Reads prioritize the inner cache and fall back to the outer cache.__
36
-
37
51
  ## Sponsors
38
52
 
39
53
  <p align="center">
@@ -45,6 +59,48 @@ To summarize: __Reads prioritize the inner cache and fall back to the outer cach
45
59
  </a>
46
60
  </p>
47
61
 
62
+ ## Why a composite cache?
63
+
64
+ Layered caching allows you to stack multiple caches with different scopes, lifetimes, and levels of reliability.
65
+ A technique that yields several benefits.
66
+
67
+ - __Improved performance__
68
+ - __Higher throughput__
69
+ - __Reduced load__
70
+ - __Enhanced capacity/scalability__
71
+
72
+ Inner cache layer(s) provide the fastest reads as they're close to the application, _typically in-memory within the same process_.
73
+ Outer layers are slower _(still fast)_ but are shared by multiple processes and servers.
74
+
75
+ <img height="250" src="https://ik.imagekit.io/hopsoft/composite_cache_store_jnHZcjAuK.svg?updatedAt=1679445477496" />
76
+
77
+ You can configure each layer with different expiration times, eviction policies, and storage mechanisms.
78
+ You're in control of balancing the trade-offs between performance and data freshness.
79
+
80
+ __Inner layers are supersonic while outer layers are speedy.__
81
+
82
+ The difference between a cache hit on a local in-memory store versus a cache hit on a remote store
83
+ is similar to making a grocery run in a
84
+ [Bugatti Chiron Super Sport 300+](https://www.bugatti.com/models/chiron-models/chiron-super-sport-300/)
85
+ compared to making the same trip on a bicyle, but all cache layers will be much faster than the underlying operations.
86
+ For example, a complete cache miss _(that triggers database queries and view rendering)_ would be equivalent to making this trip riding a sloth.
87
+
88
+ ## Eventual consistentency
89
+
90
+ Layered caching techniques exhibit some of the same traits as [distributed systems](https://en.wikipedia.org/wiki/Eventual_consistency)
91
+ because inner layers may hold onto __stale data__ until their entries expire.
92
+ __Be sure to configure inner layers appropriately with shorter lifetimes__.
93
+
94
+ This behavior is similar to the
95
+ [`race_condition_ttl`](https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-i-fetch-label-Options)
96
+ option in `ActiveSupport::Cache::Store` which helps to avoid race conditions whenever multiple threads/processes try to write to the same cache entry simultaneously.
97
+
98
+ __Be mindful of the potential gotchas.__
99
+
100
+ - __Data consistency__ - it's possible to end up with inconsistent or stale data
101
+ - __Over-caching__ - caching too much can lead to increased memory usage and even slower performance
102
+ - __Bugs/Testing__ - difficult bugs can be introduced with sophisticated caching techniques
103
+
48
104
  ## Dependencies
49
105
 
50
106
  - [ActiveSupport `>= 6.0`](https://github.com/rails/rails/tree/main/activesupport)
@@ -57,31 +113,39 @@ bundle add "composite_cache_store"
57
113
 
58
114
  ## Setup
59
115
 
60
- ### Ruby on Rails
61
-
62
- ```ruby
63
- # config/environments/production.rb
64
- module Example
65
- class Application < Rails::Application
66
- config.cache_store = :redis_cache_store, { url: "redis://example.com:6379/1" }
67
- end
68
- end
69
- ```
116
+ Here's an example of how you might set up layered caching in a Rails application.
70
117
 
71
118
  ```ruby
72
119
  # config/initializers/composite_cache_store.rb
73
120
  def Rails.composite_cache
74
121
  @composite_cache ||= CompositeCacheStore.new(
75
- # Layer 1 cache (inner) - employs an LRU eviction policy
76
- ActiveSupport::Cache::MemoryStore.new(
77
- expires_in: 15.minutes, # constrain entry lifetime so the local cache doesn't drift out of sync
78
- size: 32.megabytes # constrain max memory used by the local cache
79
- ),
80
-
81
- # Layer 2 cache (outer)
82
- Rails.cache, # use whatever makes sense for your app
83
-
84
- # additional layers are optional
122
+ layers: [
123
+ # Layer 1 cache (fastest)
124
+ # Most beneficial for high traffic volume
125
+ # Isolated to the process running an application instance
126
+ ActiveSupport::Cache::MemoryStore.new(
127
+ expires_in: 15.minutes,
128
+ size: 32.megabytes
129
+ ),
130
+
131
+ # Layer 2 cache (faster)
132
+ # Most beneficial for moderate traffic volume
133
+ # Isolated to the machine running N-number of application instances,
134
+ # and shared by all application processes on the machine
135
+ ActiveSupport::Cache::RedisCacheStore.new(
136
+ url: "redis://localhost:6379/0",
137
+ expires_in: 2.hours
138
+ ),
139
+
140
+ # Layer 3 cache (fast)
141
+ # Global cache shared by all application processes on all machines
142
+ ActiveSupport::Cache::RedisCacheStore.new(
143
+ url: "redis://remote.example.com:6379/0",
144
+ expires_in: 7.days
145
+ ),
146
+
147
+ # additional layers are optional
148
+ ]
85
149
  )
86
150
  end
87
151
  ```
@@ -91,12 +155,18 @@ end
91
155
  A composite cache is ideal for mitigating hot spot latency in frequently invoked areas of the codebase.
92
156
 
93
157
  ```ruby
94
- # method that's invoked frequently by multiple processes
158
+ # method that's invoked frequently by multiple processes/machines
95
159
  def hotspot
96
- # NOTE: expiration options are only applied to the outermost cache
97
- # inner caches use their globally configured expiration policy
98
- Rails.composite_cache.fetch("example/slow/operation", expires_in: 12.hours) do
99
- # a slow operation
160
+ Rails.composite_cache.fetch("example", expires_in: 12.hours) do
161
+ # reserve for high frequency access of slow operations
162
+ #
163
+ # examples:
164
+ # - api invocations
165
+ # - database queries
166
+ # - template renders
167
+ # - etc.
168
+
169
+ frequently_accessed_slow_operation
100
170
  end
101
171
  end
102
172
  ```
data/Rakefile CHANGED
@@ -2,9 +2,38 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "minitest/test_task"
5
+ require "paint"
6
+
7
+ # versions of rails to test against
8
+ rails_versions = %w[
9
+ v5.2.8.1
10
+ v6.1.7.3
11
+ v7.0.4.3
12
+ edge
13
+ ]
5
14
 
6
15
  task default: :test
7
16
 
8
- Minitest::TestTask.create(:test) do |t|
17
+ Minitest::TestTask.create(:minitest) do |t|
9
18
  t.test_globs = ["test/**/*_test.rb"]
10
19
  end
20
+
21
+ task :test do
22
+ ENV["COMPOSITE_CACHE_STORE_ENV"] = "test"
23
+ rails_versions.each do |rails_version|
24
+ ENV["RAILS_VERSION"] = (rails_version == "edge") ? nil : rails_version
25
+ puts Paint % ["Bundling activesupport %{version} from github ", :blue, :underline, version: [rails_version, "sky blue", :underline]]
26
+ print Paint["required for tests provided by rails... ", "slate gray"]
27
+ `bundle update activesupport`
28
+ puts "done!\n\n"
29
+ Rake::Task["minitest"].invoke
30
+ Rake::Task["minitest"].reenable unless rails_version == rails_versions.last
31
+ end
32
+ ensure
33
+ if ENV["GITHUB_ACTIONS"] != "true"
34
+ ENV["COMPOSITE_CACHE_STORE_ENV"] = nil
35
+ print Paint["Restoring bundle with activesupport from rubygems... ", :blue]
36
+ `bundle update activesupport`
37
+ puts "done!"
38
+ end
39
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class CompositeCacheStore
4
- VERSION = "0.0.3"
4
+ VERSION = "0.0.4"
5
5
  end
@@ -4,146 +4,172 @@ require "active_support/all"
4
4
  require_relative "composite_cache_store/version"
5
5
 
6
6
  class CompositeCacheStore
7
- DEFAULT_LAYER_1_OPTIONS = {
8
- expires_in: 5.minutes,
9
- size: 16.megabytes
10
- }
11
-
12
- DEFAULT_LAYER_2_OPTIONS = {
13
- expires_in: 1.day,
14
- size: 32.megabytes
15
- }
16
-
17
- attr_reader :layers
7
+ attr_reader :options, :layers
8
+ attr_accessor :logger
18
9
 
19
10
  # Returns a new CompositeCacheStore instance
20
- def initialize(*layers)
21
- if layers.blank?
22
- layers << ActiveSupport::Cache::MemoryStore.new(DEFAULT_LAYER_1_OPTIONS)
23
- layers << ActiveSupport::Cache::MemoryStore.new(DEFAULT_LAYER_2_OPTIONS)
24
- end
11
+ def initialize(options = {})
12
+ options = options.dup || {}
13
+ layers = options.delete(:layers) || []
14
+
15
+ raise ArgumentError.new("A layered cache requires more than 1 layer!") unless layers.size > 1
25
16
 
26
- message = "All layers must be instances of ActiveSupport::Cache::Store"
27
- layers.each do |layer|
28
- raise ArgumentError.new(message) unless layer.is_a?(ActiveSupport::Cache::Store)
17
+ unless layers.all? { |layer| layer.is_a? ActiveSupport::Cache::Store }
18
+ raise ArgumentError.new("All layers must be instances of ActiveSupport::Cache::Store!")
29
19
  end
30
20
 
31
- layers.freeze
32
- @layers = layers
21
+ @layers = layers.freeze
22
+ @logger = options[:logger]
23
+ @options = options
33
24
  end
34
25
 
35
- def cleanup(...)
36
- layers.each { |store| store.cleanup(...) }
26
+ def read(name, options = nil)
27
+ value = nil
28
+ warm_layer = layers.find { |layer| layer_read?(layer, name, options) { |val| value = val } }
29
+ yield(value, warm_layer) if block_given?
30
+ value
37
31
  end
38
32
 
39
- def clear(...)
40
- layers.each { |store| store.clear(...) }
33
+ def read_multi(*names)
34
+ value = {}
35
+ warm_layer = layers.find { |layer| layer_read_multi?(layer, *names) { |val| value.merge!(val) } }
36
+ yield(value, warm_layer) if block_given?
37
+ value
41
38
  end
42
39
 
43
- def decrement(...)
44
- layers.each { |store| store.decrement(...) }
40
+ def fetch(name, options = nil, &block)
41
+ options ||= {}
42
+
43
+ if options[:force]
44
+ raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block." unless block
45
+ value = block&.call(name)
46
+ layers.each { |layer| layer.write(name, value, options) }
47
+ return value
48
+ end
49
+
50
+ read(name, options) do |value, warm_layer|
51
+ value ||= block&.call(name) unless warm_layer
52
+
53
+ layers.each do |layer|
54
+ break if layer == warm_layer
55
+ layer.write(name, value, options) unless value.nil? && options[:skip_nil]
56
+ end
57
+
58
+ return value
59
+ end
45
60
  end
46
61
 
47
- def delete(...)
48
- layers.each { |store| store.delete(...) }
62
+ def fetch_multi(*names, &block)
63
+ raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block
64
+
65
+ keys = names.dup
66
+ options = keys.extract_options!
67
+
68
+ if options[:force]
69
+ value = keys.each_with_object({}) { |key, memo| memo[key] = block&.call(key) }
70
+ layers.each { |layer| layer.write_multi(value, options) }
71
+ return value
72
+ end
73
+
74
+ read_multi(*names) do |value, warm_layer|
75
+ unless warm_layer
76
+ missing_keys = keys - value.keys
77
+ missing_keys.each { |key| value[key] = block&.call(key) }
78
+ end
79
+
80
+ value.compact! if options[:skip_nil]
81
+
82
+ layers.each do |layer|
83
+ break if layer == warm_layer
84
+ layer.write_multi(value, options)
85
+ end
86
+
87
+ # return ordered hash value
88
+ return keys.each_with_object({}) { |key, memo| memo[key] = value[key] }
89
+ end
49
90
  end
50
91
 
51
- def delete_matched(...)
52
- layers.each { |store| store.delete_matched(...) }
92
+ def write(name, value, options = nil)
93
+ layers.map { |layer| layer.write(name, value, options) }.last
53
94
  end
54
95
 
55
- def delete_multi(...)
56
- layers.each { |store| store.delete_multi(...) }
96
+ def write_multi(hash, options = nil)
97
+ layers.map { |layer| layer.write_multi(hash, options) }.last
57
98
  end
58
99
 
59
- def exist?(...)
60
- layers.each do |store|
61
- return true if store.exist?(...)
62
- end
63
- false
100
+ def delete(...)
101
+ layers.map { |layer| layer.delete(...) }.last
64
102
  end
65
103
 
66
- def fetch(*args, &block)
67
- f = ->(store) do
68
- return store.fetch(*args, &block) if store == layers.last
69
- store.fetch(*args) { f.call(layers[layers.index(store) + 1]) }
70
- end
71
- f.call(layers.first)
104
+ def delete_multi(...)
105
+ layers.map { |layer| layer.delete_multi(...) }.last
72
106
  end
73
107
 
74
- def fetch_multi(*args, &block)
75
- fm = ->(store) do
76
- return store.fetch_multi(*args, &block) if store == layers.last
77
- store.fetch_multi(*args) { fm.call(layers[layers.index(store) + 1]) }
78
- end
79
- fm.call(layers.first)
108
+ def delete_matched(...)
109
+ layers.map { |layer| layer.delete_matched(...) }.last
80
110
  end
81
111
 
82
- def increment(...)
83
- layers.each { |store| store.increment(...) }
112
+ def increment(name, amount = 1, options = nil)
113
+ provisional_layers.each { |layer| layer.delete(name, options) }
114
+ layers.last.increment(name, amount, options)
84
115
  end
85
116
 
86
- def mute
87
- m = ->(store) do
88
- return store.mute { yield } if store == layers.last
89
- store.mute { m.call(layers[layers.index(store) + 1]) }
90
- end
91
- m.call(layers.first)
117
+ def decrement(name, amount = 1, options = nil)
118
+ provisional_layers.each { |layer| layer.delete(name, options) }
119
+ layers.last.decrement(name, amount, options)
92
120
  end
93
121
 
94
- def read(*args)
95
- r = ->(store) do
96
- return store.read(*args) if store == layers.last
97
- store.fetch(*args) { r.call(layers[layers.index(store) + 1]) }
98
- end
99
- r.call(layers.first)
122
+ def cleanup(...)
123
+ layers.map { |layer| layer.cleanup(...) }.last
100
124
  end
101
125
 
102
- def read_multi(...)
103
- missed_layers = []
104
- layers.each do |store|
105
- hash = store.read_multi(...)
106
- if hash.present?
107
- missed_layers.each { |s| s.write_multi(hash) }
108
- return hash
109
- end
110
- missed_layers << store
111
- end
112
- {}
126
+ def clear(...)
127
+ layers.map { |layer| layer.clear(...) }.last
113
128
  end
114
129
 
115
- def silence!
116
- layers.each { |store| store.silence! }
130
+ def exist?(...)
131
+ layers.any? { |layer| layer.exist?(...) }
117
132
  end
118
133
 
119
- def write(name, value, options = nil)
120
- layers.each do |store|
121
- store.write name, value, permitted_options(store, options)
122
- end
134
+ def mute
135
+ layers.map { |layer| layer.mute { yield } }.last
123
136
  end
124
137
 
125
- def write_multi(hash, options = nil)
126
- layers.each do |store|
127
- store.write_multi hash, permitted_options(store, options)
128
- end
138
+ def silence!
139
+ layers.map { |layer| layer.silence! }.last
129
140
  end
130
141
 
131
142
  private
132
143
 
133
- def permitted_options(store, options = {})
134
- return options if options.blank?
135
- return options if keep_expiration?(store, options)
136
- options.except(:expires_in, :expires_at)
144
+ def provisional_layers
145
+ layers.take layers.size - 1
146
+ end
147
+
148
+ def layer_read?(layer, name, options)
149
+ if layer.respond_to?(:with_local_cache)
150
+ layer.with_local_cache do
151
+ value = layer.read(name, options)
152
+ yield value
153
+ value || layer.exist?(name, options)
154
+ end
155
+ else
156
+ value = layer.read(name, options)
157
+ yield value
158
+ value || layer.exist?(name, options)
159
+ end
137
160
  end
138
161
 
139
- def keep_expiration?(store, options = {})
140
- return true if store == layers.last
141
- return true unless store.options[:expires_in]
162
+ def layer_read_multi?(layer, *names)
163
+ keys = names.dup
164
+ keys.extract_options!
142
165
 
143
- expires_in = options[:expires_in]
144
- expires_in ||= Time.current - options[:expires_at] if options[:expires_at]
145
- return false unless expires_in
166
+ value = if layer.respond_to?(:with_local_cache)
167
+ layer.with_local_cache { layer.read_multi(*names) }
168
+ else
169
+ layer.read_multi(*names)
170
+ end
146
171
 
147
- expires_in < store.options[:expires_in]
172
+ yield value
173
+ value.size == keys.size
148
174
  end
149
175
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: composite_cache_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Hopkins (hopsoft)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-14 00:00:00.000000000 Z
11
+ date: 2023-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: paint
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: pry-byebug
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -67,7 +81,49 @@ dependencies:
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
- name: standardrb
84
+ name: pry-doc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: standard
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: tocer
71
127
  requirement: !ruby/object:Gem::Requirement
72
128
  requirements:
73
129
  - - ">="