Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions .github/workflows/ancient.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,26 @@ jobs:
# Ruby 2.3
- ruby: "ruby-2.3"
appraisal: "ruby-2-3"
exec_cmd: "rspec spec/snaky_hash/serializer_spec.rb:13"
exec_cmd: "rake test"
gemfile: "Appraisal.root"
rubygems: "3.3.27"
bundler: "2.3.27"

# # Ruby 2.4
# - ruby: "ruby-2.4"
# appraisal: "ruby-2-4"
# exec_cmd: "rake test"
# gemfile: "Appraisal.root"
# rubygems: "3.3.27"
# bundler: "2.3.27"
#
# # Ruby 2.5
# - ruby: "ruby-2.5"
# appraisal: "ruby-2-5"
# exec_cmd: "rake test"
# gemfile: "Appraisal.root"
# rubygems: "3.3.27"
# bundler: "2.3.27"
# Ruby 2.4
- ruby: "ruby-2.4"
appraisal: "ruby-2-4"
exec_cmd: "rake test"
gemfile: "Appraisal.root"
rubygems: "3.3.27"
bundler: "2.3.27"

# Ruby 2.5
- ruby: "ruby-2.5"
appraisal: "ruby-2-5"
exec_cmd: "rake test"
gemfile: "Appraisal.root"
rubygems: "3.3.27"
bundler: "2.3.27"

steps:
- name: Checkout
Expand Down
4 changes: 2 additions & 2 deletions .rubocop_gradual.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"snaky_hash.gemspec:1781125773": [
[4, 23, 12, "Gemspec/RubyVersionGlobalsUsage: Do not use `RUBY_VERSION` in gemspec file.", 31296028]
],
"spec/shared_contexts/base_hash.rb:829438978": [
[3, 22, 11, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3936427458]
"spec/shared_contexts/base_hash.rb:2702242093": [
[12, 22, 11, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3936427458]
],
"spec/snaky_hash/bad_snake_spec.rb:3931746112": [
[3, 16, 11, "RSpec/DescribeClass: The first argument to describe should be the class or module being tested.", 1577626599]
Expand Down
43 changes: 35 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,31 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
## [Unreleased]
### Added
### Changed
### Fixed
### Deprecated
### Removed
### Fixed
### Security

## [2.0.3] - 2025-05-23
- TAG: [v2.0.3][2.0.3t]
- COVERAGE: 100.00% -- 132/132 lines in 7 files
- BRANCH COVERAGE: 100.00% -- 38/38 branches in 7 files
- 100.00% documented
### Added
- `#dump` instance method injected by `extend SnakyHash::Serializer` (@pboling)
- `dump_hash_extensions` - new feature, analogous to `load_hash_extensions` (@pboling)
- `dump_value_extensions` - alternate name for `dump_extensions` (@pboling)
- `load_value_extensions` - alternate name for `load_extensions` (@pboling)
- Clarifying documentation (@pboling)
### Fixed
- [gh4](https://github.com/oauth-xx/snaky_hash/pull/4) - Serializer extensions dump and load empty values properly (@pboling)
- Fixed `dump_extensions`, `load_extensions`, `load_hash_extensions`
- Intended usage is primarily JSON, and oauth2 gem
- OAuth2 spec can have legitimately empty values (e.g. scopes could be empty)
- Previous logic was inherited from design decisions made by `serialized_hashie` gem; doesn't apply here

## [2.0.2] - 2025-05-21 ([tag][2.0.2t])
## [2.0.2] - 2025-05-21
- TAG: [v2.0.2][2.0.2t]
- COVERAGE: 100.00% -- 119/119 lines in 7 files
- BRANCH COVERAGE: 100.00% -- 35/35 branches in 7 files
- 100.00% documented
Expand All @@ -26,7 +47,8 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
- Documentation site at [snaky-hash.galtzo.com](https://snaky-hash.galtzo.com) (@pboling)
- 100% documented! (@pboling)

## [2.0.1] - 2022-09-23 ([tag][2.0.1t])
## [2.0.1] - 2022-09-23
- TAG: [v2.0.1][2.0.1t]
### Added
- Certificate for signing gem releases (@pboling)
- Gemspec metadata (@pboling)
Expand All @@ -36,7 +58,8 @@ and this project adheres to [Semantic Versioning v2](https://semver.org/spec/v2.
### Changed
- Gem releases are now cryptographically signed (@pboling)

## [2.0.0] - 2022-08-29 ([tag][2.0.0t])
## [2.0.0] - 2022-08-29
- TAG: [v2.0.0][2.0.0t]
### Changed
- **BREAKING**: `SnakeHash::Snake` is now a mixin, now with support for symbol or string keys
```ruby
Expand All @@ -48,18 +71,22 @@ end
- `SnakyHash::StringKeyed`: a Hashie::Mash class with snake-cased String keys
- `SnakyHash::SymbolKeyed`: a Hashie::Mash class with snake-cased Symbol keys

## [1.0.1] - 2022-08-26 ([tag][1.0.1t])
## [1.0.1] - 2022-08-26
- TAG: [v1.0.1][1.0.1t]
### Added
- Missing LICENSE.txt file to release
### Removed
- Accidentally added bundler dependency (vestige of transpec process) is now removed

## [1.0.0] - 2022-08-26 ([tag][1.0.0t])
## [1.0.0] - 2022-08-26
- TAG: [v1.0.0][1.0.0t]
### Added
- Initial release

[Unreleased]: https://gitlab.com/oauth-xx/snaky_hash/-/compare/v2.0.2...main
[2.0.21]: https://gitlab.com/oauth-xx/snaky_hash/-/compare/v2.0.1...v2.0.2
[Unreleased]: https://gitlab.com/oauth-xx/snaky_hash/-/compare/v2.0.3...main
[2.0.3]: https://gitlab.com/oauth-xx/snaky_hash/-/compare/v2.0.2...v2.0.3
[2.0.3t]: https://gitlab.com/oauth-xx/snaky_hash/-/releases/tag/v2.0.3
[2.0.2]: https://gitlab.com/oauth-xx/snaky_hash/-/compare/v2.0.1...v2.0.2
[2.0.2t]: https://gitlab.com/oauth-xx/snaky_hash/-/releases/tag/v2.0.2
[2.0.1]: https://gitlab.com/oauth-xx/snaky_hash/-/compare/v2.0.0...v2.0.1
[2.0.1t]: https://gitlab.com/oauth-xx/snaky_hash/-/releases/tag/v2.0.1
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ GIT
PATH
remote: .
specs:
snaky_hash (2.0.2)
snaky_hash (2.0.3)
hashie (>= 0.1.0, < 6)
version_gem (>= 1.1.8, < 3)

Expand Down
109 changes: 73 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🐍 SnakyHash

[![Version][👽versioni]][👽version] [![License: MIT][📄license-img]][📄license-ref] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![Open Source Helpers][👽oss-helpi]][👽oss-help] [![Depfu][🔑depfui♻️]][🔑depfu] [![Coveralls Test Coverage][🔑coveralls-img]][🔑coveralls] [![QLTY Test Coverage][🔑qlty-covi♻️]][🔑qlty-cov] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![CI JRuby][🚎10-j-wfi]][🚎10-j-wf] [![CI Supported][🚎6-s-wfi]][🚎6-s-wf] [![CI Legacy][🚎4-lg-wfi]][🚎4-lg-wf] [![CI Unsupported][🚎7-us-wfi]][🚎7-us-wf] [![CI Ancient][🚎1-an-wfi]][🚎1-an-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![CodeQL][🖐codeQL-img]][🖐codeQL]
[![Version][👽versioni]][👽version] [![License: MIT][📄license-img]][📄license-ref] [![Downloads Rank][👽dl-ranki]][👽dl-rank] [![Open Source Helpers][👽oss-helpi]][👽oss-help] [![Depfu][🔑depfui♻️]][🔑depfu] [![Coveralls Test Coverage][🔑coveralls-img]][🔑coveralls] [![QLTY Test Coverage][🔑qlty-covi♻️]][🔑qlty-cov] [![QLTY Maintainability][🔑qlty-mnti♻️]][🔑qlty-mnt] [![CI Heads][🚎3-hd-wfi]][🚎3-hd-wf] [![CI Runtime Dependencies @ HEAD][🚎12-crh-wfi]][🚎12-crh-wf] [![CI Current][🚎11-c-wfi]][🚎11-c-wf] [![CI Truffle Ruby][🚎9-t-wfi]][🚎9-t-wf] [![CI JRuby][🚎10-j-wfi]][🚎10-j-wf] [![CI Supported][🚎6-s-wfi]][🚎6-s-wf] [![CI Legacy][🚎4-lg-wfi]][🚎4-lg-wf] [![CI Unsupported][🚎7-us-wfi]][🚎7-us-wf] [![CI Ancient][🚎1-an-wfi]][🚎1-an-wf] [![CI Test Coverage][🚎2-cov-wfi]][🚎2-cov-wf] [![CI Style][🚎5-st-wfi]][🚎5-st-wf] [![CodeQL][🖐codeQL-img]][🖐codeQL]

---

Expand All @@ -14,7 +14,7 @@ and provide a nice psuedo-object interface.
It can be thought of as a mashup of:

* `Rash` (specifically the [`rash_alt`](https://github.com/shishi/rash_alt) flavor), which is a special `Mash`, made popular by the `hashie` gem, and
* `serialized_hashie` [gem by krystal](https://github.com/krystal/serialized-hashie)
* `serialized_hashie` [gem by krystal](https://github.com/krystal/serialized-hashie), rewritten, with some behavior changes

Classes that `include SnakyHash::Snake.new` should inherit from `Hashie::Mash`.

Expand Down Expand Up @@ -46,6 +46,17 @@ SnakyHash::StringKeyed.class_eval do
end
```

or you can create a custom class

```ruby
class MyHash < Hashie::Mash
include SnakyHash::Snake.new(key_type: :string, serializer: true)
# Which is the same as:
# include SnakyHash::Snake.new(key_type: :string)
# extend SnakyHash::Serializer
end
```

You can then add serialization extensions as needed. See [serialization](#serialization) and [extensions](#extensions) for more.

| Federated [DVCS][💎d-in-dvcs] Repository | Status | Issues | PRs | Wiki | CI | Discussions |
Expand Down Expand Up @@ -212,71 +223,95 @@ This is also not a bug, though if you need different behavior, there is a soluti

You can write your own arbitrary extensions:

* "Hash Load" extensions operate on the hash, and nested hashes
* "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| }`
* since v2.0.2, bugs fixed in v2.0.3
* "Value Load" extensions operate on the values, and nested hashes' values, if any
* use `::load_value_extensions.add(:extension_name) { |value| }`
* since v2.0.2, bugs fixed in v2.0.3
* "Hash Dump" extensions operate on the hash and nested hashes
* use `::dump_hash_extensions.add(:extension_name) { |value| }`
* since v2.0.3
* "Value Dump" extensions operate on the values, and nested hashes' values, if any
* use `::dump_value_extensions.add(:extension_name) { |value| }`
* since v2.0.2, bugs fixed in v2.0.3

#### 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.
Let's say I want to really smash up my hash and make it more food-like.

```ruby
class MyExtSnakedHash < Hashie::Mash
include SnakyHash::Snake.new(
key_type: :symbol, # default :string
serializer: true, # default: false
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
# We could swap all values with indexed apples (obliteraating nested data!)
MyExtSnakedHash.dump_hash_extensions.add(:to_apple) do |value|
num = 0
value.transform_values do |_key|
key = "apple-#{num}"
num += 1
key
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'
# And then when loading the dump we could convert the yum to pear
MyExtSnakedHash.load_hash_extensions.add(:apple_to_pear) do |value|
value.transform_keys do |key|
key.to_s.sub("yum", "pear")
end
end

# We could swap all index numbers "beet-<number>"
MyExtSnakedHash.dump_value_extensions.add(:to_beet) do |value|
value.to_s.sub(/(\d+)/) { |match| "beet-#{match[0]}" }
end

# And then when loading the dump we could convert beet to corn
MyExtSnakedHash.load_value_extensions.add(:beet_to_corn) do |value|
value.to_s.sub("beet", "corn")
end

snake = MyExtSnakedHash.new({"YumBread" => "b", "YumCake" => {"b" => "b"}, "YumBoba" => [1, 2, 3]})
snake # => {yum_bread: "b", yum_cake: {b: "b"}, yum_boba: [1, 2, 3]}
snake.yum_bread # => "b"
snake.yum_cake # => {b: "b"}
snake.yum_boba # => [1, 2, 3]
dump = snake.dump
dump # => "{\"yum_bread\":\"apple-beet-0\",\"yum_cake\":\"apple-beet-1\",\"yum_boba\":\"apple-beet-2\"}"
hydrated = MyExtSnakedHash.load(dump)
hydrated # => {pear_bread: "apple-corn-0", pear_cake: "apple-corn-1", pear_boba: "apple-corn-2"}
```

See the specs for more examples.

### Stranger Things
### Bad Ideas

I don't recommend using these features... but they exist (for now).

<details>
<summary>Show me what I should *not* do!</summary>

You can still access the original un-snaked camel keys.
And through them you can even use un-snaked camel methods.
But don't.

```ruby
snake = SnakyHash::StringKeyed["VeryFineHat" => "Feathers"]
snake.key?("VeryFineHat") # => true
snake["VeryFineHat"] # => 'Feathers'
snake.VeryFineHat # => 'Feathers', PLEASE don't do this!!!
snake["VeryFineHat"] = "pop" # Please don't do this... you'll get a warning, and it works (for now), but no guarantees.
# WARN -- : You are setting a key that conflicts with a built-in method MySnakedHash#VeryFineHat defined in MySnakedHash. This can cause unexpected behavior when accessing the key as a property. You can still access the key via the #[] method.
# => "pop"
```

Since you are reading this, here's what to do instead.

```ruby
snake.very_fine_hat = "pop" # => 'pop', do this instead!!!
snake.very_fine_hat # => 'pop'
snake[:very_fine_hat] = "moose" # => 'moose', or do this instead!!!
Expand All @@ -285,6 +320,8 @@ snake["very_fine_hat"] = "cheese" # => 'cheese', or do this instead!!!
snake.very_fine_hat # => 'cheese'
```

</details>

### 🚀 Release Instructions

See [CONTRIBUTING.md][🤝contributing].
Expand Down Expand Up @@ -553,7 +590,7 @@ or one of the others at the head of this README.
[📌gitmoji]:https://gitmoji.dev
[📌gitmoji-img]:https://img.shields.io/badge/gitmoji_commits-%20😜%20😍-34495e.svg?style=flat-square
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.119-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.132-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
[🔐security]: SECURITY.md
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
Expand Down
Loading