@@ -361,7 +371,8 @@ 🔧 Basic Usage
snake = MySnakedHash.new(:a => "a", "b" => "b", 2 => 2, "VeryFineHat" => "Feathers")
snake.a # => 'a'
snake.b # => 'b'
-snake[2] # 2
+snake[2] # => 2
+snake["2"] # => nil, note that this gem only affects string / symbol keys.
snake.very_fine_hat # => 'Feathers'
snake[:very_fine_hat] # => 'Feathers'
snake["very_fine_hat"] # => 'Feathers'
@@ -372,11 +383,109 @@ 🔧 Basic Usage
Note also that keys which do not respond to to_sym, because they don’t have a natural conversion to a Symbol,
are left as-is.
+Serialization
+
+class MySerializedSnakedHash < Hashie::Mash
+ include SnakyHash::Snake.new(
+ key_type: :symbol, # default :string
+ serializer: true, # default: false
+ )
+end
+
+snake = MySerializedSnakedHash.new(:a => "a", "b" => "b", 2 => 2, "VeryFineHat" => "Feathers") # => {a: "a", b: "b", 2 => 2, very_fine_hat: "Feathers"}
+dump = MySerializedSnakedHash.dump(snake) # => "{\"a\":\"a\",\"b\":\"b\",\"2\":2,\"very_fine_hat\":\"Feathers\"}"
+hydrated = MySerializedSnakedHash.load(dump) # => {a: "a", b: "b", "2": 2, very_fine_hat: "Feathers"}
+hydrated.class # => MySerializedSnakedHash
+hydrated.a # => 'a'
+hydrated.b # => 'b'
+hydrated[2] # => nil # NOTE: this is the opposite of snake[2] => 2
+hydrated["2"] # => 2 # NOTE: this is the opposite of snake["2"] => nil
+hydrated.very_fine_hat # => 'Feathers'
+hydrated[:very_fine_hat] # => 'Feathers'
+hydrated["very_fine_hat"] # => 'Feathers'
+
+
+Note that the key VeryFineHat changed to very_fine_hat.
+That is indeed the point of this library, so not a bug.
+
+Note that the key 2 changed to "2" (because JSON keys are strings).
+When the JSON dump was reloaded it did not know to restore it as 2 instead of "2".
+This is also not a bug, though if you need different behavior, there is a solution in the next section.
+
+Extensions
+
+You can write your own arbitrary extensions:
+
+
+ - “Hash Load” extensions operate on the hash, and nested hashes
+
+ - use
::load_hash_extensions.add(:extension_name) { |hash| }
+
+
+
+ - “Load” extensions operate on the values, and nested hash’s values, if any
+
+ - use
::load_extensions.add(:extension_name) { |value| }
+
+
+
+ - “Dump” extensions operate on the values, and nested hash’s values, if any
+
+ - use
::dump_extensions.add(:extension_name) { |value| }
+
+
+
+
+
+Example
+
+Let’s say I want all integer-like keys, except 0, to be integer keys,
+while 0 converts to, and stays, a string forever.
+
+class MyExtSnakedHash < Hashie::Mash
+ include SnakyHash::Snake.new(
+ key_type: :symbol, # default :string
+ serializer: true, # default: false
+ )
+end
+
+MyExtSnakedHash.load_hash_extensions.add(:non_zero_keys_to_int) do |value|
+ if value.is_a?(Hash)
+ value.transform_keys do |key|
+ key_int = key.to_s.to_i
+ if key_int > 0
+ key_int
+ else
+ key
+ end
+ end
+ else
+ value
+ end
+end
+
+snake = MyExtSnakedHash.new(1 => "a", 0 => 4, "VeryFineHat" => {3 => "v", 5 => 7, :very_fine_hat => "feathers"}) # => {1 => "a", 0 => 4, very_fine_hat: {3 => "v", 5 => 7, very_fine_hat: "feathers"}}
+dump = MyExtSnakedHash.dump(snake) # => "{\"1\":\"a\",\"0\":4,\"very_fine_hat\":{\"3\":\"v\",\"5\":7,\"very_fine_hat\":\"feathers\"}}"
+hydrated = MyExtSnakedHash.load(dump) # => {1 => "a", "0": 4, very_fine_hat: {3 => "v", 5 => 7, very_fine_hat: "feathers"}}
+hydrated.class # => MyExtSnakedHash
+hydrated["1"] # => nil
+hydrated[1] # => "a"
+hydrated["0"] # => 4
+hydrated[0] # => nil
+hydrated.very_fine_hat # => {3 => "v", 5 => 7, very_fine_hat: "feathers"}
+hydrated.very_fine_hat.very_fine_hat # => "feathers"
+hydrated.very_fine_hat[:very_fine_hat] # => 'feathers'
+hydrated.very_fine_hat["very_fine_hat"] # => 'feathers'
+
+
+See the specs for more examples.
+
Stranger Things
I don’t recommend using these features… but they exist (for now).
You can still access the original un-snaked camel keys.
-And through them you can even use un-snaked camel methods.
+And through them you can even use un-snaked camel methods.
+But don’t.
snake.key?("VeryFineHat") # => true
snake["VeryFineHat"] # => 'Feathers'
@@ -525,7 +634,7 @@ 🤑 One more thing
diff --git a/doc/file.SECURITY.html b/doc/file.SECURITY.html
index d43bea8..dbc8288 100644
--- a/doc/file.SECURITY.html
+++ b/doc/file.SECURITY.html
@@ -104,7 +104,7 @@ Snaky Hash for Enterprise
diff --git a/doc/index.html b/doc/index.html
index 93f1274..5e5b3fb 100644
--- a/doc/index.html
+++ b/doc/index.html
@@ -57,7 +57,7 @@
- SnakyHash
+
🐍 SnakyHash

@@ -65,14 +65,22 @@

-
This library is similar in purpose to the HashWithIndifferentAccess that is famously used in Rails.
+
This library is similar in purpose to the HashWithIndifferentAccess that is famously used in Rails, but does a lot more.
-
This gem is used by oauth, oauth2, and other, gems to normalize hash keys to snake_case and lookups,
+
This gem is used by oauth and oauth2 gems to normalize hash keys to snake_case and lookups,
and provide a nice psuedo-object interface.
-
It can be thought of as a mashup, with upgrades, to the Rash (specifically the rash_alt flavor), which is a special Mash, made popular by the hashie gem, and the serialized_hashie gem by krystal.
+
It can be thought of as a mashup of:
-
Classes that include SnakyHash::Snake should inherit from Hashie::Mash.
+
+ -
+
Rash (specifically the rash_alt flavor), which is a special Mash, made popular by the hashie gem, and
+ -
+
serialized_hashie gem by krystal
+
+
+
+
Classes that include SnakyHash::Snake.new should inherit from Hashie::Mash.
New for v2.0.2: Serialization Support
@@ -87,6 +95,8 @@
New for v2.0.2: Serialization Suppor
end
+
✨ Also new dump & load plugin extensions to control the way your data is dumped and loaded.
+
@@ -361,7 +371,8 @@ 🔧 Basic Usage
snake = MySnakedHash.new(:a => "a", "b" => "b", 2 => 2, "VeryFineHat" => "Feathers")
snake.a # => 'a'
snake.b # => 'b'
-snake[2] # 2
+snake[2] # => 2
+snake["2"] # => nil, note that this gem only affects string / symbol keys.
snake.very_fine_hat # => 'Feathers'
snake[:very_fine_hat] # => 'Feathers'
snake["very_fine_hat"] # => 'Feathers'
@@ -372,11 +383,109 @@ 🔧 Basic Usage
Note also that keys which do not respond to to_sym, because they don’t have a natural conversion to a Symbol,
are left as-is.
+Serialization
+
+class MySerializedSnakedHash < Hashie::Mash
+ include SnakyHash::Snake.new(
+ key_type: :symbol, # default :string
+ serializer: true, # default: false
+ )
+end
+
+snake = MySerializedSnakedHash.new(:a => "a", "b" => "b", 2 => 2, "VeryFineHat" => "Feathers") # => {a: "a", b: "b", 2 => 2, very_fine_hat: "Feathers"}
+dump = MySerializedSnakedHash.dump(snake) # => "{\"a\":\"a\",\"b\":\"b\",\"2\":2,\"very_fine_hat\":\"Feathers\"}"
+hydrated = MySerializedSnakedHash.load(dump) # => {a: "a", b: "b", "2": 2, very_fine_hat: "Feathers"}
+hydrated.class # => MySerializedSnakedHash
+hydrated.a # => 'a'
+hydrated.b # => 'b'
+hydrated[2] # => nil # NOTE: this is the opposite of snake[2] => 2
+hydrated["2"] # => 2 # NOTE: this is the opposite of snake["2"] => nil
+hydrated.very_fine_hat # => 'Feathers'
+hydrated[:very_fine_hat] # => 'Feathers'
+hydrated["very_fine_hat"] # => 'Feathers'
+
+
+Note that the key VeryFineHat changed to very_fine_hat.
+That is indeed the point of this library, so not a bug.
+
+Note that the key 2 changed to "2" (because JSON keys are strings).
+When the JSON dump was reloaded it did not know to restore it as 2 instead of "2".
+This is also not a bug, though if you need different behavior, there is a solution in the next section.
+
+Extensions
+
+You can write your own arbitrary extensions:
+
+
+ - “Hash Load” extensions operate on the hash, and nested hashes
+
+ - use
::load_hash_extensions.add(:extension_name) { |hash| }
+
+
+
+ - “Load” extensions operate on the values, and nested hash’s values, if any
+
+ - use
::load_extensions.add(:extension_name) { |value| }
+
+
+
+ - “Dump” extensions operate on the values, and nested hash’s values, if any
+
+ - use
::dump_extensions.add(:extension_name) { |value| }
+
+
+
+
+
+Example
+
+Let’s say I want all integer-like keys, except 0, to be integer keys,
+while 0 converts to, and stays, a string forever.
+
+class MyExtSnakedHash < Hashie::Mash
+ include SnakyHash::Snake.new(
+ key_type: :symbol, # default :string
+ serializer: true, # default: false
+ )
+end
+
+MyExtSnakedHash.load_hash_extensions.add(:non_zero_keys_to_int) do |value|
+ if value.is_a?(Hash)
+ value.transform_keys do |key|
+ key_int = key.to_s.to_i
+ if key_int > 0
+ key_int
+ else
+ key
+ end
+ end
+ else
+ value
+ end
+end
+
+snake = MyExtSnakedHash.new(1 => "a", 0 => 4, "VeryFineHat" => {3 => "v", 5 => 7, :very_fine_hat => "feathers"}) # => {1 => "a", 0 => 4, very_fine_hat: {3 => "v", 5 => 7, very_fine_hat: "feathers"}}
+dump = MyExtSnakedHash.dump(snake) # => "{\"1\":\"a\",\"0\":4,\"very_fine_hat\":{\"3\":\"v\",\"5\":7,\"very_fine_hat\":\"feathers\"}}"
+hydrated = MyExtSnakedHash.load(dump) # => {1 => "a", "0": 4, very_fine_hat: {3 => "v", 5 => 7, very_fine_hat: "feathers"}}
+hydrated.class # => MyExtSnakedHash
+hydrated["1"] # => nil
+hydrated[1] # => "a"
+hydrated["0"] # => 4
+hydrated[0] # => nil
+hydrated.very_fine_hat # => {3 => "v", 5 => 7, very_fine_hat: "feathers"}
+hydrated.very_fine_hat.very_fine_hat # => "feathers"
+hydrated.very_fine_hat[:very_fine_hat] # => 'feathers'
+hydrated.very_fine_hat["very_fine_hat"] # => 'feathers'
+
+
+See the specs for more examples.
+
Stranger Things
I don’t recommend using these features… but they exist (for now).
You can still access the original un-snaked camel keys.
-And through them you can even use un-snaked camel methods.
+And through them you can even use un-snaked camel methods.
+But don’t.
snake.key?("VeryFineHat") # => true
snake["VeryFineHat"] # => 'Feathers'
@@ -525,7 +634,7 @@ 🤑 One more thing
diff --git a/doc/top-level-namespace.html b/doc/top-level-namespace.html
index e4d20a2..c5467c4 100644
--- a/doc/top-level-namespace.html
+++ b/doc/top-level-namespace.html
@@ -100,7 +100,7 @@ Defined Under Namespace
diff --git a/lib/snaky_hash/serializer.rb b/lib/snaky_hash/serializer.rb
index dc900ff..e2c2124 100644
--- a/lib/snaky_hash/serializer.rb
+++ b/lib/snaky_hash/serializer.rb
@@ -25,7 +25,7 @@ def dump(obj)
def load(raw_hash)
hash = JSON.parse(presence(raw_hash) || "{}")
- hash = load_hash(hash)
+ hash = load_value(new(hash))
new(hash)
end
@@ -107,17 +107,18 @@ def dump_value(value)
def load_hash(hash)
# The hash will be a raw hash, not a hash of this class.
# So first we make it a hash of this class.
- self[hash].transform_values do |value|
+ hash.transform_values do |value|
load_value(value)
end
end
def load_value(value)
if value.is_a?(::Hash)
- hash = load_hash_extensions.run(value)
+ # The extension might call `transform_keys, or similar, thus returning a new vanilla hash
+ hash = load_hash_extensions.run(new(value))
# If the result is still a hash, we'll return that here
- return load_hash(hash) if hash.is_a?(::Hash)
+ return load_hash(new(hash)) if hash.is_a?(::Hash)
# If the result is not a hash, we'll just return whatever
# was returned as a normal value.
diff --git a/spec/shared_examples/a_serialized_hash.rb b/spec/shared_examples/a_serialized_hash.rb
index 8641d9c..3ff313c 100644
--- a/spec/shared_examples/a_serialized_hash.rb
+++ b/spec/shared_examples/a_serialized_hash.rb
@@ -114,7 +114,7 @@
}
hash = subject.load('{"some_hash":{"name":"Michael"}}')
expect(hash).to be_a Hashie::Mash
- expect(hash).to eq({"some_hash" => {"namf" => "Michael"}})
+ expect(hash).to eq({"some_hasi" => {"namf" => "Michael"}})
end
it "passes hashes through their own extension and return non-hash values properly" do
diff --git a/spec/snaky_hash/snake_spec.rb b/spec/snaky_hash/snake_spec.rb
index 1add986..69b5825 100644
--- a/spec/snaky_hash/snake_spec.rb
+++ b/spec/snaky_hash/snake_spec.rb
@@ -32,4 +32,80 @@ class TheSnakedHash < Hashie::Mash
expect(res).not_to be_a(TheSnakedHash)
expect(res).to be_a(Hash)
end
+
+ context "when serializer: true" do
+ let(:snaky_klass) do
+ klass = Class.new(Hashie::Mash) do
+ include SnakyHash::Snake.new(
+ key_type: :symbol, # default :string
+ serializer: true, # default: false
+ )
+ end
+
+ klass.load_hash_extensions.add(:keys_are_based) do |value|
+ if value.is_a?(Hash)
+ value.keys.sort.each_with_index do |key, index|
+ key_int = key.to_s.to_i
+ ref = value.delete(key)
+ encoded_key = if key_int > 0
+ # See: https://idiosyncratic-ruby.com/4-what-the-pack.html#m0--base64-encoding-rfc-4648
+ "dog-#{index}"
+ else
+ "cat-#{index}"
+ end
+ value[encoded_key] = ref
+ end
+ end
+ value
+ end
+
+ klass
+ end
+
+ let(:snake) { snaky_klass.new("1" => "a", "0" => 4, "VeryFineHat" => {"3" => "v", "5" => 7, :very_fine_hat => "feathers"}) }
+ let(:dump) { snaky_klass.dump(snake) }
+ let(:hydrated) { snaky_klass.load(dump) }
+
+ it "can initialize" do
+ expect(snake).to eq({"0": 4, "1": "a", very_fine_hat: {"3": "v", "5": 7, very_fine_hat: "feathers"}})
+ end
+
+ it "can dump" do
+ expect(dump).to eq "{\"1\":\"a\",\"0\":4,\"very_fine_hat\":{\"3\":\"v\",\"5\":7,\"very_fine_hat\":\"feathers\"}}"
+ end
+
+ it "can load" do
+ expect(hydrated).to eq(
+ {
+ dog_1: "a",
+ cat_0: 4,
+ cat_2: {
+ dog_0: "v",
+ dog_1: 7,
+ cat_2: "feathers",
+ },
+ },
+ )
+ end
+
+ it "can access keys" do
+ expect(hydrated["1"]).to be_nil
+ expect(hydrated[1]).to be_nil
+ expect(hydrated["dog_1"]).to eq("a")
+ expect(hydrated[:dog_1]).to eq("a")
+ expect(hydrated.dog_1).to eq("a")
+ expect(hydrated["0"]).to be_nil
+ expect(hydrated[0]).to be_nil
+ expect(hydrated["cat_0"]).to eq(4)
+ expect(hydrated[:cat_0]).to eq(4)
+ expect(hydrated.cat_0).to eq(4)
+ expect(hydrated.very_fine_hat).to be_nil
+ expect(hydrated.cat_2).to eq({cat_2: "feathers", dog_0: "v", dog_1: 7})
+ expect(hydrated.cat_2.cat_2).to eq("feathers")
+ expect(hydrated.cat_2["cat_2"]).to eq("feathers")
+ expect(hydrated.cat_2[:cat_2]).to eq("feathers")
+ expect(hydrated.cat_2.dog_0).to eq("v")
+ expect(hydrated.cat_2.dog_1).to eq(7)
+ end
+ end
end