active_capture 1.1.1 → 1.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35e9f57cd205fbbb0ed7e4139101787cb4f66e53a32186b27473f9db72b40498
4
- data.tar.gz: ea2747f578ebc4dff315d542b7e6e4cb9193e53e0dd49f2231f37654d39047bc
3
+ metadata.gz: 408cdbd0f3cea245a023857573782eab133c6f8d3a41981ff190702c8f6424bb
4
+ data.tar.gz: 9726c3c013f766036550a1170d8f4b71f382043058e78701fee884654b002f6c
5
5
  SHA512:
6
- metadata.gz: f72ad16293a0f39eed9e2471ba7ece51022abdcb6f6005244f25502f7d903eb5054b983a2414fbbeb18c0b1878a93bf674333c71bcd4e4cea57a37df841cc8bf
7
- data.tar.gz: c4d02f5b01eb45a1a2e98cd349b4fd3d765a8a4ff27dfe48a57568ac06594431ba501dd69652292776724f548c8528f92b3cd1a613cff5c0a5f22024d4355868
6
+ metadata.gz: '08763c65b1bf808fbf85677e912621e34ffc2d00de886a6242f1bdc5308e425215e293d471403a6bc14c51b92bab82d4c2e6cf652421192c66977a2cd4c46a7e'
7
+ data.tar.gz: 6dfbb8e4974cc7382d5b6999413643d7483586843582eba130424ba0929519855f9d0519a14615ac0c37dfdea593bb6e73f353d0130f710e03836069e81d0bab
@@ -4,6 +4,7 @@ require 'json'
4
4
  require 'active_record'
5
5
 
6
6
  module ActiveCapture
7
+ # Captures the state of a record and its associations
7
8
  def self.take(record, associations: [], name: nil)
8
9
  validate_record(record)
9
10
 
@@ -15,18 +16,21 @@ module ActiveCapture
15
16
  }
16
17
 
17
18
  begin
19
+ # Save the captured data in json file
18
20
  Support::CaptureStorage.save_capture(record, capture_data, name)
19
21
  rescue StandardError => e
20
22
  raise "Failed to save capture: #{e.message}"
21
23
  end
22
24
  end
23
25
 
26
+ # Restores a record and its associations from a capture file
24
27
  def self.restore(record, capture_file, merge: false)
25
28
  captured_records = Support::CaptureStorage.load_capture(capture_file)
26
29
  validate_capture(record, captured_records)
27
30
 
28
31
  ActiveRecord::Base.transaction do
29
32
  begin
33
+ # Update the record's attributes and restore its associations
30
34
  record.update!(captured_records['attributes'])
31
35
  restore_associations(record, captured_records['associations'], merge: merge)
32
36
  rescue StandardError => e
@@ -35,20 +39,28 @@ module ActiveCapture
35
39
  end
36
40
  end
37
41
 
42
+ # Flushes all captured data of a specified directory
43
+ def self.flush(directory)
44
+ Support::CaptureStorage.flush(directory)
45
+ end
46
+
38
47
  private
39
48
 
49
+ # Validates that the given record is an ActiveRecord instance
40
50
  def self.validate_record(record)
41
51
  unless record.is_a?(ActiveRecord::Base)
42
52
  raise ArgumentError, 'Record must be an ActiveRecord::Base instance'
43
53
  end
44
54
  end
45
55
 
56
+ # Validates that the capture file matches the given record
46
57
  def self.validate_capture(record, captured_records)
47
58
  unless captured_records['record_id'] == record.id
48
59
  raise ArgumentError, 'Capture file does not match the given record'
49
60
  end
50
61
  end
51
62
 
63
+ # Captures the specified associations of a record
52
64
  def self.capture_associations(record, associations)
53
65
  associations.each_with_object({}) do |association, association_data|
54
66
  association_name, nested_associations = parse_association(association)
@@ -56,6 +68,7 @@ module ActiveCapture
56
68
  next unless record.respond_to?(association_name)
57
69
 
58
70
  begin
71
+ # Capture the related records for the association
59
72
  related_records = record.send(association_name)
60
73
  association_data[association_name] = capture_related_records(related_records, nested_associations)
61
74
  rescue StandardError => e
@@ -64,6 +77,7 @@ module ActiveCapture
64
77
  end
65
78
  end
66
79
 
80
+ # Captures the attributes and nested associations of related records
67
81
  def self.capture_related_records(related_records, nested_associations)
68
82
  if related_records.is_a?(ActiveRecord::Base)
69
83
  {
@@ -80,6 +94,7 @@ module ActiveCapture
80
94
  end
81
95
  end
82
96
 
97
+ # Parses an association to extract its name and nested associations
83
98
  def self.parse_association(association)
84
99
  if association.is_a?(Hash)
85
100
  [association.keys.first, association.values.flatten]
@@ -88,36 +103,44 @@ module ActiveCapture
88
103
  end
89
104
  end
90
105
 
106
+ # Restores the associations of a record from captured data
91
107
  def self.restore_associations(record, associations_data, merge: false)
92
108
  associations_data.each do |association_name, related_data|
93
109
  if related_data.is_a?(Array)
110
+ # Restore a collection association (e.g., has_many)
94
111
  restore_collection_association(record, association_name, related_data, merge)
95
112
  else
113
+ # Restore a single association (e.g., belongs_to, has_one)
96
114
  restore_single_association(record, association_name, related_data, merge)
97
115
  end
98
116
  end
99
117
  end
100
118
 
119
+ # Restores a collection association (e.g., has_many)
101
120
  def self.restore_collection_association(record, association_name, related_data, merge)
102
121
  associated_class = record.class.reflect_on_association(association_name).klass
103
122
  existing_records = record.send(association_name)
104
123
 
105
124
  related_data.each do |related_record_data|
106
125
  if merge && related_record_data['attributes']['id']
126
+ # Update existing records if merging is enabled
107
127
  existing_record = existing_records.find_by(id: related_record_data['attributes']['id'])
108
128
  if existing_record
109
129
  update_and_restore(existing_record, related_record_data, merge)
110
130
  next
111
131
  end
112
132
  end
133
+ # Create and restore new records
113
134
  create_and_restore(associated_class, related_record_data, record, association_name)
114
135
  end
115
136
  end
116
137
 
138
+ # Restores a single association (e.g., belongs_to, has_one)
117
139
  def self.restore_single_association(record, association_name, related_data, merge)
118
140
  associated_class = record.class.reflect_on_association(association_name).klass
119
141
 
120
142
  if merge && related_data['attributes']['id']
143
+ # Update existing record if merging is enabled
121
144
  existing_record = record.send(association_name)
122
145
  if existing_record&.id == related_data['attributes']['id']
123
146
  update_and_restore(existing_record, related_data, merge)
@@ -125,15 +148,18 @@ module ActiveCapture
125
148
  end
126
149
  end
127
150
 
151
+ # Create and restore a new record
128
152
  new_record = create_and_restore(associated_class, related_data)
129
153
  record.update!(association_name => new_record)
130
154
  end
131
155
 
156
+ # Updates an existing record and restores its associations
132
157
  def self.update_and_restore(record, related_data, merge)
133
158
  record.update!(related_data['attributes'])
134
159
  restore_associations(record, related_data['associations'], merge: merge)
135
160
  end
136
161
 
162
+ # Creates a new record and restores its associations
137
163
  def self.create_and_restore(associated_class, related_data, parent_record = nil, association_name = nil)
138
164
  new_record = associated_class.create!(related_data['attributes'])
139
165
  restore_associations(new_record, related_data['associations'], merge: false)
@@ -1,44 +1,95 @@
1
+ # This module provides functionality for storing and retrieving JSON-based capture data
2
+ # associated with specific records. It includes methods for saving captures, loading captures,
3
+ # and flushing (deleting) files in a specific directory.
4
+
1
5
  require 'json'
2
6
  require 'fileutils'
3
7
  require 'pathname'
4
8
 
5
9
  module Support
6
10
  class CaptureStorage
11
+ # Constants for directory and filename constraints
7
12
  CAPTURE_DIR = 'captures'.freeze
8
13
  MAX_FILENAME_LENGTH = 100
9
14
  VALID_FILENAME_REGEX = /\A[a-zA-Z0-9\-_\.]+\z/
10
15
 
11
16
  class << self
17
+ # Saves capture data to a JSON file in a structured directory.
18
+ # @param record [Object] The record object (must respond to :class and :id).
19
+ # @param capture_data [Hash] The data to be saved.
20
+ # @param name [String, nil] Optional custom filename.
21
+ # @return [Hash] Result of the operation with success status and file path or error message.
12
22
  def save_capture(record, capture_data, name = nil)
13
- validate_record(record)
14
- validate_capture_data(capture_data)
23
+ validate_record(record) # Ensure the record is valid
24
+ validate_capture_data(capture_data) # Ensure the capture data is valid
15
25
 
16
- file_name = generate_filename(record, name)
17
- dir_path = create_capture_directory(record)
18
- file_path = File.join(dir_path, file_name)
26
+ file_name = generate_filename(record, name) # Generate a valid filename
27
+ dir_path = create_capture_directory(record) # Create the directory for the capture
28
+ file_path = File.join(dir_path, file_name) # Full path to the capture file
19
29
 
20
- write_capture_file(file_path, capture_data)
30
+ write_capture_file(file_path, capture_data) # Write the capture data to the file
21
31
 
22
32
  { success: true, file_path: file_path }
23
33
  rescue => e
24
34
  { success: false, error: e.message }
25
35
  end
26
36
 
37
+ # Loads capture data from a JSON file.
38
+ # @param capture_file [String] Path to the capture file.
39
+ # @return [Hash] Parsed JSON data from the file.
40
+ # @raise [StorageError] If the file cannot be loaded or parsed.
27
41
  def load_capture(capture_file)
28
- validate_file_path(capture_file)
42
+ validate_file_path(capture_file) # Ensure the file path is valid
29
43
 
30
- file_content = read_file_safely(capture_file)
31
- parsed_data = JSON.parse(file_content)
44
+ file_content = read_file_safely(capture_file) # Read the file content
45
+ parsed_data = JSON.parse(file_content) # Parse the JSON content
32
46
 
33
- validate_parsed_data(parsed_data)
47
+ validate_parsed_data(parsed_data) # Ensure the parsed data is valid
34
48
 
35
49
  parsed_data
36
50
  rescue => e
37
51
  raise StorageError, "Failed to load capture: #{e.message}"
38
52
  end
39
53
 
54
+ # Deletes all files in a specified directory under the capture directory.
55
+ # @param directory [String] Subdirectory name under the capture directory.
56
+ def flush(directory)
57
+ begin
58
+ dir_path = File.join(CAPTURE_DIR, directory) # Full path to the directory
59
+
60
+ unless Dir.exist?(dir_path)
61
+ raise StandardError, "Directory '#{dir_path}' does not exist."
62
+ end
63
+
64
+ files = Dir.glob("#{dir_path}/*") # Get all files in the directory
65
+
66
+ if files.empty?
67
+ puts "No files found in #{dir_path} to delete."
68
+ return
69
+ end
70
+
71
+ files.each do |file|
72
+ if File.file?(file)
73
+ begin
74
+ File.delete(file) # Delete each file
75
+ rescue Errno::EACCES
76
+ puts "Permission denied when deleting file: #{file}"
77
+ rescue StandardError => e
78
+ puts "Failed to delete file: #{file}. Error: #{e.message}"
79
+ end
80
+ end
81
+ end
82
+
83
+ puts "Flushed all files in #{dir_path} successfully."
84
+
85
+ rescue StandardError => e
86
+ puts "An error occurred during flush operation: #{e.message}"
87
+ end
88
+ end
89
+
40
90
  private
41
91
 
92
+ # Validates that the record has the required attributes.
42
93
  def validate_record(record)
43
94
  unless record.respond_to?(:class) && record.respond_to?(:id)
44
95
  raise ArgumentError, "Invalid record: must respond to :class and :id"
@@ -49,6 +100,7 @@ module Support
49
100
  end
50
101
  end
51
102
 
103
+ # Validates the structure of the capture data.
52
104
  def validate_capture_data(data)
53
105
  unless data.is_a?(Hash)
54
106
  raise ArgumentError, "Capture data must be a Hash"
@@ -61,6 +113,7 @@ module Support
61
113
  end
62
114
  end
63
115
 
116
+ # Generates a valid filename for the capture file.
64
117
  def generate_filename(record, custom_name)
65
118
  model_name = record.class.name.downcase.gsub(/[^a-z0-9]/, '_')
66
119
  record_id = record.id.to_s
@@ -75,6 +128,7 @@ module Support
75
128
  end
76
129
  end
77
130
 
131
+ # Validates the custom filename.
78
132
  def validate_filename(name)
79
133
  if name.empty?
80
134
  raise ArgumentError, "Filename cannot be empty"
@@ -89,12 +143,13 @@ module Support
89
143
  end
90
144
  end
91
145
 
146
+ # Creates the directory for storing capture files.
92
147
  def create_capture_directory(record)
93
148
  model_name = record.class.name.downcase.gsub(/[^a-z0-9]/, '_')
94
149
  dir_path = File.join(CAPTURE_DIR, model_name)
95
150
 
96
151
  begin
97
- FileUtils.mkdir_p(dir_path)
152
+ FileUtils.mkdir_p(dir_path) # Create the directory if it doesn't exist
98
153
  rescue SystemCallError => e
99
154
  raise StorageError, "Failed to create directory '#{dir_path}': #{e.message}"
100
155
  end
@@ -102,17 +157,19 @@ module Support
102
157
  dir_path
103
158
  end
104
159
 
160
+ # Writes the capture data to a file safely.
105
161
  def write_capture_file(file_path, data)
106
162
  begin
107
163
  temp_path = "#{file_path}.tmp"
108
- File.write(temp_path, JSON.pretty_generate(data))
109
- File.rename(temp_path, file_path)
164
+ File.write(temp_path, JSON.pretty_generate(data)) # Write to a temporary file
165
+ File.rename(temp_path, file_path) # Rename to the final file
110
166
  rescue SystemCallError => e
111
167
  File.delete(temp_path) if File.exist?(temp_path)
112
168
  raise StorageError, "Failed to write capture file '#{file_path}': #{e.message}"
113
169
  end
114
170
  end
115
171
 
172
+ # Validates the file path for loading captures.
116
173
  def validate_file_path(file_path)
117
174
  unless File.exist?(file_path)
118
175
  raise StorageError, "File does not exist: #{file_path}"
@@ -127,12 +184,14 @@ module Support
127
184
  end
128
185
  end
129
186
 
187
+ # Reads the file content safely.
130
188
  def read_file_safely(file_path)
131
189
  File.read(file_path)
132
190
  rescue SystemCallError => e
133
191
  raise StorageError, "Failed to read file '#{file_path}': #{e.message}"
134
192
  end
135
193
 
194
+ # Validates the parsed JSON data structure.
136
195
  def validate_parsed_data(data)
137
196
  unless data.is_a?(Hash)
138
197
  raise StorageError, "Invalid capture format: expected JSON object"
@@ -146,6 +205,7 @@ module Support
146
205
  end
147
206
  end
148
207
 
208
+ # Custom error class for storage-related errors.
149
209
  class StorageError < StandardError; end
150
210
  end
151
211
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Support
4
- VERSION = "1.1.1"
4
+ VERSION = "1.1.3"
5
5
  end
@@ -0,0 +1,90 @@
1
+ require 'minitest/autorun'
2
+ require 'fileutils'
3
+ require_relative '../lib/active_capture'
4
+
5
+ # Test suite for the ActiveCapture.flush method
6
+ class ActiveCaptureFlushTest < Minitest::Test
7
+ # Directory where test captures will be stored
8
+ CAPTURES_DIR = 'captures'.freeze
9
+
10
+ # Setup method to prepare the test environment
11
+ def setup
12
+ # Create the captures directory
13
+ FileUtils.mkdir_p(CAPTURES_DIR)
14
+ # Create a user subdirectory
15
+ @user_dir = File.join(CAPTURES_DIR, 'user')
16
+ FileUtils.mkdir_p(@user_dir)
17
+ # Add test files to the user directory
18
+ File.write(File.join(@user_dir, 'test1.json'), '{}')
19
+ File.write(File.join(@user_dir, 'test2.json'), '{}')
20
+ end
21
+
22
+ # Teardown method to clean up after tests
23
+ def teardown
24
+ # Remove the captures directory and its contents
25
+ FileUtils.rm_rf(CAPTURES_DIR)
26
+ end
27
+
28
+ # Test flushing a valid directory with files
29
+ def test_flush_valid_directory
30
+ # Ensure the user directory exists and contains files
31
+ assert Dir.exist?(@user_dir)
32
+ assert_equal 2, Dir.glob(File.join(@user_dir, '*')).size
33
+
34
+ # Call the flush method
35
+ ActiveCapture.flush('user')
36
+
37
+ # Verify the directory is empty after flushing
38
+ assert_empty Dir.glob(File.join(@user_dir, '*'))
39
+ puts "test_flush_valid_directory passed"
40
+ end
41
+
42
+ # Test flushing a non-existent directory
43
+ def test_flush_non_existent_directory
44
+ # Capture the output of the flush method
45
+ output = capture_io do
46
+ ActiveCapture.flush('non_existent_dir')
47
+ end
48
+ # Verify the appropriate error message is displayed
49
+ assert_match /Directory 'captures\/non_existent_dir' does not exist./, output[0]
50
+ puts "test_flush_non_existent_directory passed"
51
+ end
52
+
53
+ # Test flushing an empty directory
54
+ def test_flush_empty_directory
55
+ # Remove and recreate the user directory to ensure it's empty
56
+ FileUtils.rm_rf(@user_dir)
57
+ FileUtils.mkdir_p(@user_dir)
58
+ assert_empty Dir.glob(File.join(@user_dir, '*'))
59
+
60
+ # Capture the output of the flush method
61
+ output = capture_io do
62
+ ActiveCapture.flush('user')
63
+ end
64
+ # Verify the appropriate message is displayed for an empty directory
65
+ assert_match /No files found in captures\/user to delete./, output[0]
66
+ puts "test_flush_empty_directory passed"
67
+ end
68
+
69
+ # Test flushing a directory with a file that has restricted permissions
70
+ def test_flush_with_permission_error
71
+ # Skip this test unless run with restricted permissions
72
+ skip "Run this test with restricted permissions to verify behavior."
73
+
74
+ # Create a protected file with no permissions
75
+ protected_file = File.join(@user_dir, 'protected.json')
76
+ File.write(protected_file, '{}')
77
+ FileUtils.chmod(0o000, protected_file)
78
+
79
+ # Capture the output of the flush method
80
+ output = capture_io do
81
+ ActiveCapture.flush('user')
82
+ end
83
+ # Verify the appropriate error message is displayed for permission issues
84
+ assert_match /Permission denied when deleting file: captures\/user\/protected.json/, output[0]
85
+
86
+ # Reset permissions for cleanup
87
+ FileUtils.chmod(0o644, protected_file)
88
+ puts "test_flush_with_permission_error passed"
89
+ end
90
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_capture
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tanmay Bhawsar
@@ -36,6 +36,7 @@ files:
36
36
  - lib/support/version.rb
37
37
  - test/support/test_capture_storage.rb
38
38
  - test/test_active_capture.rb
39
+ - test/test_active_capture_flush.rb
39
40
  - test/test_helper.rb
40
41
  homepage: https://github.com/bhawsartanmay/active_capture
41
42
  licenses: