duty_free 1.0.7 → 1.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 PT requires.
2
- # All other migrations PT provides are optional.
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.7
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: 2020-11-24 00:00:00.000000000 Z
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.89.1
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.89.1
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.0.8
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: []