functional-light-service 0.4.4 → 6.0.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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/project-build.yml +43 -11
  3. data/.rubocop.yml +101 -160
  4. data/AUDIT-functional-light-service.md +352 -0
  5. data/Appraisals +4 -0
  6. data/CHANGELOG.md +118 -0
  7. data/Gemfile +0 -2
  8. data/README.md +1544 -1426
  9. data/Rakefile +1 -1
  10. data/VERSION +1 -1
  11. data/audit/bench.rb +99 -0
  12. data/audit/verify_findings.rb +172 -0
  13. data/functional-light-service.gemspec +15 -16
  14. data/lib/functional-light-service/action.rb +97 -101
  15. data/lib/functional-light-service/configuration.rb +26 -24
  16. data/lib/functional-light-service/context/key_verifier.rb +124 -118
  17. data/lib/functional-light-service/context.rb +63 -20
  18. data/lib/functional-light-service/deprecations.rb +26 -0
  19. data/lib/functional-light-service/errors.rb +8 -6
  20. data/lib/functional-light-service/functional/enum.rb +286 -250
  21. data/lib/functional-light-service/functional/maybe.rb +21 -15
  22. data/lib/functional-light-service/functional/monad.rb +77 -66
  23. data/lib/functional-light-service/functional/null.rb +88 -74
  24. data/lib/functional-light-service/functional/option.rb +100 -97
  25. data/lib/functional-light-service/functional/result.rb +129 -116
  26. data/lib/functional-light-service/localization_adapter.rb +48 -47
  27. data/lib/functional-light-service/organizer/execute.rb +16 -14
  28. data/lib/functional-light-service/organizer/iterate.rb +30 -25
  29. data/lib/functional-light-service/organizer/reduce_if.rb +19 -17
  30. data/lib/functional-light-service/organizer/reduce_until.rb +22 -20
  31. data/lib/functional-light-service/organizer/scoped_reducable.rb +15 -13
  32. data/lib/functional-light-service/organizer/with_callback.rb +28 -26
  33. data/lib/functional-light-service/organizer/with_reducer.rb +81 -71
  34. data/lib/functional-light-service/organizer/with_reducer_factory.rb +20 -18
  35. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +110 -105
  36. data/lib/functional-light-service/organizer.rb +114 -104
  37. data/lib/functional-light-service/testing/context_factory.rb +48 -42
  38. data/lib/functional-light-service/testing.rb +3 -1
  39. data/lib/functional-light-service/version.rb +5 -3
  40. data/lib/functional-light-service.rb +30 -28
  41. data/spec/acceptance/after_actions_spec.rb +87 -71
  42. data/spec/acceptance/before_actions_spec.rb +115 -98
  43. data/spec/acceptance/custom_log_from_organizer_spec.rb +61 -60
  44. data/spec/acceptance/deprecation_warnings_spec.rb +82 -0
  45. data/spec/acceptance/fail_spec.rb +52 -50
  46. data/spec/acceptance/message_localization_spec.rb +119 -118
  47. data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
  48. data/spec/acceptance/organizer/add_to_context_spec.rb +30 -0
  49. data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +68 -65
  50. data/spec/acceptance/organizer/iterate_spec.rb +7 -0
  51. data/spec/acceptance/organizer/reduce_if_spec.rb +89 -83
  52. data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
  53. data/spec/acceptance/organizer/with_callback_spec.rb +113 -110
  54. data/spec/acceptance/{not_having_call_method_warning_spec.rb → organizer_entry_point_spec.rb} +10 -7
  55. data/spec/acceptance/rollback_spec.rb +183 -132
  56. data/spec/action_expects_and_promises_spec.rb +97 -93
  57. data/spec/action_promised_keys_spec.rb +126 -122
  58. data/spec/action_spec.rb +8 -0
  59. data/spec/context_spec.rb +289 -197
  60. data/spec/examples/controller_spec.rb +63 -63
  61. data/spec/examples/validate_address_spec.rb +38 -37
  62. data/spec/lib/deterministic/currify_spec.rb +90 -88
  63. data/spec/lib/deterministic/null_spec.rb +6 -1
  64. data/spec/lib/deterministic/option_spec.rb +140 -133
  65. data/spec/lib/deterministic/result/result_map_spec.rb +155 -154
  66. data/spec/lib/deterministic/result/result_shared.rb +3 -2
  67. data/spec/lib/deterministic/result_spec.rb +2 -2
  68. data/spec/lib/edge_cases_spec.rb +156 -0
  69. data/spec/lib/enum_spec.rb +1 -1
  70. data/spec/lib/native_pattern_matching_spec.rb +74 -0
  71. data/spec/organizer_spec.rb +115 -93
  72. data/spec/readme_spec.rb +45 -47
  73. data/spec/sample/calculates_order_tax_action_spec.rb +16 -16
  74. data/spec/sample/calculates_tax_spec.rb +1 -1
  75. data/spec/sample/looks_up_tax_percentage_action_spec.rb +55 -55
  76. data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
  77. data/spec/sample/tax/calculates_order_tax_action.rb +10 -9
  78. data/spec/sample/tax/looks_up_tax_percentage_action.rb +28 -27
  79. data/spec/sample/tax/provides_free_shipping_action.rb +11 -10
  80. data/spec/spec_helper.rb +21 -13
  81. data/spec/test_doubles.rb +628 -564
  82. data/spec/testing/context_factory_spec.rb +21 -0
  83. metadata +49 -117
  84. data/.travis.yml +0 -24
  85. data/lib/functional-light-service/organizer/verify_call_method_exists.rb +0 -29
  86. data/spec/acceptance/include_warning_spec.rb +0 -29
@@ -1,154 +1,155 @@
1
- require 'spec_helper'
2
-
3
- describe FunctionalLightService::Result do
4
- include FunctionalLightService::Prelude::Result
5
-
6
- context ">> (map)" do
7
- specify { expect(Success(0).map { |n| Success(n + 1) }).to eq Success(1) }
8
- specify { expect(Failure(0).map { |n| Success(n + 1) }).to eq Failure(0) }
9
-
10
- it "Failure stops execution" do
11
- class ChainUnderTest
12
- include FunctionalLightService::Prelude::Result
13
- alias :m :method
14
-
15
- def call
16
- init >>
17
- m(:validate) >>
18
- m(:send) >>
19
- m(:parse)
20
- end
21
-
22
- def init
23
- Success(:step => 1)
24
- end
25
-
26
- def validate(i)
27
- i[:step] = i[:step] + 1
28
- Success(i)
29
- end
30
-
31
- def send(i)
32
- i[:step] = i[:step] + 1
33
- Failure("Error @ #{i.fetch(:step)}")
34
- end
35
-
36
- def parse(i)
37
- i[:step] = i[:step] + 1
38
- Success(i)
39
- end
40
- end
41
-
42
- test = ChainUnderTest.new
43
-
44
- expect(test.call).to eq Failure("Error @ 3")
45
- end
46
-
47
- it "expects an Result" do
48
- def returns_non_result(_i)
49
- 2
50
- end
51
-
52
- expect { Success(1) >> method(:returns_non_result) }.to \
53
- raise_error(FunctionalLightService::Monad::NotMonadError)
54
- end
55
-
56
- it "works with a block" do
57
- expect(
58
- Success(1).map { |i| Success(i + 1) }
59
- ).to eq Success(2)
60
- end
61
-
62
- it "works with a lambda" do
63
- expect(
64
- Success(1) >> ->(i) { Success(i + 1) }
65
- ).to eq Success(2)
66
- end
67
-
68
- it "does not catch exceptions" do
69
- expect do
70
- Success(1) >> ->(_i) { raise "error" }
71
- end.to raise_error(RuntimeError)
72
- end
73
- end
74
-
75
- context "using self as the context for success" do
76
- class SelfContextUnderTest
77
- include FunctionalLightService::Prelude::Result
78
-
79
- def call
80
- @step = 0
81
- Success(self)
82
- .map(&:validate)
83
- .map(&:build)
84
- .map(&:send)
85
- end
86
-
87
- def validate
88
- @step = 1
89
- Success(self)
90
- end
91
-
92
- def build
93
- @step = 2
94
- Success(self)
95
- end
96
-
97
- def send
98
- @step = 3
99
- Success(self)
100
- end
101
-
102
- def inspect
103
- "Step #{@step}"
104
- end
105
-
106
- # # def self.procify(*meths)
107
- # # meths.each do |m|
108
- # # new_m = "__#{m}__procified".to_sym
109
- # # alias new_m m
110
- # # define_method new_m do |ctx|
111
- # # method(m)
112
- # # end
113
- # # end
114
- # # end
115
-
116
- # procify :send
117
- end
118
-
119
- it "works" do
120
- test = SelfContextUnderTest.new.call
121
- expect(test).to be_a described_class::Success
122
- expect(test.inspect).to eq "Success(Step 3)"
123
- end
124
- end
125
-
126
- context "<< (pipe)" do
127
- it "ignores the output of pipe" do
128
- acc = "ctx: "
129
- log = ->(ctx) { acc += ctx.inspect }
130
-
131
- actual = Success(1).pipe(log).map { Success(2) }
132
- expect(actual).to eq Success(2)
133
- expect(acc).to eq "ctx: Success(1)"
134
- end
135
-
136
- it "works with <<" do
137
- log = ->(n) { n.value + 1 }
138
- foo = ->(n) { Success(n + 1) }
139
-
140
- Success(1) << log >> foo
141
- end
142
- end
143
-
144
- context ">= (try)" do
145
- it "try (>=) catches errors and wraps them as Failure" do
146
- def error(ctx)
147
- raise "error #{ctx}"
148
- end
149
-
150
- actual = Success(1) >= method(:error)
151
- expect(actual.inspect).to eq "Failure(#<RuntimeError: error 1>)"
152
- end
153
- end
154
- end
1
+ require 'spec_helper'
2
+
3
+ describe FunctionalLightService::Result do
4
+ include FunctionalLightService::Prelude::Result
5
+
6
+ context ">> (map)" do
7
+ specify { expect(Success(0).map { |n| Success(n + 1) }).to eq Success(1) }
8
+ specify { expect(Failure(0).map { |n| Success(n + 1) }).to eq Failure(0) }
9
+
10
+ it "Failure stops execution" do
11
+ class ChainUnderTest
12
+ include FunctionalLightService::Prelude::Result
13
+
14
+ alias :m :method
15
+
16
+ def call
17
+ init >>
18
+ m(:validate) >>
19
+ m(:send) >>
20
+ m(:parse)
21
+ end
22
+
23
+ def init
24
+ Success(:step => 1)
25
+ end
26
+
27
+ def validate(i)
28
+ i[:step] = i[:step] + 1
29
+ Success(i)
30
+ end
31
+
32
+ def send(i)
33
+ i[:step] = i[:step] + 1
34
+ Failure("Error @ #{i.fetch(:step)}")
35
+ end
36
+
37
+ def parse(i)
38
+ i[:step] = i[:step] + 1
39
+ Success(i)
40
+ end
41
+ end
42
+
43
+ test = ChainUnderTest.new
44
+
45
+ expect(test.call).to eq Failure("Error @ 3")
46
+ end
47
+
48
+ it "expects an Result" do
49
+ def returns_non_result(_i)
50
+ 2
51
+ end
52
+
53
+ expect { Success(1) >> method(:returns_non_result) }.to \
54
+ raise_error(FunctionalLightService::Monad::NotMonadError)
55
+ end
56
+
57
+ it "works with a block" do
58
+ expect(
59
+ Success(1).map { |i| Success(i + 1) }
60
+ ).to eq Success(2)
61
+ end
62
+
63
+ it "works with a lambda" do
64
+ expect(
65
+ Success(1) >> ->(i) { Success(i + 1) }
66
+ ).to eq Success(2)
67
+ end
68
+
69
+ it "does not catch exceptions" do
70
+ expect do
71
+ Success(1) >> ->(_i) { raise "error" }
72
+ end.to raise_error(RuntimeError)
73
+ end
74
+ end
75
+
76
+ context "using self as the context for success" do
77
+ class SelfContextUnderTest
78
+ include FunctionalLightService::Prelude::Result
79
+
80
+ def call
81
+ @step = 0
82
+ Success(self)
83
+ .map(&:validate)
84
+ .map(&:build)
85
+ .map(&:send)
86
+ end
87
+
88
+ def validate
89
+ @step = 1
90
+ Success(self)
91
+ end
92
+
93
+ def build
94
+ @step = 2
95
+ Success(self)
96
+ end
97
+
98
+ def send
99
+ @step = 3
100
+ Success(self)
101
+ end
102
+
103
+ def inspect
104
+ "Step #{@step}"
105
+ end
106
+
107
+ # # def self.procify(*meths)
108
+ # # meths.each do |m|
109
+ # # new_m = "__#{m}__procified".to_sym
110
+ # # alias new_m m
111
+ # # define_method new_m do |ctx|
112
+ # # method(m)
113
+ # # end
114
+ # # end
115
+ # # end
116
+
117
+ # procify :send
118
+ end
119
+
120
+ it "works" do
121
+ test = SelfContextUnderTest.new.call
122
+ expect(test).to be_a described_class::Success
123
+ expect(test.inspect).to eq "Success(Step 3)"
124
+ end
125
+ end
126
+
127
+ context "<< (pipe)" do
128
+ it "ignores the output of pipe" do
129
+ acc = "ctx: "
130
+ log = ->(ctx) { acc += ctx.inspect }
131
+
132
+ actual = Success(1).pipe(log).map { Success(2) }
133
+ expect(actual).to eq Success(2)
134
+ expect(acc).to eq "ctx: Success(1)"
135
+ end
136
+
137
+ it "works with <<" do
138
+ log = ->(n) { n.value + 1 }
139
+ foo = ->(n) { Success(n + 1) }
140
+
141
+ Success(1) << log >> foo
142
+ end
143
+ end
144
+
145
+ context ">= (try)" do
146
+ it "try (>=) catches errors and wraps them as Failure" do
147
+ def error(ctx)
148
+ raise "error #{ctx}"
149
+ end
150
+
151
+ actual = Success(1) >= method(:error)
152
+ expect(actual.inspect).to eq "Failure(#<RuntimeError: error 1>)"
153
+ end
154
+ end
155
+ end
@@ -13,12 +13,13 @@ shared_examples 'Result' do
13
13
 
14
14
  it "#to_s" do
15
15
  expect(result.new(1).to_s).to eq "1"
16
- expect(result.new(:a => 1).to_s).to eq "{:a=>1}"
16
+ # il formato di Hash#to_s/inspect cambia tra Ruby 3.3 e 3.4: costruiamo l'atteso dinamicamente
17
+ expect(result.new(:a => 1).to_s).to eq({ :a => 1 }.to_s)
17
18
  end
18
19
 
19
20
  it "#inspect" do
20
21
  expect(result.new(1).inspect).to eq "#{result_name}(1)"
21
- expect(result.new(:a => 1).inspect).to eq "#{result_name}({:a=>1})"
22
+ expect(result.new(:a => 1).inspect).to eq "#{result_name}(#{{ :a => 1 }.inspect})"
22
23
  end
23
24
  end
24
25
 
@@ -4,8 +4,8 @@ describe FunctionalLightService::Result do
4
4
  include FunctionalLightService::Prelude::Result
5
5
 
6
6
  it "can't call Result#new directly" do
7
- msg = "private method `new' called for FunctionalLightService::Result:Class"
8
- expect { described_class.new(1) }.to raise_error(NoMethodError, msg)
7
+ # il formato del messaggio NoMethodError cambia tra le versioni di Ruby
8
+ expect { described_class.new(1) }.to raise_error(NoMethodError, /private method [`']new'/)
9
9
  end
10
10
 
11
11
  it "fmap" do
@@ -0,0 +1,156 @@
1
+ require 'spec_helper'
2
+ require 'functional-light-service/functional/maybe'
3
+
4
+ # Copertura dei path d'errore e delle API minori emersi dall'audit
5
+ describe "edge cases and error paths" do
6
+ include FunctionalLightService::Prelude::Result
7
+ include FunctionalLightService::Prelude::Option
8
+
9
+ describe FunctionalLightService::Context do
10
+ it "#add_to_context merges values" do
11
+ ctx = FunctionalLightService::Context.make(:a => 1)
12
+ ctx.add_to_context(:b => 2)
13
+
14
+ expect(ctx[:b]).to eq(2)
15
+ end
16
+ end
17
+
18
+ describe FunctionalLightService::Context::KeyVerifier do
19
+ it "the base verifier requires throw_error_predicate to be overridden" do
20
+ ctx = FunctionalLightService::Context.make
21
+ verifier = described_class.new(ctx, nil)
22
+
23
+ expect { verifier.throw_error_predicate([]) }.to raise_error(NotImplementedError)
24
+ end
25
+ end
26
+
27
+ describe "Object monkey patch (opt-in via functional/maybe)" do
28
+ it "defines null? and some? on every object" do
29
+ expect("anything".null?).to be(false)
30
+ expect("anything".some?).to be(true)
31
+ end
32
+ end
33
+
34
+ describe Null do
35
+ it "class-level unknown messages return the singleton" do
36
+ expect(Null.anything_at_all).to eq(Null.instance)
37
+ end
38
+
39
+ it "class-level respond_to? is permissive" do
40
+ expect(Null.respond_to?(:anything_at_all)).to be(true)
41
+ end
42
+
43
+ it "compares equal to another Null" do
44
+ expect(Null.instance == Null.instance).to be(true) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
45
+ expect(Null.instance == "not null").to be(false)
46
+ end
47
+
48
+ it "a mimic only responds to the mimicked interface" do
49
+ klass = Class.new { def foo; end }
50
+ mimic = Null.mimic(klass)
51
+
52
+ expect(mimic.respond_to?(:foo)).to be(true)
53
+ expect(mimic.respond_to?(:bar)).to be(false)
54
+ expect { mimic.bar }.to raise_error(NoMethodError)
55
+ end
56
+ end
57
+
58
+ describe "enum error paths" do
59
+ it "raises when a variant is defined twice" do
60
+ expect do
61
+ FunctionalLightService.enum do
62
+ Dup()
63
+ Dup()
64
+ end
65
+ end.to raise_error(ArgumentError, /already defined/)
66
+ end
67
+
68
+ it "the enum builder responds to any variant name" do
69
+ builder = FunctionalLightService::EnumBuilder.new(Class.new)
70
+
71
+ expect(builder.respond_to?(:AnyVariantName)).to be(true)
72
+ end
73
+
74
+ it "raises MatchError when the match is not exhaustive" do
75
+ expect do
76
+ Some(1).match do
77
+ Some() { |s| s }
78
+ end
79
+ end.to raise_error(FunctionalLightService::Enum::MatchError, /non-exhaustive/)
80
+ end
81
+
82
+ it "raises MatchError when the block arity does not match the variant" do
83
+ expect do
84
+ Some(1).match do
85
+ Some() { |a, b| [a, b] }
86
+ None() { nil }
87
+ end
88
+ end.to raise_error(FunctionalLightService::Enum::MatchError, /must match/)
89
+ end
90
+
91
+ it "raises MatchError when no guard matches" do
92
+ expect do
93
+ Some(1).match do
94
+ Some(where { s > 100 }) { |s| s }
95
+ None() { nil }
96
+ end
97
+ end.to raise_error(FunctionalLightService::Enum::MatchError, /No match could be made/)
98
+ end
99
+
100
+ it "rejects unnamed/rest block parameters" do
101
+ # su Ruby 3.1 |*| appare come [[:rest]] (ramo "Unnamed param"),
102
+ # da Ruby 3.2 come [[:rest, :*]] (ramo "Only :req & :opt")
103
+ expect do
104
+ Some(1).match do
105
+ Some() { |*| nil }
106
+ None() { nil }
107
+ end
108
+ end.to raise_error(ArgumentError, /Unnamed param|Only :req & :opt/)
109
+ end
110
+
111
+ it "rejects keyword block parameters" do
112
+ expect do
113
+ Some(1).match do
114
+ Some() { |**opts| opts }
115
+ None() { nil }
116
+ end
117
+ end.to raise_error(ArgumentError, /Only :req & :opt params allowed/)
118
+ end
119
+
120
+ it "None deconstructs to an empty hash and array" do
121
+ none = FunctionalLightService::Option::None.new
122
+
123
+ expect(none.deconstruct).to eq([])
124
+ expect(none.deconstruct_keys(nil)).to eq({})
125
+ end
126
+ end
127
+
128
+ describe "Option minor APIs" do
129
+ it "#value_to_a returns the raw value" do
130
+ expect(Some(1).value_to_a).to eq(1)
131
+ expect(FunctionalLightService::Option::None.new.value_to_a).to be_nil
132
+ end
133
+
134
+ it "Prelude None() builds the shared None" do
135
+ expect(None().none?).to be(true)
136
+ end
137
+
138
+ it "Prelude Option() returns the Option enum" do
139
+ expect(Option()).to eq(FunctionalLightService::Option)
140
+ end
141
+ end
142
+
143
+ describe "Result minor APIs" do
144
+ it "#+ raises NotMonadError for non-Result operands" do
145
+ expect { Success(1) + "not a result" } # rubocop:disable Style/StringConcatenation
146
+ .to raise_error(FunctionalLightService::Monad::NotMonadError)
147
+ end
148
+
149
+ it "Prelude try! wraps exceptions in Failure" do
150
+ result = try! { raise "boom" }
151
+
152
+ expect(result).to be_failure
153
+ expect(result.value).to be_a(RuntimeError)
154
+ end
155
+ end
156
+ end
@@ -20,7 +20,7 @@ describe FunctionalLightService::Enum do
20
20
 
21
21
  it "can't instantiate parent" do
22
22
  expect { MyEnym.new }.to \
23
- raise_error NoMethodError, "private method `new' called for MyEnym:Class"
23
+ raise_error NoMethodError, /private method [`']new'/
24
24
  end
25
25
 
26
26
  it "Nullary" do
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ describe "native Ruby pattern matching (case/in) support" do
4
+ include FunctionalLightService::Prelude::Result
5
+ include FunctionalLightService::Prelude::Option
6
+
7
+ Result = FunctionalLightService::Result
8
+ Option = FunctionalLightService::Option
9
+
10
+ it "deconstructs Success/Failure positionally" do
11
+ matched =
12
+ case Success(42)
13
+ in Result::Success[value] then "success: #{value}"
14
+ in Result::Failure[error] then "failure: #{error}"
15
+ end
16
+
17
+ expect(matched).to eq("success: 42")
18
+ end
19
+
20
+ it "deconstructs Failure positionally" do
21
+ matched =
22
+ case Failure(:boom)
23
+ in Result::Success[value] then "success: #{value}"
24
+ in Result::Failure[error] then "failure: #{error}"
25
+ end
26
+
27
+ expect(matched).to eq("failure: boom")
28
+ end
29
+
30
+ it "deconstructs by keys using the variant's field names" do
31
+ matched =
32
+ case Success(42)
33
+ in Result::Success(s:) then s
34
+ in Result::Failure(f:) then f
35
+ end
36
+
37
+ expect(matched).to eq(42)
38
+ end
39
+
40
+ it "deconstructs Some and None" do
41
+ some_result =
42
+ case Some(7)
43
+ in Option::Some[v] then v
44
+ in Option::None then :none
45
+ end
46
+ none_result =
47
+ case None()
48
+ in Option::Some[v] then v
49
+ in Option::None then :none
50
+ end
51
+
52
+ expect(some_result).to eq(7)
53
+ expect(none_result).to eq(:none)
54
+ end
55
+
56
+ Shape = FunctionalLightService.enum do
57
+ Point(:x, :y)
58
+ Origin()
59
+ end
60
+
61
+ it "deconstructs multi-field (Binary) variants by keys" do
62
+ matched =
63
+ case Shape::Point.new(1, 2)
64
+ in Shape::Point(x:, y:) then [x, y]
65
+ in Shape::Origin then [0, 0]
66
+ end
67
+
68
+ expect(matched).to eq([1, 2])
69
+ end
70
+
71
+ def None # rubocop:disable Naming/MethodName
72
+ FunctionalLightService::Option::None.new
73
+ end
74
+ end