activerecord-graph-extractor 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +4 -0
- data/CHANGELOG.md +36 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +201 -0
- data/LICENSE +21 -0
- data/README.md +532 -0
- data/Rakefile +36 -0
- data/activerecord-graph-extractor.gemspec +64 -0
- data/docs/dry_run.md +410 -0
- data/docs/examples.md +239 -0
- data/docs/s3_integration.md +381 -0
- data/docs/usage.md +363 -0
- data/examples/dry_run_example.rb +227 -0
- data/examples/s3_example.rb +247 -0
- data/exe/arge +7 -0
- data/lib/activerecord_graph_extractor/cli.rb +627 -0
- data/lib/activerecord_graph_extractor/configuration.rb +98 -0
- data/lib/activerecord_graph_extractor/dependency_resolver.rb +406 -0
- data/lib/activerecord_graph_extractor/dry_run_analyzer.rb +421 -0
- data/lib/activerecord_graph_extractor/errors.rb +33 -0
- data/lib/activerecord_graph_extractor/extractor.rb +182 -0
- data/lib/activerecord_graph_extractor/importer.rb +260 -0
- data/lib/activerecord_graph_extractor/json_serializer.rb +176 -0
- data/lib/activerecord_graph_extractor/primary_key_mapper.rb +57 -0
- data/lib/activerecord_graph_extractor/progress_tracker.rb +202 -0
- data/lib/activerecord_graph_extractor/relationship_analyzer.rb +212 -0
- data/lib/activerecord_graph_extractor/s3_client.rb +170 -0
- data/lib/activerecord_graph_extractor/version.rb +5 -0
- data/lib/activerecord_graph_extractor.rb +34 -0
- data/scripts/verify_installation.rb +192 -0
- metadata +388 -0
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordGraphExtractor
|
4
|
+
class RelationshipAnalyzer
|
5
|
+
attr_reader :config, :visited_models, :circular_paths
|
6
|
+
|
7
|
+
def initialize(config = ActiveRecordGraphExtractor.configuration)
|
8
|
+
@config = config
|
9
|
+
@visited_models = Set.new
|
10
|
+
@circular_paths = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def analyze_model(model_class)
|
14
|
+
# Validate that this is an ActiveRecord model
|
15
|
+
unless model_class.respond_to?(:reflect_on_all_associations)
|
16
|
+
raise ExtractionError, "#{model_class} is not an ActiveRecord model"
|
17
|
+
end
|
18
|
+
|
19
|
+
relationships = {}
|
20
|
+
|
21
|
+
model_class.reflect_on_all_associations.each do |association|
|
22
|
+
begin
|
23
|
+
next if association.klass.nil?
|
24
|
+
|
25
|
+
relationship_name = association.name.to_s
|
26
|
+
next unless config.relationship_included?(relationship_name)
|
27
|
+
|
28
|
+
model_name = association.klass.name
|
29
|
+
next unless config.model_included?(model_name)
|
30
|
+
|
31
|
+
relationships[relationship_name] = {
|
32
|
+
'type' => association.macro.to_s,
|
33
|
+
'model_class' => association.klass,
|
34
|
+
'model_name' => model_name,
|
35
|
+
'foreign_key' => association.foreign_key,
|
36
|
+
'polymorphic' => association.options[:polymorphic] || false,
|
37
|
+
'optional' => association.options[:optional] || false
|
38
|
+
}
|
39
|
+
rescue NameError => e
|
40
|
+
if config.skip_missing_models
|
41
|
+
next
|
42
|
+
else
|
43
|
+
raise e
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
filter_relationships(relationships)
|
49
|
+
end
|
50
|
+
|
51
|
+
def analyze_models(model_classes)
|
52
|
+
# Validate input
|
53
|
+
unless model_classes.is_a?(Array)
|
54
|
+
raise ExtractionError, "Expected an array of model classes"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Handle empty array
|
58
|
+
return {} if model_classes.empty?
|
59
|
+
|
60
|
+
# Validate each model class
|
61
|
+
model_classes.each do |model_class|
|
62
|
+
unless model_class.respond_to?(:reflect_on_all_associations)
|
63
|
+
raise ExtractionError, "#{model_class} is not an ActiveRecord model"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
relationships = {}
|
68
|
+
|
69
|
+
model_classes.each do |model_class|
|
70
|
+
model_name = model_class.name
|
71
|
+
relationships[model_name] = analyze_model(model_class)
|
72
|
+
end
|
73
|
+
|
74
|
+
relationships
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_relationship_info(record, relationship_name)
|
78
|
+
relationships = analyze_model(record.class)
|
79
|
+
relationship = relationships[relationship_name.to_s]
|
80
|
+
|
81
|
+
return nil unless relationship
|
82
|
+
|
83
|
+
# Convert to symbols for consistency with expected API
|
84
|
+
{
|
85
|
+
model_class: relationship['model_class'],
|
86
|
+
model_name: relationship['model_name'],
|
87
|
+
type: relationship['type'].to_sym
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_dependency_graph(models)
|
92
|
+
dependency_graph = {}
|
93
|
+
|
94
|
+
models.each do |model_class|
|
95
|
+
dependencies = []
|
96
|
+
relationships = analyze_model(model_class)
|
97
|
+
|
98
|
+
relationships.each do |name, info|
|
99
|
+
if info['type'] == 'belongs_to' && !info['optional']
|
100
|
+
dependencies << info['model_class'] unless dependencies.include?(info['model_class'])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
dependency_graph[model_class] = dependencies
|
105
|
+
end
|
106
|
+
|
107
|
+
dependency_graph
|
108
|
+
end
|
109
|
+
|
110
|
+
def circular_reference?(model_name, visited)
|
111
|
+
visited.include?(model_name)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def filter_relationships(relationships)
|
117
|
+
relationships.select do |name, info|
|
118
|
+
config.model_included?(info['model_name']) &&
|
119
|
+
config.relationship_included?(name)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def should_include_association?(association)
|
124
|
+
return false unless config.relationship_included?(association.name.to_s)
|
125
|
+
|
126
|
+
# Skip associations that don't have a klass (can happen with polymorphic or broken associations)
|
127
|
+
return false unless association.klass
|
128
|
+
|
129
|
+
return false unless config.model_included?(association.klass.name)
|
130
|
+
|
131
|
+
# Skip polymorphic associations that can't be resolved
|
132
|
+
return false if association.polymorphic? && association.foreign_type.nil?
|
133
|
+
|
134
|
+
true
|
135
|
+
rescue NameError
|
136
|
+
# Skip associations that reference non-existent models
|
137
|
+
false
|
138
|
+
end
|
139
|
+
|
140
|
+
def build_relationship_info(association)
|
141
|
+
{
|
142
|
+
type: association.macro,
|
143
|
+
class_name: association.klass&.name,
|
144
|
+
foreign_key: association.foreign_key,
|
145
|
+
primary_key: association.association_primary_key,
|
146
|
+
polymorphic: association.polymorphic?,
|
147
|
+
through: association.through_reflection&.name,
|
148
|
+
source: association.source_reflection&.name,
|
149
|
+
dependent: association.options[:dependent],
|
150
|
+
inverse_of: association.inverse_of&.name
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def traverse_for_dependencies(model_class, dependency_graph, path)
|
155
|
+
model_name = model_class.name
|
156
|
+
|
157
|
+
# Detect circular references
|
158
|
+
if path.include?(model_name)
|
159
|
+
handle_circular_reference(model_name, path)
|
160
|
+
return
|
161
|
+
end
|
162
|
+
|
163
|
+
return if dependency_graph.key?(model_name)
|
164
|
+
|
165
|
+
dependencies = []
|
166
|
+
current_path = path + [model_name]
|
167
|
+
|
168
|
+
model_class.reflect_on_all_associations.each do |association|
|
169
|
+
next unless should_include_association?(association)
|
170
|
+
next if association.macro == :has_many || association.macro == :has_one
|
171
|
+
|
172
|
+
# Only belongs_to creates dependencies
|
173
|
+
if association.macro == :belongs_to
|
174
|
+
# Skip if klass is nil (can happen with polymorphic associations)
|
175
|
+
next unless association.klass
|
176
|
+
|
177
|
+
associated_class = association.klass
|
178
|
+
dependencies << associated_class.name
|
179
|
+
|
180
|
+
# Recursively analyze dependencies
|
181
|
+
traverse_for_dependencies(associated_class, dependency_graph, current_path)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
dependency_graph[model_name] = dependencies.uniq
|
186
|
+
end
|
187
|
+
|
188
|
+
def handle_circular_reference(model_name, path)
|
189
|
+
cycle_start = path.index(model_name)
|
190
|
+
cycle = path[cycle_start..-1] + [model_name]
|
191
|
+
|
192
|
+
@circular_paths << cycle
|
193
|
+
|
194
|
+
case config.circular_reference_strategy
|
195
|
+
when :error
|
196
|
+
raise CircularReferenceError.new(
|
197
|
+
"Circular reference detected: #{cycle.join(' -> ')}",
|
198
|
+
model: model_name
|
199
|
+
)
|
200
|
+
when :skip
|
201
|
+
# Simply skip this path
|
202
|
+
return
|
203
|
+
when :break_at_depth
|
204
|
+
return if path.length >= config.max_circular_depth
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def get_custom_rules(from_model, to_model)
|
209
|
+
config.custom_traversal_rules.dig(from_model, to_model) || {}
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aws-sdk-s3'
|
4
|
+
|
5
|
+
module ActiveRecordGraphExtractor
|
6
|
+
class S3Client
|
7
|
+
attr_reader :bucket_name, :region, :s3_client
|
8
|
+
|
9
|
+
def initialize(bucket_name:, region: 'us-east-1', **options)
|
10
|
+
@bucket_name = bucket_name
|
11
|
+
@region = region
|
12
|
+
|
13
|
+
# Initialize AWS S3 client with optional credentials
|
14
|
+
s3_options = { region: region }
|
15
|
+
s3_options.merge!(options) if options.any?
|
16
|
+
|
17
|
+
@s3_client = Aws::S3::Client.new(s3_options)
|
18
|
+
|
19
|
+
validate_bucket_access!
|
20
|
+
end
|
21
|
+
|
22
|
+
# Upload a file to S3
|
23
|
+
def upload_file(local_file_path, s3_key = nil, **options)
|
24
|
+
raise FileError, "File not found: #{local_file_path}" unless File.exist?(local_file_path)
|
25
|
+
|
26
|
+
s3_key ||= generate_s3_key(local_file_path)
|
27
|
+
|
28
|
+
begin
|
29
|
+
File.open(local_file_path, 'rb') do |file|
|
30
|
+
s3_client.put_object(
|
31
|
+
bucket: bucket_name,
|
32
|
+
key: s3_key,
|
33
|
+
body: file,
|
34
|
+
content_type: 'application/json',
|
35
|
+
**options
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
{
|
40
|
+
bucket: bucket_name,
|
41
|
+
key: s3_key,
|
42
|
+
url: s3_url(s3_key),
|
43
|
+
size: File.size(local_file_path)
|
44
|
+
}
|
45
|
+
rescue Aws::S3::Errors::ServiceError => e
|
46
|
+
raise S3Error, "Failed to upload file to S3: #{e.message}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Download a file from S3
|
51
|
+
def download_file(s3_key, local_file_path = nil)
|
52
|
+
local_file_path ||= File.basename(s3_key)
|
53
|
+
|
54
|
+
begin
|
55
|
+
s3_client.get_object(
|
56
|
+
bucket: bucket_name,
|
57
|
+
key: s3_key,
|
58
|
+
response_target: local_file_path
|
59
|
+
)
|
60
|
+
|
61
|
+
{
|
62
|
+
bucket: bucket_name,
|
63
|
+
key: s3_key,
|
64
|
+
local_path: local_file_path,
|
65
|
+
size: File.size(local_file_path)
|
66
|
+
}
|
67
|
+
rescue Aws::S3::Errors::NoSuchKey
|
68
|
+
raise S3Error, "File not found in S3: s3://#{bucket_name}/#{s3_key}"
|
69
|
+
rescue Aws::S3::Errors::ServiceError => e
|
70
|
+
raise S3Error, "Failed to download file from S3: #{e.message}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Check if a file exists in S3
|
75
|
+
def file_exists?(s3_key)
|
76
|
+
s3_client.head_object(bucket: bucket_name, key: s3_key)
|
77
|
+
true
|
78
|
+
rescue Aws::S3::Errors::NotFound
|
79
|
+
false
|
80
|
+
rescue Aws::S3::Errors::ServiceError => e
|
81
|
+
raise S3Error, "Failed to check file existence: #{e.message}"
|
82
|
+
end
|
83
|
+
|
84
|
+
# List files in S3 with optional prefix
|
85
|
+
def list_files(prefix: nil, max_keys: 1000)
|
86
|
+
options = {
|
87
|
+
bucket: bucket_name,
|
88
|
+
max_keys: max_keys
|
89
|
+
}
|
90
|
+
options[:prefix] = prefix if prefix
|
91
|
+
|
92
|
+
begin
|
93
|
+
response = s3_client.list_objects_v2(options)
|
94
|
+
|
95
|
+
response.contents.map do |object|
|
96
|
+
{
|
97
|
+
key: object.key,
|
98
|
+
size: object.size,
|
99
|
+
last_modified: object.last_modified,
|
100
|
+
url: s3_url(object.key)
|
101
|
+
}
|
102
|
+
end
|
103
|
+
rescue Aws::S3::Errors::ServiceError => e
|
104
|
+
raise S3Error, "Failed to list files: #{e.message}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Delete a file from S3
|
109
|
+
def delete_file(s3_key)
|
110
|
+
begin
|
111
|
+
s3_client.delete_object(bucket: bucket_name, key: s3_key)
|
112
|
+
true
|
113
|
+
rescue Aws::S3::Errors::ServiceError => e
|
114
|
+
raise S3Error, "Failed to delete file: #{e.message}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Generate a presigned URL for downloading
|
119
|
+
def presigned_url(s3_key, expires_in: 3600)
|
120
|
+
begin
|
121
|
+
presigner = Aws::S3::Presigner.new(client: s3_client)
|
122
|
+
presigner.presigned_url(:get_object, bucket: bucket_name, key: s3_key, expires_in: expires_in)
|
123
|
+
rescue Aws::S3::Errors::ServiceError => e
|
124
|
+
raise S3Error, "Failed to generate presigned URL: #{e.message}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Get file metadata
|
129
|
+
def file_metadata(s3_key)
|
130
|
+
begin
|
131
|
+
response = s3_client.head_object(bucket: bucket_name, key: s3_key)
|
132
|
+
|
133
|
+
{
|
134
|
+
key: s3_key,
|
135
|
+
size: response.content_length,
|
136
|
+
last_modified: response.last_modified,
|
137
|
+
content_type: response.content_type,
|
138
|
+
etag: response.etag,
|
139
|
+
metadata: response.metadata
|
140
|
+
}
|
141
|
+
rescue Aws::S3::Errors::NotFound
|
142
|
+
raise S3Error, "File not found: s3://#{bucket_name}/#{s3_key}"
|
143
|
+
rescue Aws::S3::Errors::ServiceError => e
|
144
|
+
raise S3Error, "Failed to get file metadata: #{e.message}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def validate_bucket_access!
|
151
|
+
s3_client.head_bucket(bucket: bucket_name)
|
152
|
+
rescue Aws::S3::Errors::NotFound
|
153
|
+
raise S3Error, "Bucket not found: #{bucket_name}"
|
154
|
+
rescue Aws::S3::Errors::Forbidden
|
155
|
+
raise S3Error, "Access denied to bucket: #{bucket_name}"
|
156
|
+
rescue Aws::S3::Errors::ServiceError => e
|
157
|
+
raise S3Error, "Failed to access bucket: #{e.message}"
|
158
|
+
end
|
159
|
+
|
160
|
+
def generate_s3_key(local_file_path)
|
161
|
+
filename = File.basename(local_file_path)
|
162
|
+
timestamp = Time.now.strftime('%Y/%m/%d')
|
163
|
+
"activerecord-graph-extractor/#{timestamp}/#{filename}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def s3_url(s3_key)
|
167
|
+
"s3://#{bucket_name}/#{s3_key}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "active_support"
|
5
|
+
require "yajl"
|
6
|
+
|
7
|
+
require_relative "activerecord_graph_extractor/version"
|
8
|
+
require_relative "activerecord_graph_extractor/errors"
|
9
|
+
require_relative "activerecord_graph_extractor/configuration"
|
10
|
+
require_relative "activerecord_graph_extractor/relationship_analyzer"
|
11
|
+
require_relative "activerecord_graph_extractor/dependency_resolver"
|
12
|
+
require_relative "activerecord_graph_extractor/json_serializer"
|
13
|
+
require_relative "activerecord_graph_extractor/primary_key_mapper"
|
14
|
+
require_relative "activerecord_graph_extractor/progress_tracker"
|
15
|
+
require_relative "activerecord_graph_extractor/s3_client"
|
16
|
+
require_relative "activerecord_graph_extractor/dry_run_analyzer"
|
17
|
+
require_relative "activerecord_graph_extractor/extractor"
|
18
|
+
require_relative "activerecord_graph_extractor/importer"
|
19
|
+
|
20
|
+
module ActiveRecordGraphExtractor
|
21
|
+
class << self
|
22
|
+
def configure
|
23
|
+
yield(configuration)
|
24
|
+
end
|
25
|
+
|
26
|
+
def configuration
|
27
|
+
@configuration ||= Configuration.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def reset_configuration!
|
31
|
+
@configuration = Configuration.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Installation Verification Script for ActiveRecord Graph Extractor CLI
|
5
|
+
# Run this script to verify your installation is working correctly
|
6
|
+
|
7
|
+
puts "🔍 ActiveRecord Graph Extractor CLI Installation Verification"
|
8
|
+
puts "=" * 60
|
9
|
+
|
10
|
+
# Check if the arge command is available
|
11
|
+
def check_command_availability
|
12
|
+
print "Checking if 'arge' command is available... "
|
13
|
+
|
14
|
+
result = `which arge 2>/dev/null`.strip
|
15
|
+
if result.empty?
|
16
|
+
puts "❌ NOT FOUND"
|
17
|
+
puts
|
18
|
+
puts "The 'arge' command is not available in your PATH."
|
19
|
+
puts "This could mean:"
|
20
|
+
puts " 1. The gem is not installed globally"
|
21
|
+
puts " 2. You need to use 'bundle exec arge' instead"
|
22
|
+
puts " 3. You need to run 'rbenv rehash' (if using rbenv)"
|
23
|
+
puts
|
24
|
+
return false
|
25
|
+
else
|
26
|
+
puts "✅ FOUND at #{result}"
|
27
|
+
return true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check gem installation
|
32
|
+
def check_gem_installation
|
33
|
+
print "Checking gem installation... "
|
34
|
+
|
35
|
+
begin
|
36
|
+
require 'activerecord_graph_extractor'
|
37
|
+
version = ActiveRecordGraphExtractor::VERSION
|
38
|
+
puts "✅ INSTALLED (version #{version})"
|
39
|
+
return true
|
40
|
+
rescue LoadError
|
41
|
+
puts "❌ NOT FOUND"
|
42
|
+
puts
|
43
|
+
puts "The activerecord-graph-extractor gem is not installed or not in your load path."
|
44
|
+
puts "Try: gem install activerecord-graph-extractor"
|
45
|
+
puts
|
46
|
+
return false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Test CLI functionality
|
51
|
+
def test_cli_functionality
|
52
|
+
print "Testing CLI functionality... "
|
53
|
+
|
54
|
+
# Try to run the version command
|
55
|
+
output = `arge version 2>&1`
|
56
|
+
exit_code = $?.exitstatus
|
57
|
+
|
58
|
+
if exit_code == 0 && output.include?("ActiveRecord Graph Extractor")
|
59
|
+
puts "✅ WORKING"
|
60
|
+
puts " Output: #{output.strip}"
|
61
|
+
return true
|
62
|
+
else
|
63
|
+
puts "❌ FAILED"
|
64
|
+
puts " Exit code: #{exit_code}"
|
65
|
+
puts " Output: #{output}"
|
66
|
+
return false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Test CLI help
|
71
|
+
def test_cli_help
|
72
|
+
print "Testing CLI help... "
|
73
|
+
|
74
|
+
output = `arge help 2>&1`
|
75
|
+
exit_code = $?.exitstatus
|
76
|
+
|
77
|
+
expected_commands = ['extract', 'import', 'extract_to_s3', 's3_list', 's3_download', 'analyze', 'dry_run']
|
78
|
+
|
79
|
+
if exit_code == 0 && expected_commands.all? { |cmd| output.include?(cmd) }
|
80
|
+
puts "✅ ALL COMMANDS AVAILABLE"
|
81
|
+
puts " Available commands: #{expected_commands.join(', ')}"
|
82
|
+
return true
|
83
|
+
else
|
84
|
+
puts "❌ MISSING COMMANDS"
|
85
|
+
puts " Expected: #{expected_commands.join(', ')}"
|
86
|
+
puts " Output: #{output}"
|
87
|
+
return false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Check Ruby version
|
92
|
+
def check_ruby_version
|
93
|
+
print "Checking Ruby version... "
|
94
|
+
|
95
|
+
ruby_version = RUBY_VERSION
|
96
|
+
required_version = Gem::Version.new('2.7.0')
|
97
|
+
current_version = Gem::Version.new(ruby_version)
|
98
|
+
|
99
|
+
if current_version >= required_version
|
100
|
+
puts "✅ COMPATIBLE (#{ruby_version})"
|
101
|
+
return true
|
102
|
+
else
|
103
|
+
puts "❌ TOO OLD (#{ruby_version}, requires >= 2.7.0)"
|
104
|
+
puts
|
105
|
+
puts "Please upgrade your Ruby version to 2.7.0 or higher."
|
106
|
+
puts "Consider using rbenv or RVM to manage Ruby versions."
|
107
|
+
puts
|
108
|
+
return false
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Check if running in Rails environment
|
113
|
+
def check_rails_environment
|
114
|
+
print "Checking Rails environment... "
|
115
|
+
|
116
|
+
if defined?(Rails)
|
117
|
+
puts "✅ RAILS DETECTED (#{Rails.version})"
|
118
|
+
puts " You can use: bundle exec arge [command]"
|
119
|
+
return true
|
120
|
+
else
|
121
|
+
puts "ℹ️ NO RAILS DETECTED"
|
122
|
+
puts " This is fine for standalone usage"
|
123
|
+
return true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Main verification process
|
128
|
+
def main
|
129
|
+
puts
|
130
|
+
|
131
|
+
all_checks_passed = true
|
132
|
+
|
133
|
+
# Run all checks
|
134
|
+
all_checks_passed &= check_ruby_version
|
135
|
+
all_checks_passed &= check_gem_installation
|
136
|
+
all_checks_passed &= check_rails_environment
|
137
|
+
all_checks_passed &= check_command_availability
|
138
|
+
|
139
|
+
# Only test CLI if command is available
|
140
|
+
if check_command_availability
|
141
|
+
all_checks_passed &= test_cli_functionality
|
142
|
+
all_checks_passed &= test_cli_help
|
143
|
+
else
|
144
|
+
# Try with bundle exec if direct command failed
|
145
|
+
puts
|
146
|
+
puts "Trying with 'bundle exec'..."
|
147
|
+
|
148
|
+
bundle_output = `bundle exec arge version 2>&1`
|
149
|
+
bundle_exit_code = $?.exitstatus
|
150
|
+
|
151
|
+
if bundle_exit_code == 0
|
152
|
+
puts "✅ 'bundle exec arge' works!"
|
153
|
+
puts " Use: bundle exec arge [command]"
|
154
|
+
puts " This is normal when the gem is installed via Gemfile"
|
155
|
+
else
|
156
|
+
puts "❌ 'bundle exec arge' also failed"
|
157
|
+
puts " Output: #{bundle_output}"
|
158
|
+
all_checks_passed = false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
puts
|
163
|
+
puts "=" * 60
|
164
|
+
|
165
|
+
if all_checks_passed
|
166
|
+
puts "🎉 INSTALLATION VERIFICATION SUCCESSFUL!"
|
167
|
+
puts
|
168
|
+
puts "Your ActiveRecord Graph Extractor CLI is properly installed and working."
|
169
|
+
puts
|
170
|
+
puts "Quick start:"
|
171
|
+
puts " arge version # Check version"
|
172
|
+
puts " arge help # See all commands"
|
173
|
+
puts " arge extract Order 123 # Extract a record (requires Rails)"
|
174
|
+
puts " arge extract_to_s3 Order 123 # Extract to S3 (requires AWS config)"
|
175
|
+
puts
|
176
|
+
else
|
177
|
+
puts "❌ INSTALLATION VERIFICATION FAILED!"
|
178
|
+
puts
|
179
|
+
puts "Some checks failed. Please review the errors above and:"
|
180
|
+
puts " 1. Make sure the gem is properly installed"
|
181
|
+
puts " 2. Check your Ruby version (>= 2.7.0 required)"
|
182
|
+
puts " 3. Try 'rbenv rehash' if using rbenv"
|
183
|
+
puts " 4. Use 'bundle exec arge' if installed via Gemfile"
|
184
|
+
puts
|
185
|
+
puts "For more help, see the installation guide in the README."
|
186
|
+
end
|
187
|
+
|
188
|
+
puts
|
189
|
+
end
|
190
|
+
|
191
|
+
# Run the verification
|
192
|
+
main if __FILE__ == $0
|