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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -15
  3. data/README.md +326 -67
  4. data/Rakefile +8 -2
  5. data/lib/bcdd/result/callable_and_then/caller.rb +49 -0
  6. data/lib/bcdd/result/callable_and_then/config.rb +15 -0
  7. data/lib/bcdd/result/callable_and_then/error.rb +11 -0
  8. data/lib/bcdd/result/callable_and_then.rb +9 -0
  9. data/lib/bcdd/result/config/switchers/addons.rb +9 -4
  10. data/lib/bcdd/result/config/switchers/features.rb +5 -1
  11. data/lib/bcdd/result/config.rb +9 -3
  12. data/lib/bcdd/result/context/callable_and_then.rb +39 -0
  13. data/lib/bcdd/result/context/expectations/mixin.rb +11 -3
  14. data/lib/bcdd/result/context/mixin.rb +13 -5
  15. data/lib/bcdd/result/context/success.rb +12 -8
  16. data/lib/bcdd/result/context.rb +34 -16
  17. data/lib/bcdd/result/error.rb +20 -11
  18. data/lib/bcdd/result/expectations/mixin.rb +12 -6
  19. data/lib/bcdd/result/expectations.rb +7 -7
  20. data/lib/bcdd/result/mixin.rb +11 -5
  21. data/lib/bcdd/result/transitions/tracking/disabled.rb +14 -4
  22. data/lib/bcdd/result/transitions/tracking/enabled.rb +38 -18
  23. data/lib/bcdd/result/transitions/tree.rb +10 -6
  24. data/lib/bcdd/result/transitions.rb +8 -10
  25. data/lib/bcdd/result/version.rb +1 -1
  26. data/lib/bcdd/result.rb +38 -23
  27. data/sig/bcdd/result/callable_and_then.rbs +60 -0
  28. data/sig/bcdd/result/config.rbs +3 -0
  29. data/sig/bcdd/result/context.rbs +73 -9
  30. data/sig/bcdd/result/error.rbs +9 -6
  31. data/sig/bcdd/result/expectations.rbs +12 -8
  32. data/sig/bcdd/result/mixin.rbs +10 -2
  33. data/sig/bcdd/result/transitions.rbs +16 -5
  34. data/sig/bcdd/result.rbs +12 -8
  35. 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)`](#configaddonenablecontinue)
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 subject.
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 subject, the `#and_then` method can call methods from it.
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::Subject()`/`BCDD::Result::Failure()`, or results from another `BCDD::Result.mixin` instance with `#and_then`, it will raise an error because the subjects will be different.
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 subject's `Success()` and `Failure()` methods.
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 subject! (BCDD::Result::Error::InvalidResultSubject)
798
- # Expected subject: Divide
799
- # Given subject: ValidateNonzero
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 subject.
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 other subject result and returning our own
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 subject's method.
862
- To receive this argument, the subject's method must have an arity of two, where the first argument will be the result value and the second will be the shared value.
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 halt the step chain.
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 halted.
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
- validate_numbers(arg1, arg2)
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(arg1, arg2)
940
- arg1.is_a?(::Numeric) or return Failure(:invalid_arg, 'arg1 must be numeric')
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
- Continue([arg1, arg2])
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 Continue(numbers) unless numbers.last.zero?
965
+ return Failure(:division_by_zero, 'arg2 must not be zero') if numbers.last.zero?
948
966
 
949
- Failure(:division_by_zero, 'arg2 must not be zero')
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 halted 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 `halted: false` to `#and_expose`.
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">⬆️ &nbsp;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
- **Continue**
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
- 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 halt the step chain.
1705
+ You can turn it off by passing `given: false` to the `config:` argument or using the `BCDD::Result.configuration`.
1684
1706
 
1685
- 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 halted.
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 Divide
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
- validate_numbers(arg1, arg2)
1709
- .and_then(:validate_nonzero)
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 validate_numbers(arg1, arg2)
1717
- arg1.is_a?(::Numeric) or return Failure(:invalid_arg, message: 'arg1 must be numeric')
1718
- arg2.is_a?(::Numeric) or return Failure(:invalid_arg, message: 'arg2 must be numeric')
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(number1: arg1, number2: arg2)
1749
+ Continue()
1721
1750
  end
1722
1751
 
1723
- def validate_nonzero(number2:, **)
1724
- return Continue() if number2.nonzero?
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
- Failure(:division_by_zero, message: 'arg2 must not be zero')
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
- Divide.call(14, 2)
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
- Divide.call('14', 2)
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
- Divide.call(14, '2')
1779
+ Division.call(14, '2')
1746
1780
  #<BCDD::Result::Context::Failure type=:invalid_arg value={:message=>"arg2 must be numeric"}>
1747
1781
 
1748
- Divide.call(14, 0)
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
- require_numbers(arg1, arg2)
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 => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1827
- :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1828
- :current => {:id=>1, :name=>"Division", :desc=>"divide two numbers"},
1829
- :result => {:kind=>:success, :type=>:continued, :value=>[20, 2]},
1830
- :and_then => {},
1831
- :time => 2023-12-31 22:19:33.281619 UTC
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, :subject=>#<Division:0x0000000103e5f7c8>, :method_name=>:check_for_zeros},
1839
- :time=>2023-12-31 22:19:33.281693 UTC
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, :subject=>#<Division:0x0000000103e5f7c8>, :method_name=>:divide},
1847
- :time=>2023-12-31 22:19:33.281715 UTC
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=>:continued, :value=>[10, 2]},
1896
+ :result=>{:kind=>:success, :type=>:given, :value=>[10, 2]},
1854
1897
  :and_then=>{},
1855
- :time=>2023-12-31 22:19:33.281747 UTC
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, :subject=>#<Division:0x0000000103e5f1b0>, :method_name=>:check_for_zeros},
1863
- :time=>2023-12-31 22:19:33.281755 UTC
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, :subject=>#<Division:0x0000000103e5f1b0>, :method_name=>:divide},
1871
- :time=>2023-12-31 22:19:33.281763 UTC
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=>2023-12-31 22:19:33.281784 UTC
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">⬆️ &nbsp;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">⬆️ &nbsp;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">⬆️ &nbsp;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">⬆️ &nbsp;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">⬆️ &nbsp;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">⬆️ &nbsp;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">⬆️ &nbsp;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(:test) do |t|
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/**/*_test.rb')
21
+ t.test_files = FileList.new('test/**/duration_test.rb')
16
22
  end
17
23
 
18
24
  require 'rubocop/rake_task'