bcdd-result 0.10.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -15
- data/README.md +326 -67
- data/Rakefile +8 -2
- data/lib/bcdd/result/callable_and_then/caller.rb +49 -0
- data/lib/bcdd/result/callable_and_then/config.rb +15 -0
- data/lib/bcdd/result/callable_and_then/error.rb +11 -0
- data/lib/bcdd/result/callable_and_then.rb +9 -0
- data/lib/bcdd/result/config/switchers/addons.rb +9 -4
- data/lib/bcdd/result/config/switchers/features.rb +5 -1
- data/lib/bcdd/result/config.rb +9 -3
- data/lib/bcdd/result/context/callable_and_then.rb +39 -0
- data/lib/bcdd/result/context/expectations/mixin.rb +11 -3
- data/lib/bcdd/result/context/mixin.rb +13 -5
- data/lib/bcdd/result/context/success.rb +12 -8
- data/lib/bcdd/result/context.rb +34 -16
- data/lib/bcdd/result/error.rb +20 -11
- data/lib/bcdd/result/expectations/mixin.rb +12 -6
- data/lib/bcdd/result/expectations.rb +7 -7
- data/lib/bcdd/result/mixin.rb +11 -5
- data/lib/bcdd/result/transitions/tracking/disabled.rb +14 -4
- data/lib/bcdd/result/transitions/tracking/enabled.rb +38 -18
- data/lib/bcdd/result/transitions/tree.rb +10 -6
- data/lib/bcdd/result/transitions.rb +8 -10
- data/lib/bcdd/result/version.rb +1 -1
- data/lib/bcdd/result.rb +38 -23
- data/sig/bcdd/result/callable_and_then.rbs +60 -0
- data/sig/bcdd/result/config.rbs +3 -0
- data/sig/bcdd/result/context.rbs +73 -9
- data/sig/bcdd/result/error.rbs +9 -6
- data/sig/bcdd/result/expectations.rbs +12 -8
- data/sig/bcdd/result/mixin.rbs +10 -2
- data/sig/bcdd/result/transitions.rbs +16 -5
- data/sig/bcdd/result.rbs +12 -8
- metadata +8 -2
data/README.md
CHANGED
@@ -73,11 +73,17 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
|
|
73
73
|
- [`BCDD::Result.transitions`](#bcddresulttransitions)
|
74
74
|
- [Configuration](#configuration)
|
75
75
|
- [`BCDD::Result.configuration`](#bcddresultconfiguration)
|
76
|
-
- [`config.addon.enable!(:continue)`](#
|
76
|
+
- [`config.addon.enable!(:given, :continue)`](#configaddonenablegiven-continue)
|
77
77
|
- [`config.constant_alias.enable!('Result', 'BCDD::Context')`](#configconstant_aliasenableresult-bcddcontext)
|
78
78
|
- [`config.pattern_matching.disable!(:nil_as_valid_value_checking)`](#configpattern_matchingdisablenil_as_valid_value_checking)
|
79
79
|
- [`config.feature.disable!(:expectations)`](#configfeaturedisableexpectations)
|
80
80
|
- [`BCDD::Result.config`](#bcddresultconfig)
|
81
|
+
- [`BCDD::Result#and_then!`](#bcddresultand_then)
|
82
|
+
- [Dependency Injection](#dependency-injection-1)
|
83
|
+
- [Configuration](#configuration-1)
|
84
|
+
- [Analysis: Why is `and_then!` an Anti-pattern?](#analysis-why-is-and_then-an-anti-pattern)
|
85
|
+
- [`#and_then` versus `#and_then!`](#and_then-versus-and_then)
|
86
|
+
- [Analysis: Why is `#and_then` the antidote/standard?](#analysis-why-is-and_then-the-antidotestandard)
|
81
87
|
- [About](#about)
|
82
88
|
- [Development](#development)
|
83
89
|
- [Contributing](#contributing)
|
@@ -649,9 +655,9 @@ Divide.call(2, 2)
|
|
649
655
|
|
650
656
|
This method generates a module that any object can include or extend. It adds two methods to the target object: `Success()` and `Failure()`.
|
651
657
|
|
652
|
-
The main difference between these methods and `BCDD::Result::Success()`/`BCDD::Result::Failure()` is that the former will utilize the target object (which has received the include/extend) as the result's
|
658
|
+
The main difference between these methods and `BCDD::Result::Success()`/`BCDD::Result::Failure()` is that the former will utilize the target object (which has received the include/extend) as the result's source.
|
653
659
|
|
654
|
-
Because the result has a
|
660
|
+
Because the result has a source, the `#and_then` method can call methods from it.
|
655
661
|
|
656
662
|
##### Class example (Instance Methods)
|
657
663
|
|
@@ -746,9 +752,9 @@ Divide.call(4, '2') #<BCDD::Result::Failure type=:invalid_arg value="arg2 must b
|
|
746
752
|
|
747
753
|
To use the `#and_then` method to call methods, they must use `Success()` and `Failure()` to produce the results.
|
748
754
|
|
749
|
-
If you try to use `BCDD::Result::
|
755
|
+
If you try to use `BCDD::Result::Success()`/`BCDD::Result::Failure()`, or results from another `BCDD::Result.mixin` instance with `#and_then`, it will raise an error because the sources are different.
|
750
756
|
|
751
|
-
**Note:** You can still use the block syntax, but all the results must be produced by the
|
757
|
+
**Note:** You can still use the block syntax, but all the results must be produced by the source's `Success()` and `Failure()` methods.
|
752
758
|
|
753
759
|
```ruby
|
754
760
|
module ValidateNonzero
|
@@ -794,13 +800,13 @@ Look at the error produced by the code above:
|
|
794
800
|
```ruby
|
795
801
|
Divide.call(2, 0)
|
796
802
|
|
797
|
-
# You cannot call #and_then and return a result that does not belong to the
|
798
|
-
# Expected
|
799
|
-
# Given
|
803
|
+
# You cannot call #and_then and return a result that does not belong to the same source! (BCDD::Result::Error::InvalidResultSource)
|
804
|
+
# Expected source: Divide
|
805
|
+
# Given source: ValidateNonzero
|
800
806
|
# Given result: #<BCDD::Result::Failure type=:division_by_zero value="arg2 must not be zero">
|
801
807
|
```
|
802
808
|
|
803
|
-
In order to fix this, you must handle the result produced by `ValidateNonzero.call()` and return a result that belongs to the
|
809
|
+
In order to fix this, you must handle the result produced by `ValidateNonzero.call()` and return a result that belongs to the same source.
|
804
810
|
|
805
811
|
```ruby
|
806
812
|
module ValidateNonzero
|
@@ -832,7 +838,8 @@ module Divide
|
|
832
838
|
end
|
833
839
|
|
834
840
|
def validate_nonzero(numbers)
|
835
|
-
# In this case we are handling the
|
841
|
+
# In this case we are handling the result from other source
|
842
|
+
# and returning our own
|
836
843
|
ValidateNonzero.call(numbers).handle do |on|
|
837
844
|
on.success { |numbers| Success(:ok, numbers) }
|
838
845
|
|
@@ -858,8 +865,8 @@ Divide.call(2, 0)
|
|
858
865
|
|
859
866
|
##### Dependency Injection
|
860
867
|
|
861
|
-
The `BCDD::Result#and_then` accepts a second argument that will be used to share a value with the
|
862
|
-
To receive this argument, the
|
868
|
+
The `BCDD::Result#and_then` accepts a second argument that will be used to share a value with the source's method.
|
869
|
+
To receive this argument, the source's method must have an arity of two, where the first argument will be the result value and the second will be the injected value.
|
863
870
|
|
864
871
|
```ruby
|
865
872
|
require 'logger'
|
@@ -918,35 +925,48 @@ Divide.call(4, 2, logger: Logger.new(IO::NULL))
|
|
918
925
|
|
919
926
|
The `BCDD::Result.mixin` also accepts the `config:` argument. It is a hash that will be used to define custom behaviors for the mixin.
|
920
927
|
|
928
|
+
**given**
|
929
|
+
|
930
|
+
This addon is enabled by default. It will create the `Given(value)` method. Use it to add a value to the result chain and invoke the next step (through `and_then`).
|
931
|
+
|
932
|
+
You can turn it off by passing `given: false` to the `config:` argument or using the `BCDD::Result.configuration`.
|
933
|
+
|
921
934
|
**continue**
|
922
935
|
|
923
|
-
This addon will create the `Continue(value)` method and change the `Success()` behavior to
|
936
|
+
This addon will create the `Continue(value)` method and change the `Success()` behavior to terminate the step chain.
|
924
937
|
|
925
|
-
So, if you want to advance to the next step, you must use `Continue(value)` instead of `Success(type, value)`. Otherwise, the step chain will be
|
938
|
+
So, if you want to advance to the next step, you must use `Continue(value)` instead of `Success(type, value)`. Otherwise, the step chain will be terminated.
|
939
|
+
|
940
|
+
In this example below, the `validate_nonzero` will return a `Success(:division_completed, 0)` and terminate the chain if the first number is zero.
|
926
941
|
|
927
942
|
```ruby
|
928
943
|
module Divide
|
929
944
|
extend self, BCDD::Result.mixin(config: { addon: { continue: true } })
|
930
945
|
|
931
946
|
def call(arg1, arg2)
|
932
|
-
|
947
|
+
Given([arg1, arg2])
|
948
|
+
.and_then(:validate_numbers)
|
933
949
|
.and_then(:validate_nonzero)
|
934
950
|
.and_then(:divide)
|
935
951
|
end
|
936
952
|
|
937
953
|
private
|
938
954
|
|
939
|
-
def validate_numbers(
|
940
|
-
|
941
|
-
arg2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
955
|
+
def validate_numbers(numbers)
|
956
|
+
number1, number2 = numbers
|
942
957
|
|
943
|
-
|
958
|
+
number1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
|
959
|
+
number2.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg2 must be numeric')
|
960
|
+
|
961
|
+
Continue(numbers)
|
944
962
|
end
|
945
963
|
|
946
964
|
def validate_nonzero(numbers)
|
947
|
-
return
|
965
|
+
return Failure(:division_by_zero, 'arg2 must not be zero') if numbers.last.zero?
|
948
966
|
|
949
|
-
|
967
|
+
return Success(:division_completed, 0) if numbers.first.zero?
|
968
|
+
|
969
|
+
Continue(numbers)
|
950
970
|
end
|
951
971
|
|
952
972
|
def divide((number1, number2))
|
@@ -1571,7 +1591,7 @@ Divide.new.call(10, 5)
|
|
1571
1591
|
#<BCDD::Result::Context::Success type=:ok value={:number=>2, :number1=>10, :number2=>5}>
|
1572
1592
|
```
|
1573
1593
|
|
1574
|
-
> PS: The `#and_expose` produces a
|
1594
|
+
> PS: The `#and_expose` produces a terminal success by default. This means the next step will not be executed even if you call `#and_then` after `#and_expose`. To change this behavior, you can pass `terminal: false` to `#and_expose`.
|
1575
1595
|
|
1576
1596
|
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
1577
1597
|
|
@@ -1678,16 +1698,24 @@ Divide::Result::Success(:division_completed, number: '2')
|
|
1678
1698
|
|
1679
1699
|
The `BCDD::Result::Context.mixin` and `BCDD::Result::Context::Expectations.mixin` also accepts the `config:` argument. And it works the same way as the `BCDD::Result` mixins.
|
1680
1700
|
|
1681
|
-
**
|
1701
|
+
**given**
|
1702
|
+
|
1703
|
+
This addon is enabled by default. It will create the `Given(*value)` method. Use it to add a value to the result chain and invoke the next step (through `and_then`).
|
1682
1704
|
|
1683
|
-
|
1705
|
+
You can turn it off by passing `given: false` to the `config:` argument or using the `BCDD::Result.configuration`.
|
1684
1706
|
|
1685
|
-
|
1707
|
+
The `Given()` addon for a BCDD::Result::Context can be called with one or more arguments. The arguments will be converted to a hash (`to_h`) and merged to define the first value of the result chain.
|
1708
|
+
|
1709
|
+
**continue**
|
1710
|
+
|
1711
|
+
The `BCDD::Result::Context.mixin(config: { addon: { continue: true } })` or `BCDD::Result::Context::Expectations.mixin(config: { addon: { continue: true } })` creates the `Continue(value)` method and change the `Success()` behavior to terminate the step chain.
|
1712
|
+
|
1713
|
+
So, if you want to advance to the next step, you must use `Continue(**value)` instead of `Success(type, **value)`. Otherwise, the step chain will be terminated.
|
1686
1714
|
|
1687
1715
|
Let's use a mix of `BCDD::Result::Context` features to see in action with this add-on:
|
1688
1716
|
|
1689
1717
|
```ruby
|
1690
|
-
module
|
1718
|
+
module Division
|
1691
1719
|
require 'logger'
|
1692
1720
|
|
1693
1721
|
extend self, BCDD::Result::Context::Expectations.mixin(
|
@@ -1705,25 +1733,28 @@ module Divide
|
|
1705
1733
|
)
|
1706
1734
|
|
1707
1735
|
def call(arg1, arg2, logger: ::Logger.new(STDOUT))
|
1708
|
-
|
1709
|
-
.and_then(:
|
1736
|
+
Given(number1: arg1, number2: arg2)
|
1737
|
+
.and_then(:require_numbers)
|
1738
|
+
.and_then(:check_for_zeros)
|
1710
1739
|
.and_then(:divide, logger: logger)
|
1711
1740
|
.and_expose(:division_completed, [:number])
|
1712
1741
|
end
|
1713
1742
|
|
1714
1743
|
private
|
1715
1744
|
|
1716
|
-
def
|
1717
|
-
|
1718
|
-
|
1745
|
+
def require_numbers(number1:, number2:)
|
1746
|
+
number1.is_a?(::Numeric) or return Failure(:invalid_arg, message: 'arg1 must be numeric')
|
1747
|
+
number2.is_a?(::Numeric) or return Failure(:invalid_arg, message: 'arg2 must be numeric')
|
1719
1748
|
|
1720
|
-
Continue(
|
1749
|
+
Continue()
|
1721
1750
|
end
|
1722
1751
|
|
1723
|
-
def
|
1724
|
-
return
|
1752
|
+
def check_for_zeros(number1:, number2:)
|
1753
|
+
return Failure(:division_by_zero, message: 'arg2 must not be zero') if number2.zero?
|
1754
|
+
|
1755
|
+
return Success(:division_completed, number: 0) if number1.zero?
|
1725
1756
|
|
1726
|
-
|
1757
|
+
Continue()
|
1727
1758
|
end
|
1728
1759
|
|
1729
1760
|
def divide(number1:, number2:, logger:)
|
@@ -1735,23 +1766,26 @@ module Divide
|
|
1735
1766
|
end
|
1736
1767
|
end
|
1737
1768
|
|
1738
|
-
|
1769
|
+
Division.call(14, 2)
|
1739
1770
|
# I, [2023-10-27T02:01:05.812388 #77823] INFO -- : The division result is 7
|
1740
1771
|
#<BCDD::Result::Context::Success type=:division_completed value={:number=>7}>
|
1741
1772
|
|
1742
|
-
|
1773
|
+
Division.call(0, 2)
|
1774
|
+
##<BCDD::Result::Context::Success type=:division_completed value={:number=>0}>
|
1775
|
+
|
1776
|
+
Division.call('14', 2)
|
1743
1777
|
#<BCDD::Result::Context::Failure type=:invalid_arg value={:message=>"arg1 must be numeric"}>
|
1744
1778
|
|
1745
|
-
|
1779
|
+
Division.call(14, '2')
|
1746
1780
|
#<BCDD::Result::Context::Failure type=:invalid_arg value={:message=>"arg2 must be numeric"}>
|
1747
1781
|
|
1748
|
-
|
1782
|
+
Division.call(14, 0)
|
1749
1783
|
#<BCDD::Result::Context::Failure type=:division_by_zero value={:message=>"arg2 must not be zero"}>
|
1750
1784
|
```
|
1751
1785
|
|
1752
1786
|
### `BCDD::Result.transitions`
|
1753
1787
|
|
1754
|
-
Use `BCDD::Result.transitions(&block)` to track all transitions in the same or between different operations. When there is a nesting of transition blocks, this mechanism will be able to correlate parent and child blocks and present the duration of all operations in milliseconds.
|
1788
|
+
Use `BCDD::Result.transitions(&block)` to track all transitions in the same or between different operations (it works with `BCDD::Result` and `BCDD::Result::Context`). When there is a nesting of transition blocks, this mechanism will be able to correlate parent and child blocks and present the duration of all operations in milliseconds.
|
1755
1789
|
|
1756
1790
|
When you wrap the creation of the result with `BCDD::Result.transitions`, the final result will expose all the transition records through the `BCDD::Result#transitions` method.
|
1757
1791
|
|
@@ -1761,7 +1795,8 @@ class Division
|
|
1761
1795
|
|
1762
1796
|
def call(arg1, arg2)
|
1763
1797
|
BCDD::Result.transitions(name: 'Division', desc: 'divide two numbers') do
|
1764
|
-
|
1798
|
+
Given([arg1, arg2])
|
1799
|
+
.and_then(:require_numbers)
|
1765
1800
|
.and_then(:check_for_zeros)
|
1766
1801
|
.and_then(:divide)
|
1767
1802
|
end
|
@@ -1771,7 +1806,7 @@ class Division
|
|
1771
1806
|
|
1772
1807
|
ValidNumber = ->(arg) { arg.is_a?(Numeric) && arg != Float::NAN && arg != Float::INFINITY }
|
1773
1808
|
|
1774
|
-
def require_numbers(arg1, arg2)
|
1809
|
+
def require_numbers((arg1, arg2))
|
1775
1810
|
ValidNumber[arg1] or return Failure(:invalid_arg, 'arg1 must be a valid number')
|
1776
1811
|
ValidNumber[arg2] or return Failure(:invalid_arg, 'arg2 must be a valid number')
|
1777
1812
|
|
@@ -1823,52 +1858,68 @@ result.transitions
|
|
1823
1858
|
},
|
1824
1859
|
:records => [
|
1825
1860
|
{
|
1826
|
-
:root
|
1827
|
-
:parent
|
1828
|
-
:current
|
1829
|
-
:result
|
1830
|
-
:and_then
|
1831
|
-
:time
|
1861
|
+
:root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1862
|
+
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1863
|
+
:current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
|
1864
|
+
:result=>{:kind=>:success, :type=>:given, :value=>[20, 2]},
|
1865
|
+
:and_then=>{},
|
1866
|
+
:time=>2024-01-02 03:35:11.248418 UTC
|
1867
|
+
},
|
1868
|
+
{
|
1869
|
+
:root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1870
|
+
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1871
|
+
:current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
|
1872
|
+
:result=>{:kind=>:success, :type=>:continued, :value=>[20, 2]},
|
1873
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106099028>, :method_name=>:require_numbers},
|
1874
|
+
:time=>2024-01-02 03:35:11.248558 UTC
|
1832
1875
|
},
|
1833
1876
|
{
|
1834
1877
|
:root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1835
1878
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1836
1879
|
:current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
|
1837
1880
|
:result=>{:kind=>:success, :type=>:continued, :value=>[20, 2]},
|
1838
|
-
:and_then=>{:type=>:method, :arg=>nil, :
|
1839
|
-
:time=>
|
1881
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106099028>, :method_name=>:check_for_zeros},
|
1882
|
+
:time=>2024-01-02 03:35:11.248587 UTC
|
1840
1883
|
},
|
1841
1884
|
{
|
1842
1885
|
:root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1843
1886
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1844
1887
|
:current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
|
1845
1888
|
:result=>{:kind=>:success, :type=>:division_completed, :value=>10},
|
1846
|
-
:and_then=>{:type=>:method, :arg=>nil, :
|
1847
|
-
:time=>
|
1889
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106099028>, :method_name=>:divide},
|
1890
|
+
:time=>2024-01-02 03:35:11.248607 UTC
|
1848
1891
|
},
|
1849
1892
|
{
|
1850
1893
|
:root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1851
1894
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1852
1895
|
:current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
|
1853
|
-
:result=>{:kind=>:success, :type=>:
|
1896
|
+
:result=>{:kind=>:success, :type=>:given, :value=>[10, 2]},
|
1854
1897
|
:and_then=>{},
|
1855
|
-
:time=>
|
1898
|
+
:time=>2024-01-02 03:35:11.24865 UTC
|
1899
|
+
},
|
1900
|
+
{
|
1901
|
+
:root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1902
|
+
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1903
|
+
:current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
|
1904
|
+
:result=>{:kind=>:success, :type=>:continued, :value=>[10, 2]},
|
1905
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106097ed0>, :method_name=>:require_numbers},
|
1906
|
+
:time=>2024-01-02 03:35:11.248661 UTC
|
1856
1907
|
},
|
1857
1908
|
{
|
1858
1909
|
:root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1859
1910
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1860
1911
|
:current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
|
1861
1912
|
:result=>{:kind=>:success, :type=>:continued, :value=>[10, 2]},
|
1862
|
-
:and_then=>{:type=>:method, :arg=>nil, :
|
1863
|
-
:time=>
|
1913
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106097ed0>, :method_name=>:check_for_zeros},
|
1914
|
+
:time=>2024-01-02 03:35:11.248672 UTC
|
1864
1915
|
},
|
1865
1916
|
{
|
1866
1917
|
:root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1867
1918
|
:parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1868
1919
|
:current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
|
1869
1920
|
:result=>{:kind=>:success, :type=>:division_completed, :value=>5},
|
1870
|
-
:and_then=>{:type=>:method, :arg=>nil, :
|
1871
|
-
:time=>
|
1921
|
+
:and_then=>{:type=>:method, :arg=>nil, :source=><Division:0x0000000106097ed0>, :method_name=>:divide},
|
1922
|
+
:time=>2024-01-02 03:35:11.248682 UTC
|
1872
1923
|
},
|
1873
1924
|
{
|
1874
1925
|
:root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
@@ -1876,7 +1927,7 @@ result.transitions
|
|
1876
1927
|
:current=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
|
1877
1928
|
:result=>{:kind=>:success, :type=>:sum, :value=>15},
|
1878
1929
|
:and_then=>{},
|
1879
|
-
:time=>
|
1930
|
+
:time=>2024-01-02 03:35:11.248721 UTC
|
1880
1931
|
}
|
1881
1932
|
]
|
1882
1933
|
}
|
@@ -1909,7 +1960,7 @@ The `BCDD::Result.configuration` allows you to configure default behaviors for `
|
|
1909
1960
|
|
1910
1961
|
```ruby
|
1911
1962
|
BCDD::Result.configuration do |config|
|
1912
|
-
config.addon.enable!(:continue)
|
1963
|
+
config.addon.enable!(:given, :continue)
|
1913
1964
|
|
1914
1965
|
config.constant_alias.enable!('Result', 'BCDD::Context')
|
1915
1966
|
|
@@ -1923,9 +1974,11 @@ Use `disable!` to disable a feature and `enable!` to enable it.
|
|
1923
1974
|
|
1924
1975
|
Let's see what each configuration in the example above does:
|
1925
1976
|
|
1926
|
-
#### `config.addon.enable!(:continue)`
|
1977
|
+
#### `config.addon.enable!(:given, :continue)`
|
1927
1978
|
|
1928
|
-
This configuration enables the `Continue()` method for `BCDD::Result`, `BCDD::Result::Context`, `BCDD::Result::Expectation`, and `BCDD::Result::Context::Expectation`. Link to documentations: [(1)](#add-ons) [(2)](#mixin-add-ons).
|
1979
|
+
This configuration enables the `Continue()` method for `BCDD::Result.mixin`, `BCDD::Result::Context.mixin`, `BCDD::Result::Expectation.mixin`, and `BCDD::Result::Context::Expectation.mixin`. Link to documentations: [(1)](#add-ons) [(2)](#mixin-add-ons).
|
1980
|
+
|
1981
|
+
It is also enabling the `Given()` which is already enabled by default. Link to documentation: [(1)](#add-ons) [(2)](#mixin-add-ons).
|
1929
1982
|
|
1930
1983
|
#### `config.constant_alias.enable!('Result', 'BCDD::Context')`
|
1931
1984
|
|
@@ -1955,16 +2008,26 @@ The `BCDD::Result.config` allows you to access the current configuration.
|
|
1955
2008
|
|
1956
2009
|
```ruby
|
1957
2010
|
BCDD::Result.config.addon.enabled?(:continue)
|
2011
|
+
BCDD::Result.config.addon.enabled?(:given)
|
1958
2012
|
|
1959
2013
|
BCDD::Result.config.addon.options
|
1960
2014
|
# {
|
1961
2015
|
# :continue=>{
|
1962
2016
|
# :enabled=>false,
|
1963
2017
|
# :affects=>[
|
1964
|
-
# "BCDD::Result",
|
1965
|
-
# "BCDD::Result::Context",
|
1966
|
-
# "BCDD::Result::Expectations",
|
1967
|
-
# "BCDD::Result::Context::Expectations"
|
2018
|
+
# "BCDD::Result.mixin",
|
2019
|
+
# "BCDD::Result::Context.mixin",
|
2020
|
+
# "BCDD::Result::Expectations.mixin",
|
2021
|
+
# "BCDD::Result::Context::Expectations.mixin"
|
2022
|
+
# ]
|
2023
|
+
# },
|
2024
|
+
# :given=>{
|
2025
|
+
# :enabled=>true,
|
2026
|
+
# :affects=>[
|
2027
|
+
# "BCDD::Result.mixin",
|
2028
|
+
# "BCDD::Result::Context.mixin",
|
2029
|
+
# "BCDD::Result::Expectations.mixin",
|
2030
|
+
# "BCDD::Result::Context::Expectations.mixin"
|
1968
2031
|
# ]
|
1969
2032
|
# }
|
1970
2033
|
# }
|
@@ -2022,10 +2085,206 @@ BCDD::Result.config.feature.options
|
|
2022
2085
|
# "BCDD::Result",
|
2023
2086
|
# "BCDD::Result::Context"
|
2024
2087
|
# ]
|
2025
|
-
# }
|
2088
|
+
# },
|
2089
|
+
# :and_then!=>{
|
2090
|
+
# :enabled=>false,
|
2091
|
+
# :affects=>[
|
2092
|
+
# "BCDD::Result",
|
2093
|
+
# "BCDD::Result::Context"
|
2094
|
+
# ]
|
2095
|
+
# },
|
2026
2096
|
# }
|
2027
2097
|
```
|
2028
2098
|
|
2099
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2100
|
+
|
2101
|
+
## `BCDD::Result#and_then!`
|
2102
|
+
|
2103
|
+
In the Ruby ecosystem, several gems facilitate operation composition using classes and modules. Two notable examples are the `interactor` gem and the `u-case` gem.
|
2104
|
+
|
2105
|
+
**`interactor` gem example**
|
2106
|
+
|
2107
|
+
```ruby
|
2108
|
+
class PlaceOrder
|
2109
|
+
include Interactor::Organizer
|
2110
|
+
|
2111
|
+
organize CreateOrder,
|
2112
|
+
PayOrder,
|
2113
|
+
SendOrderConfirmation,
|
2114
|
+
NotifyAdmins
|
2115
|
+
end
|
2116
|
+
```
|
2117
|
+
|
2118
|
+
**`u-case` gem example**
|
2119
|
+
|
2120
|
+
```ruby
|
2121
|
+
class PlaceOrder < Micro::Case
|
2122
|
+
flow CreateOrder, PayOrder, SendOrderConfirmation, NotifyAdmins
|
2123
|
+
end
|
2124
|
+
|
2125
|
+
# Alternative approach
|
2126
|
+
class PlaceOrder < Micro::Case
|
2127
|
+
def call!
|
2128
|
+
call(CreateOrder)
|
2129
|
+
.then(PayOrder)
|
2130
|
+
.then(SendOrderConfirmation)
|
2131
|
+
.then(NotifyAdmins)
|
2132
|
+
end
|
2133
|
+
end
|
2134
|
+
```
|
2135
|
+
|
2136
|
+
To facilitate migration for users accustomed to the above approaches, `bcdd-result` includes the `BCDD::Result#and_then!`/`BCDD::Result::Context#and_then!` methods, which will invoke the method `call` of the given operation and expect it to return a `BCDD::Result`/`BCDD::Result::Context` object.
|
2137
|
+
|
2138
|
+
```ruby
|
2139
|
+
BCDD::Result.configure do |config|
|
2140
|
+
config.feature.enable!(:and_then!)
|
2141
|
+
end
|
2142
|
+
|
2143
|
+
class PlaceOrder
|
2144
|
+
include BCDD::Result::Context.mixin
|
2145
|
+
|
2146
|
+
def call(**input)
|
2147
|
+
Given(input)
|
2148
|
+
.and_then!(CreateOrder.new)
|
2149
|
+
.and_then!(PayOrder.new)
|
2150
|
+
.and_then!(SendOrderConfirmation.new)
|
2151
|
+
.and_then!(NotifyAdmins.new)
|
2152
|
+
end
|
2153
|
+
end
|
2154
|
+
```
|
2155
|
+
|
2156
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2157
|
+
|
2158
|
+
#### Dependency Injection
|
2159
|
+
|
2160
|
+
Like `#and_then`, `#and_then!` also supports an additional argument for dependency injection.
|
2161
|
+
|
2162
|
+
**In BCDD::Result**
|
2163
|
+
|
2164
|
+
```ruby
|
2165
|
+
class PlaceOrder
|
2166
|
+
include BCDD::Result.mixin
|
2167
|
+
|
2168
|
+
def call(input, logger:)
|
2169
|
+
Given(input)
|
2170
|
+
.and_then!(CreateOrder.new, logger)
|
2171
|
+
# Further method chaining...
|
2172
|
+
end
|
2173
|
+
end
|
2174
|
+
```
|
2175
|
+
|
2176
|
+
**In BCDD::Result::Context**
|
2177
|
+
|
2178
|
+
```ruby
|
2179
|
+
class PlaceOrder
|
2180
|
+
include BCDD::Result::Context.mixin
|
2181
|
+
|
2182
|
+
def call(logger:, **input)
|
2183
|
+
Given(input)
|
2184
|
+
.and_then!(CreateOrder.new, logger:)
|
2185
|
+
# Further method chaining...
|
2186
|
+
end
|
2187
|
+
end
|
2188
|
+
```
|
2189
|
+
|
2190
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2191
|
+
|
2192
|
+
#### Configuration
|
2193
|
+
|
2194
|
+
```ruby
|
2195
|
+
BCDD::Result.configure do |config|
|
2196
|
+
config.feature.enable!(:and_then!)
|
2197
|
+
|
2198
|
+
config.and_then!.default_method_name_to_call = :perform
|
2199
|
+
end
|
2200
|
+
```
|
2201
|
+
|
2202
|
+
**Explanation:**
|
2203
|
+
|
2204
|
+
- `enable!(:and_then!)`: Activates the `and_then!` feature.
|
2205
|
+
|
2206
|
+
- `default_method_name_to_call`: Sets a default method other than `:call` for `and_then!`.
|
2207
|
+
|
2208
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2209
|
+
|
2210
|
+
#### Analysis: Why is `and_then!` an Anti-pattern?
|
2211
|
+
|
2212
|
+
The `and_then!` approach, despite its brevity, introduces several issues:
|
2213
|
+
|
2214
|
+
- **Lack of Clarity:** The input/output relationship between the steps is not apparent.
|
2215
|
+
|
2216
|
+
- **Steps Coupling:** Each operation becomes interdependent (high coupling), complicating implementation and compromising the reusability of these operations.
|
2217
|
+
|
2218
|
+
We recommend cautious use of `#and_then!`. Due to these issues, it is turned off by default and considered an antipattern.
|
2219
|
+
|
2220
|
+
It should be a temporary solution, primarily for assisting in migration from another to gem to `bcdd-result`.
|
2221
|
+
|
2222
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2223
|
+
|
2224
|
+
#### `#and_then` versus `#and_then!`
|
2225
|
+
|
2226
|
+
The main difference between the `#and_then` and `#and_then!` is that the latter does not check the result source. However, as a drawback, the result source will change.
|
2227
|
+
|
2228
|
+
Attention: to ensure the correct behavior, do not mix `#and_then` and `#and_then!` in the same result chain.
|
2229
|
+
|
2230
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2231
|
+
|
2232
|
+
#### Analysis: Why is `#and_then` the antidote/standard?
|
2233
|
+
|
2234
|
+
The `BCDD::Result#and_then`/`BCDD::Result::Context#and_then` methods diverge from the above approach by requiring explicit invocation and mapping of the outcomes at each process step. This approach has the following advantages:
|
2235
|
+
|
2236
|
+
- **Clarity:** The input/output relationship between the steps is apparent and highly understandable.
|
2237
|
+
|
2238
|
+
- **Steps uncoupling:** Each operation becomes independent (low coupling). You can even map a failure result to a success (and vice versa).
|
2239
|
+
|
2240
|
+
See this example to understand what your code should look like:
|
2241
|
+
|
2242
|
+
```ruby
|
2243
|
+
class PlaceOrder
|
2244
|
+
include BCDD::Result::Context.mixin(config: { addon: { continue: true } })
|
2245
|
+
|
2246
|
+
def call(**input)
|
2247
|
+
Given(input)
|
2248
|
+
.and_then(:create_order)
|
2249
|
+
.and_then(:pay_order)
|
2250
|
+
.and_then(:send_order_confirmation)
|
2251
|
+
.and_then(:notify_admins)
|
2252
|
+
.and_expose(:order_placed, %i[order])
|
2253
|
+
end
|
2254
|
+
|
2255
|
+
private
|
2256
|
+
|
2257
|
+
def create_order(customer:, products:)
|
2258
|
+
CreateOrder.new.call(customer:, products:).handle do |on|
|
2259
|
+
on.success { |output| Continue(order: output[:order]) }
|
2260
|
+
on.failure { |error| Failure(:order_creation_failed, error:) }
|
2261
|
+
end
|
2262
|
+
end
|
2263
|
+
|
2264
|
+
def pay_order(customer:, order:, payment_method:, **)
|
2265
|
+
PayOrder.new.call(customer:, payment_method:, order:).handle do |on|
|
2266
|
+
on.success { |output| Continue(payment: output[:payment]) }
|
2267
|
+
on.failure { |error| Failure(:order_payment_failed, error:) }
|
2268
|
+
end
|
2269
|
+
end
|
2270
|
+
|
2271
|
+
def send_order_confirmation(customer:, order:, payment:, **)
|
2272
|
+
SendOrderConfirmation.new.call(customer:, order:, payment:).handle do |on|
|
2273
|
+
on.success { Continue() }
|
2274
|
+
on.failure { |error| Failure(:order_confirmation_failed, error:) }
|
2275
|
+
end
|
2276
|
+
end
|
2277
|
+
|
2278
|
+
def notify_admins(customer:, order:, payment:, **)
|
2279
|
+
NotifyAdmins.new.call(customer:, order:, payment:)
|
2280
|
+
|
2281
|
+
Continue()
|
2282
|
+
end
|
2283
|
+
end
|
2284
|
+
```
|
2285
|
+
|
2286
|
+
<p align="right"><a href="#-bcddresult">⬆️ back to top</a></p>
|
2287
|
+
|
2029
2288
|
## About
|
2030
2289
|
|
2031
2290
|
[Rodrigo Serradura](https://github.com/serradura) created this project. He is the B/CDD process/method creator and has already made similar gems like the [u-case](https://github.com/serradura/u-case) and [kind](https://github.com/serradura/kind/blob/main/lib/kind/result.rb). This gem is a general-purpose abstraction/monad, but it also contains key features that serve as facilitators for adopting B/CDD in the code.
|
data/Rakefile
CHANGED
@@ -3,16 +3,22 @@
|
|
3
3
|
require 'bundler/gem_tasks'
|
4
4
|
require 'rake/testtask'
|
5
5
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs += %w[lib test]
|
8
|
+
|
9
|
+
t.test_files = FileList.new('test/**/*_test.rb')
|
10
|
+
end
|
11
|
+
|
6
12
|
Rake::TestTask.new(:test_configuration) do |t|
|
7
13
|
t.libs += %w[lib test]
|
8
14
|
|
9
15
|
t.test_files = FileList.new('test/**/configuration_test.rb')
|
10
16
|
end
|
11
17
|
|
12
|
-
Rake::TestTask.new(:
|
18
|
+
Rake::TestTask.new(:test_transitions_duration) do |t|
|
13
19
|
t.libs += %w[lib test]
|
14
20
|
|
15
|
-
t.test_files = FileList.new('test
|
21
|
+
t.test_files = FileList.new('test/**/duration_test.rb')
|
16
22
|
end
|
17
23
|
|
18
24
|
require 'rubocop/rake_task'
|