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 # => [#] # 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