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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +12 -0
- data/.github/workflows/ci.yml +1 -1
- data/.sonarcloud.properties +23 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +17 -33
- data/README.md +216 -0
- data/dev_suite.gemspec +1 -0
- data/examples/helpers/api_helper.rb +51 -0
- data/examples/helpers/data_helper.rb +72 -0
- data/examples/helpers/helpers.rb +4 -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/order_processing_workflow.rb +163 -0
- data/examples/workflow/parallel_workflow.rb +17 -0
- data/lib/dev_suite/dev_suite.rb +3 -0
- data/lib/dev_suite/method_tracer/config/config.rb +11 -0
- data/lib/dev_suite/method_tracer/config/configuration.rb +16 -0
- data/lib/dev_suite/method_tracer/config.rb +9 -0
- data/lib/dev_suite/method_tracer/helpers.rb +41 -0
- data/lib/dev_suite/method_tracer/logger.rb +46 -0
- data/lib/dev_suite/method_tracer/method_tracer.rb +20 -0
- data/lib/dev_suite/method_tracer/tracer.rb +65 -0
- data/lib/dev_suite/method_tracer.rb +7 -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 +43 -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 +19 -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/logger.rb +7 -5
- 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 +56 -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 +21 -0
- data/lib/dev_suite/workflow/step.rb +9 -0
- data/lib/dev_suite/workflow/step_context.rb +46 -0
- data/lib/dev_suite/workflow/workflow.rb +39 -0
- data/lib/dev_suite/workflow.rb +7 -0
- metadata +101 -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, &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.
|
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,66 @@ 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, {})
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|