Class: Flows::Contract::Compose
- Inherits:
-
Flows::Contract
- Object
- Flows::Contract
- Flows::Contract::Compose
- Defined in:
- lib/flows/contract/compose.rb
Overview
Allows to combine two or more contracts.
From type system perspective - this composition is intersection of types. It means that value passes contract if it passes each particular contract in a composition.
Composition and Transform Laws
Golden rule: don't use contracts with transformations in composition if you can. In the most cases you can compose contracts without transformations and apply one transformation to composite contract.
Composition of contracts' transformations MUST obey Transform Laws (see Flows::Contract documentation for details). To achieve this each particular transform MUST obey following additional laws:
# let `c` be a contract composition
# 1. each transform should not leave composite type
#
# for any `x` valid for composite type
c.check!(x) == true
# and for any contract `c_i` from composition:
c.check!(c_i.transform!(x)) == true
# 2. tranforms can be applied in any order
#
# for any `x` valid for composite type
c.check!(x) == true
# for any two contracts `c_i` and `c_j` from composition:
c_i(c_j(x)) == c_j(c_i(x))
Why do we need the first law? To prevent situations when original value matches composite type, but transformed value doesn't. Example:
Flows::Contract.make do
compose(
transform(either(String, Symbol), &:to_sym),
String
)
end
Second laws makes composition of transforms to obey 2nd transform law. Example of correct composable transforms:
Flows::Contract.make do
compose(
transform(String, &:strip),
transform(String, &:trim)
)
end
Formal proof is based on this theorem proof.
Instance Method Summary collapse
- #check!(other) ⇒ Object
-
#initialize(*contracts) ⇒ Compose
constructor
A new instance of Compose.
- #transform!(other) ⇒ Object
Methods inherited from Flows::Contract
#===, #check, make, #to_proc, #transform
Constructor Details
#initialize(*contracts) ⇒ Compose
Returns a new instance of Compose.
57 58 59 60 61 |
# File 'lib/flows/contract/compose.rb', line 57 def initialize(*contracts) raise 'Contract list must not be empty' if contracts.length.zero? @contracts = contracts.map(&method(:to_contract)) end |
Instance Method Details
#check!(other) ⇒ Object
64 65 66 67 |
# File 'lib/flows/contract/compose.rb', line 64 def check!(other) @contracts.each { |con| con.check!(other) } true end |
#transform!(other) ⇒ Object
70 71 72 73 74 |
# File 'lib/flows/contract/compose.rb', line 70 def transform!(other) @contracts.reduce(other) do |value, con| con.transform!(value) end end |