duty_free 1.0.8 → 1.0.10
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/extensions.rb +39 -30
- data/lib/duty_free/suggest_template.rb +378 -166
- data/lib/duty_free/util.rb +26 -12
- data/lib/duty_free/version_number.rb +1 -1
- data/lib/duty_free.rb +32 -7
- data/lib/generators/duty_free/model_generator.rb +349 -0
- data/lib/generators/duty_free/templates/create_versions.rb.erb +2 -2
- metadata +4 -9
data/lib/duty_free/util.rb
CHANGED
@@ -97,7 +97,7 @@ module DutyFree
|
|
97
97
|
|
98
98
|
# ===================================
|
99
99
|
# Epic require patch
|
100
|
-
def self._patch_require(module_filename, folder_matcher,
|
100
|
+
def self._patch_require(module_filename, folder_matcher, replacements, autoload_symbol = nil, is_bundler = false)
|
101
101
|
mod_name_parts = module_filename.split('.')
|
102
102
|
extension = case mod_name_parts.last
|
103
103
|
when 'rb', 'so', 'o'
|
@@ -120,10 +120,17 @@ module DutyFree
|
|
120
120
|
Dir.mkdir(new_part) unless Dir.exist?(new_part)
|
121
121
|
new_part
|
122
122
|
end
|
123
|
-
if ::DutyFree::Util._write_patched(folder_matcher, module_filename, extension, custom_require_dir, nil,
|
123
|
+
if ::DutyFree::Util._write_patched(folder_matcher, module_filename, extension, custom_require_dir, nil, replacements) &&
|
124
124
|
!alp.include?(custom_require_dir)
|
125
125
|
alp.unshift(custom_require_dir)
|
126
126
|
end
|
127
|
+
elsif is_bundler
|
128
|
+
puts "Bundler hack"
|
129
|
+
require 'pry-byebug'
|
130
|
+
binding.pry
|
131
|
+
x = 5
|
132
|
+
# bin_path
|
133
|
+
# puts Bundler.require.inspect
|
127
134
|
else
|
128
135
|
unless (require_overrides = ::DutyFree::Util.instance_variable_get(:@_require_overrides))
|
129
136
|
::DutyFree::Util.instance_variable_set(:@_require_overrides, (require_overrides = {}))
|
@@ -134,19 +141,19 @@ module DutyFree
|
|
134
141
|
# then required in place of the original.
|
135
142
|
|
136
143
|
Kernel.module_exec do
|
137
|
-
# class << self
|
138
144
|
alias_method :orig_require, :require
|
139
|
-
# end
|
140
145
|
# To be most faithful to Ruby's normal behaviour, this should look like a public singleton
|
141
146
|
define_method(:require) do |name|
|
147
|
+
# %%% Can get a message such as "ActionDispatch::Routing is not missing constant RouteSet! (NameError)"
|
148
|
+
# binding.pry if name.start_with?('action_dispatch/routing/route_') # || name == 'active_support/values/time_zone'
|
142
149
|
if (require_override = ::DutyFree::Util.instance_variable_get(:@_require_overrides)[name])
|
143
|
-
extension, folder_matcher,
|
150
|
+
extension, folder_matcher, replacements, autoload_symbol = require_override
|
144
151
|
patched_filename = "/patched_#{name.tr('/', '_')}#{extension}"
|
145
152
|
if $LOADED_FEATURES.find { |f| f.end_with?(patched_filename) }
|
146
153
|
false
|
147
154
|
else
|
148
155
|
is_replaced = false
|
149
|
-
if (replacement_path = ::DutyFree::Util._write_patched(folder_matcher, name, extension, ::DutyFree::Util._custom_require_dir, patched_filename,
|
156
|
+
if (replacement_path = ::DutyFree::Util._write_patched(folder_matcher, name, extension, ::DutyFree::Util._custom_require_dir, patched_filename, replacements))
|
150
157
|
is_replaced = Kernel.send(:orig_require, replacement_path)
|
151
158
|
elsif replacement_path.nil?
|
152
159
|
puts "Couldn't find #{name} to require it!"
|
@@ -159,12 +166,13 @@ module DutyFree
|
|
159
166
|
end
|
160
167
|
end
|
161
168
|
end
|
162
|
-
require_overrides[module_filename] = [extension, folder_matcher,
|
169
|
+
require_overrides[module_filename] = [extension, folder_matcher, replacements, autoload_symbol]
|
163
170
|
end
|
164
171
|
end
|
165
172
|
|
166
173
|
def self._custom_require_dir
|
167
174
|
unless (custom_require_dir = ::DutyFree::Util.instance_variable_get(:@_custom_require_dir))
|
175
|
+
require 'tmpdir'
|
168
176
|
::DutyFree::Util.instance_variable_set(:@_custom_require_dir, (custom_require_dir = Dir.mktmpdir))
|
169
177
|
# So normal Ruby require will now pick this one up
|
170
178
|
$LOAD_PATH.unshift(custom_require_dir)
|
@@ -178,11 +186,12 @@ module DutyFree
|
|
178
186
|
|
179
187
|
# Returns the full path to the replaced filename, or
|
180
188
|
# false if the file already exists, and nil if it was unable to write anything.
|
181
|
-
def self._write_patched(folder_matcher, name, extension, dir, patched_filename,
|
189
|
+
def self._write_patched(folder_matcher, name, extension, dir, patched_filename, replacements)
|
182
190
|
# See if our replacement file might already exist for some reason
|
183
191
|
name = +"/#{name}" unless name.start_with?('/')
|
184
192
|
name << extension unless name.end_with?(extension)
|
185
|
-
|
193
|
+
puts (replacement_path = "#{dir}#{patched_filename || name}")
|
194
|
+
return false if File.exist?(replacement_path)
|
186
195
|
|
187
196
|
# Dredge up the original .rb file, doctor it, and then require it instead
|
188
197
|
num_written = nil
|
@@ -193,9 +202,14 @@ module DutyFree
|
|
193
202
|
orig_path = "#{path}#{name}"
|
194
203
|
break if path.include?(folder_matcher) && (orig_as = File.open(orig_path))
|
195
204
|
end
|
196
|
-
|
197
|
-
|
198
|
-
|
205
|
+
puts [folder_matcher, name].inspect
|
206
|
+
if (updated_text = orig_as&.read)
|
207
|
+
File.open(replacement_path, 'w') do |replaced_file|
|
208
|
+
replacements = [replacements] unless replacements.first.is_a?(Array)
|
209
|
+
replacements.each do |search_text, replacement_text|
|
210
|
+
updated_text.gsub!(search_text, replacement_text)
|
211
|
+
end
|
212
|
+
num_written = replaced_file.write(updated_text)
|
199
213
|
end
|
200
214
|
orig_as.close
|
201
215
|
end
|
data/lib/duty_free.rb
CHANGED
@@ -50,15 +50,15 @@ if ActiveRecord.version < ::Gem::Version.new('3.2') &&
|
|
50
50
|
# Remove circular reference for "now"
|
51
51
|
::DutyFree::Util._patch_require(
|
52
52
|
'active_support/values/time_zone.rb', '/activesupport',
|
53
|
-
' def parse(str, now=now)',
|
54
|
-
' def parse(str, now=now())'
|
53
|
+
[' def parse(str, now=now)',
|
54
|
+
' def parse(str, now=now())']
|
55
55
|
)
|
56
56
|
# Remove circular reference for "reflection" for ActiveRecord 3.1
|
57
57
|
if ActiveRecord.version >= ::Gem::Version.new('3.1')
|
58
58
|
::DutyFree::Util._patch_require(
|
59
59
|
'active_record/associations/has_many_association.rb', '/activerecord',
|
60
|
-
'reflection = reflection)',
|
61
|
-
'reflection = reflection())',
|
60
|
+
['reflection = reflection)',
|
61
|
+
'reflection = reflection())'],
|
62
62
|
:HasManyAssociation # Make sure the path for this guy is available to be autoloaded
|
63
63
|
)
|
64
64
|
end
|
@@ -129,9 +129,21 @@ end
|
|
129
129
|
# Major compatibility fixes for ActiveRecord < 4.2
|
130
130
|
# ================================================
|
131
131
|
ActiveSupport.on_load(:active_record) do
|
132
|
-
#
|
133
|
-
|
134
|
-
|
132
|
+
# rubocop:disable Lint/ConstantDefinitionInBlock
|
133
|
+
module ActiveRecord
|
134
|
+
class Base
|
135
|
+
unless respond_to?(:execute_sql)
|
136
|
+
class << self
|
137
|
+
def execute_sql(sql, *param_array)
|
138
|
+
param_array = param_array.first if param_array.length == 1 && param_array.first.is_a?(Array)
|
139
|
+
connection.execute(send(:sanitize_sql_array, [sql] + param_array))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Rails < 4.0 cannot do #find_by, #find_or_create_by, or do #pluck on multiple columns, so here are the patches:
|
146
|
+
if version < ::Gem::Version.new('4.0')
|
135
147
|
# Normally find_by is in FinderMethods, which older AR doesn't have
|
136
148
|
module Calculations
|
137
149
|
def find_by(*args)
|
@@ -280,6 +292,7 @@ ActiveSupport.on_load(:active_record) do
|
|
280
292
|
end
|
281
293
|
end
|
282
294
|
end
|
295
|
+
# rubocop:enable Lint/ConstantDefinitionInBlock
|
283
296
|
|
284
297
|
# Rails < 4.2 is not innately compatible with Ruby 2.4 and later, and comes up with:
|
285
298
|
# "TypeError: Cannot visit Integer" unless we patch like this:
|
@@ -322,6 +335,7 @@ ActiveSupport.on_load(:active_record) do
|
|
322
335
|
# First part of arel_table_type stuff:
|
323
336
|
# ------------------------------------
|
324
337
|
# (more found below)
|
338
|
+
# was: ActiveRecord.version >= ::Gem::Version.new('3.2') &&
|
325
339
|
if ActiveRecord.version < ::Gem::Version.new('5.0')
|
326
340
|
# Used by Util#_arel_table_type
|
327
341
|
module ActiveRecord
|
@@ -336,6 +350,17 @@ ActiveSupport.on_load(:active_record) do
|
|
336
350
|
end
|
337
351
|
|
338
352
|
include ::DutyFree::Extensions
|
353
|
+
|
354
|
+
unless ::DutyFree::Extensions::IS_AMOEBA
|
355
|
+
# Add amoeba-compatible support
|
356
|
+
module ActiveRecord
|
357
|
+
class Base
|
358
|
+
def self.amoeba(*args)
|
359
|
+
puts "Amoeba called from #{name} with #{args.inspect}"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
339
364
|
end
|
340
365
|
|
341
366
|
# Do this earlier because stuff here gets mixed into JoinDependency::JoinAssociation and AssociationScope
|
@@ -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.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lorin Thwaits
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-02 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.2'
|
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.2'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: appraisal
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -214,6 +208,7 @@ files:
|
|
214
208
|
- lib/duty_free/version_number.rb
|
215
209
|
- lib/generators/duty_free/USAGE
|
216
210
|
- lib/generators/duty_free/install_generator.rb
|
211
|
+
- lib/generators/duty_free/model_generator.rb
|
217
212
|
- lib/generators/duty_free/templates/add_object_changes_to_versions.rb.erb
|
218
213
|
- lib/generators/duty_free/templates/create_versions.rb.erb
|
219
214
|
homepage: https://github.com/lorint/duty_free
|
@@ -235,7 +230,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
235
230
|
- !ruby/object:Gem::Version
|
236
231
|
version: 1.3.6
|
237
232
|
requirements: []
|
238
|
-
rubygems_version: 3.
|
233
|
+
rubygems_version: 3.1.6
|
239
234
|
signing_key:
|
240
235
|
specification_version: 4
|
241
236
|
summary: Import and Export Data
|