This addon to the shale Ruby gem adds a simple yet powerful builder DSL.
It also adds support for sorbet and tapioca in shale. This gem includes a custom tapioca DSL compiler designed for shale.
Install the gem and add to the application's Gemfile by executing:
$ bundle add shale-builder
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install shale-builder
Build your shale classes like a boss.
transaction = Transaction.build do |t|
t.cvv_code = '123'
t.amount do |a|
a.value = 2.3
a.currency = 'PLN'
end
t.payment_instrument do |p|
p.number = '4242424242424242'
p.expiration_year = 2045
p.expiration_month = 12
end
end
This gem adds a module named Shale::Builder
. It's meant to be included
in subclasses of Shale::Mapper
to provide an easier way of building
instances.
You can use it like that:
require 'shale/builder'
class Amount < Shale::Mapper
include Shale::Builder
attribute :value, Shale::Type::Float
attribute :currency, Shale::Type::String, doc: <<~DOC
This is some custom documentation that can be used by sorbet.
It will be used by the tapioca DSL compiler
to generate the RBI documentation for this attribute.
DOC
end
Now instead of creating an instance like that:
amount = Amount.new(value: 2.3, currency: 'PLN')
You can do it like that:
amount = Amount.build do |a|
a.value = 2.3
a.currency = 'PLN'
end
If you use sorbet and run bundle exec tapioca dsl
you'll get the following RBI file.
# typed: true
class Amount
include ShaleAttributeMethods
module ShaleAttributeMethods
sig { returns(T.nilable(Float)) }
def value; end
sig { params(value: T.nilable(Float)).returns(T.nilable(Float)) }
def value=(value); end
# This is some custom documentation that can be used by sorbet.
# It will be used by the tapioca DSL compiler
# to generate the RBI documentation for this attribute.
sig { returns(T.nilable(String)) }
def currency; end
# This is some custom documentation that can be used by sorbet.
# It will be used by the tapioca DSL compiler
# to generate the RBI documentation for this attribute.
sig { params(value: T.nilable(String)).returns(T.nilable(String)) }
def currency=(value); end
end
end
It's kind of pointless when you've got a flat structure. It really shines when nested objects come into play.
Let's say that you've got a structure like this:
class Amount < Shale::Mapper
include Shale::Builder
attribute :value, Shale::Type::Float
attribute :currency, Shale::Type::String
end
class PaymentInstrument < Shale::Mapper
include Shale::Builder
attribute :number, Shale::Type::String
attribute :expiration_year, ::Shale::Type::Integer
attribute :expiration_month, ::Shale::Type::Integer
end
class Transaction < Shale::Mapper
include Shale::Builder
attribute :cvv_code, Shale::Type::String
attribute :amount, Amount
attribute :payment_instrument, PaymentInstrument
end
Normally you would instantiate it like that:
transaction = Transaction.new(
cvv_code: '123',
amount: Amount.new(
value: 2.3,
currency: 'PLN'
),
payment_instrument: PaymentInstrument.new(
number: '4242424242424242',
expiration_year: 2045,
expiration_month: 12
)
)
It's really repetitive and it makes it hard to modify the values of certain attributes or omit them conditionally.
This gem provides a better way:
transaction = Transaction.build do |t|
t.cvv_code = '123'
t.amount do |a|
a.value = 2.3
a.currency = 'PLN'
end
t.payment_instrument do |p|
p.number = '4242424242424242'
p.expiration_year = 2045
p.expiration_month = 12
end
end
That's possible because the getters of attributes with non-primitive types have been overridden to accept blocks. When a block is given to such a getter, it instantiates an empty object of its type and yields it to the block.
Whenever you call a getter with a block for a collection attribute, the built object will be appended to the array.
Let's define a schema like this.
class Client < Shale::Mapper
include Shale::Builder
attribute :first_name, Shale::Type::String
attribute :last_name, Shale::Type::String
attribute :email, Shale::Type::String
end
class Transaction < Shale::Mapper
include Shale::Builder
attribute :clients, Client, collection: true
end
You can easily build add new clients to the collection like so:
transaction = Transaction.build do |t|
# this will be added as the first element of the collection
t.clients do |c|
c.first_name = 'Foo'
c.last_name = 'Bar'
end
# this will be added as the second element of the collection
t.clients do |c|
c.first_name = 'Grant'
c.last_name = 'Taylor'
end
end
p transaction.clients
# [
# #<Client:0x00000001066c2828 @first_name="Foo", @last_name="Bar", @email=nil>,
# #<Client:0x00000001066c24b8 @first_name="Grant", @last_name="Taylor", @email=nil>
# ]
This DSL makes it extremely easy to build nested objects conditionally.
transaction = Transaction.build do |t|
t.cvv_code = '123'
t.amount do |a|
a.value = 2.3 if some_flag?
a.currency = 'PLN'
end
t.payment_instrument do |p|
p.number = '4242424242424242'
if some_condition?
p.expiration_year = 2045
p.expiration_month = 12
end
end
end
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.
Bug reports and pull requests are welcome on GitHub at https://github.com/Verseth/ruby-shale-builder.
The gem is available as open source under the terms of the MIT License.