duty_free 1.0.7 → 1.0.9
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 +4 -4
- data/lib/duty_free/column.rb +2 -2
- data/lib/duty_free/extensions.rb +603 -446
- data/lib/duty_free/suggest_template.rb +381 -170
- data/lib/duty_free/util.rb +129 -11
- data/lib/duty_free/version_number.rb +1 -1
- data/lib/duty_free.rb +227 -52
- data/lib/generators/duty_free/model_generator.rb +349 -0
- data/lib/generators/duty_free/templates/create_versions.rb.erb +2 -2
- metadata +10 -29
@@ -0,0 +1,349 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/active_record'
|
5
|
+
require 'fancy_gets'
|
6
|
+
|
7
|
+
module DutyFree
|
8
|
+
# Auto-generates an IMPORT_TEMPLATE entry for a model
|
9
|
+
class ModelGenerator < ::Rails::Generators::Base
|
10
|
+
include FancyGets
|
11
|
+
# include ::Rails::Generators::Migration
|
12
|
+
|
13
|
+
# # source_root File.expand_path('templates', __dir__)
|
14
|
+
# class_option(
|
15
|
+
# :with_changes,
|
16
|
+
# type: :boolean,
|
17
|
+
# default: false,
|
18
|
+
# desc: 'Add IMPORT_TEMPLATE to model'
|
19
|
+
# )
|
20
|
+
|
21
|
+
desc 'Adds an appropriate IMPORT_TEMPLATE entry into a model of your choosing so that' \
|
22
|
+
' DutyFree can perform exports, and with the Pro version, the same template also' \
|
23
|
+
' does imports.'
|
24
|
+
|
25
|
+
def df_model_template
|
26
|
+
# %%% If Apartment is active, ask which schema they want
|
27
|
+
|
28
|
+
# Load all models
|
29
|
+
Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
|
30
|
+
|
31
|
+
# Generate a list of viable models that can be chosen
|
32
|
+
longest_length = 0
|
33
|
+
model_info = Hash.new { |h, k| h[k] = {} }
|
34
|
+
tableless = Hash.new { |h, k| h[k] = [] }
|
35
|
+
models = ActiveRecord::Base.descendants.reject do |m|
|
36
|
+
trouble = if m.abstract_class?
|
37
|
+
true
|
38
|
+
elsif !m.table_exists?
|
39
|
+
tableless[m.table_name] << m.name
|
40
|
+
' (No Table)'
|
41
|
+
else
|
42
|
+
this_f_keys = (model_info[m][:f_keys] = m.reflect_on_all_associations.select { |a| a.macro == :belongs_to }) || []
|
43
|
+
column_names = (model_info[m][:column_names] = m.columns.map(&:name) - [m.primary_key, 'created_at', 'updated_at', 'deleted_at'] - this_f_keys.map(&:foreign_key))
|
44
|
+
if column_names.empty? && this_f_keys && !this_f_keys.empty?
|
45
|
+
fk_message = ", although #{this_f_keys.length} foreign keys"
|
46
|
+
" (No columns#{fk_message})"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
# puts "#{m.name}#{trouble}" if trouble&.is_a?(String)
|
50
|
+
trouble
|
51
|
+
end
|
52
|
+
models.sort! do |a, b| # Sort first to separate namespaced stuff from the rest, then alphabetically
|
53
|
+
is_a_namespaced = a.name.include?('::')
|
54
|
+
is_b_namespaced = b.name.include?('::')
|
55
|
+
if is_a_namespaced && !is_b_namespaced
|
56
|
+
1
|
57
|
+
elsif !is_a_namespaced && is_b_namespaced
|
58
|
+
-1
|
59
|
+
else
|
60
|
+
a.name <=> b.name
|
61
|
+
end
|
62
|
+
end
|
63
|
+
models.each do |m| # Find longest name in the list for future use to show lists on the right side of the screen
|
64
|
+
# Strangely this can't be inlined since it assigns to "len"
|
65
|
+
if longest_length < (len = m.name.length)
|
66
|
+
longest_length = len
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
model_name = ARGV[0]&.camelize
|
71
|
+
unless (starting = models.find { |m| m.name == model_name })
|
72
|
+
puts "#{"Couldn't find #{model_name}. " if model_name}Pick a model to start from:"
|
73
|
+
starting = gets_list(
|
74
|
+
list: models,
|
75
|
+
on_select: proc do |item|
|
76
|
+
selected = item[:selected] || item[:focused]
|
77
|
+
this_model_info = model_info[selected]
|
78
|
+
selected.name + " (#{(this_model_info[:column_names] + this_model_info[:f_keys].map(&:name).map(&:upcase)).join(', ')})"
|
79
|
+
end
|
80
|
+
)
|
81
|
+
end
|
82
|
+
puts "\nThinking..."
|
83
|
+
|
84
|
+
# %%% Find out how many hops at most we can go from this model
|
85
|
+
max_hm_nav = starting.suggest_template(-1, true, false)
|
86
|
+
max_bt_nav = starting.suggest_template(-1, false, false)
|
87
|
+
hops_with_hm, num_hm_hops_tables = calc_num_hops([[starting, max_hm_nav[:all]]], models)
|
88
|
+
hops, num_hops_tables = calc_num_hops([[starting, max_bt_nav[:all]]], models)
|
89
|
+
# print "\b" * 11
|
90
|
+
unless hops_with_hm.length == hops.length
|
91
|
+
starting_name = starting.name
|
92
|
+
unless (is_hm = ARGV[1]&.downcase)
|
93
|
+
puts "Navigate from #{starting_name} using:\n#{'=' * (21 + starting_name.length)}"
|
94
|
+
is_hm = gets_list(
|
95
|
+
["Only belongs_to (max of #{hops.length} hops and #{num_hops_tables} tables)",
|
96
|
+
"has_many as well as belongs_to (max of #{hops_with_hm.length} hops and #{num_hm_hops_tables} tables)"]
|
97
|
+
)
|
98
|
+
end
|
99
|
+
is_hm = is_hm.start_with?('has_many') || is_hm[0] == 'y'
|
100
|
+
hops = hops_with_hm if is_hm
|
101
|
+
end
|
102
|
+
|
103
|
+
unless (num_hops = ARGV[2]&.to_i)
|
104
|
+
puts "\nNow, how many hops total would you like to navigate?"
|
105
|
+
index = 0
|
106
|
+
cumulative = 0
|
107
|
+
hops_list = ['0'] + hops.map { |h| "#{index += 1} (#{cumulative += h.length} linkages)" }
|
108
|
+
num_hops = gets_list(
|
109
|
+
list: hops_list,
|
110
|
+
on_select: proc do |value|
|
111
|
+
associations = Hash.new { |h, k| h[k] = 0 }
|
112
|
+
index = (value[:selected] || value[:focused]).split(' ').first.to_i - 1
|
113
|
+
layer = hops[index] if index >= 0
|
114
|
+
layer ||= []
|
115
|
+
layer.each { |i| associations[i.last] += 1 }
|
116
|
+
associations.each { |k, v| associations.delete(k) if v == 1 }
|
117
|
+
layer.map do |l|
|
118
|
+
associations.keys.include?(l.last) ? "#{l.first.name.demodulize} #{l.last}" : l.last
|
119
|
+
end.join(', ')
|
120
|
+
# y = model_info[data[:focused].name]
|
121
|
+
# data[:focused].name + " (#{(y[:column_names] + y[:f_keys].map(&:name).map(&:upcase)).join(', ')})"
|
122
|
+
# layer.inspect
|
123
|
+
end
|
124
|
+
).split(' ').first.to_i
|
125
|
+
end
|
126
|
+
|
127
|
+
print "Navigating from #{starting_name}" if model_name
|
128
|
+
puts "\nOkay, #{num_hops} hops#{', including has_many,' if is_hm} it is!"
|
129
|
+
# Grab the console output from this:
|
130
|
+
original_stdout = $stdout
|
131
|
+
$stdout = StringIO.new
|
132
|
+
starting.suggest_template(num_hops, is_hm)
|
133
|
+
output = $stdout
|
134
|
+
$stdout = original_stdout
|
135
|
+
filename = nil
|
136
|
+
output.rewind
|
137
|
+
lines = output.each_line.each_with_object([]) do |line, s|
|
138
|
+
if line == "\n"
|
139
|
+
# Do nothing
|
140
|
+
elsif filename
|
141
|
+
s << line
|
142
|
+
elsif line.start_with?('# Place the following into ')
|
143
|
+
filename = line[27..-1]&.split(':')&.first
|
144
|
+
end
|
145
|
+
s
|
146
|
+
end
|
147
|
+
|
148
|
+
model_file = File.open(filename, 'r+')
|
149
|
+
insert_at = nil
|
150
|
+
starting_name = starting.name.demodulize
|
151
|
+
loop do
|
152
|
+
break if model_file.eof?
|
153
|
+
|
154
|
+
line_parts = model_file.readline.strip.gsub(/ +/, ' ').split(' ')
|
155
|
+
if line_parts.first == 'class' && line_parts[1] && (line_parts[1] == starting_name || line_parts[1].end_with?("::#{starting_name}"))
|
156
|
+
insert_at = model_file.pos
|
157
|
+
break
|
158
|
+
end
|
159
|
+
end
|
160
|
+
line = nil
|
161
|
+
import_template_blocks = []
|
162
|
+
import_template_block = nil
|
163
|
+
indentation = nil
|
164
|
+
# See if there's already any IMPORT_TEMPLATE entries in the model file.
|
165
|
+
# If there already is just one, we will comment it out if needs be before adding a fresh one.
|
166
|
+
loop do
|
167
|
+
break if model_file.eof?
|
168
|
+
|
169
|
+
line_parts = (line = model_file.readline).strip.split(/[\s=]+/)
|
170
|
+
indentation ||= line[0...(/\S/ =~ line)]
|
171
|
+
case line_parts[-2..-1]
|
172
|
+
when ['IMPORT_TEMPLATE', '{']
|
173
|
+
import_template_blocks << import_template_block if import_template_block
|
174
|
+
import_template_block = [model_file.pos - line.length, nil, line.strip[0] == '#', []]
|
175
|
+
when ['#', '------------------------------------------']
|
176
|
+
import_template_block[1] = model_file.pos if import_template_block # && import_template_block[1].nil?
|
177
|
+
end
|
178
|
+
next unless import_template_block
|
179
|
+
|
180
|
+
# Collect all the lines of any existing block
|
181
|
+
import_template_block[3] << line
|
182
|
+
# Cap this one if it's done
|
183
|
+
if import_template_block[1]
|
184
|
+
import_template_blocks << import_template_block
|
185
|
+
import_template_block = nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
import_template_blocks << import_template_block if import_template_block
|
189
|
+
comments = nil
|
190
|
+
is_add_cr = nil
|
191
|
+
if import_template_blocks.length > 1
|
192
|
+
# %%% maybe in the future: remove any older commented ones
|
193
|
+
puts 'Found multiple existing import template blocks. Will not attempt to automatically add yet another.'
|
194
|
+
insert_at = nil
|
195
|
+
elsif import_template_blocks.length == 1
|
196
|
+
# Get set up to add the new block after the existing one
|
197
|
+
insert_at = (import_template_block = import_template_blocks.first)[1]
|
198
|
+
if insert_at.nil?
|
199
|
+
puts "Found what looked like the start of an existing IMPORT_TEMPLATE block, but couldn't determine where it ends. Will not attempt to automatically add anything."
|
200
|
+
elsif import_template_block[2] # Already commented
|
201
|
+
is_add_cr = true
|
202
|
+
else # Needs to be commented
|
203
|
+
# Find what kind and how much indentation is present from the first commented line
|
204
|
+
indentation = import_template_block[3].first[0...(/\S/ =~ import_template_block[3].first)]
|
205
|
+
comments = import_template_block[3].map { |l| "#{l[0...indentation.length]}# #{l[indentation.length..-1]}" }
|
206
|
+
end
|
207
|
+
# else # Must be no IMPORT_TEMPLATE block yet
|
208
|
+
# insert_at = model_file.pos
|
209
|
+
end
|
210
|
+
if insert_at.nil?
|
211
|
+
puts "Please edit #{filename} manually and add this code:\n\n#{lines.join}"
|
212
|
+
else
|
213
|
+
is_good = ARGV[3]&.downcase&.start_with?('y')
|
214
|
+
args = [starting_name,
|
215
|
+
is_hm ? 'has_many' : 'no',
|
216
|
+
num_hops]
|
217
|
+
args << 'yes' if is_good
|
218
|
+
args = args.each_with_object(+'') do |v, s|
|
219
|
+
s << " #{v}"
|
220
|
+
s
|
221
|
+
end
|
222
|
+
lines.unshift("# Added #{DateTime.now.strftime('%b %d, %Y %I:%M%P')} by running `bin/rails g duty_free:model#{args}`\n")
|
223
|
+
# add a new one afterwards
|
224
|
+
print is_good ? 'Will' : 'OK to'
|
225
|
+
print "#{" comment #{comments.length} existing lines and" if comments} add #{lines.length} new lines to #{filename}"
|
226
|
+
puts is_good ? '.' : '?'
|
227
|
+
if is_good || gets_list(%w[Yes No]) == 'Yes'
|
228
|
+
# Store rest of file
|
229
|
+
model_file.pos = insert_at
|
230
|
+
rest_of_file = model_file.read
|
231
|
+
if comments
|
232
|
+
model_file.pos = import_template_block[0]
|
233
|
+
model_file.write("#{comments.join}\n")
|
234
|
+
puts "Commented #{comments.length} existing lines"
|
235
|
+
else
|
236
|
+
model_file.pos = insert_at
|
237
|
+
end
|
238
|
+
model_file.write("\n") if is_add_cr
|
239
|
+
model_file.write(lines.map { |l| "#{indentation}#{l}" }.join)
|
240
|
+
model_file.write(rest_of_file)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
model_file.close
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
# def calc_num_hops(all, num = 0)
|
249
|
+
# max_num = num
|
250
|
+
# all.each do |item|
|
251
|
+
# if item.is_a?(Hash)
|
252
|
+
# item.each do |k, v|
|
253
|
+
# # puts "#{k} - #{num}"
|
254
|
+
# this_num = calc_num_hops(item[k], num + 1)
|
255
|
+
# max_num = this_num if this_num > max_num
|
256
|
+
# end
|
257
|
+
# end
|
258
|
+
# end
|
259
|
+
# max_num
|
260
|
+
# end
|
261
|
+
|
262
|
+
# Breadth first approach
|
263
|
+
def calc_num_hops(this_layer, models = nil)
|
264
|
+
seen_it = {}
|
265
|
+
layers = []
|
266
|
+
loop do
|
267
|
+
this_keys = []
|
268
|
+
next_layer = []
|
269
|
+
this_layer.each do |grouping|
|
270
|
+
klass = grouping.first
|
271
|
+
# binding.pry #unless klass.is_a?(Class)
|
272
|
+
grouping.last.each do |item|
|
273
|
+
next unless item.is_a?(Hash) && !seen_it.include?([klass, (k, v = item.first).first])
|
274
|
+
|
275
|
+
seen_it[[klass, k]] = nil
|
276
|
+
this_keys << [klass, k]
|
277
|
+
this_klass = klass.reflect_on_association(k)&.klass
|
278
|
+
if this_klass.nil? # Perhaps it's polymorphic
|
279
|
+
polymorphics = klass.reflect_on_all_associations.each_with_object([]) do |r, s|
|
280
|
+
prefix = "#{r.name}_"
|
281
|
+
if r.polymorphic? && k.to_s.start_with?(prefix)
|
282
|
+
suffix = k.to_s[prefix.length..-1]
|
283
|
+
possible_klass = models.find { |m| m.name.underscore == suffix }
|
284
|
+
s << [suffix, possible_klass] if possible_klass
|
285
|
+
end
|
286
|
+
s
|
287
|
+
end
|
288
|
+
# binding.pry if polymorphics.length != 1
|
289
|
+
this_klass = polymorphics.first&.last
|
290
|
+
end
|
291
|
+
next_layer << [this_klass, v.select { |ip| ip.is_a?(Hash) }] if this_klass
|
292
|
+
end
|
293
|
+
end
|
294
|
+
layers << this_keys unless this_keys.empty?
|
295
|
+
break if next_layer.empty?
|
296
|
+
|
297
|
+
this_layer = next_layer
|
298
|
+
end
|
299
|
+
# puts "#{k} - #{num}"
|
300
|
+
[layers, seen_it.keys.map(&:first).uniq.length]
|
301
|
+
end
|
302
|
+
|
303
|
+
# # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes.
|
304
|
+
# def item_type_options
|
305
|
+
# opt = { null: false }
|
306
|
+
# opt[:limit] = 191 if mysql?
|
307
|
+
# ", #{opt}"
|
308
|
+
# end
|
309
|
+
|
310
|
+
# def migration_version
|
311
|
+
# return unless (major = ActiveRecord::VERSION::MAJOR) >= 5
|
312
|
+
|
313
|
+
# "[#{major}.#{ActiveRecord::VERSION::MINOR}]"
|
314
|
+
# end
|
315
|
+
|
316
|
+
# # Class names of MySQL adapters.
|
317
|
+
# # - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`.
|
318
|
+
# # - `Mysql2Adapter` - Used by `mysql2` gem.
|
319
|
+
# def mysql?
|
320
|
+
# [
|
321
|
+
# 'ActiveRecord::ConnectionAdapters::MysqlAdapter',
|
322
|
+
# 'ActiveRecord::ConnectionAdapters::Mysql2Adapter'
|
323
|
+
# ].freeze.include?(ActiveRecord::Base.connection.class.name)
|
324
|
+
# end
|
325
|
+
|
326
|
+
# # Even modern versions of MySQL still use `latin1` as the default character
|
327
|
+
# # encoding. Many users are not aware of this, and run into trouble when they
|
328
|
+
# # try to use DutyFree in apps that otherwise tend to use UTF-8. Postgres, by
|
329
|
+
# # comparison, uses UTF-8 except in the unusual case where the OS is configured
|
330
|
+
# # with a custom locale.
|
331
|
+
# #
|
332
|
+
# # - https://dev.mysql.com/doc/refman/5.7/en/charset-applications.html
|
333
|
+
# # - http://www.postgresql.org/docs/9.4/static/multibyte.html
|
334
|
+
# #
|
335
|
+
# # Furthermore, MySQL's original implementation of UTF-8 was flawed, and had
|
336
|
+
# # to be fixed later by introducing a new charset, `utf8mb4`.
|
337
|
+
# #
|
338
|
+
# # - https://mathiasbynens.be/notes/mysql-utf8mb4
|
339
|
+
# # - https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
|
340
|
+
# #
|
341
|
+
# def versions_table_options
|
342
|
+
# if mysql?
|
343
|
+
# ', { options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" }'
|
344
|
+
# else
|
345
|
+
# ''
|
346
|
+
# end
|
347
|
+
# end
|
348
|
+
end
|
349
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
|
-
# This migration creates the `versions` table, the only schema
|
2
|
-
# All other migrations
|
1
|
+
# This migration creates the `versions` table, the only schema DF requires.
|
2
|
+
# All other migrations DF provides are optional.
|
3
3
|
class CreateVersions < ActiveRecord::Migration<%= migration_version %>
|
4
4
|
|
5
5
|
# The largest text column available in all supported RDBMS is
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: duty_free
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lorin Thwaits
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -17,9 +17,6 @@ dependencies:
|
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '3.0'
|
20
|
-
- - "<"
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: '6.1'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -27,9 +24,6 @@ dependencies:
|
|
27
24
|
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '3.0'
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '6.1'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: appraisal
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -134,14 +128,14 @@ dependencies:
|
|
134
128
|
requirements:
|
135
129
|
- - "~>"
|
136
130
|
- !ruby/object:Gem::Version
|
137
|
-
version: 0.
|
131
|
+
version: '0.93'
|
138
132
|
type: :development
|
139
133
|
prerelease: false
|
140
134
|
version_requirements: !ruby/object:Gem::Requirement
|
141
135
|
requirements:
|
142
136
|
- - "~>"
|
143
137
|
- !ruby/object:Gem::Version
|
144
|
-
version: 0.
|
138
|
+
version: '0.93'
|
145
139
|
- !ruby/object:Gem::Dependency
|
146
140
|
name: rubocop-rspec
|
147
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -156,20 +150,6 @@ dependencies:
|
|
156
150
|
- - "~>"
|
157
151
|
- !ruby/object:Gem::Version
|
158
152
|
version: 1.42.0
|
159
|
-
- !ruby/object:Gem::Dependency
|
160
|
-
name: mysql2
|
161
|
-
requirement: !ruby/object:Gem::Requirement
|
162
|
-
requirements:
|
163
|
-
- - "~>"
|
164
|
-
- !ruby/object:Gem::Version
|
165
|
-
version: '0.5'
|
166
|
-
type: :development
|
167
|
-
prerelease: false
|
168
|
-
version_requirements: !ruby/object:Gem::Requirement
|
169
|
-
requirements:
|
170
|
-
- - "~>"
|
171
|
-
- !ruby/object:Gem::Version
|
172
|
-
version: '0.5'
|
173
153
|
- !ruby/object:Gem::Dependency
|
174
154
|
name: pg
|
175
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -206,7 +186,7 @@ dependencies:
|
|
206
186
|
version: '1.4'
|
207
187
|
description: 'Simplify data imports and exports with this slick ActiveRecord extension
|
208
188
|
|
209
|
-
'
|
189
|
+
'
|
210
190
|
email: lorint@gmail.com
|
211
191
|
executables: []
|
212
192
|
extensions: []
|
@@ -228,13 +208,14 @@ files:
|
|
228
208
|
- lib/duty_free/version_number.rb
|
229
209
|
- lib/generators/duty_free/USAGE
|
230
210
|
- lib/generators/duty_free/install_generator.rb
|
211
|
+
- lib/generators/duty_free/model_generator.rb
|
231
212
|
- lib/generators/duty_free/templates/add_object_changes_to_versions.rb.erb
|
232
213
|
- lib/generators/duty_free/templates/create_versions.rb.erb
|
233
214
|
homepage: https://github.com/lorint/duty_free
|
234
215
|
licenses:
|
235
216
|
- MIT
|
236
217
|
metadata: {}
|
237
|
-
post_install_message:
|
218
|
+
post_install_message:
|
238
219
|
rdoc_options: []
|
239
220
|
require_paths:
|
240
221
|
- lib
|
@@ -249,8 +230,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
249
230
|
- !ruby/object:Gem::Version
|
250
231
|
version: 1.3.6
|
251
232
|
requirements: []
|
252
|
-
rubygems_version: 3.
|
253
|
-
signing_key:
|
233
|
+
rubygems_version: 3.1.6
|
234
|
+
signing_key:
|
254
235
|
specification_version: 4
|
255
236
|
summary: Import and Export Data
|
256
237
|
test_files: []
|