Objectable
This library streamlines value setting and getting for any type of object. It can give an object a hash-like interface without modifying/changing the underlying object's implementation. It uses the following methodology:
- If the object is a hash and a string key exists then return the key's value.
- If the object is a hash and a symbol key exists then return the key's value.
- If the object publicly responds to the key then call key on the object.
This seems rather trivial but consider the following additional value propositions:
- It can handle dot-notation/key paths for nested objects.
- It can recursively traverse object graphs to set/get the desired value. This can be used to build up deep object graphs.
See the examples
section for more information.
Installation
To install through Rubygems:
gem install install objectable
You can also add this to your Gemfile:
bundle add objectable
Examples
Let's define a set of objects built using different constructs but all essentially represent the same graphs:
Employee = Struct.new(:id, :demographics)
Demographics = Struct.new(:first)
symbol_based_hash = { id: 1, demographics: { first: 'Matt' } }
string_based_hash = { 'id' => 1, 'demographics' => { 'first' => 'Matt' } }
open_struct = OpenStruct.new(id: 1, demographics: OpenStruct.new(first: 'Matt'))
object = Employee.new(1, Demographics.new('Matt'))
Getting Values
The following calls will all resolve to the same respective values:
resolver = Objectable.resolver
# All resolve to the value of: 1
resolver.get(symbol_based_hash, :id)
resolver.get(symbol_based_hash, 'id')
resolver.get(string_based_hash, :id)
resolver.get(string_based_hash, 'id')
resolver.get(open_struct, :id)
resolver.get(open_struct, 'id')
resolver.get(object, :id)
resolver.get(object, 'id')
# All resolve to the value of: Matt
resolver.get(symbol_based_hash, :'demographics.first')
resolver.get(symbol_based_hash, 'demographics.first')
resolver.get(string_based_hash, :'demographics.first')
resolver.get(string_based_hash, 'demographics.first')
resolver.get(open_struct, :'demographics.first')
resolver.get(open_struct, 'demographics.first')
resolver.get(object, :'demographics.first')
resolver.get(object, 'demographics.first')
As you can see you do not have to worry about nested object traversal, dot-notation, or key type; this library gives you a unified interface when accessing values.
Setting Values
Say we want to update a the id
and first
attributes from the objects:
resolver = Objectable.resolver
# All calls will set the object's respective id attribute to: 999
resolver.set(symbol_based_hash, :id, 999)
resolver.set(symbol_based_hash, 'id', 999)
resolver.set(string_based_hash, :id, 999)
resolver.set(string_based_hash, 'id', 999)
resolver.set(open_struct, :id, 999)
resolver.set(open_struct, 'id', 999)
resolver.set(object, :id, 999)
resolver.set(object, 'id', 999)
# All calls will set the object's respective id attribute to: Nick
resolver.set(symbol_based_hash, :'demographics.first', 'Nick')
resolver.set(symbol_based_hash, 'demographics.first', 'Nick')
resolver.set(string_based_hash, :'demographics.first', 'Nick')
resolver.set(string_based_hash, 'demographics.first', 'Nick')
resolver.set(open_struct, :'demographics.first', 'Nick')
resolver.set(open_struct, 'demographics.first', 'Nick')
resolver.set(object, :'demographics.first', 'Nick')
resolver.set(object, 'demographics.first', 'Nick')
Dot-Notation Customization
By default dot-notation is turned on and the path separator is set as a period. You can disable or customize this by passing in a separator option into resolver
:
resolver_without_dot_notation = Objectable.resolver(separator: nil)
resolver_with_custom_dot_notation = Objectable.resolver(separator: '$')
Also note that you can choose to pass in an array into the expression and it will be used for customized traversal but without using dot-notation. The following are equivalent:
Objectable.resolver.get([:demographics, :first], symbol_based_hash)
Objectable.resolver.get('demographics.first', symbol_based_hash)
Gaps
When setting values of nested objects:
- If the parent object is null then the parent object will be initialized based on the preceding class. This is not ideal for complex object graphs but works quite lovely for Hash and OpenStruct objects. In the future this can be extended so the object type graph can be passed in and used as a blueprint for parent initialization.
Contributing
Development Environment Configuration
Basic steps to take to get this repository compiling:
- Install Ruby (check objectable.gemspec for versions supported)
- Install bundler (gem install bundler)
- Clone the repository (git clone [email protected]:bluemarblepayroll/objectable.git)
- Navigate to the root folder (cd objectable)
- Install dependencies (bundle)
Running Tests
To execute the test suite run:
bundle exec rspec spec --format documentation
Alternatively, you can have Guard watch for changes:
bundle exec guard
Also, do not forget to run Rubocop:
bundle exec rubocop
Publishing
Note: ensure you have proper authorization before trying to publish new versions.
After code changes have successfully gone through the Pull Request review process then the following steps should be followed for publishing new versions:
- Merge Pull Request into master
- Update
lib/objectable/version.rb
using semantic versioning - Install dependencies:
bundle
- Update
CHANGELOG.md
with release notes - Commit & push master to remote and ensure CI builds master successfully
- Run
bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the.gem
file to rubygems.org.
Code of Conduct
Everyone interacting in this codebase, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
License
This project is MIT Licensed.