pgdice 0.4.2 → 2.0.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +4 -1
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.github/workflows/gem-push.yml +20 -0
  6. data/.github/workflows/ruby.yml +62 -0
  7. data/.rubocop.yml +12 -2
  8. data/.ruby-version +1 -1
  9. data/CHANGELOG.md +21 -0
  10. data/README.md +22 -26
  11. data/SECURITY.md +15 -0
  12. data/examples/aws/README.md +28 -0
  13. data/examples/aws/cloudformation/scheduled_events.json +59 -0
  14. data/examples/aws/lib/sqs_listener/default_event_handler.rb +32 -0
  15. data/examples/aws/lib/sqs_listener/exceptions/unknown_task_error.rb +4 -0
  16. data/examples/aws/lib/sqs_listener/fallthrough_event_handler.rb +18 -0
  17. data/examples/aws/lib/sqs_listener/sqs_event_router.rb +32 -0
  18. data/examples/aws/lib/sqs_listener/typed_event_handler/task_event_handler.rb +46 -0
  19. data/examples/aws/lib/sqs_listener/typed_event_handler/tasks/database_tasks.rb +37 -0
  20. data/examples/aws/lib/sqs_listener.rb +47 -0
  21. data/examples/aws/lib/sqs_message_deleter.rb +32 -0
  22. data/examples/aws/lib/sqs_poller.rb +67 -0
  23. data/examples/aws/tasks/poll_sqs.rake +8 -0
  24. data/examples/aws/workers/pg_dice_worker.rb +54 -0
  25. data/lib/pgdice/approved_tables.rb +3 -2
  26. data/lib/pgdice/configuration.rb +5 -5
  27. data/lib/pgdice/configuration_file_loader.rb +1 -1
  28. data/lib/pgdice/database_connection_factory.rb +5 -6
  29. data/lib/pgdice/date_helper.rb +2 -2
  30. data/lib/pgdice/error.rb +2 -2
  31. data/lib/pgdice/partition_dropper.rb +1 -1
  32. data/lib/pgdice/partition_helper.rb +2 -2
  33. data/lib/pgdice/pg_slice_manager.rb +5 -5
  34. data/lib/pgdice/query_executor.rb +3 -3
  35. data/lib/pgdice/query_executor_factory.rb +20 -0
  36. data/lib/pgdice/table.rb +2 -2
  37. data/lib/pgdice/version.rb +1 -1
  38. data/lib/pgdice.rb +1 -0
  39. data/pgdice.gemspec +26 -22
  40. metadata +87 -49
  41. data/.circleci/config.yml +0 -66
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sqs'
4
+
5
+ # READ_ONLY_SQS can be set to ensure we don't delete good messages
6
+ class SqsListener
7
+ DEFAULT_VISIBILITY_TIMEOUT ||= 600
8
+ attr_reader :logger, :queue_url, :visibility_timeout
9
+
10
+ def initialize(opts = {})
11
+ @logger = opts[:logger] ||= Sidekiq.logger
12
+ @queue_url = opts[:queue_url] ||= ENV['SqsQueueUrl']
13
+ @sqs_client = opts[:sqs_client] ||= Aws::SQS::Client.new
14
+ @sqs_event_router = opts[:sqs_event_router] ||= SqsEventRouter.new(logger: logger)
15
+ increase_timeout_resolver = opts[:increase_timeout_resolver] ||= -> { ENV['READ_ONLY_SQS'].to_s == 'true' }
16
+ @visibility_timeout = calculate_visibility_timeout(increase_timeout_resolver.call)
17
+
18
+ logger.debug { "Running in environment: #{ENV['RAILS_ENV']} and using sqs queue: #{queue_url}" }
19
+ end
20
+
21
+ # http://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/sqs-example-get-messages-with-long-polling.html
22
+ def call
23
+ # This uses long polling to retrieve sqs events so we can process them
24
+ response = @sqs_client.receive_message(queue_url: queue_url,
25
+ max_number_of_messages: 10,
26
+ wait_time_seconds: 20,
27
+ visibility_timeout: visibility_timeout)
28
+
29
+ if response.messages&.size&.positive?
30
+ logger.debug { "The number of messages received from the queue was: #{response.messages&.size}" }
31
+ end
32
+
33
+ # Iterate over all the messages in the response (Response is a Struct which acts like an object with methods)
34
+ response.messages&.each do |message|
35
+ @sqs_event_router.handle_message(message)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def calculate_visibility_timeout(increase_timeout)
42
+ visibility_timeout = increase_timeout ? DEFAULT_VISIBILITY_TIMEOUT * 4 : DEFAULT_VISIBILITY_TIMEOUT
43
+
44
+ logger.info { "Visibility timeout set to: #{visibility_timeout} seconds" }
45
+ visibility_timeout
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sqs'
4
+
5
+ class SqsMessageDeleter
6
+ attr_reader :logger
7
+
8
+ def initialize(opts = {})
9
+ @logger = opts[:logger] ||= Sidekiq.logger
10
+ @queue_url = opts[:queue_url] ||= ENV['SqsQueueUrl']
11
+ @sqs_client = opts[:sqs_client] ||= Aws::SQS::Client.new
12
+ @skip_delete_predicate = opts[:skip_delete_predicate] ||= proc do
13
+ Rails.env != 'production' || ENV['READ_ONLY_SQS'].to_s == 'true'
14
+ end
15
+ end
16
+
17
+ def call(sqs_message_receipt_handle)
18
+ if @skip_delete_predicate.call
19
+ logger.info { "Not destroying sqs message because environment is not prod or READ_ONLY_SQS was set to 'true'" }
20
+ return false
21
+ end
22
+
23
+ logger.debug { "Destroying sqs message with handle: #{sqs_message_receipt_handle}" }
24
+
25
+ response = @sqs_client.delete_message(queue_url: @queue_url, receipt_handle: sqs_message_receipt_handle)
26
+ unless response.successful?
27
+ raise "Attempt to delete SQS message: #{sqs_message_receipt_handle} was not successful. Response: #{response}"
28
+ end
29
+
30
+ true
31
+ end
32
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sqs'
4
+
5
+ class SqsPoller
6
+ attr_reader :logger, :queue_url
7
+
8
+ MAX_RETRIES ||= 3
9
+ DEFAULT_WAIT_TIME ||= 5
10
+
11
+ def initialize(opts = {})
12
+ @logger = opts[:logger] ||= ActiveSupport::TaggedLogging.new(Logger.new(ENV['POLL_SQS_LOG_OUTPUT'] || STDOUT))
13
+ @max_retries = opts[:max_retries] ||= MAX_RETRIES
14
+ @sleep_seconds = opts[:sleep_seconds] ||= DEFAULT_WAIT_TIME
15
+ @error_sleep_seconds = opts[:error_sleep_seconds] ||= @sleep_seconds * 2
16
+ @sqs_listener = opts[:sqs_listener] ||= SqsListener.new(logger: logger)
17
+ end
18
+
19
+ def poll(iterations = Float::INFINITY)
20
+ logger.info { "Starting loop to #{iterations}, press Ctrl-C to exit" }
21
+
22
+ retries = 0
23
+ i = 0
24
+
25
+ while i < iterations
26
+ begin
27
+ i += 1
28
+ execute_loop
29
+ rescue StandardError => e
30
+ if retries < MAX_RETRIES
31
+ retries = handle_retry(retries, e)
32
+ retry
33
+ else
34
+ die(e)
35
+ end
36
+ rescue Exception => e
37
+ die(e)
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def execute_loop
45
+ @sqs_listener.call
46
+ sleep @sleep_seconds
47
+ end
48
+
49
+ def handle_retry(retries, error)
50
+ logger.error do
51
+ "Polling loop encountered an error. Will retry in #{@error_sleep_seconds} seconds. "\
52
+ "Error: #{error}. Retries: #{retries}"
53
+ end
54
+ retries += 1
55
+
56
+ # Handle error with error tracking service
57
+ # @error_handler.call(error)
58
+
59
+ sleep @error_sleep_seconds
60
+ retries
61
+ end
62
+
63
+ def die(error)
64
+ logger.fatal { "Polling loop is stopping due to exception: #{error}" }
65
+ raise error
66
+ end
67
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # You can set READ_ONLY_SQS=true if you don't want to delete messages
4
+
5
+ desc 'Poll SQS for any new test executions'
6
+ task poll_sqs: :environment do
7
+ SqsPoller.new.poll
8
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # https://github.com/mperham/sidekiq/wiki/Best-Practices
4
+ # Sidekiq job parameters must be JSON serializable. That means Ruby symbols are
5
+ # lost when they are sent through JSON!
6
+ class PgdiceWorker
7
+ include Sidekiq::Worker
8
+ attr_reader :logger
9
+ sidekiq_options queue: :default, backtrace: true, retry: 5
10
+
11
+ def initialize(opts = {})
12
+ @pgdice = opts[:pgdice] ||= PgDice
13
+ @logger = opts[:logger] ||= Sidekiq.logger
14
+ @validator = opts[:validator] ||= lambda do |table_name, params|
15
+ @pgdice.public_send(:assert_tables, table_name, params)
16
+ end
17
+ end
18
+
19
+ def perform(method, params)
20
+ table_names = params.delete('table_names')
21
+ validate = params.delete('validate').present?
22
+ # Don't pass in params to PgDice if the hash is empty. PgDice will behave differently when params are passed.
23
+ pgdice_params = params.keys.size.zero? ? nil : handle_pgdice_params(params)
24
+
25
+ logger.debug { "PgdiceWorker called with method: #{method} and table_names: #{table_names}. Validate: #{validate}" }
26
+
27
+ [table_names].flatten.compact.each do |table_name|
28
+ @pgdice.public_send(method, table_name, pgdice_params)
29
+ @validator.call(table_name, pgdice_params) if validate
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def handle_pgdice_params(pgdice_params)
36
+ convert_pgdice_param_values(pgdice_known_symbol_keys(pgdice_params))
37
+ end
38
+
39
+ def pgdice_known_symbol_keys(params)
40
+ convertable_keys = ['only']
41
+ params.keys.each do |key|
42
+ params[key.to_sym] = params.delete(key) if convertable_keys.include?(key)
43
+ end
44
+ params
45
+ end
46
+
47
+ def convert_pgdice_param_values(params)
48
+ symbolize_values_for_keys = [:only]
49
+ params.each do |key, value|
50
+ params[key] = value.to_sym if symbolize_values_for_keys.include?(key)
51
+ end
52
+ params
53
+ end
54
+ end
@@ -4,6 +4,7 @@ module PgDice
4
4
  # Hash-like object to contain approved tables. Adds some convenience validation and a simpleish interface.
5
5
  class ApprovedTables
6
6
  attr_reader :tables
7
+
7
8
  extend Forwardable
8
9
 
9
10
  def_delegators :@tables, :size, :empty?, :map, :each, :each_with_index, :to_a
@@ -11,12 +12,12 @@ module PgDice
11
12
  def initialize(*args)
12
13
  @tables = args.flatten.compact
13
14
 
14
- raise ArgumentError, 'Objects must be a PgDice::Table!' unless tables.all? { |item| item.is_a?(PgDice::Table) }
15
+ raise ArgumentError, 'Objects must be a PgDice::Table!' unless tables.all?(PgDice::Table)
15
16
  end
16
17
 
17
18
  def [](arg)
18
19
  key = check_string_args(arg)
19
- tables.select { |table| table.name == key }.first
20
+ tables.find { |table| table.name == key }
20
21
  end
21
22
 
22
23
  def include?(arg)
@@ -14,10 +14,10 @@ module PgDice
14
14
 
15
15
  # Configuration class which holds all configurable values
16
16
  class Configuration
17
- DEFAULT_VALUES ||= { logger_factory: proc { Logger.new(STDOUT) },
18
- database_url: nil,
19
- dry_run: false,
20
- batch_size: 7 }.freeze
17
+ DEFAULT_VALUES = { logger_factory: proc { Logger.new($stdout) },
18
+ database_url: nil,
19
+ dry_run: false,
20
+ batch_size: 7 }.freeze
21
21
 
22
22
  attr_writer :logger,
23
23
  :logger_factory,
@@ -63,7 +63,7 @@ module PgDice
63
63
  raise PgDice::InvalidConfigurationError, 'approved_tables must be an instance of PgDice::ApprovedTables!'
64
64
  end
65
65
 
66
- if !config_file_loader.file_loaded? && config_file.present?
66
+ if !config_file_loader.file_loaded? && !config_file.nil?
67
67
  config_file_loader.load_file
68
68
  @approved_tables
69
69
  end
@@ -17,7 +17,7 @@ module PgDice
17
17
  end
18
18
  @config_loader = opts[:config_loader] ||= lambda do |file|
19
19
  logger.debug { "Loading PgDice configuration file: '#{config_file}'" }
20
- YAML.safe_load(ERB.new(IO.read(file)).result)
20
+ YAML.safe_load(ERB.new(File.read(file)).result)
21
21
  end
22
22
  @file_loaded = opts[:file_loaded]
23
23
  end
@@ -1,21 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Entry point for PartitionManager
3
+ # Entry point
4
4
  module PgDice
5
- # PartitionListerFactory is a class used to build PartitionListers
5
+ # DatabaseConnectionFactory is a class used to build DatabaseConnections
6
6
  class DatabaseConnectionFactory
7
7
  extend Forwardable
8
8
 
9
- def_delegators :@configuration, :logger, :pg_connection, :dry_run
9
+ def_delegators :@configuration, :logger, :dry_run
10
10
 
11
11
  def initialize(configuration, opts = {})
12
12
  @configuration = configuration
13
- @query_executor = opts[:query_executor] ||= PgDice::QueryExecutor.new(logger: logger,
14
- connection_supplier: -> { pg_connection })
13
+ @query_executor_factory = opts[:query_executor_factory] ||= PgDice::QueryExecutorFactory.new(configuration, opts)
15
14
  end
16
15
 
17
16
  def call
18
- PgDice::DatabaseConnection.new(logger: logger, query_executor: @query_executor, dry_run: dry_run)
17
+ PgDice::DatabaseConnection.new(logger: logger, query_executor: @query_executor_factory.call, dry_run: dry_run)
19
18
  end
20
19
  end
21
20
  end
@@ -8,9 +8,9 @@ module PgDice
8
8
 
9
9
  case numbers.size
10
10
  when 6
11
- return numbers + '01'
11
+ "#{numbers}01"
12
12
  when 4
13
- return numbers + '0101'
13
+ "#{numbers}0101"
14
14
  else
15
15
  raise ArgumentError, "Invalid date. Cannot parse date from #{numbers}"
16
16
  end
data/lib/pgdice/error.rb CHANGED
@@ -25,7 +25,7 @@ module PgDice
25
25
  class InsufficientTablesError < Error
26
26
  def initialize(direction, table_name, expected, period, found_count)
27
27
  super("Insufficient #{direction} tables exist for table: #{table_name}. "\
28
- "Expected: #{expected} having period of: #{period} but found: #{found_count}")
28
+ "Expected: #{expected} having period of: #{period} but found: #{found_count}")
29
29
  end
30
30
  end
31
31
 
@@ -51,7 +51,7 @@ module PgDice
51
51
  class NotConfiguredError < ConfigurationError
52
52
  def initialize(method_name)
53
53
  super("Cannot use #{method_name} before PgDice has been configured! "\
54
- 'See README.md for configuration help.')
54
+ 'See README.md for configuration help.')
55
55
  end
56
56
  end
57
57
 
@@ -28,7 +28,7 @@ module PgDice
28
28
  sql_query = old_partitions.reduce("BEGIN;\n") do |sql, table_name|
29
29
  sql + "DROP TABLE IF EXISTS #{table_name} CASCADE;\n"
30
30
  end
31
- sql_query + 'COMMIT;'
31
+ "#{sql_query}COMMIT;"
32
32
  end
33
33
  end
34
34
  end
@@ -40,8 +40,8 @@ module PgDice
40
40
 
41
41
  def undo_partitioning(table_name)
42
42
  undo_partitioning!(table_name)
43
- rescue PgDice::PgSliceError => error
44
- logger.error { "Rescued PgSliceError: #{error}" }
43
+ rescue PgDice::PgSliceError => e
44
+ logger.error { "Rescued PgSliceError: #{e}" }
45
45
  false
46
46
  end
47
47
 
@@ -73,8 +73,8 @@ module PgDice
73
73
  table_name = params.fetch(:table_name)
74
74
 
75
75
  run_pgslice("unprep #{table_name}", params[:dry_run])
76
- rescue PgSliceError => error
77
- logger.error { "Rescued PgSliceError: #{error}" }
76
+ rescue PgSliceError => e
77
+ logger.error { "Rescued PgSliceError: #{e}" }
78
78
  false
79
79
  end
80
80
 
@@ -82,8 +82,8 @@ module PgDice
82
82
  table_name = params.fetch(:table_name)
83
83
 
84
84
  run_pgslice("unswap #{table_name}", params[:dry_run])
85
- rescue PgSliceError => error
86
- logger.error { "Rescued PgSliceError: #{error}" }
85
+ rescue PgSliceError => e
86
+ logger.error { "Rescued PgSliceError: #{e}" }
87
87
  false
88
88
  end
89
89
 
@@ -97,7 +97,7 @@ module PgDice
97
97
  if status.to_i.positive?
98
98
  raise PgDice::PgSliceError,
99
99
  "pgslice with arguments: '#{argument_string}' failed with status: '#{status}' "\
100
- "STDOUT: '#{stdout}' STDERR: '#{stderr}'"
100
+ "STDOUT: '#{stdout}' STDERR: '#{stderr}'"
101
101
  end
102
102
  true
103
103
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Entry point for DatabaseConnection
3
+ # Entry point
4
4
  module PgDice
5
5
  # Wrapper class around pg_connection to reset connection on PG errors
6
6
  class QueryExecutor
@@ -13,8 +13,8 @@ module PgDice
13
13
 
14
14
  def call(query)
15
15
  @connection_supplier.call.exec(query)
16
- rescue PG::Error => error
17
- logger.error { "Caught error: #{error}. Going to reset connection and try again" }
16
+ rescue PG::Error => e
17
+ logger.error { "Caught error: #{e}. Going to reset connection and try again" }
18
18
  @connection_supplier.call.reset
19
19
  @connection_supplier.call.exec(query)
20
20
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point
4
+ module PgDice
5
+ # QueryExecutorFactory is a class used to build QueryExecutors
6
+ class QueryExecutorFactory
7
+ extend Forwardable
8
+
9
+ def_delegators :@configuration, :logger, :pg_connection
10
+
11
+ def initialize(configuration, opts = {})
12
+ @configuration = configuration
13
+ @connection_supplier = opts[:connection_supplier] ||= -> { pg_connection }
14
+ end
15
+
16
+ def call
17
+ PgDice::QueryExecutor.new(logger: logger, connection_supplier: @connection_supplier)
18
+ end
19
+ end
20
+ end
data/lib/pgdice/table.rb CHANGED
@@ -70,7 +70,7 @@ module PgDice
70
70
  end
71
71
 
72
72
  def self.from_hash(hash)
73
- Table.new(**hash.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v; })
73
+ Table.new(**hash.transform_keys(&:to_sym))
74
74
  end
75
75
 
76
76
  private
@@ -79,7 +79,7 @@ module PgDice
79
79
  unless send(field).is_a?(expected_type)
80
80
  raise ArgumentError,
81
81
  "PgDice::Table: #{name} failed validation on field: #{field}. "\
82
- "Expected type of: #{expected_type} but found #{send(field).class}"
82
+ "Expected type of: #{expected_type} but found #{send(field).class}"
83
83
  end
84
84
  true
85
85
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgDice
4
- VERSION = '0.4.2'
4
+ VERSION = '2.0.0'
5
5
  end
data/lib/pgdice.rb CHANGED
@@ -43,6 +43,7 @@ require 'pgdice/partition_dropper'
43
43
  require 'pgdice/partition_dropper_factory'
44
44
 
45
45
  require 'pgdice/query_executor'
46
+ require 'pgdice/query_executor_factory'
46
47
 
47
48
  require 'pgdice/database_connection'
48
49
  require 'pgdice/database_connection_factory'
data/pgdice.gemspec CHANGED
@@ -5,39 +5,43 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'pgdice/version'
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.name = 'pgdice'
9
- spec.version = PgDice::VERSION
10
- spec.authors = ['Andrew Newell']
11
- spec.email = ['andrew@andrewcn.com illuminuslimited@gmail.com']
12
-
13
- spec.summary = 'Postgres table partitioning with a Ruby API!'
14
- spec.description = 'Postgres table partitioning with a Ruby API built on top of https://github.com/ankane/pgslice'
15
- spec.homepage = 'https://github.com/IlluminusLimited/pgdice'
16
- spec.license = 'MIT'
8
+ spec.name = 'pgdice'
9
+ spec.version = PgDice::VERSION
10
+ spec.authors = ['Andrew Newell']
11
+ spec.email = ['andrew@illuminusltd.com']
12
+ spec.summary = 'Postgres table partitioning with a Ruby API!'
13
+ spec.description = 'Postgres table partitioning with a Ruby API built on top of https://github.com/ankane/pgslice'
14
+ spec.homepage = 'https://github.com/IlluminusLimited/pgdice'
15
+ spec.license = 'MIT'
17
16
 
18
17
  # Specify which files should be added to the gem when it is released.
19
18
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
19
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
20
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
21
  end
23
- spec.bindir = 'exe'
24
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.bindir = 'exe'
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
24
  spec.require_paths = ['lib']
26
25
 
26
+ spec.required_ruby_version = '>= 3.0.0'
27
+
28
+ # It looks like there's a bug in pg 1.3+ that I don't have time to fix
29
+ spec.add_runtime_dependency 'pg', '~> 1.2.3', '>= 1.1.0'
27
30
  # Locked because we depend on internal behavior for table commenting
28
- spec.add_runtime_dependency 'pg', '~> 1.1.0', '>= 1.1.0'
29
- spec.add_runtime_dependency 'pgslice', '0.4.5'
31
+ spec.add_runtime_dependency 'pgslice', '0.4.7'
30
32
 
31
- spec.add_development_dependency 'bundler', '~> 1.16', '>= 1.16'
32
- spec.add_development_dependency 'coveralls', '~> 0.8.22', '>= 0.8.22'
33
- spec.add_development_dependency 'guard', '~> 2.14.2', '>= 2.14.2'
33
+ spec.add_development_dependency 'bundler', '~> 2.3.6', '>= 1.16'
34
+ spec.add_development_dependency 'guard', '~> 2.18.0', '>= 2.14.2'
34
35
  spec.add_development_dependency 'guard-minitest', '~> 2.4.6', '>= 2.4.6'
35
- spec.add_development_dependency 'guard-rubocop', '~> 1.3.0', '>= 1.3.0'
36
+ spec.add_development_dependency 'guard-rubocop', '~> 1.5.0', '>= 1.3.0'
36
37
  spec.add_development_dependency 'guard-shell', '~> 0.7.1', '>= 0.7.1'
37
38
  spec.add_development_dependency 'minitest', '~> 5.0', '>= 5.0'
38
39
  spec.add_development_dependency 'minitest-ci', '~> 3.4.0', '>= 3.4.0'
39
- spec.add_development_dependency 'minitest-reporters', '~> 1.3.4', '>= 1.3.4'
40
- spec.add_development_dependency 'rake', '~> 10.0', '>= 10.0'
41
- spec.add_development_dependency 'rubocop', '0.59'
42
- spec.add_development_dependency 'simplecov', '~> 0.16.1', '>= 0.16.1'
40
+ spec.add_development_dependency 'minitest-reporters', '~> 1.5.0', '>= 1.3.4'
41
+ spec.add_development_dependency 'rake', '~> 13.0.6', '>= 10.0'
42
+ spec.add_development_dependency 'rubocop', '1.25.1'
43
+ spec.add_development_dependency 'rubocop-performance', '~> 1.13.2', '>= 1.13.2'
44
+ spec.add_development_dependency 'rubocop-rake', '~> 0.6.0', '>= 0.6.0'
45
+ spec.add_development_dependency 'simplecov', '~> 0.21.2', '>= 0.16.1'
46
+ spec.metadata['rubygems_mfa_required'] = 'true'
43
47
  end