dev_suite 0.2.7 → 0.2.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +12 -0
  3. data/.github/workflows/ci.yml +1 -1
  4. data/.sonarcloud.properties +23 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +17 -33
  7. data/README.md +216 -0
  8. data/dev_suite.gemspec +1 -0
  9. data/examples/helpers/api_helper.rb +51 -0
  10. data/examples/helpers/data_helper.rb +72 -0
  11. data/examples/helpers/helpers.rb +4 -0
  12. data/examples/workflow/basic_workflow.rb +15 -0
  13. data/examples/workflow/composite_workflow.rb +21 -0
  14. data/examples/workflow/conditional_workflow.rb +17 -0
  15. data/examples/workflow/full_workflow.rb +79 -0
  16. data/examples/workflow/loop_workflow.rb +17 -0
  17. data/examples/workflow/order_processing_workflow.rb +163 -0
  18. data/examples/workflow/parallel_workflow.rb +17 -0
  19. data/lib/dev_suite/dev_suite.rb +3 -0
  20. data/lib/dev_suite/method_tracer/config/config.rb +11 -0
  21. data/lib/dev_suite/method_tracer/config/configuration.rb +16 -0
  22. data/lib/dev_suite/method_tracer/config.rb +9 -0
  23. data/lib/dev_suite/method_tracer/helpers.rb +41 -0
  24. data/lib/dev_suite/method_tracer/logger.rb +46 -0
  25. data/lib/dev_suite/method_tracer/method_tracer.rb +20 -0
  26. data/lib/dev_suite/method_tracer/tracer.rb +65 -0
  27. data/lib/dev_suite/method_tracer.rb +7 -0
  28. data/lib/dev_suite/request_builder/builder/base.rb +27 -0
  29. data/lib/dev_suite/request_builder/builder/builder.rb +10 -0
  30. data/lib/dev_suite/request_builder/builder/http.rb +32 -0
  31. data/lib/dev_suite/request_builder/builder.rb +9 -0
  32. data/lib/dev_suite/request_builder/config/config.rb +11 -0
  33. data/lib/dev_suite/request_builder/config/configuration.rb +24 -0
  34. data/lib/dev_suite/request_builder/config.rb +9 -0
  35. data/lib/dev_suite/request_builder/formatter/base.rb +13 -0
  36. data/lib/dev_suite/request_builder/formatter/formatter.rb +10 -0
  37. data/lib/dev_suite/request_builder/formatter/graphql.rb +19 -0
  38. data/lib/dev_suite/request_builder/formatter.rb +9 -0
  39. data/lib/dev_suite/request_builder/request_builder.rb +21 -0
  40. data/lib/dev_suite/request_builder/tool/base.rb +19 -0
  41. data/lib/dev_suite/request_builder/tool/curl.rb +91 -0
  42. data/lib/dev_suite/request_builder/tool/tool.rb +11 -0
  43. data/lib/dev_suite/request_builder/tool/validator/curl.rb +38 -0
  44. data/lib/dev_suite/request_builder/tool/validator/validator.rb +11 -0
  45. data/lib/dev_suite/request_builder/tool/validator.rb +11 -0
  46. data/lib/dev_suite/request_builder/tool.rb +9 -0
  47. data/lib/dev_suite/request_builder.rb +7 -0
  48. data/lib/dev_suite/request_logger/adapter/adapter.rb +11 -9
  49. data/lib/dev_suite/request_logger/adapter/faraday.rb +12 -1
  50. data/lib/dev_suite/request_logger/adapter/middleware/faraday.rb +3 -3
  51. data/lib/dev_suite/request_logger/adapter/net_http.rb +15 -6
  52. data/lib/dev_suite/request_logger/config/configuration.rb +1 -0
  53. data/lib/dev_suite/request_logger/extractor/base.rb +8 -2
  54. data/lib/dev_suite/request_logger/extractor/extractor.rb +5 -6
  55. data/lib/dev_suite/request_logger/extractor/faraday.rb +32 -14
  56. data/lib/dev_suite/request_logger/extractor/net_http.rb +53 -12
  57. data/lib/dev_suite/request_logger/logger.rb +9 -3
  58. data/lib/dev_suite/request_logger/request.rb +12 -0
  59. data/lib/dev_suite/request_logger/response.rb +34 -9
  60. data/lib/dev_suite/utils/construct/component/base.rb +13 -0
  61. data/lib/dev_suite/utils/construct/component/component.rb +1 -0
  62. data/lib/dev_suite/utils/construct/component/manager.rb +27 -10
  63. data/lib/dev_suite/utils/construct/component/validator/base.rb +25 -0
  64. data/lib/dev_suite/utils/construct/component/validator/validation_error.rb +21 -0
  65. data/lib/dev_suite/utils/construct/component/validator/validation_rule.rb +68 -0
  66. data/lib/dev_suite/utils/construct/component/validator/validator.rb +15 -0
  67. data/lib/dev_suite/utils/construct/component/validator.rb +13 -0
  68. data/lib/dev_suite/utils/construct/config/dependency_handler.rb +25 -34
  69. data/lib/dev_suite/utils/construct/config/settings/base.rb +43 -26
  70. data/lib/dev_suite/utils/data/base_operations.rb +61 -0
  71. data/lib/dev_suite/utils/data/data.rb +19 -0
  72. data/lib/dev_suite/utils/data/path_access.rb +172 -0
  73. data/lib/dev_suite/utils/data/search_filter.rb +60 -0
  74. data/lib/dev_suite/utils/data/serialization.rb +29 -0
  75. data/lib/dev_suite/utils/data/transformations.rb +45 -0
  76. data/lib/dev_suite/utils/data.rb +9 -0
  77. data/lib/dev_suite/utils/dependency_loader.rb +2 -2
  78. data/lib/dev_suite/utils/emoji.rb +19 -0
  79. data/lib/dev_suite/utils/file_loader/file_loader.rb +1 -5
  80. data/lib/dev_suite/utils/file_loader/loader/json.rb +4 -1
  81. data/lib/dev_suite/utils/file_loader/loader/loader.rb +23 -19
  82. data/lib/dev_suite/utils/file_loader/loader.rb +0 -2
  83. data/lib/dev_suite/utils/file_writer/atomic_writer.rb +53 -0
  84. data/lib/dev_suite/utils/file_writer/backup_manager.rb +21 -0
  85. data/lib/dev_suite/utils/file_writer/file_writer.rb +24 -0
  86. data/lib/dev_suite/utils/file_writer/writer/base.rb +43 -0
  87. data/lib/dev_suite/utils/file_writer/writer/json.rb +24 -0
  88. data/lib/dev_suite/utils/file_writer/writer/text.rb +27 -0
  89. data/lib/dev_suite/utils/file_writer/writer/writer.rb +14 -0
  90. data/lib/dev_suite/utils/file_writer/writer/yaml.rb +44 -0
  91. data/lib/dev_suite/utils/file_writer/writer.rb +11 -0
  92. data/lib/dev_suite/utils/file_writer/writer_manager.rb +32 -0
  93. data/lib/dev_suite/utils/file_writer.rb +9 -0
  94. data/lib/dev_suite/utils/logger.rb +7 -5
  95. data/lib/dev_suite/utils/store/config/config.rb +13 -0
  96. data/lib/dev_suite/utils/store/config/configuration.rb +30 -0
  97. data/lib/dev_suite/utils/store/config.rb +11 -0
  98. data/lib/dev_suite/utils/store/driver/base.rb +35 -0
  99. data/lib/dev_suite/utils/store/driver/driver.rb +18 -0
  100. data/lib/dev_suite/utils/store/driver/file.rb +61 -0
  101. data/lib/dev_suite/utils/store/driver/memory.rb +42 -0
  102. data/lib/dev_suite/utils/store/driver.rb +11 -0
  103. data/lib/dev_suite/utils/store/store.rb +69 -0
  104. data/lib/dev_suite/utils/store.rb +9 -0
  105. data/lib/dev_suite/utils/utils.rb +17 -5
  106. data/lib/dev_suite/utils/warning_handler.rb +25 -0
  107. data/lib/dev_suite/version.rb +1 -1
  108. data/lib/dev_suite/workflow/engine.rb +27 -0
  109. data/lib/dev_suite/workflow/step/base.rb +56 -0
  110. data/lib/dev_suite/workflow/step/composite.rb +27 -0
  111. data/lib/dev_suite/workflow/step/conditional.rb +23 -0
  112. data/lib/dev_suite/workflow/step/loop.rb +21 -0
  113. data/lib/dev_suite/workflow/step/parallel.rb +18 -0
  114. data/lib/dev_suite/workflow/step/step.rb +21 -0
  115. data/lib/dev_suite/workflow/step.rb +9 -0
  116. data/lib/dev_suite/workflow/step_context.rb +46 -0
  117. data/lib/dev_suite/workflow/workflow.rb +39 -0
  118. data/lib/dev_suite/workflow.rb +7 -0
  119. metadata +101 -3
  120. data/lib/dev_suite/utils/construct/component/initializer.rb +0 -28
@@ -17,26 +17,28 @@ module DevSuite
17
17
  @registered_components ||= {}
18
18
  end
19
19
 
20
- # Register a new component
21
- def register_component(component_class)
22
- raise ArgumentError,
23
- "#{component_class} must define a component_key" unless component_class.respond_to?(:component_key)
24
-
25
- registered_components[component_class.component_key] = component_class
20
+ # Check if a component is registered
21
+ def component_registered?(component_key)
22
+ unless registered_components.key?(component_key)
23
+ Utils::Logger.log("Component not found for key: #{component_key}", level: :warn, emoji: :warning)
24
+ return false
25
+ end
26
+ true
26
27
  end
27
28
 
28
29
  # Build a single component
29
- def build_component(component_key)
30
+ def build_component(component_key, **options, &block)
30
31
  component_class = registered_components[component_key]
31
32
 
32
33
  raise ArgumentError, "Component not found for key: #{component_key}" unless component_class
33
34
 
34
- component_class.new
35
+ component_class.new(**options, &block)
35
36
  end
36
37
 
37
- # Build multiple components
38
+ # Build multiple components by filtering registered ones
38
39
  def build_components(component_keys)
39
- component_keys.map { |key| build_component(key) }
40
+ component_keys.select { |key| component_registered?(key) }
41
+ .map { |key| build_component(key) }
40
42
  end
41
43
 
42
44
  def build_component_from_instance(instance)
@@ -49,6 +51,21 @@ module DevSuite
49
51
 
50
52
  component_class.last.new
51
53
  end
54
+
55
+ private
56
+
57
+ # Register a new component
58
+ def register_component(component_class)
59
+ raise ArgumentError,
60
+ "#{component_class} must define a component_key" unless component_class.respond_to?(:component_key)
61
+
62
+ registered_components[component_class.component_key] = component_class
63
+ end
64
+
65
+ # Load a dependency and execute a block if successful
66
+ def load_dependency(dependencies = [], on_failure: -> {}, &block)
67
+ DependencyLoader.safe_load_dependencies(dependencies, on_failure: on_failure, &block)
68
+ end
52
69
  end
53
70
  end
54
71
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Construct
6
+ module Component
7
+ module Validator
8
+ class Base
9
+ include ValidationRule
10
+
11
+ def validate!(**args)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ protected
16
+
17
+ def raise_validation_error(field, message)
18
+ raise ValidationError.new(field, message)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Construct
6
+ module Component
7
+ module Validator
8
+ class ValidationError < StandardError
9
+ attr_reader :field, :message
10
+
11
+ def initialize(field, message)
12
+ @field = field
13
+ @message = message
14
+ super("[#{field}] #{message}")
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Construct
6
+ module Component
7
+ module Validator
8
+ module ValidationRule
9
+ def validate_inclusion!(value, allowed_values, field_name: "Value")
10
+ unless allowed_values.include?(value)
11
+ raise_validation_error(
12
+ field_name,
13
+ "#{value} is not valid. Allowed values: #{allowed_values.join(", ")}",
14
+ )
15
+ end
16
+ end
17
+
18
+ def validate_non_empty_string!(value, field_name: "Value")
19
+ unless value.is_a?(String) && !value.strip.empty?
20
+ raise_validation_error(field_name, "must be a non-empty string.")
21
+ end
22
+ end
23
+
24
+ def validate_url!(url, field_name: "URL")
25
+ unless url =~ /\A#{URI::DEFAULT_PARSER.make_regexp(["http", "https"])}\z/
26
+ raise_validation_error(field_name, "#{url} is not a valid URL.")
27
+ end
28
+ end
29
+
30
+ def validate_hash!(value, field_name: "Value")
31
+ unless value.is_a?(Hash)
32
+ raise_validation_error(field_name, "must be a Hash. Provided value is #{value.class.name}.")
33
+ end
34
+ end
35
+
36
+ def validate_array_of_type!(array, klass, field_name: "Array")
37
+ unless array.is_a?(Array) && array.all? { |item| item.is_a?(klass) }
38
+ raise_validation_error(field_name, "must be an Array of #{klass.name} instances.")
39
+ end
40
+ end
41
+
42
+ def validate_range!(value, range, field_name: "Value")
43
+ unless range.include?(value)
44
+ raise_validation_error(field_name, "#{value} is not within the allowed range: #{range}.")
45
+ end
46
+ end
47
+
48
+ def validate_presence!(value, field_name: "Value")
49
+ if value.nil? || (value.respond_to?(:empty?) && value.empty?)
50
+ raise_validation_error(field_name, "is required and cannot be empty.")
51
+ end
52
+ end
53
+
54
+ def validate_type!(value, allowed_types, field_name: "Value")
55
+ return if allowed_types.any? { |type| value.is_a?(type) }
56
+
57
+ allowed_names = allowed_types.map(&:name).join(", ")
58
+ raise_validation_error(
59
+ field_name,
60
+ "must be one of the following types: #{allowed_names}. Got #{value.class.name} instead.",
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Construct
6
+ module Component
7
+ module Validator
8
+ require_relative "validation_error"
9
+ require_relative "validation_rule"
10
+ require_relative "base"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Construct
6
+ module Component
7
+ module Validator
8
+ require_relative "validator/validator"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -5,50 +5,41 @@ module DevSuite
5
5
  module Construct
6
6
  module Config
7
7
  module DependencyHandler
8
- class << self
9
- def included(base)
10
- base.class_eval do
11
- attr_accessor(:missing_dependencies)
12
- end
13
- base.include(InstanceMethods)
14
- end
8
+ # Use a lazy initializer in the getter method
9
+ def missing_dependencies
10
+ @missing_dependencies ||= []
15
11
  end
16
12
 
17
- module InstanceMethods
18
- def initialize(*args)
19
- super
20
- @missing_dependencies ||= []
21
- end
13
+ def remove_failed_dependency(attr_name, option_key, *missing_dependencies)
14
+ track_missing_dependency(missing_dependencies)
22
15
 
23
- def delete_option_on_failure(attr_name, option_key, *missing_dependencies)
16
+ attribute = send(attr_name)
17
+ original_attribute = send("original_#{attr_name}")
18
+
19
+ if original_attribute.is_a?(Array) && original_attribute.include?(option_key)
20
+ attribute.delete(option_key)
24
21
  log_missing_dependency(attr_name, option_key, missing_dependencies)
25
- send(attr_name).delete(option_key)
26
- track_missing_dependency(missing_dependencies)
27
22
  end
23
+ end
28
24
 
29
- private
25
+ private
30
26
 
31
- def track_missing_dependency(missing_dependencies)
32
- @missing_dependencies = missing_dependencies
33
- end
27
+ def track_missing_dependency(missing_dependencies)
28
+ @missing_dependencies ||= []
29
+ @missing_dependencies += missing_dependencies
30
+ end
34
31
 
35
- def log_missing_dependency(attr_name, option_key, missing_dependencies)
36
- missing_dependencies.each do |dependency|
37
- Utils::Logger.log(
38
- "Missing dependency: #{dependency}. " \
39
- "Please add `gem '#{dependency}'` to your Gemfile and run `bundle install`.",
40
- level: :warn,
41
- emoji: :warning,
42
- )
43
-
44
- Utils::Logger.log(
45
- "Deleting option #{option_key} from `config.#{attr_name}`",
46
- level: :warn,
47
- emoji: :warning,
48
- )
49
- end
32
+ def log_missing_dependency(attr_name, option_key, missing_dependencies)
33
+ missing_dependencies.each do |dependency|
34
+ log_warning("Missing dependency: #{dependency}. " \
35
+ "Please add `gem '#{dependency}'` to your Gemfile and run `bundle install`.")
36
+ log_warning("Deleted option #{option_key} from `config.#{attr_name}`")
50
37
  end
51
38
  end
39
+
40
+ def log_warning(message)
41
+ Utils::Logger.log(message, level: :warn, emoji: :warning)
42
+ end
52
43
  end
53
44
  end
54
45
  end
@@ -8,49 +8,66 @@ module DevSuite
8
8
  class Base
9
9
  def initialize(settings = {})
10
10
  @default_settings = settings
11
- @settings = merge_settings(@default_settings, settings)
11
+ @settings = Utils::Data.deep_merge(@default_settings, settings)
12
12
  end
13
13
 
14
14
  def set(*keys, value)
15
- key_path = normalize_keys(keys)
16
- last_key = key_path.pop
17
- target = key_path.each_with_object(@settings) do |key, nested|
18
- nested[key] ||= {}
19
- end
20
- target[last_key] = value
15
+ key_path = extract_path_from_keys(keys)
16
+ Utils::Data.set_value_by_path(@settings, key_path, value)
21
17
  end
22
18
 
23
- def get(*keys)
24
- key_path = normalize_keys(keys)
25
- key_path.reduce(@settings) do |nested, key|
26
- nested.is_a?(Hash) ? nested[key] : nil
27
- end
19
+ def get(*keys, default: nil)
20
+ key_path = extract_path_from_keys(keys)
21
+ value = Utils::Data.get_value_by_path(@settings, key_path)
22
+ value.nil? ? default : value
28
23
  end
29
24
 
30
25
  def reset!
31
- @settings = @default_settings
26
+ @settings = Utils::Data.deep_merge(@default_settings, {})
27
+ end
28
+
29
+ def to_h
30
+ @settings
31
+ end
32
+
33
+ def merge(options)
34
+ Utils::Data.deep_merge(@settings, options)
32
35
  end
33
36
 
34
37
  private
35
38
 
36
- def normalize_keys(keys)
37
- key_path = keys.flatten
38
- if key_path.size == 1 && key_path.first.is_a?(String)
39
- key_path.first.to_s.split(".").map(&:to_sym)
40
- else
41
- key_path.map(&:to_sym)
39
+ # Extract path from mixed input (strings or arrays) and handle array-like syntax
40
+ def extract_path_from_keys(keys)
41
+ keys = keys.flatten
42
+
43
+ # Handle case where keys is a single dot-separated string with array-like notation
44
+ if keys.size == 1 && keys.first.is_a?(String)
45
+ return parse_string_key(keys.first)
42
46
  end
47
+
48
+ # Otherwise, symbolize all keys and convert string-based array indices
49
+ keys.map { |key| parse_key_component(key) }
50
+ end
51
+
52
+ # Parse dot-separated strings and handle array-like notation (e.g., "users[1].avt")
53
+ def parse_string_key(key)
54
+ key.split(".").flat_map { |segment| parse_key_component(segment) }
43
55
  end
44
56
 
45
- def merge_settings(defaults, overrides)
46
- defaults.merge(overrides) do |_key, oldval, newval|
47
- if oldval.is_a?(Hash) && newval.is_a?(Hash)
48
- merge_settings(oldval, newval)
49
- else
50
- newval
51
- end
57
+ # Parse individual components of the key (convert "users[1]" to [:users, 1])
58
+ def parse_key_component(component)
59
+ if component.match?(/\[\d+\]/)
60
+ # Handle array-like notation in strings like "users[1]"
61
+ component.scan(/[^\[\]]+/).map { |part| integer_or_symbol(part) }
62
+ else
63
+ integer_or_symbol(component)
52
64
  end
53
65
  end
66
+
67
+ # Convert to integer if it's a number, otherwise symbolize
68
+ def integer_or_symbol(part)
69
+ part.match?(/^\d+$/) ? part.to_i : part.to_sym
70
+ end
54
71
  end
55
72
  end
56
73
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Data
6
+ module BaseOperations
7
+ # Recursively delete a key from any level in the data structure
8
+ def deep_delete_key(data, key_to_delete)
9
+ case data
10
+ when Hash, Array
11
+ delete_key_from_structure(data, key_to_delete)
12
+ else
13
+ data
14
+ end
15
+ end
16
+
17
+ # Destructive deep merge (modifies hash1)
18
+ def deep_merge!(hash1, hash2)
19
+ return hash1 if hash2.nil?
20
+
21
+ hash2.each do |key, new_val|
22
+ hash1[key] = merge_value(hash1[key], new_val)
23
+ end
24
+ hash1
25
+ end
26
+
27
+ # Non-destructive deep merge
28
+ def deep_merge(hash1, hash2)
29
+ deep_merge!(hash1.dup, hash2)
30
+ end
31
+
32
+ private
33
+
34
+ # Helper to merge individual values based on their types
35
+ def merge_value(old_val, new_val)
36
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
37
+ deep_merge!(old_val, new_val)
38
+ else
39
+ new_val
40
+ end
41
+ end
42
+
43
+ # Helper method to delete key from both Hash and Array recursively
44
+ def delete_key_from_structure(data, key_to_delete)
45
+ case data
46
+ when Hash
47
+ data.each_with_object({}) do |(key, value), result|
48
+ unless key == key_to_delete
49
+ result[key] = deep_delete_key(value, key_to_delete)
50
+ end
51
+ end
52
+ when Array
53
+ data.map { |item| deep_delete_key(item, key_to_delete) }
54
+ else
55
+ data
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Data
6
+ require_relative "base_operations"
7
+ require_relative "transformations"
8
+ require_relative "search_filter"
9
+ require_relative "path_access"
10
+ require_relative "serialization"
11
+
12
+ extend BaseOperations
13
+ extend Transformations
14
+ extend SearchFilter
15
+ extend PathAccess
16
+ extend Serialization
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Data
6
+ module PathAccess
7
+ # Fetch value from a nested structure using a path
8
+ def get_value_by_path(data, path)
9
+ keys = parse_path(path)
10
+ traverse_path_for_get(data, keys)
11
+ end
12
+
13
+ # Set value in a nested structure using a path
14
+ def set_value_by_path(data, path, value)
15
+ keys = parse_path(path)
16
+ last_key = keys.pop
17
+ target = traverse_path_for_set(data, keys)
18
+
19
+ set_final_value(target, last_key, value)
20
+ end
21
+
22
+ # Delete a key from a nested structure using a path
23
+ def delete_key_by_path(data, path)
24
+ keys = parse_path(path)
25
+ last_key = keys.pop
26
+ target = traverse_path_for_delete(data, keys)
27
+
28
+ delete_final_key(target, last_key)
29
+ end
30
+
31
+ private
32
+
33
+ # Parse the path into an array of keys/symbols/integers
34
+ def parse_path(path)
35
+ return path if path.is_a?(Array)
36
+ return [path] if single_symbol_path?(path)
37
+
38
+ parse_symbol_or_string_path(path)
39
+ end
40
+
41
+ # Check if the path is a symbol without dots
42
+ def single_symbol_path?(path)
43
+ path.is_a?(Symbol) && !path.to_s.include?(".")
44
+ end
45
+
46
+ # Parse a symbol or string path into an array of keys
47
+ def parse_symbol_or_string_path(path)
48
+ if path.is_a?(Symbol)
49
+ parse_symbol_path(path)
50
+ else
51
+ parse_string_path(path)
52
+ end
53
+ end
54
+
55
+ # Parse a symbol path that contains dots (e.g., :"test.1")
56
+ def parse_symbol_path(path)
57
+ path.to_s.split(".").map { |part| parse_part(part) }
58
+ end
59
+
60
+ # Parse a string path into keys (e.g., 'users.1.name')
61
+ def parse_string_path(path)
62
+ path.to_s.split(/\.|\[|\]/).reject(&:empty?).map { |part| parse_part(part) }
63
+ end
64
+
65
+ # Parse each part into either a symbol or integer
66
+ def parse_part(part)
67
+ part.match?(/^\d+$/) ? part.to_i : part.to_sym
68
+ end
69
+
70
+ # Helper to traverse the path for getting values
71
+ def traverse_path_for_get(data, keys)
72
+ keys.reduce(data) do |current_data, key|
73
+ check_invalid_path(current_data, key)
74
+
75
+ case current_data
76
+ when Hash
77
+ fetch_from_hash(current_data, key)
78
+ when Array
79
+ fetch_from_array(current_data, key)
80
+ else
81
+ raise KeyError, "Invalid data type at '#{key}'"
82
+ end
83
+ end
84
+ end
85
+
86
+ # Helper to traverse the path for setting values
87
+ def traverse_path_for_set(data, keys)
88
+ keys.reduce(data) do |current_data, key|
89
+ check_invalid_path(current_data, key)
90
+
91
+ case current_data
92
+ when Hash
93
+ current_data[find_existing_key(current_data, key)] ||= {}
94
+ when Array
95
+ current_data[key.to_i] ||= {}
96
+ else
97
+ break nil
98
+ end
99
+ end
100
+ end
101
+
102
+ # Helper to check for invalid paths
103
+ def check_invalid_path(data, key)
104
+ raise KeyError, "Invalid path at '#{key}'" if data.nil?
105
+ end
106
+
107
+ # Fetch value from a hash, trying both symbol and string keys
108
+ def fetch_from_hash(hash, key)
109
+ hash[key.to_sym] || hash[key.to_s]
110
+ end
111
+
112
+ # Fetch value from an array if the key is an integer
113
+ def fetch_from_array(array, key)
114
+ key.is_a?(Integer) ? array[key] : nil
115
+ end
116
+
117
+ # Set the final value in a hash or array
118
+ def set_final_value(target, last_key, value)
119
+ case target
120
+ when Hash
121
+ target[find_existing_key(target, last_key)] = value
122
+ when Array
123
+ if last_key.is_a?(Integer)
124
+ target[last_key] = value
125
+ else
126
+ raise KeyError, "Invalid path or type at '#{last_key}'"
127
+ end
128
+ else
129
+ raise KeyError, "Invalid target type for path at '#{last_key}'"
130
+ end
131
+ end
132
+
133
+ # Check if the key already exists and return the key in its original type (symbol or string)
134
+ def find_existing_key(hash, key)
135
+ return key if hash.key?(key) # Key already exists in original form
136
+ return key.to_s if hash.key?(key.to_s) # Exists as a string
137
+ return key.to_sym if hash.key?(key.to_sym) # Exists as a symbol
138
+
139
+ key # Otherwise, return the key as-is (use the incoming type)
140
+ end
141
+
142
+ # Helper to traverse the path for deletion
143
+ def traverse_path_for_delete(data, keys)
144
+ keys.reduce(data) do |current_data, key|
145
+ check_invalid_path(current_data, key)
146
+
147
+ case current_data
148
+ when Hash
149
+ current_data[find_existing_key(current_data, key)]
150
+ when Array
151
+ current_data[key.to_i]
152
+ else
153
+ raise KeyError, "Invalid data type at '#{key}'"
154
+ end
155
+ end
156
+ end
157
+
158
+ # Helper to delete the final key in a hash or array
159
+ def delete_final_key(target, last_key)
160
+ case target
161
+ when Hash
162
+ target.delete(find_existing_key(target, last_key))
163
+ when Array
164
+ target.delete_at(last_key.to_i) if last_key.is_a?(Integer)
165
+ else
166
+ raise KeyError, "Cannot delete key from unsupported data type"
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end