dev_suite 0.2.7 → 0.2.9

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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/Gemfile.lock +9 -11
  4. data/README.md +142 -0
  5. data/dev_suite.gemspec +1 -0
  6. data/examples/workflow/basic_workflow.rb +15 -0
  7. data/examples/workflow/composite_workflow.rb +21 -0
  8. data/examples/workflow/conditional_workflow.rb +17 -0
  9. data/examples/workflow/full_workflow.rb +79 -0
  10. data/examples/workflow/loop_workflow.rb +17 -0
  11. data/examples/workflow/parallel_workflow.rb +17 -0
  12. data/lib/dev_suite/dev_suite.rb +2 -0
  13. data/lib/dev_suite/request_builder/builder/base.rb +27 -0
  14. data/lib/dev_suite/request_builder/builder/builder.rb +10 -0
  15. data/lib/dev_suite/request_builder/builder/http.rb +32 -0
  16. data/lib/dev_suite/request_builder/builder.rb +9 -0
  17. data/lib/dev_suite/request_builder/config/config.rb +11 -0
  18. data/lib/dev_suite/request_builder/config/configuration.rb +24 -0
  19. data/lib/dev_suite/request_builder/config.rb +9 -0
  20. data/lib/dev_suite/request_builder/formatter/base.rb +13 -0
  21. data/lib/dev_suite/request_builder/formatter/formatter.rb +10 -0
  22. data/lib/dev_suite/request_builder/formatter/graphql.rb +19 -0
  23. data/lib/dev_suite/request_builder/formatter.rb +9 -0
  24. data/lib/dev_suite/request_builder/request_builder.rb +21 -0
  25. data/lib/dev_suite/request_builder/tool/base.rb +19 -0
  26. data/lib/dev_suite/request_builder/tool/curl.rb +91 -0
  27. data/lib/dev_suite/request_builder/tool/tool.rb +11 -0
  28. data/lib/dev_suite/request_builder/tool/validator/curl.rb +38 -0
  29. data/lib/dev_suite/request_builder/tool/validator/validator.rb +11 -0
  30. data/lib/dev_suite/request_builder/tool/validator.rb +11 -0
  31. data/lib/dev_suite/request_builder/tool.rb +9 -0
  32. data/lib/dev_suite/request_builder.rb +7 -0
  33. data/lib/dev_suite/request_logger/adapter/adapter.rb +11 -9
  34. data/lib/dev_suite/request_logger/adapter/faraday.rb +12 -1
  35. data/lib/dev_suite/request_logger/adapter/middleware/faraday.rb +3 -3
  36. data/lib/dev_suite/request_logger/adapter/net_http.rb +15 -6
  37. data/lib/dev_suite/request_logger/config/configuration.rb +1 -0
  38. data/lib/dev_suite/request_logger/extractor/base.rb +8 -2
  39. data/lib/dev_suite/request_logger/extractor/extractor.rb +5 -6
  40. data/lib/dev_suite/request_logger/extractor/faraday.rb +32 -14
  41. data/lib/dev_suite/request_logger/extractor/net_http.rb +53 -12
  42. data/lib/dev_suite/request_logger/logger.rb +9 -3
  43. data/lib/dev_suite/request_logger/request.rb +12 -0
  44. data/lib/dev_suite/request_logger/response.rb +34 -9
  45. data/lib/dev_suite/utils/construct/component/base.rb +13 -0
  46. data/lib/dev_suite/utils/construct/component/component.rb +1 -0
  47. data/lib/dev_suite/utils/construct/component/manager.rb +27 -10
  48. data/lib/dev_suite/utils/construct/component/validator/base.rb +25 -0
  49. data/lib/dev_suite/utils/construct/component/validator/validation_error.rb +21 -0
  50. data/lib/dev_suite/utils/construct/component/validator/validation_rule.rb +68 -0
  51. data/lib/dev_suite/utils/construct/component/validator/validator.rb +15 -0
  52. data/lib/dev_suite/utils/construct/component/validator.rb +13 -0
  53. data/lib/dev_suite/utils/construct/config/dependency_handler.rb +25 -34
  54. data/lib/dev_suite/utils/construct/config/settings/base.rb +35 -26
  55. data/lib/dev_suite/utils/data/base_operations.rb +61 -0
  56. data/lib/dev_suite/utils/data/data.rb +19 -0
  57. data/lib/dev_suite/utils/data/path_access.rb +172 -0
  58. data/lib/dev_suite/utils/data/search_filter.rb +60 -0
  59. data/lib/dev_suite/utils/data/serialization.rb +29 -0
  60. data/lib/dev_suite/utils/data/transformations.rb +45 -0
  61. data/lib/dev_suite/utils/data.rb +9 -0
  62. data/lib/dev_suite/utils/dependency_loader.rb +2 -2
  63. data/lib/dev_suite/utils/emoji.rb +1 -0
  64. data/lib/dev_suite/utils/file_loader/file_loader.rb +1 -5
  65. data/lib/dev_suite/utils/file_loader/loader/json.rb +4 -1
  66. data/lib/dev_suite/utils/file_loader/loader/loader.rb +23 -19
  67. data/lib/dev_suite/utils/file_loader/loader.rb +0 -2
  68. data/lib/dev_suite/utils/file_writer/atomic_writer.rb +53 -0
  69. data/lib/dev_suite/utils/file_writer/backup_manager.rb +21 -0
  70. data/lib/dev_suite/utils/file_writer/file_writer.rb +24 -0
  71. data/lib/dev_suite/utils/file_writer/writer/base.rb +43 -0
  72. data/lib/dev_suite/utils/file_writer/writer/json.rb +24 -0
  73. data/lib/dev_suite/utils/file_writer/writer/text.rb +27 -0
  74. data/lib/dev_suite/utils/file_writer/writer/writer.rb +14 -0
  75. data/lib/dev_suite/utils/file_writer/writer/yaml.rb +44 -0
  76. data/lib/dev_suite/utils/file_writer/writer.rb +11 -0
  77. data/lib/dev_suite/utils/file_writer/writer_manager.rb +32 -0
  78. data/lib/dev_suite/utils/file_writer.rb +9 -0
  79. data/lib/dev_suite/utils/store/config/config.rb +13 -0
  80. data/lib/dev_suite/utils/store/config/configuration.rb +30 -0
  81. data/lib/dev_suite/utils/store/config.rb +11 -0
  82. data/lib/dev_suite/utils/store/driver/base.rb +35 -0
  83. data/lib/dev_suite/utils/store/driver/driver.rb +18 -0
  84. data/lib/dev_suite/utils/store/driver/file.rb +61 -0
  85. data/lib/dev_suite/utils/store/driver/memory.rb +42 -0
  86. data/lib/dev_suite/utils/store/driver.rb +11 -0
  87. data/lib/dev_suite/utils/store/store.rb +69 -0
  88. data/lib/dev_suite/utils/store.rb +9 -0
  89. data/lib/dev_suite/utils/utils.rb +17 -5
  90. data/lib/dev_suite/utils/warning_handler.rb +25 -0
  91. data/lib/dev_suite/version.rb +1 -1
  92. data/lib/dev_suite/workflow/engine.rb +27 -0
  93. data/lib/dev_suite/workflow/step/base.rb +48 -0
  94. data/lib/dev_suite/workflow/step/composite.rb +27 -0
  95. data/lib/dev_suite/workflow/step/conditional.rb +23 -0
  96. data/lib/dev_suite/workflow/step/loop.rb +21 -0
  97. data/lib/dev_suite/workflow/step/parallel.rb +18 -0
  98. data/lib/dev_suite/workflow/step/step.rb +13 -0
  99. data/lib/dev_suite/workflow/step.rb +9 -0
  100. data/lib/dev_suite/workflow/step_context.rb +32 -0
  101. data/lib/dev_suite/workflow/workflow.rb +35 -0
  102. data/lib/dev_suite/workflow.rb +7 -0
  103. metadata +87 -3
  104. 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)
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)
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,58 @@ 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, {})
32
27
  end
33
28
 
34
29
  private
35
30
 
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)
31
+ # Extract path from mixed input (strings or arrays) and handle array-like syntax
32
+ def extract_path_from_keys(keys)
33
+ keys = keys.flatten
34
+
35
+ # Handle case where keys is a single dot-separated string with array-like notation
36
+ if keys.size == 1 && keys.first.is_a?(String)
37
+ return parse_string_key(keys.first)
42
38
  end
39
+
40
+ # Otherwise, symbolize all keys and convert string-based array indices
41
+ keys.map { |key| parse_key_component(key) }
43
42
  end
44
43
 
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
44
+ # Parse dot-separated strings and handle array-like notation (e.g., "users[1].avt")
45
+ def parse_string_key(key)
46
+ key.split(".").flat_map { |segment| parse_key_component(segment) }
47
+ end
48
+
49
+ # Parse individual components of the key (convert "users[1]" to [:users, 1])
50
+ def parse_key_component(component)
51
+ if component.match?(/\[\d+\]/)
52
+ # Handle array-like notation in strings like "users[1]"
53
+ component.scan(/[^\[\]]+/).map { |part| integer_or_symbol(part) }
54
+ else
55
+ integer_or_symbol(component)
52
56
  end
53
57
  end
58
+
59
+ # Convert to integer if it's a number, otherwise symbolize
60
+ def integer_or_symbol(part)
61
+ part.match?(/^\d+$/) ? part.to_i : part.to_sym
62
+ end
54
63
  end
55
64
  end
56
65
  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
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevSuite
4
+ module Utils
5
+ module Data
6
+ module SearchFilter
7
+ # Deep search for all occurrences of a key
8
+ def deep_find_by_key(data, search_key)
9
+ result = []
10
+ traverse_and_collect(data) do |key, value|
11
+ result << value if key == search_key
12
+ end
13
+ result
14
+ end
15
+
16
+ # Recursively filter a nested hash or array based on a key-value condition
17
+ def deep_filter_by_key_value(data, filter_key, filter_value)
18
+ case data
19
+ when Hash
20
+ filter_hash_by_key_value(data, filter_key, filter_value)
21
+ when Array
22
+ filter_array_by_key_value(data, filter_key, filter_value)
23
+ else
24
+ raise ArgumentError, "Unsupported data type: #{data.class}"
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Helper method to filter a hash by a key-value condition
31
+ def filter_hash_by_key_value(hash, filter_key, filter_value)
32
+ return hash if hash[filter_key] == filter_value
33
+
34
+ hash.each_with_object({}) do |(key, value), result|
35
+ filtered_value = deep_filter_by_key_value(value, filter_key, filter_value)
36
+ result[key] = filtered_value unless filtered_value.nil? || filtered_value.empty?
37
+ end
38
+ end
39
+
40
+ # Helper method to filter an array by a key-value condition
41
+ def filter_array_by_key_value(array, filter_key, filter_value)
42
+ array.map { |item| deep_filter_by_key_value(item, filter_key, filter_value) }.compact
43
+ end
44
+
45
+ # Helper method to traverse and collect values
46
+ def traverse_and_collect(data, &block)
47
+ case data
48
+ when Hash
49
+ data.each do |key, value|
50
+ block.call(key, value)
51
+ traverse_and_collect(value, &block)
52
+ end
53
+ when Array
54
+ data.each { |item| traverse_and_collect(item, &block) }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end