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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/Gemfile.lock +9 -11
- data/README.md +142 -0
- data/dev_suite.gemspec +1 -0
- data/examples/workflow/basic_workflow.rb +15 -0
- data/examples/workflow/composite_workflow.rb +21 -0
- data/examples/workflow/conditional_workflow.rb +17 -0
- data/examples/workflow/full_workflow.rb +79 -0
- data/examples/workflow/loop_workflow.rb +17 -0
- data/examples/workflow/parallel_workflow.rb +17 -0
- data/lib/dev_suite/dev_suite.rb +2 -0
- data/lib/dev_suite/request_builder/builder/base.rb +27 -0
- data/lib/dev_suite/request_builder/builder/builder.rb +10 -0
- data/lib/dev_suite/request_builder/builder/http.rb +32 -0
- data/lib/dev_suite/request_builder/builder.rb +9 -0
- data/lib/dev_suite/request_builder/config/config.rb +11 -0
- data/lib/dev_suite/request_builder/config/configuration.rb +24 -0
- data/lib/dev_suite/request_builder/config.rb +9 -0
- data/lib/dev_suite/request_builder/formatter/base.rb +13 -0
- data/lib/dev_suite/request_builder/formatter/formatter.rb +10 -0
- data/lib/dev_suite/request_builder/formatter/graphql.rb +19 -0
- data/lib/dev_suite/request_builder/formatter.rb +9 -0
- data/lib/dev_suite/request_builder/request_builder.rb +21 -0
- data/lib/dev_suite/request_builder/tool/base.rb +19 -0
- data/lib/dev_suite/request_builder/tool/curl.rb +91 -0
- data/lib/dev_suite/request_builder/tool/tool.rb +11 -0
- data/lib/dev_suite/request_builder/tool/validator/curl.rb +38 -0
- data/lib/dev_suite/request_builder/tool/validator/validator.rb +11 -0
- data/lib/dev_suite/request_builder/tool/validator.rb +11 -0
- data/lib/dev_suite/request_builder/tool.rb +9 -0
- data/lib/dev_suite/request_builder.rb +7 -0
- data/lib/dev_suite/request_logger/adapter/adapter.rb +11 -9
- data/lib/dev_suite/request_logger/adapter/faraday.rb +12 -1
- data/lib/dev_suite/request_logger/adapter/middleware/faraday.rb +3 -3
- data/lib/dev_suite/request_logger/adapter/net_http.rb +15 -6
- data/lib/dev_suite/request_logger/config/configuration.rb +1 -0
- data/lib/dev_suite/request_logger/extractor/base.rb +8 -2
- data/lib/dev_suite/request_logger/extractor/extractor.rb +5 -6
- data/lib/dev_suite/request_logger/extractor/faraday.rb +32 -14
- data/lib/dev_suite/request_logger/extractor/net_http.rb +53 -12
- data/lib/dev_suite/request_logger/logger.rb +9 -3
- data/lib/dev_suite/request_logger/request.rb +12 -0
- data/lib/dev_suite/request_logger/response.rb +34 -9
- data/lib/dev_suite/utils/construct/component/base.rb +13 -0
- data/lib/dev_suite/utils/construct/component/component.rb +1 -0
- data/lib/dev_suite/utils/construct/component/manager.rb +27 -10
- data/lib/dev_suite/utils/construct/component/validator/base.rb +25 -0
- data/lib/dev_suite/utils/construct/component/validator/validation_error.rb +21 -0
- data/lib/dev_suite/utils/construct/component/validator/validation_rule.rb +68 -0
- data/lib/dev_suite/utils/construct/component/validator/validator.rb +15 -0
- data/lib/dev_suite/utils/construct/component/validator.rb +13 -0
- data/lib/dev_suite/utils/construct/config/dependency_handler.rb +25 -34
- data/lib/dev_suite/utils/construct/config/settings/base.rb +35 -26
- data/lib/dev_suite/utils/data/base_operations.rb +61 -0
- data/lib/dev_suite/utils/data/data.rb +19 -0
- data/lib/dev_suite/utils/data/path_access.rb +172 -0
- data/lib/dev_suite/utils/data/search_filter.rb +60 -0
- data/lib/dev_suite/utils/data/serialization.rb +29 -0
- data/lib/dev_suite/utils/data/transformations.rb +45 -0
- data/lib/dev_suite/utils/data.rb +9 -0
- data/lib/dev_suite/utils/dependency_loader.rb +2 -2
- data/lib/dev_suite/utils/emoji.rb +1 -0
- data/lib/dev_suite/utils/file_loader/file_loader.rb +1 -5
- data/lib/dev_suite/utils/file_loader/loader/json.rb +4 -1
- data/lib/dev_suite/utils/file_loader/loader/loader.rb +23 -19
- data/lib/dev_suite/utils/file_loader/loader.rb +0 -2
- data/lib/dev_suite/utils/file_writer/atomic_writer.rb +53 -0
- data/lib/dev_suite/utils/file_writer/backup_manager.rb +21 -0
- data/lib/dev_suite/utils/file_writer/file_writer.rb +24 -0
- data/lib/dev_suite/utils/file_writer/writer/base.rb +43 -0
- data/lib/dev_suite/utils/file_writer/writer/json.rb +24 -0
- data/lib/dev_suite/utils/file_writer/writer/text.rb +27 -0
- data/lib/dev_suite/utils/file_writer/writer/writer.rb +14 -0
- data/lib/dev_suite/utils/file_writer/writer/yaml.rb +44 -0
- data/lib/dev_suite/utils/file_writer/writer.rb +11 -0
- data/lib/dev_suite/utils/file_writer/writer_manager.rb +32 -0
- data/lib/dev_suite/utils/file_writer.rb +9 -0
- data/lib/dev_suite/utils/store/config/config.rb +13 -0
- data/lib/dev_suite/utils/store/config/configuration.rb +30 -0
- data/lib/dev_suite/utils/store/config.rb +11 -0
- data/lib/dev_suite/utils/store/driver/base.rb +35 -0
- data/lib/dev_suite/utils/store/driver/driver.rb +18 -0
- data/lib/dev_suite/utils/store/driver/file.rb +61 -0
- data/lib/dev_suite/utils/store/driver/memory.rb +42 -0
- data/lib/dev_suite/utils/store/driver.rb +11 -0
- data/lib/dev_suite/utils/store/store.rb +69 -0
- data/lib/dev_suite/utils/store.rb +9 -0
- data/lib/dev_suite/utils/utils.rb +17 -5
- data/lib/dev_suite/utils/warning_handler.rb +25 -0
- data/lib/dev_suite/version.rb +1 -1
- data/lib/dev_suite/workflow/engine.rb +27 -0
- data/lib/dev_suite/workflow/step/base.rb +48 -0
- data/lib/dev_suite/workflow/step/composite.rb +27 -0
- data/lib/dev_suite/workflow/step/conditional.rb +23 -0
- data/lib/dev_suite/workflow/step/loop.rb +21 -0
- data/lib/dev_suite/workflow/step/parallel.rb +18 -0
- data/lib/dev_suite/workflow/step/step.rb +13 -0
- data/lib/dev_suite/workflow/step.rb +9 -0
- data/lib/dev_suite/workflow/step_context.rb +32 -0
- data/lib/dev_suite/workflow/workflow.rb +35 -0
- data/lib/dev_suite/workflow.rb +7 -0
- metadata +87 -3
- 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
|
-
#
|
21
|
-
def
|
22
|
-
|
23
|
-
"
|
24
|
-
|
25
|
-
|
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.
|
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
|
@@ -5,50 +5,41 @@ module DevSuite
|
|
5
5
|
module Construct
|
6
6
|
module Config
|
7
7
|
module DependencyHandler
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
25
|
+
private
|
30
26
|
|
31
|
-
|
32
|
-
|
33
|
-
|
27
|
+
def track_missing_dependency(missing_dependencies)
|
28
|
+
@missing_dependencies ||= []
|
29
|
+
@missing_dependencies += missing_dependencies
|
30
|
+
end
|
34
31
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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 =
|
11
|
+
@settings = Utils::Data.deep_merge(@default_settings, settings)
|
12
12
|
end
|
13
13
|
|
14
14
|
def set(*keys, value)
|
15
|
-
key_path =
|
16
|
-
|
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 =
|
25
|
-
|
26
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|