Science Score: 44.0%
This score indicates how likely this project is to be science-related based on various indicators:
-
✓CITATION.cff file
Found CITATION.cff file -
✓codemeta.json file
Found codemeta.json file -
✓.zenodo.json file
Found .zenodo.json file -
○DOI references
-
○Academic publication links
-
○Academic email domains
-
○Institutional organization owner
-
○JOSS paper metadata
-
○Scientific vocabulary similarity
Low similarity (10.1%) to scientific vocabulary
Keywords
equality
value
value-object
whole
Last synced: 4 months ago
·
JSON representation
·
Repository
Provides whole value object behavior.
Basic Info
- Host: GitHub
- Owner: bkuhlmann
- License: other
- Language: Ruby
- Default Branch: main
- Homepage: https://alchemists.io/projects/wholeable
- Size: 76.2 KB
Statistics
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 0
- Releases: 0
Topics
equality
value
value-object
whole
Created over 1 year ago
· Last pushed 5 months ago
Metadata Files
Readme
Funding
License
Citation
README.adoc
:toc: macro
:toclevels: 5
:figure-caption!:
:data_link: link:https://alchemists.io/articles/ruby_data[Data]
:pattern_matching_link: link:https://alchemists.io/articles/ruby_pattern_matching[pattern matching]
:ruby_link: link:https://www.ruby-lang.org[Ruby]
:data_link: link:https://alchemists.io/articles/ruby_data[Data]
:structs_link: link:https://alchemists.io/articles/ruby_structs[Structs]
= Wholeable
Wholeable allows you to turn your object into a _whole value object_ by ensuring object equality is determined by the values of the object instead of by identity. Whole value objects -- or value objects in general -- have the following traits as noted via link:https://en.wikipedia.org/wiki/Value_object[Wikipedia]:
* Equality is determined by the values that make up an object and not by link:https://en.wikipedia.org/wiki/Identity_(object-oriented_programming)[identity] (i.e. memory address) which is the default behavior for all {ruby_link} objects except for {data_link} and {structs_link}.
* Identity remains unique since two objects can have the same values but different identity. This means `BasicObject#equal?` is never overwritten -- which is strongly discouraged -- as per link:https://rubyapi.org/o/basicobject#method-i-3D-3D[BasicObject] documentation.
* Value objects should be immutable (i.e. frozen) by default. This implementation enforces a strict adherence to immutability in order to ensure value objects remain equal and discourage mutation.
toc::[]
== Features
* Ensures equality (i.e. `#==` and `#eql?`) is determined by attribute values and not object identity (i.e. `#equal?`).
* Allows you to compare two objects of same or different types and see their differences.
* Provides {pattern_matching_link}.
* Provides inheritance so you can subclass and add attributes or provide additional behavior.
* Automatically defines public attribute readers (i.e. `.attr_reader`) if _immutable_ (default) or public attribute readers and writers (i.e. `.attr_accessor`) if _mutable_.
* Ensures object inspection (i.e. `#inspect`) shows all registered attributes.
* Ensures object is frozen upon initialization by default.
== Requirements
. {ruby_link}.
== Setup
To install _with_ security, run:
[source,bash]
----
# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install wholeable --trust-policy HighSecurity
----
To install _without_ security, run:
[source,bash]
----
gem install wholeable
----
You can also add the gem directly to your project:
[source,bash]
----
bundle add wholeable
----
Once the gem is installed, you only need to require it:
[source,ruby]
----
require "wholeable"
----
== Usage
To use, include Wholeable along with a list of attributes that make up your whole value object:
[source,ruby]
----
class Person
include Wholeable[:name, :email]
def initialize name:, email:
@name = name
@email = email
end
end
jill = Person[name: "Jill Smith", email: "jill@example.com"]
jill_two = Person[name: "Jill Smith", email: "jill@example.com"]
jack = Person[name: "Jack Smith", email: "jack@example.com"]
Person.members # [:name, :email]
jill.members # [:name, :email]
jill.name # "Jill Smith"
jill.email # "jill@example.com"
jill.frozen? # true
jill_two.frozen? # true
jack.frozen? # true
jill.inspect # "#"
jill_two.inspect # "#"
jack.inspect # "#"
jill == jill # true
jill == jill_two # true
jill == jack # false
jill.diff(jill) # {}
jill.diff(jack) # {
# name: ["Jill Smith", "Jack Smith"],
# email: ["jill@example.com", "jack@example.com"]
# }
jill.diff(Object.new) # {:name=>["Jill Smith", nil], :email=>["jill@example.com", nil]}
jill.eql? jill # true
jill.eql? jill_two # true
jill.eql? jack # false
jill.equal? jill # true
jill.equal? jill_two # false
jill.equal? jack # false
jill.hash # 3650965837788801745
jill_two.hash # 3650965837788801745
jack.hash # 4460658980509842640
jill.to_a # ["Jill Smith", "jill@example.com"]
jack.to_a # ["Jack Smith", "jack@example.com"]
jill.to_h # {:name=>"Jill Smith", :email=>"jill@example.com"}
jack.to_h # {:name=>"Jack Smith", :email=>"jack@example.com"}
jill.to_s # "#"
jill_two.to_s # "#"
jack.to_s # "#"
jill.with name: "Sue" # #
jill.with bad: "!" # unknown keyword: :bad (ArgumentError)
----
As you can see, object equality is determined by the object's values and _not_ by the object's identity. When you include `Wholeable` along with a list of keys, the following happens:
. The corresponding _public_ `attr_reader` (or `attr_accessor` if mutable) for each key is created which saves you time and reduces double entry when implementing your whole value object.
. The `#to_a`, `#to_h`, and `#to_s` methods are added for convenience and to be compatible with {data_link} and {structs_link}.
. The `#deconstruct` and `#deconstruct_keys` aliases are created so you can leverage {pattern_matching_link}.
. The `#==`, `#eql?`, `#hash`, `#inspect`, and `#with` methods are added to provide whole value behavior.
. The object is immediately frozen after initialization to ensure your instance is _immutable_ by default.
=== Initialization
As shown above, you can create an instance of your whole value object by using `.[]`. Example:
[source,ruby]
----
Person[name: "Jill Smith", email: "jill@example.com"]
----
Alternatively, you can create new instances using `.new`. Example:
[source,ruby]
----
Person.new name: "Jill Smith", email: "jill@example.com"
----
Both methods work but use `.[]` when supplying arguments and `.new` when you don't have any arguments.
=== Mutability
All whole value objects are frozen by default. You can change behavior by specifying whether instances should be mutable by passing `kind: :mutable` as a keyword argument. Example:
[source,ruby]
----
class Person
include Wholeable[:name, :email, kind: :mutable]
def initialize name: "Jill", email: "jill@example.com"
@name = name
@email = email
end
end
jill = Person.new
jill.frozen? # false
----
When your object is mutable, you'll also have access to setter methods in addition to the normal getter methods. Example:
[source,ruby]
----
jill.name # "Jill"
jill.name = "Jayne"
jill.name # "Jayne"
----
You can also make your object immutable by using `kind: :immutable` but this is default behavior and redundant. Any invalid kind (example: `kind: :bogus`) will be ignored and default to being immutable.
=== Inheritance
Unlike {data_link} or {structs_link}, you can subclass a whole value object. Example:
[source,ruby]
----
class Person
include Wholeable[:name]
def initialize name:
@name = name
end
end
class Contact < Person
include Wholeable[:email]
def initialize(email:, **)
super(**)
@email = email
end
end
contact = Contact[name: "Jill Smith", email: "jill@example.com"]
contact.to_h # {name: "Jill Smith", email: "jill@example.com"}
contact.frozen? # true
----
Notice `Contact` inherits from `Person` while only defining the attributes that make it unique. You don't need to redefine the same attributes found in the superclass as that would be redundant and defeat the purpose of subclassing in the first place.
When subclassing, each subclass has access to the same attributes defined by the superclass no matter how deep your ancestry is. This does mean you must pass the remaining attributes to the superclass via the double splat.
Mutability is honored but is specific to each object in the ancestry. In other words, if the entire ancestry is immutable then no object can mutate an attribute defined in the ancestry. The same applies if the entire ancestry is mutable except, now, any child can mutate any attribute previously defined by the ancestry. Any attribute that is mutated is only mutated specific to the subclass as is standard inheritance behavior.
If your ancestry is a mixed (immutable and mutable) then behavior is specific to each child in the ancestry. This means a mutable child won't make the entire ancestry mutable, only the child will be mutable. Best practice is to architect your ancestry so immutability or mutability is the same across all objects. To illustrate, here's an example with an immutable parent and mutable child:
[source,ruby]
----
class Parent
include Wholeable[:one]
def initialize one: 1
@one = one
end
end
class Child < Parent
include Wholeable[:two, kind: :mutable]
def initialize(two: 2, **)
super(**)
@two = two
end
end
child = Child.new
child.one = 100 # NoMethodError
child.two = 200 # 200
child.frozen? # false
----
Notice, when attempting to mutate the `one` attribute, you get a `NoMethodError`. This is because `#one=` is defined by the _immutable_ parent while `#two=` is defined on the _mutable_ child.
If you the flip mutability of your ancestry, you can make your parent mutable while the child immutable for different behavior. Example:
[source,ruby]
----
class Parent
include Wholeable[:one, kind: :mutable]
def initialize one: 1
@one = one
end
end
class Child < Parent
include Wholeable[:two]
def initialize(two: 2, **)
super(**)
@two = two
end
end
child = Child.new
child.one = 100 # FrozenError
child.two = 200 # NoMethodError
child.frozen? # true
----
In this case, you get a `FrozenError` for `#one=` because the parent is _mutable_ and defined the `#one=` method but the child is _immutable_ which caused the associated attribute to be frozen. On the other hand, the `#two=` method is never defined by the subclass due to being immutable and so you you get a: `NoMethodError`.
_Again, if using inheritance, ensure immutability or mutability remains consistent throughout the entire ancestry._
== Caveats
Whole values can be broken via the following situations:
* *Post Attributes*: Adding additional attributes after what is defined when including `Wholeable` will break your whole value object. To prevent this, let Wholeable manage this for you (easiest). Otherwise (harder), you can manually override `#==`, `#eql?`, `#hash`, `#inspect`, `#to_a`, and `#to_h` behavior at which point you don't need Wholeable anymore.
* *Deep Freezing*: The automatic freezing of your instances is shallow and will not deep freeze nested attributes. This behavior mimics the behavior of {data_link} objects.
== Performance
The performance of this gem is good but definitely slower than native support for {data_link} and {structs_link} because they are written in C. To illustrate, here's a micro benchmark for comparison:
----
INITIALIZATION
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) +YJIT [arm64-darwin23.6.0]
Warming up --------------------------------------
Data 470.027k i/100ms
Struct 422.010k i/100ms
Whole 805.945k i/100ms
Calculating -------------------------------------
Data 4.750M (± 1.1%) i/s (210.53 ns/i) - 23.971M in 5.047225s
Struct 4.579M (± 1.1%) i/s (218.38 ns/i) - 23.211M in 5.069228s
Whole 9.408M (± 1.2%) i/s (106.29 ns/i) - 47.551M in 5.055033s
Comparison:
Whole: 9407938.7 i/s - 1.60x slower
Data: 4750013.8 i/s - 3.17x slower
Struct: 4579253.1 i/s - 3.28x slower
BEHAVIOR
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) +YJIT [arm64-darwin23.6.0]
Warming up --------------------------------------
Data 129.006k i/100ms
Struct 129.832k i/100ms
Wholeable 78.861k i/100ms
Calculating -------------------------------------
Data 1.336M (± 3.6%) i/s (748.33 ns/i) - 6.708M in 5.027517s
Struct 1.341M (± 1.7%) i/s (745.89 ns/i) - 6.751M in 5.037050s
Wholeable 816.232k (± 1.9%) i/s (1.23 μs/i) - 4.101M in 5.025751s
Comparison:
Struct: 1340687.5 i/s
Data: 1336304.1 i/s - same-ish: difference falls within error
Wholeable: 816232.0 i/s - 1.64x slower
----
While the above isn't bad, you can definitely see this gem is slower than Ruby's own native objects when interacting with it despite being faster upon initialization.
Default to using {data_link} or {structs_link} but, if you find yourself needing a whole value object with more behavior than what a `Data` or `Struct` can provide, then this gem is a good solution.
== Development
To contribute, run:
[source,bash]
----
git clone https://github.com/bkuhlmann/wholeable
cd wholeable
bin/setup
----
You can also use the IRB console for direct access to all objects:
[source,bash]
----
bin/console
----
== Tests
To test, run:
[source,bash]
----
bin/rake
----
== link:https://alchemists.io/policies/license[License]
== link:https://alchemists.io/policies/security[Security]
== link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]
== link:https://alchemists.io/policies/contributions[Contributions]
== link:https://alchemists.io/policies/developer_certificate_of_origin[Developer Certificate of Origin]
== link:https://alchemists.io/projects/wholeable/versions[Versions]
== link:https://alchemists.io/community[Community]
== Credits
* Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].
* Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].
Owner
- Name: Brooke Kuhlmann
- Login: bkuhlmann
- Kind: user
- Location: Boulder, CO USA
- Company: Alchemists
- Website: https://alchemists.io
- Repositories: 56
- Profile: https://github.com/bkuhlmann
Quality over quantity.
Citation (CITATION.cff)
cff-version: 1.2.0
message: Please use the following metadata when citing this project in your work.
title: Wholeable
abstract: Provides whole value object behavior.
version: 1.3.0
license: Hippocratic-2.1
date-released: 2025-07-24
authors:
- family-names: Kuhlmann
given-names: Brooke
affiliation: Alchemists
orcid: https://orcid.org/0000-0002-5810-6268
keywords:
- ruby
- whole
- value
- equality
repository-code: https://github.com/bkuhlmann/wholeable
repository-artifact: https://rubygems.org/gems/wholeable
url: https://alchemists.io/projects/wholeable
GitHub Events
Total
- Delete event: 10
- Push event: 23
- Create event: 7
Last Year
- Delete event: 10
- Push event: 23
- Create event: 7
Issues and Pull Requests
Last synced: 5 months ago
All Time
- Total issues: 0
- Total pull requests: 0
- Average time to close issues: N/A
- Average time to close pull requests: N/A
- Total issue authors: 0
- Total pull request authors: 0
- Average comments per issue: 0
- Average comments per pull request: 0
- Merged pull requests: 0
- Bot issues: 0
- Bot pull requests: 0
Past Year
- Issues: 0
- Pull requests: 0
- Average time to close issues: N/A
- Average time to close pull requests: N/A
- Issue authors: 0
- Pull request authors: 0
- Average comments per issue: 0
- Average comments per pull request: 0
- Merged pull requests: 0
- Bot issues: 0
- Bot pull requests: 0
Top Authors
Issue Authors
Pull Request Authors
Top Labels
Issue Labels
Pull Request Labels
Packages
- Total packages: 1
-
Total downloads:
- rubygems 1,771 total
- Total dependent packages: 0
- Total dependent repositories: 0
- Total versions: 7
- Total maintainers: 1
rubygems.org: wholeable
Provides whole value object behavior.
- Homepage: https://alchemists.io/projects/wholeable
- Documentation: http://www.rubydoc.info/gems/wholeable/
- License: Hippocratic-2.1
-
Latest release: 1.3.0
published 6 months ago
Rankings
Dependent packages count: 14.6%
Dependent repos count: 44.8%
Average: 50.7%
Downloads: 92.8%
Maintainers (1)
Funding
- https://github.com/sponsors/bkuhlmann
Last synced:
5 months ago