This library provides RSpec-style let() declartions for lazy evaluation, concerns, and a few functional operators.
Unlike ruby-let, this does not actually mimic the let of functional programming. Here,
let()
defines a method in the class. The values are memoized. This allows for both
lazy-evaluation and class-based scoping.
By defining methods for lazy evaluation and grouping them into concerns, you can create mixins that can be shared across multiple objects. Lazy-eval lends itself to functional programming, so a small refinment is made available for use.
gem install rlet
Or from bundler
gem 'rlet'
The gems contain two modules, Let and Concern. You can use them like so:
class ContactsController
include Let
include RestfulResource
let(:model) { Contact }
def show
respond_with resource
end
end
module RestfulResource
extend Concern
included do
let(:resource) { model.find(id) }
let(:id) { params[:id] }
end
end
Normally, you want to expose the methods as public methods. Sometimes,
you want to use protected methods. You can do so with letp
:
class ContactsController
include Let
include RestfulResource
letp(:model) { Contact }
def show
respond_with resource
end
end
module RestfulResource
extend Concern
included do
letp(:resource) { model.find(id) }
letp(:id) { params[:id] }
end
end
This is useful for Rails installation that still have dynamically inferred routes.
In Version 0.8.1+ both: let() and letp() return the name of the resource, there if you want to privatize let ruby 2.1+ you can do
class ContactsController
include Let
include RestfulResource
private let(:model) { Contact }
end
or in Rails, for the lazy
class ContactsController
include Let
include RestfulResource
helper_method letp(:model) { Contact }
end
Concern is embedded from ActiveSupport. If ActiveSupport::Concern is loaded, it will use that. This allows one to use concerns without having ActiveSupport as a dependency.
Additionally, to expose let()
with instance variables for use in templates, you can use expose
require 'rlet'
require 'rlet/expose'
class ContactsController
include Let
include RLet::Expose
include RestfulResource
let(:model) { Contact }
expose :resource, only: :show
def show
respond_with resource
end
end
module RestfulResource
extend Concern
included do
let(:resource) { model.find(id) }
let(:id) { params[:id] }
end
end
DEPRECATION NOTICE Expose is not needed in Rails. Use helper_method
instead
One pattern that comes up is creating a class which takes an option as an initializer, and then
using let()
to define values based on those options.
For example:
module Intentions
class Promise
include Let
extend Intentions::Concerns::Register
attr_reader :identifier, :promiser, :promisee, :body, :metadata
let(:identifier) { Digest::SHA2.new.update(to_digest.inspect).to_s }
let(:sign) { metadata[:sign] }
let(:scope) { metadata[:scope] }
let(:timestamp) { Time.now.utc }
let(:salt) { rand(100000) }
def initialize(_promiser, _promisee, _body, _metadata = {})
@promiser = _promiser.freeze
@promisee = _promisee.freeze
@body = _body.freeze
@metadata = _metadata.freeze
self
end
def to_h
to_digest.merge(identifier: identifier)
end
def to_digest
{
promiser: promiser,
promisee: promisee,
body: body,
metadata: metadata,
timestamp: timestamp,
salt: salt
}
end
end
end
We can refactor into:
module Intentions
class Promise
include Let
extend Intentions::Concerns::Register
attr_reader :options
let(:promiser) { options[:promiser] }
let(:promisee) { options[:promisee] }
let(:body) { options[:body] }
let(:metadata) { options[:metadata] }
let(:identifier) { Digest::SHA2.new.update(to_digest.inspect).to_s }
let(:sign) { metadata[:sign] }
let(:scope) { metadata[:scope] }
let(:timestamp) { Time.now.utc }
let(:salt) { rand(100000) }
def initialize(_options = {})
@options = _options
end
def to_h
to_digest.merge(identifier: identifier)
end
def to_digest
{
promiser: promiser,
promisee: promisee,
body: body,
metadata: metadata,
timestamp: timestamp,
salt: salt
}
end
end
end
This pattern occurs so frequently that rlet
now includes a LazyOptions
concern.
module Intentions
class Promise
include Let
include RLet::LazyOptions
extend Intentions::Concerns::Register
let(:promiser) { options[:promiser] }
let(:promisee) { options[:promisee] }
let(:body) { options[:body] }
let(:metadata) { options[:metadata] }
let(:identifier) { Digest::SHA2.new.update(to_digest.inspect).to_s }
let(:sign) { metadata[:sign] }
let(:scope) { metadata[:scope] }
let(:timestamp) { Time.now.utc }
let(:salt) { rand(100000) }
def to_h
to_digest.merge(identifier: identifier)
end
def to_digest
{
promiser: promiser,
promisee: promisee,
body: body,
metadata: metadata,
timestamp: timestamp,
salt: salt
}
end
end
end
Lazy-eval lends itself well to functional patterns. However, Ruby doesn't include many
built-in operators for working with higher order functions. rlet
includes a refinment (Ruby 2.0+)
that adds in some operators.
Currently, only two are supported: |
which composes right, and *
which composes left.
The best example is when working with Intermodal presenters:
require 'rlet/functional'
module SomeApi
module API
class V1_0 < Intermodal::API
using RLet::Functional
self.default_per_page = 25
map_data do
attribute = ->(field) { ->(r) { r.send(field) } }
helper = ->(_method) { ActionController::Base.helpers.method(_method) }
presentation_for :book do
presents :id
presents :price, with: attribute.(:price) | helper.(:number_to_currency)
end
end
end
end
end