bcdd-result 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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'
|