pgdump_scrambler 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'yaml'
3
4
  require 'erb'
4
5
  require 'set'
@@ -19,14 +20,14 @@ module PgdumpScrambler
19
20
  'prefix' => 'YOUR_S3_PATH_PREFIX',
20
21
  'access_key_id' => "<%= ENV['AWS_ACCESS_KEY_ID'] %>",
21
22
  'secret_key' => "<%= ENV['AWS_SECRET_KEY'] %>"
22
- }
23
+ }.freeze
23
24
  attr_reader :dump_path, :s3, :resolved_s3, :exclude_tables, :pgdump_args
24
25
 
25
- def initialize(tables, dump_path, s3, exclude_tables, pgdump_args)
26
- @table_hash = tables.sort_by(&:name).map { |table| [table.name, table] }.to_h
26
+ def initialize(tables, dump_path, s3, exclude_tables, pgdump_args) # rubocop:disable Naming/MethodParameterName
27
+ @table_hash = tables.sort_by(&:name).to_h { |table| [table.name, table] }
27
28
  @dump_path = dump_path
28
29
  @s3 = s3
29
- @resolved_s3 = s3.map { |k, v| [k, ERB.new(v).result] }.to_h if s3
30
+ @resolved_s3 = s3.transform_values { |v| ERB.new(v).result } if s3
30
31
  @exclude_tables = exclude_tables
31
32
  @pgdump_args = pgdump_args
32
33
  end
@@ -45,7 +46,7 @@ module PgdumpScrambler
45
46
 
46
47
  def update_with(other)
47
48
  new_tables = @table_hash.map do |_, table|
48
- if other_table = other.table(table.name)
49
+ if (other_table = other.table(table.name))
49
50
  table.update_with(other_table)
50
51
  else
51
52
  table
@@ -66,15 +67,15 @@ module PgdumpScrambler
66
67
  yml = {}
67
68
  yml[KEY_DUMP_PATH] = @dump_path
68
69
  yml[KEY_S3] = @s3 if @s3
69
- yml[KEY_EXCLUDE_TABLES] = @exclude_tables if @exclude_tables.size > 0
70
+ yml[KEY_EXCLUDE_TABLES] = @exclude_tables if @exclude_tables.size.positive?
70
71
  yml[KEY_TABLES] = @table_hash.map do |_, table|
71
72
  columns = table.columns
72
- unless columns.empty?
73
- [
74
- table.name,
75
- columns.map { |column| [column.name, column.scramble_method] }.to_h,
76
- ]
77
- end
73
+ next if columns.empty?
74
+
75
+ [
76
+ table.name,
77
+ columns.to_h { |column| [column.name, column.scramble_method] }
78
+ ]
78
79
  end.compact.to_h
79
80
  YAML.dump(yml, io)
80
81
  end
@@ -92,21 +93,22 @@ module PgdumpScrambler
92
93
  class << self
93
94
  def read(io)
94
95
  yml = YAML.safe_load(io, permitted_classes: [], permitted_symbols: [], aliases: true)
95
- if yml[KEY_TABLES]
96
- tables = yml[KEY_TABLES].map do |table_name, columns|
97
- Table.new(
98
- table_name,
99
- columns.map { |name, scramble_method| Column.new(name, scramble_method) }
100
- )
96
+ tables =
97
+ if yml[KEY_TABLES]
98
+ yml[KEY_TABLES].map do |table_name, columns|
99
+ Table.new(
100
+ table_name,
101
+ columns.map { |name, scramble_method| Column.new(name, scramble_method) }
102
+ )
103
+ end
104
+ else
105
+ []
101
106
  end
102
- else
103
- tables = []
104
- end
105
107
  Config.new(tables, yml[KEY_DUMP_PATH], yml[KEY_S3], yml[KEY_EXCLUDE_TABLES] || [], yml[KEY_PGDUMP_ARGS])
106
108
  end
107
109
 
108
110
  def read_file(path)
109
- open(path, 'r') do |f|
111
+ File.open(path, 'r') do |f|
110
112
  read(f)
111
113
  end
112
114
  end
@@ -118,18 +120,17 @@ module PgdumpScrambler
118
120
  else
119
121
  Rails.application.eager_load!
120
122
  end
121
- klasses_by_table = ActiveRecord::Base.descendants.map { |klass| [klass.table_name, klass] }.to_h
123
+ klasses_by_table = ActiveRecord::Base.descendants.to_h { |klass| [klass.table_name, klass] }
122
124
  table_names = ActiveRecord::Base.connection.tables.sort - IGNORED_ACTIVE_RECORD_TABLES
123
125
  tables = table_names.map do |table_name|
124
126
  klass = klasses_by_table[table_name]
125
- if klass
126
- columns = klass.columns.map(&:name).reject do |name|
127
- IGNORED_ACTIVE_RECORD_COLUMNS.member?(name)
128
- end.map do |name|
129
- Column.new(name)
130
- end
131
- Table.new(table_name, columns)
127
+ next unless klass
128
+
129
+ column_names = klass.columns.map(&:name).reject do |name|
130
+ IGNORED_ACTIVE_RECORD_COLUMNS.member?(name)
132
131
  end
132
+ columns = column_names.map { |name| Column.new(name) }
133
+ Table.new(table_name, columns)
133
134
  end.compact
134
135
  Config.new(tables, 'scrambled.dump.gz', Config::DEFAULT_S3_PROPERTIES, [], nil)
135
136
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'pgdump_scrambler/utils'
2
4
  require 'open3'
5
+
3
6
  module PgdumpScrambler
4
7
  class Dumper
5
8
  def initialize(config, db_config = {})
@@ -9,48 +12,53 @@ module PgdumpScrambler
9
12
  end
10
13
 
11
14
  def run
12
- puts "executing pg_dump..."
15
+ puts 'Executing pg_dump...'
13
16
  puts full_command
14
- if system(full_command)
15
- puts "done!"
16
- else
17
- raise "pg_dump failed!"
18
- end
17
+ raise 'pg_dump failed!' unless system(env_vars, full_command)
18
+
19
+ puts 'Done!'
19
20
  end
20
21
 
21
22
  private
22
23
 
24
+ def env_vars
25
+ vars = {}
26
+ vars['PGPASSWORD'] = @db_config['password'] if @db_config['password']
27
+ vars
28
+ end
29
+
23
30
  def full_command
24
31
  [pgdump_command, obfuscator_command, 'gzip -c'].compact.join(' | ') + "> #{@output_path}"
25
32
  end
26
33
 
27
34
  def obfuscator_command
28
- if options = @config.obfuscator_options
29
- command = File.expand_path('../../../bin/pgdump-obfuscator', __FILE__)
30
- "#{command} #{options}"
31
- end
35
+ return unless (options = @config.obfuscator_options)
36
+
37
+ command = File.expand_path('../../bin/pgdump-obfuscator', __dir__)
38
+ "#{command} #{options}"
32
39
  end
33
40
 
34
41
  def pgdump_command
35
42
  command = []
36
- command << "PGPASSWORD=#{Shellwords.escape(@db_config['password'])}" if @db_config['password']
37
43
  command << 'pg_dump'
38
44
  command << @config.pgdump_args if @config.pgdump_args
39
45
  command << "--username=#{Shellwords.escape(@db_config['username'])}" if @db_config['username']
40
46
  command << "--host='#{@db_config['host']}'" if @db_config['host']
41
47
  command << "--port='#{@db_config['port']}'" if @db_config['port']
42
- command << @config.exclude_tables.map { |exclude_table| "--exclude-table-data=#{exclude_table}" }.join(' ') if @config.exclude_tables.present?
48
+ if @config.exclude_tables.present?
49
+ command << @config.exclude_tables.map do |exclude_table|
50
+ "--exclude-table-data=#{exclude_table}"
51
+ end.join(' ')
52
+ end
43
53
  command << @db_config['database']
44
54
  command.join(' ')
45
55
  end
46
56
 
47
57
  def load_database_yml
48
- if defined?(Rails)
49
- db_config = open(Rails.root.join('config', 'database.yml'), 'r') do |f|
50
- YAML.safe_load(f, permitted_classes: [], permitted_symbols: [], aliases: true)
51
- end
52
- db_config[Rails.env]
53
- end
58
+ return unless defined?(Rails)
59
+
60
+ db_config = Utils.load_yaml_with_erb(Rails.root.join('config', 'database.yml'))
61
+ db_config[Rails.env]
54
62
  end
55
63
  end
56
64
  end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module PgdumpScrambler
3
4
  class Railtie < ::Rails::Railtie
4
5
  rake_tasks do
5
- load File.expand_path('../../tasks/pgdump_scrambler_tasks.rake', __FILE__)
6
+ load File.expand_path('../tasks/pgdump_scrambler_tasks.rake', __dir__)
6
7
  end
7
8
  end
8
9
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'uri'
3
4
  require 'digest'
4
5
  require 'openssl'
5
6
 
6
7
  module PgdumpScrambler
7
8
  class S3Request
8
- def initialize(s3_path:, verb:, region:, bucket:, access_key_id:, secret_key:, time: nil)
9
+ def initialize(s3_path:, verb:, region:, bucket:, access_key_id:, secret_key:, time: nil) # rubocop:disable Metrics/ParameterLists
9
10
  @s3_path = s3_path.start_with?('/') ? s3_path : "/#{s3_path}"
10
11
  @verb = verb
11
12
  @time = time || Time.now.utc
@@ -18,7 +19,7 @@ module PgdumpScrambler
18
19
  def canonical_request
19
20
  [
20
21
  @verb,
21
- URI.encode(@s3_path),
22
+ self.class.uri_encode(@s3_path),
22
23
  canonical_query_string,
23
24
  "host:#{@bucket}.s3.amazonaws.com\n", # canonical headers
24
25
  'host', # signed headers
@@ -38,13 +39,15 @@ module PgdumpScrambler
38
39
  end
39
40
 
40
41
  def url
41
- File.join("https://#{@bucket}.s3.amazonaws.com/", "#{@s3_path}?#{canonical_query_string}&X-Amz-Signature=#{signature}")
42
+ encoded_path = self.class.uri_encode(@s3_path)
43
+ File.join("https://#{@bucket}.s3.amazonaws.com/",
44
+ "#{encoded_path}?#{canonical_query_string}&X-Amz-Signature=#{signature}")
42
45
  end
43
46
 
44
47
  private
45
48
 
46
49
  def iso_time
47
- @time.strftime("%Y%m%dT%H%M%SZ")
50
+ @time.strftime('%Y%m%dT%H%M%SZ')
48
51
  end
49
52
 
50
53
  def iso_date
@@ -52,15 +55,17 @@ module PgdumpScrambler
52
55
  end
53
56
 
54
57
  def hmac_sha256(key, message)
55
- OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, key, message)
58
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), key, message)
56
59
  end
57
60
 
58
61
  def hmac_sha256_hex(key, message)
59
- OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, key, message)
62
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA256'), key, message)
60
63
  end
61
64
 
62
65
  def canonical_query_string
66
+ # rubocop:disable Layout/LineLength
63
67
  "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=#{@access_key_id}%2F#{iso_date}%2F#{@region}%2Fs3%2Faws4_request&X-Amz-Date=#{iso_time}&X-Amz-Expires=86400&X-Amz-SignedHeaders=host"
68
+ # rubocop:enable Layout/LineLength
64
69
  end
65
70
 
66
71
  def string_to_sign
@@ -68,39 +73,29 @@ module PgdumpScrambler
68
73
  'AWS4-HMAC-SHA256',
69
74
  iso_time,
70
75
  "#{iso_date}/#{@region}/s3/aws4_request",
71
- Digest::SHA256.hexdigest(canonical_request),
76
+ Digest::SHA256.hexdigest(canonical_request)
72
77
  ].join("\n")
73
78
  end
74
- end
75
- end
76
-
77
- if $0 == __FILE__
78
- # https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
79
- require "minitest/autorun"
80
- class TestS3Request < Minitest::Test
81
- def setup
82
- @s3_request = PgdumpScrambler::S3Request.new(verb: 'GET', s3_path: '/test.txt', region: 'us-east-1', bucket: 'examplebucket', access_key_id: 'AKIAIOSFODNN7EXAMPLE', secret_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', time: Time.utc(2013, 5, 24, 0, 0, 0))
83
- end
84
-
85
- def test_canonical_request
86
- assert_equal <<~EOS.chomp, @s3_request.canonical_request
87
- GET
88
- /test.txt
89
- X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host
90
- host:examplebucket.s3.amazonaws.com
91
-
92
- host
93
- UNSIGNED-PAYLOAD
94
- EOS
95
- end
96
-
97
- def test_signature
98
- assert_equal 'aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404', @s3_request.signature
99
- end
100
79
 
101
- def test_url
102
- exected_url = 'https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404'
103
- assert_equal exected_url, @s3_request.url
80
+ class << self
81
+ # https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
82
+ # * URI encode every byte except the unreserved characters: 'A'-'Z', 'a'-'z', '0'-'9', '-', '.', '_', and '~'.
83
+ # * The space character is a reserved character and must be encoded as "%20" (and not as "+").
84
+ # * Each URI encoded byte is formed by a '%' and the two-digit hexadecimal value of the byte.
85
+ # * Letters in the hexadecimal value must be uppercase, for example "%1A".
86
+ # * Encode the forward slash character, '/', everywhere except in the object key name.
87
+ # For example, if the object key name is photos/Jan/sample.jpg,
88
+ # the forward slash in the key name is not encoded.
89
+ def uri_encode(str)
90
+ str.gsub(%r{[^A-Za-z0-9\-._~/]}) do
91
+ us = Regexp.last_match(0)
92
+ tmp = +''
93
+ us.each_byte do |uc|
94
+ tmp << sprintf('%%%02X', uc)
95
+ end
96
+ tmp
97
+ end.force_encoding(Encoding::US_ASCII)
98
+ end
104
99
  end
105
100
  end
106
101
  end
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'net/http'
3
4
  require 'uri'
4
- require_relative './s3_request'
5
+ require_relative 's3_request'
5
6
 
6
7
  module PgdumpScrambler
7
8
  class S3UploadError < StandardError
8
9
  attr_reader :response
10
+
9
11
  def initialize(response)
10
12
  @response = response
11
13
  super "S3 upload failed: #{response.body}"
@@ -13,17 +15,25 @@ module PgdumpScrambler
13
15
  end
14
16
 
15
17
  class S3Uploader
16
- def initialize(s3_path:, local_path:, region:, bucket:, access_key_id:, secret_key:)
18
+ def initialize(s3_path:, local_path:, region:, bucket:, access_key_id:, secret_key:) # rubocop:disable Metrics/ParameterLists
17
19
  raise 'missing access_key_id' if access_key_id.nil? || access_key_id.empty?
18
20
  raise 'missing secret_key' if secret_key.nil? || secret_key.empty?
19
- @s3_request = S3Request.new(s3_path: s3_path, verb: 'PUT', region: region, bucket: bucket, access_key_id: access_key_id, secret_key: secret_key)
21
+
22
+ @s3_request = S3Request.new(
23
+ s3_path: s3_path,
24
+ verb: 'PUT',
25
+ region: region,
26
+ bucket: bucket,
27
+ access_key_id: access_key_id,
28
+ secret_key: secret_key
29
+ )
20
30
  @local_path = local_path
21
31
  end
22
32
 
23
33
  def run
24
34
  uri = URI.parse(@s3_request.url)
25
- puts "upload #{@local_path} to #{uri.host}#{uri.path}"
26
- open(@local_path, 'r') do |io|
35
+ puts "Uploading #{@local_path} to #{uri.host}#{uri.path}"
36
+ File.open(@local_path, 'r') do |io|
27
37
  uri_path = uri.path
28
38
  uri_path += "?#{uri.query}" if uri.query
29
39
  req = Net::HTTP::Put.new(uri_path)
@@ -33,10 +43,9 @@ module PgdumpScrambler
33
43
  http = Net::HTTP.new(uri.host, uri.port)
34
44
  http.use_ssl = true
35
45
  res = http.request(req)
36
- if res.code != '200'
37
- raise S3UploadError.new(res)
38
- end
46
+ raise S3UploadError, res if res.code != '200'
39
47
  end
48
+ puts 'Done.'
40
49
  end
41
50
  end
42
51
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'erb'
5
+
6
+ module PgdumpScrambler
7
+ module Utils
8
+ module_function
9
+
10
+ def load_yaml_with_erb(path)
11
+ yaml_content = File.read(path)
12
+ resolved = ERB.new(yaml_content).result
13
+ YAML.safe_load(
14
+ resolved,
15
+ permitted_classes: [],
16
+ permitted_symbols: [],
17
+ aliases: true
18
+ )
19
+ end
20
+ end
21
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module PgdumpScrambler
3
- VERSION = "0.4.0"
4
+ VERSION = '0.5.0'
4
5
  end
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
- require "pgdump_scrambler/version"
3
- require "pgdump_scrambler/config"
4
- require "pgdump_scrambler/dumper"
5
- require "pgdump_scrambler/s3_uploader"
6
- if defined?(Rails)
7
- require 'pgdump_scrambler/railtie'
8
- end
2
+
3
+ require 'pgdump_scrambler/version'
4
+ require 'pgdump_scrambler/config'
5
+ require 'pgdump_scrambler/dumper'
6
+ require 'pgdump_scrambler/s3_uploader'
7
+ require 'pgdump_scrambler/railtie' if defined?(Rails)
9
8
 
10
9
  module PgdumpScrambler
11
10
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  namespace :pgdump_scrambler do
3
4
  default_config_path = ENV['SCRAMBLER_CONFIG_PATH'] || 'config/pgdump_scrambler.yml'
4
5
 
@@ -6,15 +7,16 @@ namespace :pgdump_scrambler do
6
7
  task config_from_db: :environment do
7
8
  config =
8
9
  if File.exist?(default_config_path)
9
- puts "#{default_config_path} found!\nmerge existing config with config from database"
10
+ puts "#{default_config_path} found!\nMerging existing config with config from database"
10
11
  PgdumpScrambler::Config
11
12
  .read_file(default_config_path)
12
13
  .update_with(PgdumpScrambler::Config.from_db)
13
14
  else
14
- puts "craete config from database"
15
+ puts 'Creating a config file from database'
15
16
  PgdumpScrambler::Config.from_db
16
17
  end
17
18
  config.write_file(default_config_path)
19
+ puts "Wrote to #{default_config_path}"
18
20
  end
19
21
 
20
22
  desc 'check if new columns exist'
@@ -24,7 +26,7 @@ namespace :pgdump_scrambler do
24
26
  .update_with(PgdumpScrambler::Config.from_db)
25
27
  unspecified_columns = config.unspecified_columns
26
28
  count = unspecified_columns.sum { |_, columns| columns.size }
27
- if count > 0
29
+ if count.positive?
28
30
  unspecified_columns.each_key do |table_name|
29
31
  puts "#{table_name}:"
30
32
  unspecified_columns[table_name].each do |column_name|
@@ -34,7 +36,7 @@ namespace :pgdump_scrambler do
34
36
  puts "#{count} unspecified columns found!"
35
37
  exit 1
36
38
  else
37
- puts "No unspecified columns found."
39
+ puts 'No unspecified columns found.'
38
40
  end
39
41
  end
40
42
 
@@ -47,7 +49,7 @@ namespace :pgdump_scrambler do
47
49
  desc 'create scrambled dump'
48
50
  task clear_dump: :environment do
49
51
  config = PgdumpScrambler::Config.read_file(default_config_path)
50
- if File.exists? config.dump_path
52
+ if File.exist? config.dump_path
51
53
  File.delete(config.dump_path)
52
54
  puts "Dump file #{config.dump_path} has been deleted."
53
55
  end
@@ -57,7 +59,7 @@ namespace :pgdump_scrambler do
57
59
  task s3_upload: :environment do
58
60
  config = PgdumpScrambler::Config.read_file(default_config_path)
59
61
  uploader = PgdumpScrambler::S3Uploader.new(
60
- s3_path: File.join(config.resolved_s3['prefix'], File::basename(config.dump_path)),
62
+ s3_path: File.join(config.resolved_s3['prefix'], File.basename(config.dump_path)),
61
63
  local_path: config.dump_path,
62
64
  region: config.resolved_s3['region'],
63
65
  bucket: config.resolved_s3['bucket'],
@@ -1,37 +1,30 @@
1
+ # frozen_string_literal: true
1
2
 
2
- lib = File.expand_path("../lib", __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "pgdump_scrambler/version"
5
+ require 'pgdump_scrambler/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "pgdump_scrambler"
8
+ spec.name = 'pgdump_scrambler'
8
9
  spec.version = PgdumpScrambler::VERSION
9
- spec.authors = ["Shunichi Ikegami"]
10
- spec.email = ["sike.tm@gmail.com"]
10
+ spec.authors = ['Shunichi Ikegami']
11
+ spec.email = ['sike.tm@gmail.com']
11
12
 
12
- spec.summary = %q{scramble pg_dump columns}
13
- spec.description = %q{scramble pg_dump columns.}
13
+ spec.summary = 'Scramble pg_dump columns'
14
+ spec.description = 'Scramble pg_dump columns.'
14
15
  spec.homepage = 'https://github.com/shunichi/pgdump_scrambler'
15
- spec.license = "MIT"
16
+ spec.license = 'MIT'
17
+ spec.required_ruby_version = '>= 3.0'
16
18
 
17
- # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
- # to allow pushing to a single host or delete this section to allow pushing to any host.
19
- if spec.respond_to?(:metadata)
20
- spec.metadata["allowed_push_host"] = 'https://rubygems.org'
21
- else
22
- raise "RubyGems 2.0 or newer is required to protect against " \
23
- "public gem pushes."
24
- end
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/shunichi/pgdump_scrambler'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/shunichi/pgdump_scrambler/blob/main/CHANGELOG.md'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
23
 
26
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
24
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
25
  f.match(%r{^(test|spec|features)/})
28
26
  end
29
- spec.bindir = "exe"
27
+ spec.bindir = 'exe'
30
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
- spec.require_paths = ["lib"]
32
-
33
- spec.add_development_dependency "rake", "~> 13.0"
34
- spec.add_development_dependency "rspec", "~> 3.12"
35
- spec.add_development_dependency "rails", "~> 7.0"
36
- spec.add_development_dependency "rubocop"
29
+ spec.require_paths = ['lib']
37
30
  end