Shrink::Wrap is a dead-simple framework to manipulate and map JSON data to Ruby object instances.
Imagine a JSON structure such as:
{
"name": "Milky Way",
"age": "13510000000",
"solarSystems": [
{
"name": "Sol",
"age": "4571000000",
"planets": [
{
"name": "mercury"
},
{
"name": "venus"
},
{
"name": "earth"
}
]
}
]
}
With Shrink::Wrap, you'd be able to map the JSON data to Ruby instances as simply as:
data = JSON.parse(File.read('galaxy.json'))
Galaxy.shrink_wrap(data) # => #<Galaxy:0x007fec71254828
# @age=13510000000,
# @name="Milky Way",
# @solar_systems=
# [#<SolarSystem:0x007fec71254850
# @age=4571000000,
# @name="Sol",
# @planets=
# [#<Planet:0x007fec712555c0 @name="MERCURY">,
# #<Planet:0x007fec712553e0 @name="VENUS">,
# #<Planet:0x007fec71255200 @name="EARTH">>]>]>
You must include the Shrink::Wrap
module in your classes to gain access to the shrink_wrap
method.
For the example above, the implementation would look like:
require 'shrink/wrap'
class Planet
include Shrink::Wrap
transform(Shrink::Wrap::Transformer::Symbolize)
translate(
name: { from: :name }
)
coerce(
name: ->(v) { v.upcase }
)
attr_accessor :name
def initialize(opts = {})
self.name = opts.fetch(:name)
end
end
class SolarSystem
include Shrink::Wrap
transform(Shrink::Wrap::Transformer::Symbolize)
translate(
name: { from: :name },
age: { from: :age },
planets: { from: :planets }
)
coerce(
age: ->(v) { v.to_i },
planets: Array[Planet]
)
attr_accessor :name, :age, :planets
def initialize(opts = {})
self.name = opts.fetch(:name)
self.age = opts.fetch(:age)
self.planets = opts.fetch(:planets)
end
end
class Galaxy
include Shrink::Wrap
transform(Shrink::Wrap::Transformer::Symbolize)
translate(
name: { from: :name },
age: { from: :age },
solar_systems: { from: :solarSystems }
)
coerce(
age: ->(v) { v.to_i },
solar_systems: Array[SolarSystem]
)
attr_accessor :name, :age, :solar_systems
def initialize(opts = {})
self.name = opts.fetch(:name)
self.age = opts.fetch(:age)
self.solar_systems = opts.fetch(:solar_systems)
end
end
Shrink::Wrap operations are always deterministically performed in the following order:
transform
translate
coerce
Shrink::Wrap will call the initialize
method with the data after all operations have been completed.
The transform
operation accepts a transformation class as well as an options hash as parameters.
The transformation class is any Ruby class that inherits from Shrink::Wrap::Transformer::Base
.
The class must define a transform(data = {})
method that returns the data after transformations are applied.
You can chain transformers together by calling transform
multiple times in your class. Transformers are always executed sequentially.
You can create your own transformer as such:
class FilterEmpty < Shrink::Wrap::Transformer::Base
def transform(opts = {})
data.each_with_object({}) do |(key, value), memo|
memo[key] = value unless value.empty?
end
end
end
Shrink::Wrap provides a few built-in transformer classes for use with common transformation patterns.
In Ruby, it's very common to convert Hash keys to Symbol instances.
The Symbolize transformer works similar to ActiveSupport's deep_symbolize_keys
method, but it is also able to traverse nested data structures (Array
, Enumerable
) and symbolize the keys of any nested elements as well.
The Symbolize transformer accepts an optional depth
parameter that defines that maximum depth for symbolization in nested data structures.
Example:
class Example
include Shrink::Wrap
transform(Shrink::Wrap::Transformer::Symbolize, depth: 2)
translate_all
attr_accessor :data
def initialize(data)
self.data = data
end
end
data = { 'root' => [{ 'nested' => 'test' }] }
instance = Example.shrink_wrap(data)
instance.data # => {:root=>[{:nested=>"test"}]}
Some data Hashes contain keys that contain relevant data for object instances.
The CollectionFromKey transformer accepts a Hash option that contains a key => attribute mapping.
The transformation then creates an Array of elements taken from the key argument and merges the key into each element in the collection.
Example:
class Example
include Shrink::Wrap
transform(Shrink::Wrap::Transformer::CollectionFromKey, weekends: :day)
translate_all
attr_accessor :data
def initialize(data)
self.data = data
end
end
data = {
weekends: {
saturday: {
index: 6
},
sunday: {
index: 0
}
}
}
instance = Example.shrink_wrap(data)
instance.data # => {:weekends=>[{:index=>6, :day=>:saturday}, {:index=>0, :day=>:sunday}]}
The translate
operation accepts a Hash that contains key/value pairs that map incoming data to attributes.
You can pass the following parameters for each attribute:
from
[required]: The input key that the attribute is mapped to.allow_nil
[optional]: Iftrue
, the mapped value may benil
. Whenfalse
, raisesKeyError
if the mapped value isnil
.default
[optional]: AProc
orLambda
that returns a particular value. This argument will be called if the value isnil
.
By default, any attributes not specified in a translate
call are not passed to the underlying object during initialization.
You can call translate_all
if you wish to pass all of the data but don't wish to explicitly define the attributes in a corresponding translate
call.
The coerce
operation accepts a hash of attribute keys and coercion values.
Coercions must be one of the following types:
Class
: Tries callingClass.shrink_wrap(data)
,Class.coerce(data)
, thenClass.new(data)
, returning the first successful result.Enumerable
: Coerces the value into the collection of type defined as the Enumerable elements. The first element of the enumerable must be a class name (eg.Array[MyClass]
,Hash[MyClass => MyClass]
).Lambda/Proc
: Coerces the value by calling theProc
with the value as the only argument.
Bug reports and pull requests are welcome on GitHub at https://github.com/jessedoyle/shrink_wrap.
When making code changes please fork the main repository, create a feature branch and then create a pull request with your change. All code changes must contain adequate test coverage.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the ShrinkWrap project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.