bcdd-result 0.11.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +16 -1
  3. data/CHANGELOG.md +97 -15
  4. data/README.md +508 -95
  5. data/Steepfile +4 -4
  6. data/examples/multiple_listeners/Rakefile +55 -0
  7. data/examples/multiple_listeners/app/models/account/member.rb +10 -0
  8. data/examples/multiple_listeners/app/models/account/owner_creation.rb +62 -0
  9. data/examples/multiple_listeners/app/models/account.rb +11 -0
  10. data/examples/multiple_listeners/app/models/user/creation.rb +67 -0
  11. data/examples/multiple_listeners/app/models/user/token/creation.rb +51 -0
  12. data/examples/multiple_listeners/app/models/user/token.rb +7 -0
  13. data/examples/multiple_listeners/app/models/user.rb +15 -0
  14. data/examples/multiple_listeners/config/boot.rb +16 -0
  15. data/examples/multiple_listeners/config/initializers/bcdd.rb +11 -0
  16. data/examples/multiple_listeners/config.rb +27 -0
  17. data/examples/multiple_listeners/db/setup.rb +61 -0
  18. data/examples/multiple_listeners/lib/bcdd/result/rollback_on_failure.rb +15 -0
  19. data/examples/multiple_listeners/lib/bcdd/result/transitions_record.rb +28 -0
  20. data/examples/multiple_listeners/lib/runtime_breaker.rb +11 -0
  21. data/examples/multiple_listeners/lib/transitions_listener/stdout.rb +54 -0
  22. data/examples/single_listener/Rakefile +92 -0
  23. data/examples/single_listener/app/models/account/member.rb +10 -0
  24. data/examples/single_listener/app/models/account/owner_creation.rb +62 -0
  25. data/examples/single_listener/app/models/account.rb +11 -0
  26. data/examples/single_listener/app/models/user/creation.rb +67 -0
  27. data/examples/single_listener/app/models/user/token/creation.rb +51 -0
  28. data/examples/single_listener/app/models/user/token.rb +7 -0
  29. data/examples/single_listener/app/models/user.rb +15 -0
  30. data/examples/single_listener/config/boot.rb +16 -0
  31. data/examples/single_listener/config/initializers/bcdd.rb +11 -0
  32. data/examples/single_listener/config.rb +23 -0
  33. data/examples/single_listener/db/setup.rb +49 -0
  34. data/examples/single_listener/lib/bcdd/result/rollback_on_failure.rb +15 -0
  35. data/examples/single_listener/lib/runtime_breaker.rb +11 -0
  36. data/examples/single_listener/lib/single_transitions_listener.rb +108 -0
  37. data/lib/bcdd/result/callable_and_then/caller.rb +49 -0
  38. data/lib/bcdd/result/callable_and_then/config.rb +15 -0
  39. data/lib/bcdd/result/callable_and_then/error.rb +11 -0
  40. data/lib/bcdd/result/callable_and_then.rb +9 -0
  41. data/lib/bcdd/result/config/switchers/features.rb +5 -1
  42. data/lib/bcdd/result/config.rb +15 -4
  43. data/lib/bcdd/result/context/callable_and_then.rb +39 -0
  44. data/lib/bcdd/result/context/expectations/mixin.rb +2 -2
  45. data/lib/bcdd/result/context/mixin.rb +3 -3
  46. data/lib/bcdd/result/context/success.rb +29 -7
  47. data/lib/bcdd/result/context.rb +34 -16
  48. data/lib/bcdd/result/contract/for_types.rb +1 -1
  49. data/lib/bcdd/result/contract/for_types_and_values.rb +2 -0
  50. data/lib/bcdd/result/error.rb +20 -11
  51. data/lib/bcdd/result/expectations/mixin.rb +3 -3
  52. data/lib/bcdd/result/expectations.rb +6 -6
  53. data/lib/bcdd/result/ignored_types.rb +14 -0
  54. data/lib/bcdd/result/mixin.rb +3 -3
  55. data/lib/bcdd/result/transitions/config.rb +26 -0
  56. data/lib/bcdd/result/transitions/listener.rb +51 -0
  57. data/lib/bcdd/result/transitions/listeners.rb +87 -0
  58. data/lib/bcdd/result/transitions/tracking/disabled.rb +4 -6
  59. data/lib/bcdd/result/transitions/tracking/enabled.rb +103 -24
  60. data/lib/bcdd/result/transitions/tracking.rb +8 -3
  61. data/lib/bcdd/result/transitions/tree.rb +36 -6
  62. data/lib/bcdd/result/transitions.rb +11 -14
  63. data/lib/bcdd/result/version.rb +1 -1
  64. data/lib/bcdd/result.rb +39 -22
  65. data/sig/bcdd/result/callable_and_then.rbs +60 -0
  66. data/sig/bcdd/result/config.rbs +3 -0
  67. data/sig/bcdd/result/context.rbs +65 -4
  68. data/sig/bcdd/result/error.rbs +9 -6
  69. data/sig/bcdd/result/expectations.rbs +4 -4
  70. data/sig/bcdd/result/ignored_types.rbs +9 -0
  71. data/sig/bcdd/result/transitions.rbs +107 -7
  72. data/sig/bcdd/result.rbs +10 -6
  73. metadata +48 -6
data/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  <p align="center">
2
2
  <h1 align="center" id="-bcddresult">🔀 BCDD::Result</h1>
3
- <p align="center"><i>Empower Ruby apps with pragmatic use of Result pattern (monad), Railway Oriented Programming, and B/CDD.</i></p>
3
+ <p align="center"><i>Unleash a pragmatic and observable use of Result Pattern and Railway-Oriented Programming in Ruby.</i></p>
4
4
  <p align="center">
5
5
  <img src="https://img.shields.io/badge/ruby->%3D%202.7.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
6
6
  <a href="https://rubygems.org/gems/bcdd-result"><img src="https://badge.fury.io/rb/bcdd-result.svg" alt="bcdd-result gem version" height="18"></a>
7
+ <a href="https://codeclimate.com/github/B-CDD/result/maintainability"><img src="https://api.codeclimate.com/v1/badges/aa8360f8f012d7dedd62/maintainability" /></a>
8
+ <a href="https://codeclimate.com/github/B-CDD/result/test_coverage"><img src="https://api.codeclimate.com/v1/badges/aa8360f8f012d7dedd62/test_coverage" /></a>
7
9
  </p>
8
10
  </p>
9
11
 
@@ -70,14 +72,21 @@ Use it to enable the [Railway Oriented Programming](https://fsharpforfunandprofi
70
72
  - [Module example (Singleton Methods)](#module-example-singleton-methods-1)
71
73
  - [`BCDD::Result::Context::Expectations`](#bcddresultcontextexpectations)
72
74
  - [Mixin add-ons](#mixin-add-ons)
73
- - [`BCDD::Result.transitions`](#bcddresulttransitions)
74
- - [Configuration](#configuration)
75
- - [`BCDD::Result.configuration`](#bcddresultconfiguration)
76
- - [`config.addon.enable!(:given, :continue)`](#configaddonenablegiven-continue)
77
- - [`config.constant_alias.enable!('Result', 'BCDD::Context')`](#configconstant_aliasenableresult-bcddcontext)
78
- - [`config.pattern_matching.disable!(:nil_as_valid_value_checking)`](#configpattern_matchingdisablenil_as_valid_value_checking)
79
- - [`config.feature.disable!(:expectations)`](#configfeaturedisableexpectations)
75
+ - [`BCDD::Result.transitions`](#bcddresulttransitions)
76
+ - [`ids_tree` *versus* `ids_matrix`](#ids_tree-versus-ids_matrix)
77
+ - [Configuration](#configuration)
78
+ - [Turning on/off](#turning-onoff)
79
+ - [Setting a `trace_id` fetcher](#setting-a-trace_id-fetcher)
80
+ - [Setting a `listener`](#setting-a-listener)
81
+ - [Setting multiple `listeners`](#setting-multiple-listeners)
82
+ - [`BCDD::Result.configuration`](#bcddresultconfiguration)
80
83
  - [`BCDD::Result.config`](#bcddresultconfig)
84
+ - [`BCDD::Result#and_then!`](#bcddresultand_then)
85
+ - [Dependency Injection](#dependency-injection-1)
86
+ - [Configuration](#configuration-1)
87
+ - [Analysis: Why is `and_then!` an Anti-pattern?](#analysis-why-is-and_then-an-anti-pattern)
88
+ - [`#and_then` versus `#and_then!`](#and_then-versus-and_then)
89
+ - [Analysis: Why is `#and_then` the antidote/standard?](#analysis-why-is-and_then-the-antidotestandard)
81
90
  - [About](#about)
82
91
  - [Development](#development)
83
92
  - [Contributing](#contributing)
@@ -649,9 +658,9 @@ Divide.call(2, 2)
649
658
 
650
659
  This method generates a module that any object can include or extend. It adds two methods to the target object: `Success()` and `Failure()`.
651
660
 
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.
661
+ 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
662
 
654
- Because the result has a subject, the `#and_then` method can call methods from it.
663
+ Because the result has a source, the `#and_then` method can call methods from it.
655
664
 
656
665
  ##### Class example (Instance Methods)
657
666
 
@@ -746,9 +755,9 @@ Divide.call(4, '2') #<BCDD::Result::Failure type=:invalid_arg value="arg2 must b
746
755
 
747
756
  To use the `#and_then` method to call methods, they must use `Success()` and `Failure()` to produce the results.
748
757
 
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.
758
+ 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
759
 
751
- **Note:** You can still use the block syntax, but all the results must be produced by the subject's `Success()` and `Failure()` methods.
760
+ **Note:** You can still use the block syntax, but all the results must be produced by the source's `Success()` and `Failure()` methods.
752
761
 
753
762
  ```ruby
754
763
  module ValidateNonzero
@@ -794,13 +803,13 @@ Look at the error produced by the code above:
794
803
  ```ruby
795
804
  Divide.call(2, 0)
796
805
 
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
806
+ # You cannot call #and_then and return a result that does not belong to the same source! (BCDD::Result::Error::InvalidResultSource)
807
+ # Expected source: Divide
808
+ # Given source: ValidateNonzero
800
809
  # Given result: #<BCDD::Result::Failure type=:division_by_zero value="arg2 must not be zero">
801
810
  ```
802
811
 
803
- In order to fix this, you must handle the result produced by `ValidateNonzero.call()` and return a result that belongs to the subject.
812
+ 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
813
 
805
814
  ```ruby
806
815
  module ValidateNonzero
@@ -832,7 +841,8 @@ module Divide
832
841
  end
833
842
 
834
843
  def validate_nonzero(numbers)
835
- # In this case we are handling the other subject result and returning our own
844
+ # In this case we are handling the result from other source
845
+ # and returning our own
836
846
  ValidateNonzero.call(numbers).handle do |on|
837
847
  on.success { |numbers| Success(:ok, numbers) }
838
848
 
@@ -858,8 +868,8 @@ Divide.call(2, 0)
858
868
 
859
869
  ##### Dependency Injection
860
870
 
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.
871
+ The `BCDD::Result#and_then` accepts a second argument that will be used to share a value with the source's method.
872
+ 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
873
 
864
874
  ```ruby
865
875
  require 'logger'
@@ -1411,7 +1421,7 @@ result = Divide.new.call(4, 2)
1411
1421
 
1412
1422
  # The example below shows an error because the :ok type is not allowed.
1413
1423
  # But look at the allowed types have only one type (:division_completed).
1414
- # This is because the :continued type is ignored by the expectations.
1424
+ # This is because the :_continue_ type is ignored by the expectations.
1415
1425
  #
1416
1426
  result.success?(:ok)
1417
1427
  # type :ok is not allowed. Allowed types: :division_completed (BCDD::Result::Contract::Error::UnexpectedType)
@@ -1776,7 +1786,9 @@ Division.call(14, 0)
1776
1786
  #<BCDD::Result::Context::Failure type=:division_by_zero value={:message=>"arg2 must not be zero"}>
1777
1787
  ```
1778
1788
 
1779
- ### `BCDD::Result.transitions`
1789
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
1790
+
1791
+ ## `BCDD::Result.transitions`
1780
1792
 
1781
1793
  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.
1782
1794
 
@@ -1797,7 +1809,7 @@ class Division
1797
1809
 
1798
1810
  private
1799
1811
 
1800
- ValidNumber = ->(arg) { arg.is_a?(Numeric) && arg != Float::NAN && arg != Float::INFINITY }
1812
+ ValidNumber = ->(arg) { arg.is_a?(Numeric) && (!arg.respond_to?(:finite?) || arg.finite?) }
1801
1813
 
1802
1814
  def require_numbers((arg1, arg2))
1803
1815
  ValidNumber[arg1] or return Failure(:invalid_arg, 'arg1 must be a valid number')
@@ -1844,83 +1856,85 @@ result = SumDivisionsByTwo.call(20, 10)
1844
1856
 
1845
1857
  result.transitions
1846
1858
  {
1847
- :version =>1,
1859
+ :version => 1,
1848
1860
  :metadata => {
1849
- :duration => 0, # milliseconds
1850
- :tree_map => [0, [[1, []], [2, []]]], # represents the tree of transitions using the id of each transition block
1861
+ :duration => 0, # milliseconds
1862
+ :trace_id => nil, # can be set through configuration
1863
+ :ids_tree => [0, [[1, []], [2, []]]],
1864
+ :ids_matrix => {0 => [0, 0], 1 => [1, 1], 2 => [2, 1]}
1851
1865
  },
1852
- :records => [
1866
+ :records=> [
1853
1867
  {
1854
- :root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1855
- :parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1856
- :current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
1857
- :result=>{:kind=>:success, :type=>:given, :value=>[20, 2]},
1858
- :and_then=>{},
1859
- :time=>2024-01-02 03:35:11.248418 UTC
1868
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1869
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1870
+ :current => {:id=>1, :name=>"Division", :desc=>"divide two numbers"},
1871
+ :result => {:kind=>:success, :type=>:_given_, :value=>[20, 2], :source=><Division:0x0000000102fd7ed0>},
1872
+ :and_then => {},
1873
+ :time => 2024-01-26 02:53:11.310346 UTC
1860
1874
  },
1861
1875
  {
1862
- :root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1863
- :parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1864
- :current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
1865
- :result=>{:kind=>:success, :type=>:continued, :value=>[20, 2]},
1866
- :and_then=>{:type=>:method, :arg=>nil, :subject=><Division:0x0000000106099028>, :method_name=>:require_numbers},
1867
- :time=>2024-01-02 03:35:11.248558 UTC
1876
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1877
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1878
+ :current => {:id=>1, :name=>"Division", :desc=>"divide two numbers"},
1879
+ :result => {:kind=>:success, :type=>:_continue_, :value=>[20, 2], :source=><Division:0x0000000102fd7ed0>},
1880
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:require_numbers},
1881
+ :time => 2024-01-26 02:53:11.310392 UTC
1868
1882
  },
1869
1883
  {
1870
- :root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1871
- :parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1872
- :current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
1873
- :result=>{:kind=>:success, :type=>:continued, :value=>[20, 2]},
1874
- :and_then=>{:type=>:method, :arg=>nil, :subject=><Division:0x0000000106099028>, :method_name=>:check_for_zeros},
1875
- :time=>2024-01-02 03:35:11.248587 UTC
1884
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1885
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1886
+ :current => {:id=>1, :name=>"Division", :desc=>"divide two numbers"},
1887
+ :result => {:kind=>:success, :type=>:_continue_, :value=>[20, 2], :source=><Division:0x0000000102fd7ed0>},
1888
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:check_for_zeros},
1889
+ :time=>2024-01-26 02:53:11.310403 UTC
1876
1890
  },
1877
1891
  {
1878
- :root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1879
- :parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1880
- :current=>{:id=>1, :name=>"Division", :desc=>"divide two numbers"},
1881
- :result=>{:kind=>:success, :type=>:division_completed, :value=>10},
1882
- :and_then=>{:type=>:method, :arg=>nil, :subject=><Division:0x0000000106099028>, :method_name=>:divide},
1883
- :time=>2024-01-02 03:35:11.248607 UTC
1892
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1893
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1894
+ :current => {:id=>1, :name=>"Division", :desc=>"divide two numbers"},
1895
+ :result => {:kind=>:success, :type=>:division_completed, :value=>10, :source=><Division:0x0000000102fd7ed0>},
1896
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:divide},
1897
+ :time => 2024-01-26 02:53:11.310409 UTC
1884
1898
  },
1885
1899
  {
1886
- :root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1887
- :parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1888
- :current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
1889
- :result=>{:kind=>:success, :type=>:given, :value=>[10, 2]},
1890
- :and_then=>{},
1891
- :time=>2024-01-02 03:35:11.24865 UTC
1900
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1901
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1902
+ :current => {:id=>2, :name=>"Division", :desc=>"divide two numbers"},
1903
+ :result => {:kind=>:success, :type=>:_given_, :value=>[10, 2], :source=><Division:0x0000000102fd6378>},
1904
+ :and_then => {},
1905
+ :time => 2024-01-26 02:53:11.310424 UTC
1892
1906
  },
1893
1907
  {
1894
- :root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1895
- :parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1896
- :current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
1897
- :result=>{:kind=>:success, :type=>:continued, :value=>[10, 2]},
1898
- :and_then=>{:type=>:method, :arg=>nil, :subject=><Division:0x0000000106097ed0>, :method_name=>:require_numbers},
1899
- :time=>2024-01-02 03:35:11.248661 UTC
1908
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1909
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1910
+ :current => {:id=>2, :name=>"Division", :desc=>"divide two numbers"},
1911
+ :result => {:kind=>:success, :type=>:_continue_, :value=>[10, 2], :source=><Division:0x0000000102fd6378>},
1912
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:require_numbers},
1913
+ :time => 2024-01-26 02:53:11.310428 UTC
1900
1914
  },
1901
1915
  {
1902
- :root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1903
- :parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1904
- :current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
1905
- :result=>{:kind=>:success, :type=>:continued, :value=>[10, 2]},
1906
- :and_then=>{:type=>:method, :arg=>nil, :subject=><Division:0x0000000106097ed0>, :method_name=>:check_for_zeros},
1907
- :time=>2024-01-02 03:35:11.248672 UTC
1916
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1917
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1918
+ :current => {:id=>2, :name=>"Division", :desc=>"divide two numbers"},
1919
+ :result => {:kind=>:success, :type=>:_continue_, :value=>[10, 2], :source=><Division:0x0000000102fd6378>},
1920
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:check_for_zeros},
1921
+ :time => 2024-01-26 02:53:11.310431 UTC
1908
1922
  },
1909
1923
  {
1910
- :root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1911
- :parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1912
- :current=>{:id=>2, :name=>"Division", :desc=>"divide two numbers"},
1913
- :result=>{:kind=>:success, :type=>:division_completed, :value=>5},
1914
- :and_then=>{:type=>:method, :arg=>nil, :subject=><Division:0x0000000106097ed0>, :method_name=>:divide},
1915
- :time=>2024-01-02 03:35:11.248682 UTC
1924
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1925
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1926
+ :current => {:id=>2, :name=>"Division", :desc=>"divide two numbers"},
1927
+ :result => {:kind=>:success, :type=>:division_completed, :value=>5, :source=><Division:0x0000000102fd6378>},
1928
+ :and_then => {:type=>:method, :arg=>nil, :method_name=>:divide},
1929
+ :time => 2024-01-26 02:53:11.310434 UTC
1916
1930
  },
1917
1931
  {
1918
- :root=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1919
- :parent=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1920
- :current=>{:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1921
- :result=>{:kind=>:success, :type=>:sum, :value=>15},
1922
- :and_then=>{},
1923
- :time=>2024-01-02 03:35:11.248721 UTC
1932
+ :root => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1933
+ :parent => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1934
+ :current => {:id=>0, :name=>"SumDivisionsByTwo", :desc=>nil},
1935
+ :result => {:kind=>:success, :type=>:sum, :value=>15, :source=>SumDivisionsByTwo},
1936
+ :and_then => {},
1937
+ :time => 2024-01-26 02:53:11.310444 UTC
1924
1938
  }
1925
1939
  ]
1926
1940
  }
@@ -1928,7 +1942,46 @@ result.transitions
1928
1942
 
1929
1943
  <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
1930
1944
 
1931
- #### Configuration
1945
+ ### `ids_tree` *versus* `ids_matrix`
1946
+
1947
+ The `:ids_matrix`. It is a simplification of the `:ids_tree` property (a graph/tree representation of the transitions ids).
1948
+
1949
+ The matrix rows are the direct transitions from the root transition block, and the columns are the transitions nested from the direct transitions.
1950
+
1951
+ Use these data structures to build your own visualization of the transitions.
1952
+
1953
+ > Check out [Transitions Listener example](examples/single_listener/lib/single_transitions_listener.rb) to see how a listener can be used to build a visualization of the transitions, using these properties.
1954
+
1955
+ ```ruby
1956
+ # ids_tree #
1957
+ 0 # [0, [
1958
+ |- 1 # [1, [[2, []]]],
1959
+ | |- 2 # [3, []],
1960
+ |- 3 # [4, [
1961
+ |- 4 # [5, []],
1962
+ | |- 5 # [6, [[7, []]]]
1963
+ | |- 6 # ]],
1964
+ | |- 7 # [8, []]
1965
+ |- 8 # ]]
1966
+
1967
+ # ids_matrix # {
1968
+ 0 | 1 | 2 | 3 | 4 # 0 => [0, 0],
1969
+ - | - | - | - | - # 1 => [1, 1],
1970
+ 0 | | | | # 2 => [1, 2],
1971
+ 1 | 1 | 2 | | # 3 => [2, 1],
1972
+ 2 | 3 | | | # 4 => [3, 1],
1973
+ 3 | 4 | 5 | 6 | 7 # 5 => [3, 2],
1974
+ 4 | 8 | | | # 6 => [3, 3],
1975
+ # 7 => [3, 4],
1976
+ # 8 => [4, 1]
1977
+ # }
1978
+ ```
1979
+
1980
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
1981
+
1982
+ ### Configuration
1983
+
1984
+ #### Turning on/off
1932
1985
 
1933
1986
  You can use `BCDD::Result.config.feature.disable!(:transitions)` and `BCDD::Result.config.feature.enable!(:transitions)` to turn on/off the `BCDD::Result.transitions` feature.
1934
1987
 
@@ -1942,15 +1995,179 @@ result = SumDivisionsByTwo.call(20, 10)
1942
1995
 
1943
1996
  result.transitions
1944
1997
 
1945
- {:version=>1, :records=>[], :metadata=>{:duration=>0, :tree_map=>[]}}
1998
+ {:version=>1, :records=>[], :metadata=>{:duration=>0, :ids_tree=>[]}}
1999
+ ```
2000
+
2001
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2002
+
2003
+ #### Setting a `trace_id` fetcher
2004
+
2005
+ You can define a lambda (arity 0) to fetch the trace_id. This lambda will be called before the first transition and will be used to set the `:trace_id` in the `:metadata` property.
2006
+
2007
+ Use to correlate different or the same operation (executed multiple times).
2008
+
2009
+ ```ruby
2010
+ BCDD::Result.config.transitions.trace_id = -> { Thread.current[:bcdd_result_transitions_trace_id] }
2011
+ ```
2012
+
2013
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2014
+
2015
+ #### Setting a `listener`
2016
+
2017
+ You can define a listener to be called during the result transitions tracking (check out [this example](examples/single_listener/lib/single_transitions_listener.rb)). It must be a class that includes `BCDD::Result::Transitions::Listener`.
2018
+
2019
+ Use it to build your additional logic on top of the transitions tracking. Examples:
2020
+ - Log the transitions.
2021
+ - Perform a trace of the transitions.
2022
+ - Instrument the transitions (measure/report).
2023
+ - Build a visualization of the transitions (Diagrams, using the `records` + `:ids_tree` and `:ids_matrix` properties).
2024
+
2025
+ After implementing your listener, you can set it to the `BCDD::Result.config.transitions.listener=`:
2026
+
2027
+ ```ruby
2028
+ BCDD::Result.config.transitions.listener = MyTransitionsListener
2029
+ ```
2030
+
2031
+ See the example below to understand how to implement one:
2032
+
2033
+ ```ruby
2034
+ class MyTransitionsListener
2035
+ include BCDD::Result::Transitions::Listener
2036
+
2037
+ # A listener will be initialized before the first transition, and it is discarded after the last one.
2038
+ def initialize
2039
+ end
2040
+
2041
+ # This method will be called before each transition block.
2042
+ # The parent transition block will be called first in the case of nested transition blocks.
2043
+ #
2044
+ # @param scope: {:id=>1, :name=>"SomeOperation", :desc=>"Optional description"}
2045
+ def on_start(scope:)
2046
+ end
2047
+
2048
+ # This method will wrap all the transitions in the same block.
2049
+ # It can be used to perform an instrumentation (measure/report) of the transitions.
2050
+ #
2051
+ # @param scope: {:id=>1, :name=>"SomeOperation", :desc=>"Optional description"}
2052
+ def around_transitions(scope:)
2053
+ yield
2054
+ end
2055
+
2056
+ # This method will wrap each and_then call.
2057
+ # It can be used to perform an instrumentation (measure/report) of the and_then calls.
2058
+ #
2059
+ # @param scope: {:id=>1, :name=>"SomeOperation", :desc=>"Optional description"}
2060
+ # @param and_then:
2061
+ # {:type=>:block, :arg=>:some_injected_value}
2062
+ # {:type=>:method, :arg=>:some_injected_value, :method_name=>:some_method_name}
2063
+ def around_and_then(scope:, and_then:)
2064
+ yield
2065
+ end
2066
+
2067
+ # This method will be called after each result recording/tracking.
2068
+ #
2069
+ # @param record:
2070
+ # {
2071
+ # :root => {:id=>0, :name=>"RootOperation", :desc=>nil},
2072
+ # :parent => {:id=>0, :name=>"RootOperation", :desc=>nil},
2073
+ # :current => {:id=>1, :name=>"SomeOperation", :desc=>nil},
2074
+ # :result => {:kind=>:success, :type=>:_continue_, :value=>{some: :thing}, :source=><MyProcess:0x0000000102fd6378>},
2075
+ # :and_then => {:type=>:method, :arg=>nil, :method_name=>:some_method},
2076
+ # :time => 2024-01-26 02:53:11.310431 UTC
2077
+ # }
2078
+ def on_record(record:)
2079
+ end
2080
+
2081
+ # This method will be called at the end of the transitions tracking.
2082
+ #
2083
+ # @param transitions:
2084
+ # {
2085
+ # :version => 1,
2086
+ # :metadata => {
2087
+ # :duration => 0,
2088
+ # :trace_id => nil,
2089
+ # :ids_tree => [0, [[1, []], [2, []]]],
2090
+ # :ids_matrix => {0 => [0, 0], 1 => [1, 1], 2 => [2, 1]}
2091
+ # },
2092
+ # :records => [
2093
+ # # ...
2094
+ # ]
2095
+ # }
2096
+ def on_finish(transitions:)
2097
+ end
2098
+
2099
+ # This method will be called when an exception is raised during the transitions tracking.
2100
+ #
2101
+ # @param exception: Exception
2102
+ # @param transitions: Hash
2103
+ def before_interruption(exception:, transitions:)
2104
+ end
2105
+ end
2106
+ ```
2107
+
2108
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2109
+
2110
+ #### Setting multiple `listeners`
2111
+
2112
+ You can use `BCDD::Result::Transitions::Listeners[]` to creates a listener of listeners (check out [this example](examples/multiple_listeners/Rakefile)), which will be called in the order they were added.
2113
+
2114
+ **Attention:** It only allows one listener to handle `around_and_then` and another `around_transitions` events.
2115
+
2116
+ > The example below defines different listeners to handle `around_and_then` and `around_transitions,` but it is also possible to define a listener to handle both.
2117
+
2118
+ ```ruby
2119
+ class AroundAndThenListener
2120
+ include BCDD::Result::Transitions::Listener
2121
+
2122
+ # It must be a static/singleton method.
2123
+ def self.around_and_then?
2124
+ true
2125
+ end
2126
+
2127
+ def around_and_then(scope:, and_then:)
2128
+ #...
2129
+ end
2130
+ end
2131
+
2132
+ class AroundTransitionsListener
2133
+ include BCDD::Result::Transitions::Listener
2134
+
2135
+ # It must be a static/singleton method.
2136
+ def self.around_transitions?
2137
+ true
2138
+ end
2139
+
2140
+ def around_transitions(scope:)
2141
+ #...
2142
+ end
2143
+ end
2144
+
2145
+ class MyTransitionsListener
2146
+ include BCDD::Result::Transitions::Listener
2147
+ end
2148
+ ```
2149
+
2150
+ How to use it:
2151
+
2152
+ ```ruby
2153
+ # The listeners will be called in the order they were added.
2154
+ BCDD::Result.config.transitions.listener = BCDD::Result::Transitions::Listeners[
2155
+ MyTransitionsListener,
2156
+ AroundAndThenListener,
2157
+ AroundTransitionsListener
2158
+ ]
1946
2159
  ```
1947
2160
 
2161
+ > Check out [this example](examples/multiple_listeners) to see a listener to print the transitions and another to store them in the database.
2162
+
1948
2163
  <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
1949
2164
 
1950
- ### `BCDD::Result.configuration`
2165
+ ## `BCDD::Result.configuration`
1951
2166
 
1952
2167
  The `BCDD::Result.configuration` allows you to configure default behaviors for `BCDD::Result` and `BCDD::Result::Context` through a configuration block. After using it, the configuration is frozen, ensuring the expected behaviors for your application.
1953
2168
 
2169
+ > Note: You can use `BCDD::Result.configuration(freeze: false) {}` to avoid the freezing. This can be useful in tests. Please be sure to use it with caution.
2170
+
1954
2171
  ```ruby
1955
2172
  BCDD::Result.configuration do |config|
1956
2173
  config.addon.enable!(:given, :continue)
@@ -1967,13 +2184,13 @@ Use `disable!` to disable a feature and `enable!` to enable it.
1967
2184
 
1968
2185
  Let's see what each configuration in the example above does:
1969
2186
 
1970
- #### `config.addon.enable!(:given, :continue)`
2187
+ ### `config.addon.enable!(:given, :continue)` <!-- omit in toc -->
1971
2188
 
1972
2189
  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).
1973
2190
 
1974
2191
  It is also enabling the `Given()` which is already enabled by default. Link to documentation: [(1)](#add-ons) [(2)](#mixin-add-ons).
1975
2192
 
1976
- #### `config.constant_alias.enable!('Result', 'BCDD::Context')`
2193
+ ### `config.constant_alias.enable!('Result', 'BCDD::Context')` <!-- omit in toc -->
1977
2194
 
1978
2195
  This configuration make `Result` a constant alias for `BCDD::Result`, and `BCDD::Context` a constant alias for `BCDD::Result::Context`.
1979
2196
 
@@ -1981,23 +2198,23 @@ Link to documentations:
1981
2198
  - [Result alias](#bcddresult-versus-result)
1982
2199
  - [Context aliases](#constant-aliases)
1983
2200
 
1984
- #### `config.pattern_matching.disable!(:nil_as_valid_value_checking)`
2201
+ ### `config.pattern_matching.disable!(:nil_as_valid_value_checking)` <!-- omit in toc -->
1985
2202
 
1986
2203
  This configuration disables the `nil_as_valid_value_checking` for `BCDD::Result` and `BCDD::Result::Context`. Link to [documentation](#pattern-matching-support).
1987
2204
 
1988
- <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
1989
-
1990
- #### `config.feature.disable!(:expectations)`
2205
+ ### `config.feature.disable!(:expectations)` <!-- omit in toc -->
1991
2206
 
1992
2207
  This configuration turns off the expectations for `BCDD::Result` and `BCDD::Result::Context`. The expectations are helpful in development and test environments, but they can be disabled in production environments for performance gain.
1993
2208
 
1994
2209
  PS: I'm using `::Rails.env.production?` to check the environment, but you can use any logic you want.
1995
2210
 
2211
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2212
+
1996
2213
  ### `BCDD::Result.config`
1997
2214
 
1998
2215
  The `BCDD::Result.config` allows you to access the current configuration.
1999
2216
 
2000
- **BCDD::Result.config.addon**
2217
+ #### **BCDD::Result.config.addon** <!-- omit in toc -->
2001
2218
 
2002
2219
  ```ruby
2003
2220
  BCDD::Result.config.addon.enabled?(:continue)
@@ -2026,7 +2243,7 @@ BCDD::Result.config.addon.options
2026
2243
  # }
2027
2244
  ```
2028
2245
 
2029
- **BCDD::Result.config.constant_alias**
2246
+ #### **BCDD::Result.config.constant_alias** <!-- omit in toc -->
2030
2247
 
2031
2248
  ```ruby
2032
2249
  BCDD::Result.config.constant_alias.enabled?('Result')
@@ -2041,7 +2258,7 @@ BCDD::Result.config.constant_alias.options
2041
2258
  # }
2042
2259
  ```
2043
2260
 
2044
- **BCDD::Result.config.pattern_matching**
2261
+ #### **BCDD::Result.config.pattern_matching** <!-- omit in toc -->
2045
2262
 
2046
2263
  ```ruby
2047
2264
  BCDD::Result.config.pattern_matching.enabled?(:nil_as_valid_value_checking)
@@ -2058,7 +2275,7 @@ BCDD::Result.config.pattern_matching.options
2058
2275
  # }
2059
2276
  ```
2060
2277
 
2061
- **BCDD::Result.config.feature**
2278
+ #### **BCDD::Result.config.feature** <!-- omit in toc -->
2062
2279
 
2063
2280
  ```ruby
2064
2281
  BCDD::Result.config.feature.enabled?(:expectations)
@@ -2078,13 +2295,209 @@ BCDD::Result.config.feature.options
2078
2295
  # "BCDD::Result",
2079
2296
  # "BCDD::Result::Context"
2080
2297
  # ]
2081
- # }
2298
+ # },
2299
+ # :and_then!=>{
2300
+ # :enabled=>false,
2301
+ # :affects=>[
2302
+ # "BCDD::Result",
2303
+ # "BCDD::Result::Context"
2304
+ # ]
2305
+ # },
2082
2306
  # }
2083
2307
  ```
2084
2308
 
2309
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2310
+
2311
+ ## `BCDD::Result#and_then!`
2312
+
2313
+ 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.
2314
+
2315
+ **`interactor` gem example**
2316
+
2317
+ ```ruby
2318
+ class PlaceOrder
2319
+ include Interactor::Organizer
2320
+
2321
+ organize CreateOrder,
2322
+ PayOrder,
2323
+ SendOrderConfirmation,
2324
+ NotifyAdmins
2325
+ end
2326
+ ```
2327
+
2328
+ **`u-case` gem example**
2329
+
2330
+ ```ruby
2331
+ class PlaceOrder < Micro::Case
2332
+ flow CreateOrder, PayOrder, SendOrderConfirmation, NotifyAdmins
2333
+ end
2334
+
2335
+ # Alternative approach
2336
+ class PlaceOrder < Micro::Case
2337
+ def call!
2338
+ call(CreateOrder)
2339
+ .then(PayOrder)
2340
+ .then(SendOrderConfirmation)
2341
+ .then(NotifyAdmins)
2342
+ end
2343
+ end
2344
+ ```
2345
+
2346
+ 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.
2347
+
2348
+ ```ruby
2349
+ BCDD::Result.configure do |config|
2350
+ config.feature.enable!(:and_then!)
2351
+ end
2352
+
2353
+ class PlaceOrder
2354
+ include BCDD::Result::Context.mixin
2355
+
2356
+ def call(**input)
2357
+ Given(input)
2358
+ .and_then!(CreateOrder.new)
2359
+ .and_then!(PayOrder.new)
2360
+ .and_then!(SendOrderConfirmation.new)
2361
+ .and_then!(NotifyAdmins.new)
2362
+ end
2363
+ end
2364
+ ```
2365
+
2366
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2367
+
2368
+ #### Dependency Injection
2369
+
2370
+ Like `#and_then`, `#and_then!` also supports an additional argument for dependency injection.
2371
+
2372
+ **In BCDD::Result**
2373
+
2374
+ ```ruby
2375
+ class PlaceOrder
2376
+ include BCDD::Result.mixin
2377
+
2378
+ def call(input, logger:)
2379
+ Given(input)
2380
+ .and_then!(CreateOrder.new, logger)
2381
+ # Further method chaining...
2382
+ end
2383
+ end
2384
+ ```
2385
+
2386
+ **In BCDD::Result::Context**
2387
+
2388
+ ```ruby
2389
+ class PlaceOrder
2390
+ include BCDD::Result::Context.mixin
2391
+
2392
+ def call(logger:, **input)
2393
+ Given(input)
2394
+ .and_then!(CreateOrder.new, logger:)
2395
+ # Further method chaining...
2396
+ end
2397
+ end
2398
+ ```
2399
+
2400
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2401
+
2402
+ #### Configuration
2403
+
2404
+ ```ruby
2405
+ BCDD::Result.configure do |config|
2406
+ config.feature.enable!(:and_then!)
2407
+
2408
+ config.and_then!.default_method_name_to_call = :perform
2409
+ end
2410
+ ```
2411
+
2412
+ **Explanation:**
2413
+
2414
+ - `enable!(:and_then!)`: Activates the `and_then!` feature.
2415
+
2416
+ - `default_method_name_to_call`: Sets a default method other than `:call` for `and_then!`.
2417
+
2418
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2419
+
2420
+ #### Analysis: Why is `and_then!` an Anti-pattern?
2421
+
2422
+ The `and_then!` approach, despite its brevity, introduces several issues:
2423
+
2424
+ - **Lack of Clarity:** The input/output relationship between the steps is not apparent.
2425
+
2426
+ - **Steps Coupling:** Each operation becomes interdependent (high coupling), complicating implementation and compromising the reusability of these operations.
2427
+
2428
+ We recommend cautious use of `#and_then!`. Due to these issues, it is turned off by default and considered an antipattern.
2429
+
2430
+ It should be a temporary solution, primarily for assisting in migration from another to gem to `bcdd-result`.
2431
+
2432
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2433
+
2434
+ #### `#and_then` versus `#and_then!`
2435
+
2436
+ 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.
2437
+
2438
+ Attention: to ensure the correct behavior, do not mix `#and_then` and `#and_then!` in the same result chain.
2439
+
2440
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2441
+
2442
+ #### Analysis: Why is `#and_then` the antidote/standard?
2443
+
2444
+ 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:
2445
+
2446
+ - **Clarity:** The input/output relationship between the steps is apparent and highly understandable.
2447
+
2448
+ - **Steps uncoupling:** Each operation becomes independent (low coupling). You can even map a failure result to a success (and vice versa).
2449
+
2450
+ See this example to understand what your code should look like:
2451
+
2452
+ ```ruby
2453
+ class PlaceOrder
2454
+ include BCDD::Result::Context.mixin(config: { addon: { continue: true } })
2455
+
2456
+ def call(**input)
2457
+ Given(input)
2458
+ .and_then(:create_order)
2459
+ .and_then(:pay_order)
2460
+ .and_then(:send_order_confirmation)
2461
+ .and_then(:notify_admins)
2462
+ .and_expose(:order_placed, %i[order])
2463
+ end
2464
+
2465
+ private
2466
+
2467
+ def create_order(customer:, products:)
2468
+ CreateOrder.new.call(customer:, products:).handle do |on|
2469
+ on.success { |output| Continue(order: output[:order]) }
2470
+ on.failure { |error| Failure(:order_creation_failed, error:) }
2471
+ end
2472
+ end
2473
+
2474
+ def pay_order(customer:, order:, payment_method:, **)
2475
+ PayOrder.new.call(customer:, payment_method:, order:).handle do |on|
2476
+ on.success { |output| Continue(payment: output[:payment]) }
2477
+ on.failure { |error| Failure(:order_payment_failed, error:) }
2478
+ end
2479
+ end
2480
+
2481
+ def send_order_confirmation(customer:, order:, payment:, **)
2482
+ SendOrderConfirmation.new.call(customer:, order:, payment:).handle do |on|
2483
+ on.success { Continue() }
2484
+ on.failure { |error| Failure(:order_confirmation_failed, error:) }
2485
+ end
2486
+ end
2487
+
2488
+ def notify_admins(customer:, order:, payment:, **)
2489
+ NotifyAdmins.new.call(customer:, order:, payment:)
2490
+
2491
+ Continue()
2492
+ end
2493
+ end
2494
+ ```
2495
+
2496
+ <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2497
+
2085
2498
  ## About
2086
2499
 
2087
- [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.
2500
+ [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 can be used independently, but it also contains essential features that facilitate the adoption of B/CDD in code.
2088
2501
 
2089
2502
  <p align="right"><a href="#-bcddresult">⬆️ &nbsp;back to top</a></p>
2090
2503