Skip to content

Instantly share code, notes, and snippets.

@ismasan
Created May 7, 2020 14:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ismasan/7a9ddef43075165cc7daf59b78649cd3 to your computer and use it in GitHub Desktop.
Save ismasan/7a9ddef43075165cc7daf59b78649cd3 to your computer and use it in GitHub Desktop.
module Identities
TypeError = Class.new(ArgumentError)
class Nope
def matches?(_)
false
end
def call(value)
value
end
end
class Identity
NOOP = ->(v) { v }
attr_reader :name
def initialize(name, sub: Nope.new)
@name = name
@coercions = {}
@sub = sub
end
def coerces(type, coercion = nil, &block)
coercion = block unless coercion
coercion = NOOP unless coercion
coercions[type] = coercion
self
end
def [](child)
copy(sub: child)
end
def call(value)
return sub.call(value) if sub.matches?(value)
coercion = coercion_for(value)
raise TypeError, "#{value.inspect} (#{value.class}) cannot be coerced into #{name}" unless coercion
coercion.call(value)
end
def matches?(value)
sub.matches?(value) || !!coercion_for(value)
end
def copy(sub: nil)
self.class.new(name, sub: sub || @sub).tap do |i|
coercions.each do |k, v|
i.coerces k, v
end
end
end
protected
attr_reader :coercions
def coercion_for(value)
coercion = nil
coercions.each do |type, callable|
if type.is_a?(::Class)
coercion = callable and break if value.is_a?(type)
else
coercion = callable and break if type === value
end
end
coercion
end
private
attr_reader :sub
end
String = Identity.new('String').tap do |i|
i.coerces ::String, ->(value) { value }
end
Integer = Identity.new('Integer').tap do |i|
i.coerces ::Numeric, ->(value) { value.to_i }
end
Boolean = Identity.new('Boolean').tap do |i|
i.coerces TrueClass, ->(v) { v }
i.coerces FalseClass, ->(v) { v }
end
Maybe = Identity.new('Maybe').tap do |i|
i.coerces NilClass, ->(v) { v }
end
CSV = Identity.new('CSV').tap do |i|
i.coerces ::String, ->(v) { v.split(/\s*,\s*/) }
end
module Lax
String = Identities::String.copy.tap do |i|
i.coerces BigDecimal, ->(value) { value.to_s('F') }
i.coerces Numeric, ->(value) { value.to_s }
end
Integer = Identities::Integer.copy.tap do |i|
i.coerces /^\d+$/, ->(value) { value.to_i }
i.coerces /^\d+.\d*?$/, ->(value) { value.to_i }
end
end
module Forms
Boolean = Identities::Boolean.copy.tap do |i|
i.coerces /^true$/i, ->(_) { true }
i.coerces '1', ->(_) { true }
i.coerces 1, ->(_) { true }
i.coerces /^false$/i, ->(_) { false }
i.coerces '0', ->(_) { false }
i.coerces 0, ->(_) { false }
end
end
end
RSpec.describe Identities do
specify Identities::String do
expect(Identities::String.call('aa')).to eq('aa')
expect {
Identities::String.call(10)
}.to raise_error(Identities::TypeError)
end
specify Identities::Lax::String do
expect(Identities::Lax::String.call('aa')).to eq('aa')
expect(Identities::Lax::String.call(11)).to eq('11')
expect(Identities::Lax::String.call(11.10)).to eq('11.1')
expect(Identities::Lax::String.call(BigDecimal('111.2011'))).to eq('111.2011')
expect {
Identities::String.call(true)
}.to raise_error(Identities::TypeError)
end
specify Identities::Lax::Integer do
expect(Identities::Lax::Integer.call(113)).to eq(113)
expect(Identities::Lax::Integer.call(113.10)).to eq(113)
expect(Identities::Lax::Integer.call('113')).to eq(113)
expect(Identities::Lax::Integer.call('113.10')).to eq(113)
expect {
Identities::Lax::Integer.call('nope')
}.to raise_error(Identities::TypeError)
end
specify Identities::Boolean do
expect(Identities::Boolean.call(true)).to be true
expect(Identities::Boolean.call(false)).to be false
expect {
Identities::Boolean.call('true')
}.to raise_error(Identities::TypeError)
end
specify Identities::Forms::Boolean do
expect(Identities::Forms::Boolean.call(true)).to be true
expect(Identities::Forms::Boolean.call(false)).to be false
expect(Identities::Forms::Boolean.call('true')).to be true
expect(Identities::Forms::Boolean.matches?('true')).to be true
expect(Identities::Forms::Boolean.matches?('false')).to be true
expect(Identities::Forms::Boolean.call('false')).to be false
expect(Identities::Forms::Boolean.call('1')).to be true
expect(Identities::Forms::Boolean.call('0')).to be false
expect(Identities::Forms::Boolean.call(1)).to be true
expect(Identities::Forms::Boolean.call(0)).to be false
expect {
Identities::Forms::Boolean.call('nope')
}.to raise_error(Identities::TypeError)
end
specify Identities::Maybe do
expect(Identities::Maybe[Identities::String].call(nil)).to be nil
expect(Identities::Maybe[Identities::String].call('foo')).to eq 'foo'
expect(Identities::Maybe[Identities::String].matches?(nil)).to be true
expect(Identities::Maybe[Identities::String].matches?('foo')).to be true
expect(Identities::Maybe[Identities::String].matches?(11)).to be false
expect(Identities::Maybe[Identities::Lax::String].matches?(11)).to be true
end
specify Identities::CSV do
expect(Identities::CSV.call('one,two, three , four')).to eq(%w[one two three four])
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment