switchyard 7.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.
- checksums.yaml +7 -0
- data/.github/workflows/project-build.yml +71 -0
- data/.gitignore +24 -0
- data/.rspec +3 -0
- data/.rubocop.yml +101 -0
- data/.solargraph.yml +11 -0
- data/Appraisals +7 -0
- data/CHANGELOG.md +303 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +1663 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/audit/bench.rb +99 -0
- data/audit/verify_findings.rb +172 -0
- data/gemfiles/dry_inflector_0_2_1.gemfile +5 -0
- data/gemfiles/i18n_1_8_11.gemfile +5 -0
- data/lib/switchyard/action.rb +145 -0
- data/lib/switchyard/configuration.rb +36 -0
- data/lib/switchyard/context/key_verifier.rb +124 -0
- data/lib/switchyard/context.rb +208 -0
- data/lib/switchyard/deprecations.rb +26 -0
- data/lib/switchyard/errors.rb +9 -0
- data/lib/switchyard/functional/enum.rb +286 -0
- data/lib/switchyard/functional/maybe.rb +21 -0
- data/lib/switchyard/functional/monad.rb +77 -0
- data/lib/switchyard/functional/null.rb +88 -0
- data/lib/switchyard/functional/option.rb +100 -0
- data/lib/switchyard/functional/result.rb +129 -0
- data/lib/switchyard/functional/sequencer.rb +144 -0
- data/lib/switchyard/i18n/localization_adapter.rb +50 -0
- data/lib/switchyard/localization_adapter.rb +39 -0
- data/lib/switchyard/localization_map.rb +9 -0
- data/lib/switchyard/organizer/execute.rb +16 -0
- data/lib/switchyard/organizer/iterate.rb +30 -0
- data/lib/switchyard/organizer/reduce_case.rb +50 -0
- data/lib/switchyard/organizer/reduce_if.rb +19 -0
- data/lib/switchyard/organizer/reduce_if_else.rb +23 -0
- data/lib/switchyard/organizer/reduce_until.rb +22 -0
- data/lib/switchyard/organizer/reduce_while.rb +31 -0
- data/lib/switchyard/organizer/scoped_reducable.rb +15 -0
- data/lib/switchyard/organizer/with_callback.rb +28 -0
- data/lib/switchyard/organizer/with_reducer.rb +81 -0
- data/lib/switchyard/organizer/with_reducer_factory.rb +20 -0
- data/lib/switchyard/organizer/with_reducer_log_decorator.rb +111 -0
- data/lib/switchyard/organizer.rb +129 -0
- data/lib/switchyard/testing/context_factory.rb +48 -0
- data/lib/switchyard/testing.rb +3 -0
- data/lib/switchyard/version.rb +5 -0
- data/lib/switchyard.rb +36 -0
- data/resources/fail_actions.png +0 -0
- data/resources/light-service.png +0 -0
- data/resources/organizer_and_actions.png +0 -0
- data/resources/skip_actions.png +0 -0
- data/spec/acceptance/add_numbers_spec.rb +11 -0
- data/spec/acceptance/after_actions_spec.rb +87 -0
- data/spec/acceptance/around_each_spec.rb +19 -0
- data/spec/acceptance/before_actions_spec.rb +115 -0
- data/spec/acceptance/custom_log_from_organizer_spec.rb +61 -0
- data/spec/acceptance/deprecation_warnings_spec.rb +82 -0
- data/spec/acceptance/fail_spec.rb +52 -0
- data/spec/acceptance/log_from_organizer_spec.rb +154 -0
- data/spec/acceptance/message_localization_spec.rb +119 -0
- data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
- data/spec/acceptance/organizer/add_to_context_spec.rb +54 -0
- data/spec/acceptance/organizer/around_each_with_reduce_if_spec.rb +42 -0
- data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +90 -0
- data/spec/acceptance/organizer/execute_spec.rb +67 -0
- data/spec/acceptance/organizer/iterate_spec.rb +44 -0
- data/spec/acceptance/organizer/reduce_case_spec.rb +65 -0
- data/spec/acceptance/organizer/reduce_if_else_spec.rb +60 -0
- data/spec/acceptance/organizer/reduce_if_spec.rb +89 -0
- data/spec/acceptance/organizer/reduce_until_spec.rb +49 -0
- data/spec/acceptance/organizer/reduce_while_spec.rb +96 -0
- data/spec/acceptance/organizer/with_callback_spec.rb +113 -0
- data/spec/acceptance/organizer_entry_point_spec.rb +35 -0
- data/spec/acceptance/rollback_spec.rb +183 -0
- data/spec/acceptance/skip_all_remaining_spec.rb +139 -0
- data/spec/acceptance/testing/context_factory_spec.rb +54 -0
- data/spec/action_expected_keys_spec.rb +63 -0
- data/spec/action_expects_and_promises_spec.rb +97 -0
- data/spec/action_optional_expected_keys_spec.rb +107 -0
- data/spec/action_promised_keys_spec.rb +126 -0
- data/spec/action_spec.rb +97 -0
- data/spec/context/inspect_spec.rb +52 -0
- data/spec/context_spec.rb +301 -0
- data/spec/examples/amount_spec.rb +77 -0
- data/spec/examples/controller_spec.rb +63 -0
- data/spec/examples/validate_address_spec.rb +38 -0
- data/spec/i18n_localization_adapter_spec.rb +83 -0
- data/spec/lib/deterministic/class_mixin_spec.rb +24 -0
- data/spec/lib/deterministic/currify_spec.rb +90 -0
- data/spec/lib/deterministic/monad_axioms.rb +46 -0
- data/spec/lib/deterministic/monad_spec.rb +47 -0
- data/spec/lib/deterministic/null_spec.rb +65 -0
- data/spec/lib/deterministic/option_spec.rb +140 -0
- data/spec/lib/deterministic/result/failure_spec.rb +65 -0
- data/spec/lib/deterministic/result/result_map_spec.rb +155 -0
- data/spec/lib/deterministic/result/result_shared.rb +25 -0
- data/spec/lib/deterministic/result/success_spec.rb +41 -0
- data/spec/lib/deterministic/result_spec.rb +63 -0
- data/spec/lib/deterministic/sequencer_spec.rb +506 -0
- data/spec/lib/edge_cases_spec.rb +156 -0
- data/spec/lib/enum_spec.rb +114 -0
- data/spec/lib/native_pattern_matching_spec.rb +74 -0
- data/spec/localization_adapter_spec.rb +66 -0
- data/spec/organizer/with_reducer_spec.rb +56 -0
- data/spec/organizer_key_aliases_spec.rb +29 -0
- data/spec/organizer_spec.rb +115 -0
- data/spec/readme_spec.rb +45 -0
- data/spec/sample/calculates_order_tax_action_spec.rb +16 -0
- data/spec/sample/calculates_tax_spec.rb +30 -0
- data/spec/sample/looks_up_tax_percentage_action_spec.rb +55 -0
- data/spec/sample/provides_free_shipping_action_spec.rb +25 -0
- data/spec/sample/tax/calculates_order_tax_action.rb +10 -0
- data/spec/sample/tax/calculates_tax.rb +11 -0
- data/spec/sample/tax/looks_up_tax_percentage_action.rb +28 -0
- data/spec/sample/tax/provides_free_shipping_action.rb +11 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support.rb +1 -0
- data/spec/test_doubles.rb +656 -0
- data/spec/testing/context_factory/iterate_spec.rb +39 -0
- data/spec/testing/context_factory/reduce_if_spec.rb +40 -0
- data/spec/testing/context_factory/reduce_until_spec.rb +40 -0
- data/spec/testing/context_factory/with_callback_spec.rb +38 -0
- data/spec/testing/context_factory_spec.rb +76 -0
- data/switchyard.gemspec +35 -0
- metadata +351 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'switchyard/functional/enum'
|
|
3
|
+
|
|
4
|
+
Amount = Switchyard.enum do
|
|
5
|
+
Due(:amount)
|
|
6
|
+
Paid(:amount)
|
|
7
|
+
Info(:amount)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class Amount
|
|
11
|
+
def self.from_f(f)
|
|
12
|
+
f >= 0 ? Amount::Due.new(f) : Amount::Paid.new(-1 * f)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Switchyard.impl(Amount) do
|
|
17
|
+
def to_s
|
|
18
|
+
match do
|
|
19
|
+
Due() { |a| format("%0.2f", a) }
|
|
20
|
+
Paid() { |a| format("-%0.2f", a) }
|
|
21
|
+
Info() { |a| format("(%0.2f)", a) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_f
|
|
26
|
+
match do
|
|
27
|
+
Info() { |_a| 0 }
|
|
28
|
+
Due() { |a| a }
|
|
29
|
+
Paid() { |a| -1 * a }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def +(other)
|
|
34
|
+
raise TypeError "Expected other to be an Amount, got #{other.class}" unless other.is_a? Amount
|
|
35
|
+
|
|
36
|
+
Amount.from_f(to_f + other.to_f)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe Amount do
|
|
41
|
+
# rubocop:disable Naming/MethodName
|
|
42
|
+
def Due(a)
|
|
43
|
+
Amount::Due.new(a)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def Paid(a)
|
|
47
|
+
Amount::Paid.new(a)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def Info(a)
|
|
51
|
+
Amount::Info.new(a)
|
|
52
|
+
end
|
|
53
|
+
# rubocop:enable Naming/MethodName
|
|
54
|
+
|
|
55
|
+
it "due" do
|
|
56
|
+
amount = Amount::Due.new(100.2)
|
|
57
|
+
expect(amount.to_s).to eq "100.20"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "paid" do
|
|
61
|
+
amount = Amount::Paid.new(100.1)
|
|
62
|
+
expect(amount.to_s).to eq "-100.10"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "paid" do
|
|
66
|
+
amount = Amount::Info.new(100.31)
|
|
67
|
+
expect(amount.to_s).to eq "(100.31)"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "+" do
|
|
71
|
+
expect(Due(10) + Paid(20)).to eq Paid(10)
|
|
72
|
+
expect(Due(10) + Paid(10)).to eq Due(0)
|
|
73
|
+
expect(Due(10) + Due(10)).to eq Due(20)
|
|
74
|
+
expect(Paid(10) + Paid(10)).to eq Paid(20)
|
|
75
|
+
expect(Paid(10) + Due(1) + Info(99)).to eq Paid(9)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Switchyard
|
|
4
|
+
module Procify
|
|
5
|
+
def py(m, *args)
|
|
6
|
+
args.any? ? method(m).to_proc.curry[*args] : method(m)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class BookingController
|
|
12
|
+
include Switchyard::Prelude::Result
|
|
13
|
+
include Switchyard::Procify
|
|
14
|
+
|
|
15
|
+
Context = Struct.new(:booking, :ability, :format)
|
|
16
|
+
|
|
17
|
+
def index(id, format = :html)
|
|
18
|
+
get_booking(id) << log(:booking) >>
|
|
19
|
+
py(:ability) << log(:ability) >>
|
|
20
|
+
py(:present, format) << log(:presenter) >>
|
|
21
|
+
py(:render) << log(:render)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def log(step)
|
|
25
|
+
->(data) { [step, data] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ability(ctx)
|
|
29
|
+
ctx.ability = {} # Ability.new(@booking)
|
|
30
|
+
Success(ctx)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def present(format, ctx)
|
|
34
|
+
ctx.format = format
|
|
35
|
+
|
|
36
|
+
Success(ctx)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render(ctx)
|
|
40
|
+
send(ctx.format, ctx)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def html(ctx)
|
|
44
|
+
Success(ctx)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_booking(id)
|
|
48
|
+
ctx = Context.new
|
|
49
|
+
ctx.booking = { :ref_anixe => id }
|
|
50
|
+
Success(ctx)
|
|
51
|
+
# @booking = @bms.booking_by_id(id)
|
|
52
|
+
# rescue BSON::InvalidObjectId => ex
|
|
53
|
+
# @booking = nil
|
|
54
|
+
# @ui.error(404, ex.message)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe BookingController do
|
|
59
|
+
it "does something" do
|
|
60
|
+
bc = BookingController.new
|
|
61
|
+
bc.index('1234', :html)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# A Unit of Work for validating an address
|
|
4
|
+
module ValidateAddress
|
|
5
|
+
extend Switchyard::Prelude::Result
|
|
6
|
+
|
|
7
|
+
def self.call(candidate)
|
|
8
|
+
errors = {}
|
|
9
|
+
errors[:street] = "Street cannot be empty" unless candidate.key? :street
|
|
10
|
+
errors[:city] = "Street cannot be empty" unless candidate.key? :city
|
|
11
|
+
errors[:postal] = "Street cannot be empty" unless candidate.key? :postal
|
|
12
|
+
|
|
13
|
+
errors.empty? ? Success(candidate) : Failure(errors)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe ValidateAddress do
|
|
18
|
+
include Switchyard
|
|
19
|
+
|
|
20
|
+
subject { ValidateAddress.call(candidate) }
|
|
21
|
+
context 'sunny day' do
|
|
22
|
+
let(:candidate) do
|
|
23
|
+
{ :title => "Hobbiton",
|
|
24
|
+
:street => "501 Buckland Rd",
|
|
25
|
+
:city => "Matamata",
|
|
26
|
+
:postal => "3472",
|
|
27
|
+
:country => "nz" }
|
|
28
|
+
end
|
|
29
|
+
specify { expect(subject).to be_a Switchyard::Result::Success }
|
|
30
|
+
specify { expect(subject.value).to eq candidate }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context 'empty data' do
|
|
34
|
+
let(:candidate) { {} }
|
|
35
|
+
specify { expect(subject).to be_a Switchyard::Result::Failure }
|
|
36
|
+
specify { expect(subject.value).to include(:street, :city, :postal) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require 'test_doubles'
|
|
3
|
+
|
|
4
|
+
describe Switchyard::I18n::LocalizationAdapter do
|
|
5
|
+
let(:action_class) { TestDoubles::AnAction }
|
|
6
|
+
let(:adapter) { described_class.new }
|
|
7
|
+
|
|
8
|
+
describe "#failure" do
|
|
9
|
+
subject { adapter.failure(message_or_key, action_class) }
|
|
10
|
+
|
|
11
|
+
context "when provided a Symbol" do
|
|
12
|
+
let(:message_or_key) { :not_found }
|
|
13
|
+
|
|
14
|
+
it "translates the message" do
|
|
15
|
+
expected_scope = "test_doubles/an_action.switchyard.failures"
|
|
16
|
+
|
|
17
|
+
expect(I18n).to receive(:t)
|
|
18
|
+
.with(message_or_key, :scope => expected_scope)
|
|
19
|
+
.and_return("message")
|
|
20
|
+
|
|
21
|
+
expect(subject).to eq("message")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "allows passing interpolation options to I18n layer" do
|
|
25
|
+
expect(I18n).to receive(:t)
|
|
26
|
+
.with(message_or_key, hash_including(:i18n_variable => "value"))
|
|
27
|
+
.and_return("message")
|
|
28
|
+
|
|
29
|
+
subject = adapter.failure(message_or_key,
|
|
30
|
+
action_class,
|
|
31
|
+
:i18n_variable => "value")
|
|
32
|
+
|
|
33
|
+
expect(subject).to eq("message")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context "when provided a String" do
|
|
38
|
+
let(:message_or_key) { "action failed" }
|
|
39
|
+
|
|
40
|
+
it "returns the message" do
|
|
41
|
+
expect(subject).to eq(message_or_key)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe "#success" do
|
|
47
|
+
subject { adapter.success(message_or_key, action_class) }
|
|
48
|
+
|
|
49
|
+
context "when provided a Symbol" do
|
|
50
|
+
let(:message_or_key) { :not_found }
|
|
51
|
+
|
|
52
|
+
it "translates the message" do
|
|
53
|
+
expected_scope = "test_doubles/an_action.switchyard.successes"
|
|
54
|
+
|
|
55
|
+
expect(I18n).to receive(:t)
|
|
56
|
+
.with(message_or_key, :scope => expected_scope)
|
|
57
|
+
.and_return("message")
|
|
58
|
+
|
|
59
|
+
expect(subject).to eq("message")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "allows passing interpolation options to I18n layer" do
|
|
63
|
+
expect(I18n).to receive(:t)
|
|
64
|
+
.with(message_or_key, hash_including(:i18n_variable => "value"))
|
|
65
|
+
.and_return("message")
|
|
66
|
+
|
|
67
|
+
subject = adapter.success(message_or_key,
|
|
68
|
+
action_class,
|
|
69
|
+
:i18n_variable => "value")
|
|
70
|
+
|
|
71
|
+
expect(subject).to eq("message")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context "when provided a String" do
|
|
76
|
+
let(:message_or_key) { "action failed" }
|
|
77
|
+
|
|
78
|
+
it "returns the message" do
|
|
79
|
+
expect(subject).to eq(message_or_key)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'Class Mixin' do
|
|
4
|
+
describe 'try' do
|
|
5
|
+
module MyApp
|
|
6
|
+
class Thing
|
|
7
|
+
include Switchyard::Prelude::Result
|
|
8
|
+
|
|
9
|
+
def run
|
|
10
|
+
Success(11) >> method(:double)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def double(num)
|
|
14
|
+
Success(num * 2)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "cleanly mixes into a class" do
|
|
20
|
+
result = MyApp::Thing.new.run
|
|
21
|
+
expect(result).to eq Switchyard::Result::Success.new(22)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'pry'
|
|
3
|
+
|
|
4
|
+
module Switchyard
|
|
5
|
+
module Currify
|
|
6
|
+
module ClassMethods
|
|
7
|
+
def currify(*names)
|
|
8
|
+
names.each do |name|
|
|
9
|
+
unbound_method = instance_method(name)
|
|
10
|
+
|
|
11
|
+
define_method(name) do |*args|
|
|
12
|
+
curried_method = unbound_method.bind(self).to_proc.curry
|
|
13
|
+
curried_method[*args]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.included(curried)
|
|
20
|
+
curried.extend ClassMethods
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Object
|
|
26
|
+
class Proc
|
|
27
|
+
def self.compose(f, g)
|
|
28
|
+
->(*args) { f[g[*args]] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# rubocop:disable Naming/BinaryOperatorParameterName
|
|
32
|
+
# Compose left to right
|
|
33
|
+
def |(g)
|
|
34
|
+
Proc.compose(g, self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Compose right to left
|
|
38
|
+
def *(g)
|
|
39
|
+
Proc.compose(self, g)
|
|
40
|
+
end
|
|
41
|
+
# rubocop:enable Naming/BinaryOperatorParameterName
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Booking
|
|
46
|
+
include Switchyard::Currify
|
|
47
|
+
include Switchyard::Prelude::Result
|
|
48
|
+
|
|
49
|
+
def initialize(deps)
|
|
50
|
+
@deps = deps
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build(id, format)
|
|
54
|
+
validate(id) | req | find | render(format)
|
|
55
|
+
|
|
56
|
+
validate(id) | request(id) | find
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate(id)
|
|
60
|
+
Success(id)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def req(a, id)
|
|
64
|
+
Success(:id => id + a)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def find(req)
|
|
68
|
+
Success(:found => req)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def render(format, req)
|
|
72
|
+
Success("rendered in #{format}: #{req[:found]}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
currify :find, :render, :req
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "Pref" do
|
|
79
|
+
include Switchyard::Prelude::Result
|
|
80
|
+
|
|
81
|
+
it "does something" do
|
|
82
|
+
b = Booking.new(1)
|
|
83
|
+
actual = b.validate(1) >> b.req(2) >> b.find >> b.render(:html)
|
|
84
|
+
|
|
85
|
+
# il formato di Hash#to_s cambia tra Ruby 3.3 e 3.4: costruiamo l'atteso dinamicamente
|
|
86
|
+
rendered_params = { :id => 3 }
|
|
87
|
+
expected = Switchyard::Result::Success.new("rendered in html: #{rendered_params}")
|
|
88
|
+
expect(actual).to eq expected
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
shared_examples 'a Monad' do
|
|
2
|
+
describe 'axioms' do
|
|
3
|
+
it '1st monadic law: left-identity' do
|
|
4
|
+
f = ->(value) { monad.new(value + 1) }
|
|
5
|
+
expect(
|
|
6
|
+
monad.new(1).bind do |value|
|
|
7
|
+
f.call(value)
|
|
8
|
+
end
|
|
9
|
+
).to eq f.call(1)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it '2nd monadic law: right-identy - new and bind do not change the value' do
|
|
13
|
+
expect(
|
|
14
|
+
monad.new(1).bind do |value|
|
|
15
|
+
monad.new(value)
|
|
16
|
+
end
|
|
17
|
+
).to eq monad.new(1)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it '3rd monadic law: associativity' do
|
|
21
|
+
f = ->(value) { monad.new(value + 1) }
|
|
22
|
+
g = ->(value) { monad.new(value + 100) }
|
|
23
|
+
|
|
24
|
+
id1 = monad.new(1).bind { |a| f.call(a) }.bind { |b| g.call(b) }
|
|
25
|
+
|
|
26
|
+
id2 = monad.new(1).bind do |a|
|
|
27
|
+
f.call(a).bind do |b|
|
|
28
|
+
g.call(b)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
expect(id1).to eq id2
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it '#bind must return a monad' do
|
|
36
|
+
expect(monad.new(1).bind { |v| monad.new(v) }).to eq monad.new(1)
|
|
37
|
+
# rubocop:disable Lint/EmptyBlock
|
|
38
|
+
expect { monad.new(1).bind {} }.to raise_error(Switchyard::Monad::NotMonadError)
|
|
39
|
+
# rubocop:enable Lint/EmptyBlock
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it '#new must return a monad' do
|
|
43
|
+
expect(monad.new(1)).to be_a monad
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require_relative 'monad_axioms'
|
|
3
|
+
|
|
4
|
+
describe Switchyard::Monad do
|
|
5
|
+
class Identity
|
|
6
|
+
include Switchyard::Monad
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
let(:monad) { Identity }
|
|
10
|
+
it_behaves_like 'a Monad' do
|
|
11
|
+
# let(:monad) { monad }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
specify { expect(Identity.new(1).inspect).to eq 'Identity(1)' }
|
|
15
|
+
specify { expect(Identity.new(1).to_s).to eq '1' }
|
|
16
|
+
specify { expect(Identity.new(nil).inspect).to eq 'Identity(nil)' }
|
|
17
|
+
specify { expect(Identity.new(nil).to_s).to eq '' }
|
|
18
|
+
specify { expect(Identity.new([1, 2]).fmap(&:to_s)).to eq Identity.new("[1, 2]") }
|
|
19
|
+
specify { expect(Identity.new(1).fmap { |v| v + 2 }).to eq Identity.new(3) }
|
|
20
|
+
specify { expect(Identity.new('foo').fmap(&:upcase)).to eq Identity.new('FOO') }
|
|
21
|
+
|
|
22
|
+
context '#bind' do
|
|
23
|
+
it "raises an error if the passed function does not return a monad of the same class" do
|
|
24
|
+
# rubocop:disable Lint/EmptyBlock
|
|
25
|
+
expect { Identity.new(1).bind {} }.to \
|
|
26
|
+
raise_error(Switchyard::Monad::NotMonadError)
|
|
27
|
+
# rubocop:enable Lint/EmptyBlock
|
|
28
|
+
end
|
|
29
|
+
specify { expect(Identity.new(1).bind { |value| Identity.new(value) }).to eq Identity.new(1) }
|
|
30
|
+
|
|
31
|
+
it "passes the monad class, this is ruby-fu?!" do
|
|
32
|
+
Identity.new(1)
|
|
33
|
+
.bind do |v|
|
|
34
|
+
expect(monad).to eq Identity
|
|
35
|
+
monad.new(v)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
specify do
|
|
40
|
+
expect(
|
|
41
|
+
monad.new(1).bind { |value| monad.new(value + 1) }
|
|
42
|
+
).to eq Identity.new(2)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
specify { expect(Identity.new(Identity.new(1))).to eq Identity.new(1) }
|
|
46
|
+
end
|
|
47
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Null do
|
|
4
|
+
it "Null is a Singleton" do
|
|
5
|
+
expect(Null.instance).to be_a Null
|
|
6
|
+
expect { Null.new }.to raise_error(NoMethodError, /private method [`']new'/)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it "respond_to? accepts the standard two-argument form" do
|
|
10
|
+
expect(Null.instance.respond_to?(:anything, true)).to be(true)
|
|
11
|
+
expect(Null.instance.respond_to?(:anything)).to be(true)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "explicit conversions" do
|
|
15
|
+
expect(Null.to_s).to eq 'Null'
|
|
16
|
+
expect(Null.inspect).to eq 'Null'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "answer if Null is null" do
|
|
20
|
+
expect(Null.null?).to eq true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "answer if Null is some" do
|
|
24
|
+
expect(Null.some?).to eq false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "compares to Null" do
|
|
28
|
+
# rubocop:disable Style/CaseEquality
|
|
29
|
+
expect(Null === Null.instance).to be_truthy
|
|
30
|
+
expect(Null.instance === Null).to be_truthy
|
|
31
|
+
expect(Null.instance).to eq Null
|
|
32
|
+
expect(Null).to eq Null.instance
|
|
33
|
+
expect(1).not_to be Null
|
|
34
|
+
expect(1).not_to be Null.instance
|
|
35
|
+
expect(Null.instance).not_to be 1
|
|
36
|
+
expect(Null).not_to be 1
|
|
37
|
+
expect(Null.instance).not_to be_nil
|
|
38
|
+
expect(Null).not_to be_nil
|
|
39
|
+
# rubocop:enable Style/CaseEquality
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "implicit conversions" do
|
|
43
|
+
null = Null.instance
|
|
44
|
+
expect(null.to_str).to eq ""
|
|
45
|
+
expect(null.to_ary).to eq []
|
|
46
|
+
# rubocop:disable Style/StringConcatenation
|
|
47
|
+
expect("" + null).to eq ""
|
|
48
|
+
# rubocop:enable Style/StringConcatenation
|
|
49
|
+
|
|
50
|
+
a, b, c = null
|
|
51
|
+
expect(a).to be_nil
|
|
52
|
+
expect(b).to be_nil
|
|
53
|
+
expect(c).to be_nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "mimicks other classes and returns Null for their public methods" do
|
|
57
|
+
class UnderMimickTest
|
|
58
|
+
def test; end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
mimick = Null.mimic(UnderMimickTest)
|
|
62
|
+
expect(mimick.test).to be_null
|
|
63
|
+
expect { mimick.i_dont_exist }.to raise_error(NoMethodError)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
include Switchyard::Prelude::Option
|
|
4
|
+
|
|
5
|
+
describe Switchyard::Option do
|
|
6
|
+
it "Some cannot wrap nil" do
|
|
7
|
+
expect { described_class::Some.new(nil) }
|
|
8
|
+
.to raise_error(ArgumentError, /use None instead/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
specify { expect(described_class::Some.new(0)).to be_a described_class::Some }
|
|
12
|
+
specify { expect(described_class::Some.new(0)).to be_a described_class }
|
|
13
|
+
specify { expect(described_class::Some.new(0)).to eq Some(0) }
|
|
14
|
+
|
|
15
|
+
specify { expect(described_class::None.new).to eq described_class::None.new }
|
|
16
|
+
specify { expect(described_class::None.new).to be_a described_class::None }
|
|
17
|
+
specify { expect(described_class::None.new).to be_a described_class }
|
|
18
|
+
specify { expect(described_class::None.new).to eq None }
|
|
19
|
+
|
|
20
|
+
it "join" do
|
|
21
|
+
expect(Some(Some(1))).to eq Some(1)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "fmap" do
|
|
25
|
+
expect(Some(1).fmap { |n| n + 1 }).to eq Some(2)
|
|
26
|
+
expect(None.fmap { |n| n + 1 }).to eq None
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "map" do
|
|
30
|
+
expect(Some(1).map { |n| Some(n + 1) }).to eq Some(2)
|
|
31
|
+
expect(Some(1).map { |_n| None }).to eq None
|
|
32
|
+
expect(None.map { |n| Some(n + 1) }).to eq None
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "some?" do
|
|
36
|
+
expect(Some(1).some?).to be_truthy
|
|
37
|
+
expect(None.some?).to be_falsey
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "none?" do
|
|
41
|
+
expect(None.none?).to be_truthy
|
|
42
|
+
expect(Some(1).none?).to be_falsey
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "value" do
|
|
46
|
+
expect(Some(1).value).to eq 1
|
|
47
|
+
expect { None.value }.to raise_error NoMethodError
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "value_or" do
|
|
51
|
+
expect(Some(1).value_or(2)).to eq 1
|
|
52
|
+
expect(None.value_or(0)).to eq 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "+" do
|
|
56
|
+
expect(Some([1]) + None).to eq Some([1])
|
|
57
|
+
expect(Some(1) + None + None).to eq Some(1)
|
|
58
|
+
expect(Some(1) + Some(1)).to eq Some(2)
|
|
59
|
+
expect(None + Some(1)).to eq Some(1)
|
|
60
|
+
expect(None + None + Some(1)).to eq Some(1)
|
|
61
|
+
expect(None + None + Some(1) + None).to eq Some(1)
|
|
62
|
+
expect(None + Some(:foo => 1)).to eq Some(:foo => 1)
|
|
63
|
+
expect(Some([1]) + Some([1])).to eq Some([1, 1])
|
|
64
|
+
expect { Some([1]) + Some(1) }.to raise_error TypeError
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "inspect" do
|
|
68
|
+
expect(Some(1).inspect).to eq "Some(1)"
|
|
69
|
+
expect(described_class::None.new.inspect).to eq "None"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "to_s" do
|
|
73
|
+
expect(Some(1).to_s).to eq "1"
|
|
74
|
+
expect(described_class::None.new.to_s).to eq ""
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "match" do
|
|
78
|
+
expect(
|
|
79
|
+
# rubocop:disable Lint/UnusedBlockArgument
|
|
80
|
+
# rubocop:disable Lint/EmptyBlock
|
|
81
|
+
Some(0).match do
|
|
82
|
+
Some(where { s == 1 }) { |s| 99 }
|
|
83
|
+
Some(where { s == 0 }) { |s| s + 1 }
|
|
84
|
+
None() {}
|
|
85
|
+
end
|
|
86
|
+
).to eq 1
|
|
87
|
+
|
|
88
|
+
expect(
|
|
89
|
+
Some(1).match do
|
|
90
|
+
None() { 0 }
|
|
91
|
+
Some() { |s| 1 }
|
|
92
|
+
end
|
|
93
|
+
).to eq 1
|
|
94
|
+
|
|
95
|
+
expect(
|
|
96
|
+
Some(1).match do
|
|
97
|
+
None() { 0 }
|
|
98
|
+
Some(where { s.is_a? Integer }) { |s| 1 }
|
|
99
|
+
end
|
|
100
|
+
# rubocop:enable Lint/UnusedBlockArgument
|
|
101
|
+
# rubocop:enable Lint/EmptyBlock
|
|
102
|
+
).to eq 1
|
|
103
|
+
|
|
104
|
+
expect(
|
|
105
|
+
None.match do
|
|
106
|
+
None() { 0 }
|
|
107
|
+
Some() { 1 }
|
|
108
|
+
end
|
|
109
|
+
).to eq 0
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "nil?" do
|
|
113
|
+
expect(described_class.some?(nil)).to eq None
|
|
114
|
+
expect(described_class.some?(1)).to be_some
|
|
115
|
+
expect(described_class.some?(1)).to eq Some(1)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "any?" do
|
|
119
|
+
expect(described_class.any?(nil)).to be_none
|
|
120
|
+
expect(described_class.any?(None)).to be_none
|
|
121
|
+
expect(described_class.any?("")).to be_none
|
|
122
|
+
expect(described_class.any?([])).to be_none
|
|
123
|
+
expect(described_class.any?({})).to be_none
|
|
124
|
+
expect(described_class.any?([1])).to eq Some([1])
|
|
125
|
+
expect(described_class.any?(:foo => 1)).to eq Some(:foo => 1)
|
|
126
|
+
expect(described_class.any?(1)).to eq Some(1)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "try!" do
|
|
130
|
+
expect(described_class.try! { raise "error" }).to be_none
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
require_relative 'monad_axioms'
|
|
135
|
+
|
|
136
|
+
describe Switchyard::Option::Some do
|
|
137
|
+
it_behaves_like 'a Monad' do
|
|
138
|
+
let(:monad) { described_class }
|
|
139
|
+
end
|
|
140
|
+
end
|