Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Transition composition type #453

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/dry/types/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ def >(other)
compose(other, Implication)
end

# Compose two types into an Transition type
#
# @param [Type] right resulting type
#
# @return [Transition, Transition::Constrained]
#
# @api public
def >=(right)
compose(right, Transition)
end

# Compose two types into an Transition type
#
# @param [Type] left transitive type
#
# @return [Transition, Transition::Constrained]
#
# @api public
def <=(left)
left >= self
end

# Turn a type into an optional type
#
# @return [Sum]
Expand Down
15 changes: 15 additions & 0 deletions lib/dry/types/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,21 @@ def visit_any(meta)
registry["any"].meta(meta)
end

def visit_intersection(node)
*types, meta = node
types.map { |type| visit(type) }.reduce(:&).meta(meta)
end

def visit_implication(node)
*types, meta = node
types.map { |type| visit(type) }.reduce(:>).meta(meta)
end

def visit_transition(node)
*types, meta = node
types.map { |type| visit(type) }.reduce(:>=).meta(meta)
end

def compile_fn(fn)
type, *node = fn

Expand Down
4 changes: 3 additions & 1 deletion lib/dry/types/printer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ class Printer
Intersection,
Intersection::Constrained,
Implication,
Implication::Constrained
Implication::Constrained,
Transition,
Transition::Constrained
] => :visit_composition,
Any.class => :visit_any
}.flat_map { |k, v| Array(k).map { |kk| [kk, v] } }.to_h
Expand Down
135 changes: 135 additions & 0 deletions lib/dry/types/transition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# frozen_string_literal: true

module Dry
module Types
# # Transition type
#
# It is a composition type, where the left type is an intermediary one and
# the right type is a resulting.
#
# It differs from an {Implication} by an input for the right type: instead of
# bypassing the original input, it will pass a coerced result of the left type.
# The same effect is possible with a {Constructor} but {Transition} allows to
# keep a transitive object as type instead of function.
#
# When the left type if failed it bypasses input value to the right type.
#
# @example Usage
# coercible_proc = Dry::Types::Nominal.new(Proc).constructor(&:to_proc)
# ceorcible_sym = Dry::Types['coercible.symbol']
#
# # the left-hand type is resulting when is built with {Builder#<=}
# (coercible_proc <= coercible_sym)['example'] # => #<Proc:(&:example)>
#
# # the right-hand type is resulting when is built with {Builder#>=}
# (coercible_sym >= coercible_proc)['example'] # => #<Proc:(&:example)>
#
# @api public
class Transition
include Composition

def self.operator
:>=
end

class Constrained < Transition
def rule
right.rule | left.rule
end

def pristine
self.class.new(left, right.pristine, **@options)
end
end

# @param [Object] input
#
# @return [Object]
#
# @api private
def call_unsafe(input)
left_result = left.try(input)
if left_result.success?
right.call_unsafe(left_result.input)
else
right.call_unsafe(input)
end
end

# @param [Object] input
#
# @return [Object]
#
# @api private
def call_safe(input, &block)
left_result = left.try(input)
if left_result.success?
right.call_safe(left_result.input, &block)
else
right.call_safe(input, &block)
end
end

# @param [Object] input
#
# @api public
def try(input)
left_result = left.try(input)

if left_result.success?
right.try(left_result.input)
else
right.try(input)
end
end

# @param [Object] value
#
# @return [Boolean]
#
# @api private
def primitive?(value)
left_result = left.try(input)

if left.primitive?(value)
right.primitive?(left_result.input)
else
right.primitive?(value)
end
end

# #meta always delegates to the right branch of Transition type
#
# @see [Meta#meta]
#
# @api public
def meta(data = Undefined)
if Undefined.equal?(data)
right.meta
else
self.class.new(left, right.meta(data), **@options)
end
end

# @param [Hash] options
#
# @return [Constrained,Sum]
#
# @see Builder#constrained
#
# @api public
def constrained(options)
self.class.new(left, right.constrained(options), **@options)
end

# Resets meta in the right type
#
# @return [Dry::Types::Type]
#
# @api public
def pristine
self.class.new(left, right.pristine, **@options)
end
end
end
end
42 changes: 42 additions & 0 deletions spec/dry/types/compiler_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -377,4 +377,46 @@
expect(type).to eql(source)
end
end

context "composites" do
let(:strict_nil_ast) do
[:constrained,
[[:nominal, [NilClass, {}]],
[:predicate, [:type?, [[:type, NilClass], [:input, Undefined]]]]]]
end

let(:strict_integer_ast) do
[:constrained,
[[:nominal, [Integer, {}]],
[:predicate, [:type?, [[:type, Integer], [:input, Undefined]]]]]]
end

let(:any_numeric_ast) do
[:constrained, [[:any, {}], [:predicate, [:type?, [[:type, Numeric], [:input, Undefined]]]]]]
end

it 'builds a sum' do
ast = [:sum, [strict_nil_ast, strict_integer_ast, {}]]
type = compiler.(ast)
expect(type).to eql(Dry::Types['integer'].optional)
end

it 'builds an implication' do
ast = [:implication, [any_numeric_ast, strict_integer_ast, {}]]
type = compiler.(ast)
expect(type).to eql(Dry::Types['any'].constrained(type: Numeric) > Dry::Types['integer'])
end

it 'builds an intersection' do
ast = [:intersection, [any_numeric_ast, strict_integer_ast, {}]]
type = compiler.(ast)
expect(type).to eql(Dry::Types['any'].constrained(type: Numeric) & Dry::Types['integer'])
end

it 'builds a transition' do
ast = [:transition, [any_numeric_ast, strict_integer_ast, {}]]
type = compiler.(ast)
expect(type).to eql(Dry::Types['any'].constrained(type: Numeric) >= Dry::Types['integer'])
end
end
end
13 changes: 6 additions & 7 deletions spec/dry/types/implication_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,14 @@
end

describe "#meta" do
context "optional types" do
let(:meta) { {foo: :bar} }
let(:meta) { {foo: :bar} }

subject(:type) { t::Nominal::String.optional }
subject(:type) { t::Nominal::Hash > t.Hash(foo: t::Nominal::Integer) }

it "uses meta from the right branch" do
expect(type.meta(meta).meta).to eql(meta)
expect(type.meta(meta).right.meta).to eql(meta)
end
it "has no special meta handling" do
expect(type.meta(meta).meta).to eql(meta)
expect(type.meta(meta).left.meta).to eql({})
expect(type.meta(meta).right.meta).to eql({})
end
end
end
13 changes: 6 additions & 7 deletions spec/dry/types/intersection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,14 @@
end

describe "#meta" do
context "optional types" do
let(:meta) { {foo: :bar} }
let(:meta) { {foo: :bar} }

subject(:type) { Dry::Types["string"].optional }
subject(:type) { t::Nominal::Hash & t.Hash(foo: t::Nominal::Integer) }

it "uses meta from the right branch" do
expect(type.meta(meta).meta).to eql(meta)
expect(type.meta(meta).right.meta).to eql(meta)
end
it "has no special meta handling" do
expect(type.meta(meta).meta).to eql(meta)
expect(type.meta(meta).left.meta).to eql({})
expect(type.meta(meta).right.meta).to eql({})
end
end
end
Loading