This repository was archived by the owner on Jun 1, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 206
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
177 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |