Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/alessandro-fazzi/shy-methodological_hash
No-ceremony *embedded document* pattern for hashes.
https://github.com/alessandro-fazzi/shy-methodological_hash
Last synced: 30 days ago
JSON representation
No-ceremony *embedded document* pattern for hashes.
- Host: GitHub
- URL: https://github.com/alessandro-fazzi/shy-methodological_hash
- Owner: alessandro-fazzi
- License: mit
- Created: 2024-12-01T15:33:18.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2024-12-01T17:14:52.000Z (about 1 month ago)
- Last Synced: 2024-12-01T18:23:21.474Z (about 1 month ago)
- Language: Ruby
- Size: 11.7 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
> [!WARNING]
> This is a POC, born unmaintained by its own nature.# Shy::MethodologicalHash
No-ceremony *embedded document* pattern for hashes.
## Installation
> [!NOTE]
> This gem is not and will not be published on rubygems since it's just a POCInstall the gem and add to the application's Gemfile by executing:
$ bundle add shy-methodological_hash --github "alessandro-fazzi/shy-methodological_hash"
## Usage
Given an hash
```ruby
user_hash = {
id: 1,
name: "Alessandro",
preferences: {
languages: ["italian", "ruby", "english"],
color: "green"
}
}
```you maybe want to *embed* it into an object before propagating it throughout
your entire system. `Shy::MethodologicalHash` is a device to cut-out all the
ceremony, still letting the resulting object open to extension.By default you'll get accessors methods on hash's keys.
```ruby
class User < Shy::MethodologicalHash; enduser = User.new(user_hash)
user.id # 1
user.preferences.color # "green"
```and nested hashes will be converted to `Shy::MethodologicalHash` instances
```ruby
user
# =>
# #1,
# :name=>"Alessandro",
# :preferences=>
# #["italian", "ruby", "english"], :color=>"green"},
# @path=[:preferences]>},
# @path=[]>
```You can obtain the embedded document as an hash with `#unwrap` or `#to_h` methods
```ruby
user.unwrap
# => {:id=>1, :name=>"Alessandro", :preferences=>{:languages=>["italian", "ruby", "english"], :color=>"green"}}
```It's possible to set values on keys, but, by design, it's not possible to
add or remove keys to the embedded document (the hash).```ruby
user.id = 3
```You can extend the object like any other PORO. The instance has access to
generated accessor methods.```ruby
class User < Shy::MethodologicalHash
def aka
name + " aka Fuzzy"
enddef languages
preferences.languages
end
enduser = User.new(user_hash)
user.aka # "Alessandro aka Fuzzy"
user.languages # ["italian", "ruby", "english"]
```You can override a getter/setter by the help of the `document` method like this:
```ruby
class User < Shy::MethodologicalHash
def preferences
puts "overridden preferences"
document[:preferences]
end
enduser = User.new(user_hash)
user.preferences
# overridden preferences
# #["italian", "ruby", "english"], :color=>"green"},
# @path=[:preferences]>
```### Nested hashes
As noticed in the previous snippet, any nested hash will be vivified into a
nested `Shy::MethodologicalHash` object.It's possible to decorate each nested object using the `decorate_path` class
method; it has two different signatures:1. `.decorate_path(path, &block)`
1. `.decorate_path(path, SomeClass)``path` is an array of symbols where each one is a key in the hash. E.g., for
this hash```ruby
{a: {b: {c: 1}}}
````[:a, :b]` is the path for the inner hash `{c: 1}`.
Keep in mind that's only possible to decorate **nested hashes**: if you give a
path to something else then the decoration will be ignored. An example:```ruby
class User < Shy::MethodologicalHash
decorate_path [:preferences] do
def light_color
"light #{color}"
end
end
enduser = User.new(user_hash)
user.preferences.light_color
# "light green"
```You can decorate hashes at any nesting level, but you must always
specify the "absolute" path starting from the root. This should make sense
because an hash could have multiple keys with the same name inside different
nested hashes, thus the gem needs a full path in order to have a uniq identifier.Here's an example
```ruby
my_hash = {
a: {
b: {
c: { foo: 1}
},
d: {
c: { foo: 2}
}
}
}class MyHash < Shy::MethodologicalHash
decorate_path [:a] do
decorate_path [:a, :b] do
decorate_path [:a, :b, :c] do
def double_foo
foo * 2
end
end
enddecorate_path [:a, :d] do
def hello
"hello #{document}"
enddecorate_path [:a, :d, :c] do
def triple_foo
foo * 3
end
end
end
end
endmethodological = MyHash.new(my_hash)
methodological.a.b.c.double_foo
# 2
methodological.a.d.c.triple_foo
# 6
methodological.a.d.hello
# "hello {:c=>#2}, @path=[:a, :d, :c]>}"
```> [!NOTE]
> when decorating this way, nested objects have a decorated class name such as
> `MyHash([:a, :d])` representing the path they're actually decorating. The goal
> is to make sense of complex objects at a glance.With the same approach you can decorate nested hashes your types. Those classes
can but are not expected to inherit from `Shy::MethodologicalHash`. Custom
classes are just required to have a constructor with to positional arguments:1. `hash` is the nested hash we're going to decorate
1. `path` is the path illustrated above; probably you'll just want to discard it```ruby
my_hash = {a: {foo: 1}, b: {foo: 2}}
class A < Shy::MethodologicalHash
def a_foo
"A #{foo}"
end
endclass B
def initialize(hash, _path)
@hash = hash
enddef b_foo
"B #{@hash.fetch(:foo)}"
end
endclass MyHash < Shy::MethodologicalHash
decorate_path [:a], A
decorate_path [:b], B
endmethodological = MyHash.new(my_hash)
methodological
methodological
# =>
# ##1}, @path=[:a]>, :b=>#2}>},
# @path=[]>
methodological.a.a_foo
# "A 1"
methodological.b.b_foo
# "B 2"
methodological.a.foo
# 1
methodological.b.foo
# undefined method `foo' for an instance of B (NoMethodError)
```Decorations can be placed "ahead of existence"; if you start with a hash not
having an optional key, but you foresee that key could be added with a nested
hash as value, then you can do something like```ruby
my_hash = {a: {}}
class MyHash < Shy::MethodologicalHash
decorate_path [:a] do
decorate_path [:a, :b] do
def double_bar
bar * 2
end
end
end
endmethodological = MyHash.new(my_hash)
methodological
# ##},
# @path=[]>
methodological.a = {b: {bar: 1}}
# {:b=>{:bar=>1}}
methodological.a.b.double_bar
# 2
```## Why?
When you have a peripheral object or an external service producing a hash
(or json), propagating the hash itself through various levels of your stack
may be risky: all the consumers along the way will depend on hash's structure.Depending on a data structure is generally a maintenance pain and a rigid
constraint.On the other hand manually creating objects to menage nested hashes can be
tedious or overkill in some scenarios.This gem bridges you from the uncontrolled data structure to the 100% hand
written nested objects solution, sitting in the middle, requiring no or very
few code and being hot-replaceable by custom solution would you need it.## How much slower it is?
Also given nobody cared about performances, a lot slower! But the comparison
is a bit apples vs. pineapples... and just to add some pears to the table I've
added Hashie::Mash to the comparison. Mash does a LOT more than this gem, but
this demonstrate that Mash could be overkill if you just need talk through
messages to an hash.```ruby
# See bin/bench for the actual codeBenchmark.ips do |x|
x.config(times: 1_000)
x.report("hash") { hash.fetch(:dipartimenti).each { _1.dig(:responsabile, :email) } }
x.report("methodological") { methodological.dipartimenti.each { _1.responsabile.email } }
x.report("hashie mash") { hashie_mash.dipartimenti.each { _1.responsabile.email } }
x.compare!
end
``````
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) +YJIT [arm64-darwin24]
Warming up --------------------------------------
hash 710.792k i/100ms
methodological 298.614k i/100ms
hashie mash 92.135k i/100ms
Calculating -------------------------------------
hash 7.509M (± 0.5%) i/s (133.17 ns/i) - 37.672M in 5.017037s
methodological 3.269M (± 1.4%) i/s (305.86 ns/i) - 16.424M in 5.024323s
hashie mash 925.379k (± 0.4%) i/s (1.08 μs/i) - 4.699M in 5.077871sComparison:
hash: 7509025.0 i/s
methodological: 3269485.2 i/s - 2.30x slower
hashie mash: 925379.5 i/s - 8.11x slower
```## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/shy-methodological_hash. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/shy-methodological_hash/blob/main/CODE_OF_CONDUCT.md).
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## Code of Conduct
Everyone interacting in the Shy::MethodologicalHash project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/shy-methodological_hash/blob/main/CODE_OF_CONDUCT.md).