diff --git a/lib/schemable/schema_modifier.rb b/lib/schemable/schema_modifier.rb new file mode 100644 index 0000000..7234801 --- /dev/null +++ b/lib/schemable/schema_modifier.rb @@ -0,0 +1,142 @@ +module Schemable + class SchemaModifier + def parse_path(path) + path.split('.').map(&:to_sym) + end + + def path_exists?(schema, path) + path_segments = parse_path(path) + + path_segments.reduce(schema) do |current_segment, next_segment| + if current_segment.is_a?(Array) + # The regex pattern '/\[(\d+)\]|\d+/' matches square brackets containing one or more digits, + # or standalone digits. Used for parsing array indices in a path. + index = next_segment.to_s.match(/\[(\d+)\]|\d+/)[1] + # The regex pattern '/\A\d+\z/' matches a sequence of one or more digits from the start ('\A') + # to the end ('\z') of a string. It checks if a string consists of only digits. + return false if index.nil? || !index.match?(/\A\d+\z/) || index.to_i >= current_segment.length + + current_segment[index.to_i] + else + return false unless current_segment.is_a?(Hash) && current_segment.key?(next_segment) + + current_segment[next_segment] + end + end + + true + end + + def deep_merge_hashes(destination, new_data) + if destination.is_a?(Array) && new_data.is_a?(Array) + destination.concat(new_data) + elsif destination.is_a?(Array) && new_data.is_a?(Hash) + destination.push(new_data) + elsif destination.is_a?(Hash) && new_data.is_a?(Hash) + new_data.each do |key, value| + if destination[key].is_a?(Hash) && value.is_a?(Hash) + destination[key] = deep_merge_hashes(destination[key], value) + elsif destination[key].is_a?(Array) && value.is_a?(Array) + destination[key].concat(value) + elsif destination[key].is_a?(Array) && value.is_a?(Hash) + destination[key].push(value) + else + destination[key] = value + end + end + end + + destination + end + + def add_properties(original_schema, new_schema, path) + return deep_merge_hashes(original_schema, new_schema) if path == '.' + + unless path_exists?(original_schema, path) + puts "Error: Path '#{path}' does not exist in the original schema" + return original_schema + end + + path_segments = parse_path(path) + current_segment = original_schema + last_segment = path_segments.pop + + # Navigate to the specified location in the schema + path_segments.each do |segment| + if current_segment.is_a?(Array) + index = segment.to_s.match(/\[(\d+)\]|\d+/)[1] + if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length + current_segment = current_segment[index.to_i] + else + puts "Error: Invalid index in path '#{path}'" + return original_schema + end + elsif current_segment.is_a?(Hash) && current_segment.key?(segment) + current_segment = current_segment[segment] + else + puts "Error: Expected a Hash but found #{current_segment.class} in path '#{path}'" + return original_schema + end + end + + # Merge the new schema into the specified location + if current_segment.is_a?(Array) + index = last_segment.to_s.match(/\[(\d+)\]|\d+/)[1] + if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length + current_segment[index.to_i] = deep_merge_hashes(current_segment[index.to_i], new_schema) + else + puts "Error: Invalid index in path '#{path}'" + end + else + current_segment[last_segment] = deep_merge_hashes(current_segment[last_segment], new_schema) + end + + original_schema + end + + def delete_properties(original_schema, path) + return original_schema if path == '.' + + unless path_exists?(original_schema, path) + puts "Error: Path '#{path}' does not exist in the original schema" + return original_schema + end + + path_segments = parse_path(path) + current_segment = original_schema + last_segment = path_segments.pop + + # Navigate to the parent of the last segment in the path + path_segments.each do |segment| + if current_segment.is_a?(Array) + index = segment.to_s.match(/\[(\d+)\]|\d+/)[1] + if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length + current_segment = current_segment[index.to_i] + else + puts "Error: Invalid index in path '#{path}'" + return original_schema + end + elsif current_segment.is_a?(Hash) && current_segment.key?(segment) + current_segment = current_segment[segment] + else + puts "Error: Expected a Hash but found #{current_segment.class} in path '#{path}'" + return original_schema + end + end + + # Delete the last segment in the path + if current_segment.is_a?(Array) + index = last_segment.to_s.match(/\[(\d+)\]|\d+/)[1] + if index&.match?(/\A\d+\z/) && index.to_i < current_segment.length + current_segment.delete_at(index.to_i) + else + puts "Error: Invalid index in path '#{path}'" + end + else + current_segment.delete(last_segment) + end + + original_schema + end + end +end diff --git a/sig/schemable/schema_modifier.rbs b/sig/schemable/schema_modifier.rbs new file mode 100644 index 0000000..76fa6f8 --- /dev/null +++ b/sig/schemable/schema_modifier.rbs @@ -0,0 +1,42 @@ +# == SchemaModifier +# +# This module provides methods for working with Hash/JSON-like schemas. +# It includes methods to parse paths, check if a path exists in a schema, +# deep merge two hashes or an array and a hash, add properties to a specific +# location in a schema, and delete properties at a specified path. +# +# === Examples +# +# schema_modifier = Schemable::SchemaModifier.new +# +# path = "properties.name.items.[0].properties.age" +# parsed_path = schema_modifier.parse_path(path) +# # => [:properties, :name, :items, :'[0]', :properties, :age] +# +# schema = { properties: { name: "John" } } +# exists = schema_modifier.path_exists?(schema, "properties.name") +# # => true +# +# new_data = { age: 25 } +# merged_data = schema_modifier.deep_merge_hashes({ name: "John" }, new_data) +# # => { name: "John", age: 25 } +# +# original_schema = { properties: { name: "John" } } +# new_schema = { age: 25 } +# updated_schema = schema_modifier.add_properties(original_schema, new_schema, "properties") +# # => { properties: { name: "John", age: 25 } } +# +# schema = { properties: { name: "John", age: 25 } } +# path_to_delete = "properties.name.age" +# updated_schema = schema_modifier.delete_properties(schema, path_to_delete) +# # => { properties: { name: "John" } } +# +module Schemable + class SchemaModifier + def parse_path: (path: String) -> Array[Symbol] + def path_exists?: (schema: Hash[Symbol, any], path: String) -> bool + def deep_merge_hashes: (destination: Hash[Symbol, any], new_data: Hash[Symbol, any]) -> (Hash[Symbol, any] | Array[any]) + def add_properties: (original_schema: (Hash[Symbol, any] | Array[any]), new_schema: Hash[Symbol, any], path: String) -> (Hash[Symbol, any] | Array[any]) + def delete_properties: (original_schema: (Hash[Symbol, any] | Array[any]), path: String) -> (Hash[Symbol, any] | Array[any]) + end +end