Skip to content
This repository was archived by the owner on Jun 1, 2023. It is now read-only.

Commit

Permalink
TransformDataStructure
Browse files Browse the repository at this point in the history
  • Loading branch information
t6d committed Mar 9, 2021
1 parent ecd79eb commit e558d53
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 0 deletions.
1 change: 1 addition & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ namespace :rdoc do
"lib/shopify-cli/process_supervision.rb",
"lib/shopify-cli/project.rb",
"lib/shopify-cli/result.rb",
"lib/shopify-cli/transform_data_structure.rb",
"lib/shopify-cli/tunnel.rb",
"lib/shopify-cli/lazy_delegator.rb",
]
Expand Down
86 changes: 86 additions & 0 deletions lib/shopify-cli/transform_data_structure.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@

module ShopifyCli
##
# `TransformDataStructure` helps to standardize data structure access. It
# traverses nested data structures and can convert
#
# * all strings used as keys to symbols,
# * all strings used as keys from `CamelCase` to `snake_case` and
# * associative array containers, e.g. from Hash to OpenStruct.
#
# Standardizing how a data structure is accessed greatly reduces the risk
# of subtle bugs especially when dealing with API responses.
#
# TransformDataStructure.new(
# symbolize_keys: true,
# underscore_keys: true,
# associative_array_container: OpenStruct
# ).call([{"SomeKey" => "value"}]).tap do |result|
# result.value # => [#<OpenStruct: some_key: "value">]
# end
#
# Since `TransformDataStructure` is a method object, it can easily be chained:
#
# require 'open-uri'
# ShopifyCli::Result
# .call { open("https://jsonplaceholder.typicode.com/todos/1") }
# .map(&TransformDataStructure.new(symbolize_keys: true, underscore_keys: true))
# .value # => { id: 1, user_id: 1, ... }
#
class TransformDataStructure
include ShopifyCli::MethodObject

class << self
private

def valid_associative_array_container(klass)
klass.respond_to?(:new) && klass.method_defined?(:[]=)
end
end

property! :underscore_keys, accepts: [true, false], default: false, reader: :underscore_keys?
property! :symbolize_keys, accepts: [true, false], default: false, reader: :symbolize_keys?
property! :associative_array_container,
accepts: ->(c) { c.respond_to?(:new) && c.method_defined?(:[]=) },
default: Hash

def call(object)
case object
when Array
object.map(&self).map(&:value)
when Hash
object.each.with_object(associative_array_container.new) do |(key, value), result|
result[transform_key(key)] = call(value).value
end
else
ShopifyCli::Result.success(object)
end
end

private

def transform_key(key)
key
.yield_self(&method(:underscore_key))
.yield_self(&method(:symbolize_key))
end

def underscore_key(key)
return key unless underscore_keys? && key.respond_to?(:to_str)

key.to_str.dup.yield_self do |k|
k.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
k.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
k.tr!("-", "_")
k.gsub!(/\s/, "_")
k.gsub!(/__+/, "_")
k.downcase!
end
end

def symbolize_key(key)
return key unless symbolize_keys? && key.respond_to?(:to_sym)
key.to_sym
end
end
end
1 change: 1 addition & 0 deletions lib/shopify_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ module ShopifyCli
autoload :SubCommand, "shopify-cli/sub_command"
autoload :Task, "shopify-cli/task"
autoload :Tasks, "shopify-cli/tasks"
autoload :TransformDataStructure, "shopify-cli/transform_data_structure"
autoload :Tunnel, "shopify-cli/tunnel"

require "shopify-cli/messages/messages"
Expand Down
89 changes: 89 additions & 0 deletions test/shopify-cli/transform_data_structure_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
require "test_helper"

module ShopifyCli
class TransformDataStructureTest < MiniTest::Test
def test_primitive_values_are_returned_as_is
[true, false, 1, "hello", :hello].each do |value|
TransformDataStructure.call(value).tap do |result|
assert_predicate(result, :success?)
assert_equal(value, result.value)
end
end

TransformDataStructure.call(nil).tap do |result|
assert_predicate(result, :success?)
assert_nil(result.value)
end
end

def test_deep_symbolize_keys_of_flat_structure
TransformDataStructure.new(symbolize_keys: true).call({ "a" => 1, "b" => 2 }).tap do |result|
assert_predicate(result, :success?)
assert_equal({ a: 1, b: 2 }, result.value)
end
end

def test_deep_symbolize_keys_of_hash_in_array
TransformDataStructure.new(symbolize_keys: true).call([{ "a" => 1, "b" => 2 }]).tap do |result|
assert_predicate(result, :success?)
assert_equal([{ a: 1, b: 2 }], result.value)
end
end

def test_symbolizing_the_keys_of_a_deeply_nested_data_structure
given_input = { "Nodes" => [{ "ID" => 1, "Active" => true }] }
expected_output = { Nodes: [{ ID: 1, Active: true }] }
TransformDataStructure
.new(symbolize_keys: true)
.call(given_input).tap do |result|
assert_predicate(result, :success?)
assert_equal(expected_output, result.value)
end
end

def test_snake_case_conversion_with_symbolization
given_input = {
"MessageId" => 1,
"TTL" => 3600,
"useTLS" => true,
"Message-Priority" => "high",
"Transport Protocol" => "tcp",
}

expected_output = {
message_id: 1,
ttl: 3600,
use_tls: true,
message_priority: "high",
transport_protocol: "tcp",
}

TransformDataStructure
.new(symbolize_keys: true, underscore_keys: true)
.call(given_input).tap do |result|
assert_predicate(result, :success?)
assert_equal(expected_output, result.value)
end
end

def test_snake_case_conversation_without_symbolzation
given_input = { "MessageId" => 1 }
expected_output = { "message_id" => 1 }

TransformDataStructure
.new(underscore_keys: true)
.call(given_input).tap do |result|
assert_predicate(result, :success?)
assert_equal(expected_output, result.value)
end
end

def test_replacing_hashes_with_another_associative_array_container
TransformDataStructure.new(associative_array_container: OpenStruct).call({ a: 1 }).tap do |result|
assert_predicate(result, :success?)
assert_kind_of(OpenStruct, result.value)
assert_equal(1, result.value.a)
end
end
end
end

0 comments on commit e558d53

Please sign in to comment.