functional-light-service 0.2.4
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/.gitignore +22 -0
- data/.rspec +3 -0
- data/.rubocop.yml +72 -0
- data/.travis.yml +22 -0
- data/Appraisals +3 -0
- data/CHANGELOG.md +37 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +1424 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/functional-light-service.gemspec +26 -0
- data/gemfiles/activesupport_5.gemfile +8 -0
- data/gemfiles/activesupport_5.gemfile.lock +82 -0
- data/lib/functional-light-service/action.rb +102 -0
- data/lib/functional-light-service/configuration.rb +24 -0
- data/lib/functional-light-service/context/key_verifier.rb +118 -0
- data/lib/functional-light-service/context.rb +165 -0
- data/lib/functional-light-service/errors.rb +6 -0
- data/lib/functional-light-service/functional/enum.rb +254 -0
- data/lib/functional-light-service/functional/maybe.rb +14 -0
- data/lib/functional-light-service/functional/monad.rb +66 -0
- data/lib/functional-light-service/functional/null.rb +74 -0
- data/lib/functional-light-service/functional/option.rb +99 -0
- data/lib/functional-light-service/functional/result.rb +122 -0
- data/lib/functional-light-service/localization_adapter.rb +44 -0
- data/lib/functional-light-service/organizer/execute.rb +14 -0
- data/lib/functional-light-service/organizer/iterate.rb +22 -0
- data/lib/functional-light-service/organizer/reduce_if.rb +17 -0
- data/lib/functional-light-service/organizer/reduce_until.rb +20 -0
- data/lib/functional-light-service/organizer/scoped_reducable.rb +13 -0
- data/lib/functional-light-service/organizer/verify_call_method_exists.rb +28 -0
- data/lib/functional-light-service/organizer/with_callback.rb +26 -0
- data/lib/functional-light-service/organizer/with_reducer.rb +71 -0
- data/lib/functional-light-service/organizer/with_reducer_factory.rb +18 -0
- data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +105 -0
- data/lib/functional-light-service/organizer.rb +105 -0
- data/lib/functional-light-service/testing/context_factory.rb +40 -0
- data/lib/functional-light-service/testing.rb +1 -0
- data/lib/functional-light-service/version.rb +3 -0
- data/lib/functional-light-service.rb +29 -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 +71 -0
- data/spec/acceptance/around_each_spec.rb +19 -0
- data/spec/acceptance/before_actions_spec.rb +98 -0
- data/spec/acceptance/custom_log_from_organizer_spec.rb +60 -0
- data/spec/acceptance/fail_spec.rb +24 -0
- data/spec/acceptance/include_warning_spec.rb +29 -0
- data/spec/acceptance/log_from_organizer_spec.rb +154 -0
- data/spec/acceptance/message_localization_spec.rb +118 -0
- data/spec/acceptance/not_having_call_method_warning_spec.rb +39 -0
- data/spec/acceptance/organizer/around_each_with_reduce_if_spec.rb +42 -0
- data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +65 -0
- data/spec/acceptance/organizer/execute_spec.rb +46 -0
- data/spec/acceptance/organizer/iterate_spec.rb +37 -0
- data/spec/acceptance/organizer/reduce_if_spec.rb +51 -0
- data/spec/acceptance/organizer/reduce_until_spec.rb +43 -0
- data/spec/acceptance/organizer/with_callback_spec.rb +110 -0
- data/spec/acceptance/rollback_spec.rb +132 -0
- data/spec/acceptance/skip_all_warning_spec.rb +20 -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 +93 -0
- data/spec/action_promised_keys_spec.rb +122 -0
- data/spec/action_spec.rb +89 -0
- data/spec/context/inspect_spec.rb +57 -0
- data/spec/context_spec.rb +197 -0
- data/spec/examples/amount_spec.rb +77 -0
- data/spec/examples/controller_spec.rb +63 -0
- data/spec/examples/validate_address_spec.rb +37 -0
- data/spec/lib/deterministic/class_mixin_spec.rb +24 -0
- data/spec/lib/deterministic/currify_spec.rb +88 -0
- data/spec/lib/deterministic/monad_axioms.rb +44 -0
- data/spec/lib/deterministic/monad_spec.rb +45 -0
- data/spec/lib/deterministic/null_spec.rb +58 -0
- data/spec/lib/deterministic/option_spec.rb +133 -0
- data/spec/lib/deterministic/result/failure_spec.rb +65 -0
- data/spec/lib/deterministic/result/result_map_spec.rb +154 -0
- data/spec/lib/deterministic/result/result_shared.rb +24 -0
- data/spec/lib/deterministic/result/success_spec.rb +41 -0
- data/spec/lib/deterministic/result_spec.rb +63 -0
- data/spec/lib/enum_spec.rb +112 -0
- data/spec/localization_adapter_spec.rb +83 -0
- data/spec/organizer/with_reducer_spec.rb +56 -0
- data/spec/organizer_key_aliases_spec.rb +29 -0
- data/spec/organizer_spec.rb +93 -0
- data/spec/readme_spec.rb +47 -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 +53 -0
- data/spec/sample/provides_free_shipping_action_spec.rb +25 -0
- data/spec/sample/tax/calculates_order_tax_action.rb +9 -0
- data/spec/sample/tax/calculates_tax.rb +11 -0
- data/spec/sample/tax/looks_up_tax_percentage_action.rb +27 -0
- data/spec/sample/tax/provides_free_shipping_action.rb +10 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support.rb +1 -0
- data/spec/test_doubles.rb +552 -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 +55 -0
- metadata +285 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
module FunctionalLightService
|
|
2
|
+
module Enum
|
|
3
|
+
class MatchError < StandardError; end
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class EnumBuilder
|
|
7
|
+
def initialize(parent)
|
|
8
|
+
@parent = parent
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class DataType
|
|
12
|
+
module AnyEnum
|
|
13
|
+
include FunctionalLightService::Monad
|
|
14
|
+
|
|
15
|
+
def match(&block)
|
|
16
|
+
parent.match(self, &block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_s
|
|
20
|
+
value.to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def name
|
|
24
|
+
self.class.name.split("::")[-1]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns array. Will fail on Nullary objects.
|
|
28
|
+
# TODO: define a Unary module so we can define this method differently on Unary vs Binary
|
|
29
|
+
def wrapped_values
|
|
30
|
+
if is_a?(FunctionalLightService::EnumBuilder::DataType::Binary)
|
|
31
|
+
value.values
|
|
32
|
+
else
|
|
33
|
+
[value]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module Nullary
|
|
39
|
+
def initialize(*_args)
|
|
40
|
+
@value = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def inspect
|
|
44
|
+
name
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# TODO: this should probably be named Multary
|
|
49
|
+
module Binary
|
|
50
|
+
def initialize(*init)
|
|
51
|
+
unless (init.count == 1 && init[0].is_a?(Hash)) || init.count == args.count
|
|
52
|
+
raise ArgumentError, "Expected arguments for #{args}, got #{init}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@value = if init.count == 1 && init[0].is_a?(Hash)
|
|
56
|
+
Hash[args.zip(init[0].values)]
|
|
57
|
+
else
|
|
58
|
+
Hash[args.zip(init)]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inspect
|
|
63
|
+
params = value.map { |k, v| "#{k}: #{v.inspect}" }
|
|
64
|
+
"#{name}(#{params.join(', ')})"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# rubocop:disable Metrics/MethodLength
|
|
69
|
+
def self.create(parent, args)
|
|
70
|
+
# rubocop:disable Style/AccessModifierDeclarations
|
|
71
|
+
if args.include? :value
|
|
72
|
+
raise ArgumentError, "#{args} may not contain the reserved name :value"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
dt = Class.new(parent)
|
|
76
|
+
|
|
77
|
+
dt.instance_eval do
|
|
78
|
+
public_class_method :new
|
|
79
|
+
include AnyEnum
|
|
80
|
+
define_method(:args) { args }
|
|
81
|
+
|
|
82
|
+
define_method(:parent) { parent }
|
|
83
|
+
private :parent
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
case args.count
|
|
87
|
+
when 0
|
|
88
|
+
dt.instance_eval do
|
|
89
|
+
include Nullary
|
|
90
|
+
private :value
|
|
91
|
+
end
|
|
92
|
+
when 1
|
|
93
|
+
dt.instance_eval do
|
|
94
|
+
define_method(args[0].to_sym) { value }
|
|
95
|
+
end
|
|
96
|
+
else
|
|
97
|
+
dt.instance_eval do
|
|
98
|
+
include Binary
|
|
99
|
+
|
|
100
|
+
args.each do |m|
|
|
101
|
+
define_method(m) do
|
|
102
|
+
@value[m]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
dt
|
|
109
|
+
# rubocop:enable Style/AccessModifierDeclarations
|
|
110
|
+
end
|
|
111
|
+
# rubocop:enable Metrics/MethodLength
|
|
112
|
+
|
|
113
|
+
class << self
|
|
114
|
+
# rubocop:disable Style/AccessModifierDeclarations
|
|
115
|
+
public :new
|
|
116
|
+
# rubocop:enable Style/AccessModifierDeclarations
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def method_missing(m, *args)
|
|
121
|
+
if @parent.const_defined?(m)
|
|
122
|
+
super
|
|
123
|
+
else
|
|
124
|
+
@parent.const_set(m, DataType.create(@parent, args))
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
module_function
|
|
130
|
+
|
|
131
|
+
# rubocop:disable Metrics/AbcSize
|
|
132
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
133
|
+
# rubocop:disable Metrics/MethodLength
|
|
134
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
135
|
+
def enum(&block)
|
|
136
|
+
mod = Class.new do # the enum to be built
|
|
137
|
+
private_class_method :new
|
|
138
|
+
|
|
139
|
+
def self.match(obj, &block)
|
|
140
|
+
caller_ctx = block.binding.eval 'self'
|
|
141
|
+
|
|
142
|
+
matcher = self::Matcher.new(obj)
|
|
143
|
+
matcher.instance_eval(&block)
|
|
144
|
+
|
|
145
|
+
variants_in_match = matcher.matches.collect do |e|
|
|
146
|
+
e[1].name.split('::')[-1].to_sym
|
|
147
|
+
end.uniq.sort
|
|
148
|
+
variants_not_covered = variants - variants_in_match
|
|
149
|
+
unless variants_not_covered.empty?
|
|
150
|
+
raise Enum::MatchError, "Match is non-exhaustive, #{variants_not_covered} not covered"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
type_matches = matcher.matches.select { |r| r[0].is_a?(r[1]) }
|
|
154
|
+
|
|
155
|
+
type_matches.each do |match|
|
|
156
|
+
obj, _type, block, args, guard = match
|
|
157
|
+
|
|
158
|
+
return caller_ctx.instance_eval(&block) if args.count.zero?
|
|
159
|
+
|
|
160
|
+
if args.count != obj.args.count
|
|
161
|
+
msg = "Pattern (#{args.join(', ')}) must match (#{obj.args.join(', ')})"
|
|
162
|
+
raise Enum::MatchError, msg
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
guard_ctx = guard_context(obj, args)
|
|
166
|
+
return caller_ctx.instance_exec(* obj.wrapped_values, &block) unless guard
|
|
167
|
+
|
|
168
|
+
if guard && guard_ctx.instance_exec(obj, &guard)
|
|
169
|
+
return caller_ctx.instance_exec(* obj.wrapped_values, &block)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
raise Enum::MatchError, "No match could be made"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def self.variants
|
|
177
|
+
constants - %i[Matcher MatchError]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def self.guard_context(obj, args)
|
|
181
|
+
if obj.is_a?(FunctionalLightService::EnumBuilder::DataType::Binary)
|
|
182
|
+
Struct.new(*args).new(*obj.value.values)
|
|
183
|
+
else
|
|
184
|
+
Struct.new(*args).new(obj.value)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
enum = EnumBuilder.new(mod)
|
|
189
|
+
enum.instance_eval(&block)
|
|
190
|
+
|
|
191
|
+
type_variants = mod.constants
|
|
192
|
+
|
|
193
|
+
matcher = Class.new do
|
|
194
|
+
def initialize(obj)
|
|
195
|
+
@obj = obj
|
|
196
|
+
@matches = []
|
|
197
|
+
@vars = []
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
attr_reader :matches, :vars
|
|
201
|
+
|
|
202
|
+
def where(&guard)
|
|
203
|
+
guard
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
type_variants.each do |m|
|
|
207
|
+
define_method(m) do |guard = nil, &inner_block|
|
|
208
|
+
raise ArgumentError, "No block given to `#{m}`" if inner_block.nil?
|
|
209
|
+
|
|
210
|
+
params_spec = inner_block.parameters
|
|
211
|
+
if params_spec.any? { |spec| spec.size < 2 }
|
|
212
|
+
msg = "Unnamed param found in block parameters: #{params_spec.inspect}"
|
|
213
|
+
raise ArgumentError, msg
|
|
214
|
+
end
|
|
215
|
+
if params_spec.any? { |spec| spec[0] != :req && spec[0] != :opt }
|
|
216
|
+
msg = "Only :req & :opt params allowed; parameters=#{params_spec.inspect}"
|
|
217
|
+
raise ArgumentError, msg
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
args = params_spec.map { |spec| spec[1] }
|
|
221
|
+
|
|
222
|
+
type = Kernel.eval("#{mod.name}::#{m}")
|
|
223
|
+
|
|
224
|
+
guard = nil if guard && !guard.is_a?(Proc)
|
|
225
|
+
|
|
226
|
+
@matches << [@obj, type, inner_block, args, guard]
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
mod.const_set(:Matcher, matcher)
|
|
232
|
+
|
|
233
|
+
type_variants.each do |variant|
|
|
234
|
+
mod.singleton_class.class_exec do
|
|
235
|
+
define_method(variant) do |*args|
|
|
236
|
+
const_get(variant).new(*args)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
mod
|
|
241
|
+
end
|
|
242
|
+
# rubocop:enable Metrics/AbcSize
|
|
243
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
244
|
+
# rubocop:enable Metrics/MethodLength
|
|
245
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
246
|
+
|
|
247
|
+
def impl(enum_type, &block)
|
|
248
|
+
enum_type.variants.each do |v|
|
|
249
|
+
name = "#{enum_type.name}::#{v}"
|
|
250
|
+
type = Kernel.eval(name)
|
|
251
|
+
type.class_eval(&block)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module FunctionalLightService
|
|
2
|
+
module Monad
|
|
3
|
+
class NotMonadError < StandardError; end
|
|
4
|
+
|
|
5
|
+
# Basicly the `pure` function
|
|
6
|
+
def initialize(init)
|
|
7
|
+
@value = join(init)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# If the passed value is monad already, get the value to avoid nesting
|
|
11
|
+
# M[M[A]] is equivalent to M[A]
|
|
12
|
+
def join(other)
|
|
13
|
+
if other.is_a? self.class
|
|
14
|
+
other.value
|
|
15
|
+
else
|
|
16
|
+
other
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The functor: takes a function (a -> b) and applies it to the inner value of the monad (Ma),
|
|
21
|
+
# boxes it back to the same monad (Mb)
|
|
22
|
+
# fmap :: (a -> b) -> M a -> M b
|
|
23
|
+
def fmap(proc = nil, &block)
|
|
24
|
+
result = (proc || block).call(value)
|
|
25
|
+
self.class.new(result)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The monad: takes a function which returns a monad (of the same type), applies the function
|
|
29
|
+
# bind :: (a -> Mb) -> M a -> M b
|
|
30
|
+
# the self.class, i.e. the containing monad is passed as a second (optional) arg to the function
|
|
31
|
+
def bind(proc = nil, &block)
|
|
32
|
+
(proc || block).call(value).tap do |result|
|
|
33
|
+
# rubocop:disable Style/CaseEquality
|
|
34
|
+
parent = self.class.superclass === Object ? self.class : self.class.superclass
|
|
35
|
+
# rubocop:enable Style/CaseEquality
|
|
36
|
+
unless result.is_a? parent
|
|
37
|
+
raise NotMonadError, "Expected #{result.inspect} to be an #{parent}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
alias :'>>=' :bind
|
|
42
|
+
|
|
43
|
+
# Get the underlying value, return in Haskell
|
|
44
|
+
# return :: M a -> a
|
|
45
|
+
def value
|
|
46
|
+
@value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
value.to_s
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Two monads are equivalent if they are of the same type and when their values are equal
|
|
54
|
+
def ==(other)
|
|
55
|
+
return false unless other.is_a? self.class
|
|
56
|
+
|
|
57
|
+
@value == other.instance_variable_get(:@value)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Return the string representation of the Monad
|
|
61
|
+
def inspect
|
|
62
|
+
pretty_class_name = self.class.name.split('::')[-1]
|
|
63
|
+
"#{pretty_class_name}(#{value.inspect})"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# The simplest NullObject there can be
|
|
2
|
+
class Null
|
|
3
|
+
class << self
|
|
4
|
+
def method_missing(m, *args)
|
|
5
|
+
if m == :new
|
|
6
|
+
super
|
|
7
|
+
else
|
|
8
|
+
Null.instance
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def instance
|
|
13
|
+
@instance ||= new([])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def null?
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def some?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def mimic(klas)
|
|
25
|
+
new(klas.instance_methods(false))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ==(other)
|
|
29
|
+
other.respond_to?(:null?) && other.null?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
private_class_method :new
|
|
33
|
+
|
|
34
|
+
def initialize(methods)
|
|
35
|
+
@methods = methods
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# implicit conversions
|
|
39
|
+
def to_str
|
|
40
|
+
''
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_ary
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def method_missing(m, *args)
|
|
48
|
+
return self if respond_to?(m)
|
|
49
|
+
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def null?
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def some?
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def respond_to?(m, include_private = false)
|
|
62
|
+
return true if @methods.empty? || @methods.include?(m)
|
|
63
|
+
|
|
64
|
+
super
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def inspect
|
|
68
|
+
'Null'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ==(other)
|
|
72
|
+
other.respond_to?(:null?) && other.null?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module FunctionalLightService
|
|
2
|
+
Option = FunctionalLightService.enum do
|
|
3
|
+
Some(:s)
|
|
4
|
+
None()
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class Option
|
|
8
|
+
class << self
|
|
9
|
+
def some?(expr)
|
|
10
|
+
to_option(expr) { expr.nil? }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def any?(expr)
|
|
14
|
+
to_option(expr) { expr.nil? || (expr.respond_to?(:empty?) && expr.empty?) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_option(expr)
|
|
18
|
+
yield(expr) ? None.new : Some.new(expr)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def try!
|
|
22
|
+
yield
|
|
23
|
+
rescue StandardError
|
|
24
|
+
None.new
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# rubocop:disable Metrics/BlockLength
|
|
30
|
+
impl(Option) do
|
|
31
|
+
class NoneValueError < StandardError; end
|
|
32
|
+
|
|
33
|
+
def fmap
|
|
34
|
+
match do
|
|
35
|
+
Some() { |s| self.class.new(yield(s)) }
|
|
36
|
+
None() { self }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def map(&fn)
|
|
41
|
+
match do
|
|
42
|
+
Some() { |_s| bind(&fn) }
|
|
43
|
+
None() { self }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def some?
|
|
48
|
+
is_a? Option::Some
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def none?
|
|
52
|
+
is_a? Option::None
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
alias :empty? :none?
|
|
56
|
+
|
|
57
|
+
def value_or(n)
|
|
58
|
+
match do
|
|
59
|
+
Some() { |s| s }
|
|
60
|
+
None() { n }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def value_to_a
|
|
65
|
+
@value
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def +(other)
|
|
69
|
+
match do
|
|
70
|
+
None() { other }
|
|
71
|
+
Some(where { !other.is_a?(Option) }) { |_| raise TypeError, "Other must be an #{Option}" }
|
|
72
|
+
Some(where { other.some? }) { |s| Option::Some.new(s + other.value) }
|
|
73
|
+
Some() { |_| self }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
# rubocop:enable Metrics/BlockLength
|
|
78
|
+
|
|
79
|
+
module Prelude
|
|
80
|
+
module Option
|
|
81
|
+
None = FunctionalLightService::Option::None.new
|
|
82
|
+
Option = FunctionalLightService::Option
|
|
83
|
+
# rubocop:disable Naming/MethodName
|
|
84
|
+
def Some(s)
|
|
85
|
+
FunctionalLightService::Option::Some.new(s)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def None
|
|
89
|
+
FunctionalLightService::Prelude::Option::None
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def Option
|
|
93
|
+
FunctionalLightService::Option
|
|
94
|
+
end
|
|
95
|
+
# rubocop:enable Naming/MethodName
|
|
96
|
+
# include Option
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
module FunctionalLightService
|
|
2
|
+
Result = FunctionalLightService.enum do
|
|
3
|
+
Success(:s)
|
|
4
|
+
Failure(:f)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class Result
|
|
8
|
+
class << self
|
|
9
|
+
def try!
|
|
10
|
+
Success.new(yield)
|
|
11
|
+
rescue StandardError => err
|
|
12
|
+
Failure.new(err)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# rubocop:disable Metrics/BlockLength
|
|
18
|
+
FunctionalLightService.impl(Result) do
|
|
19
|
+
def map(proc = nil, &block)
|
|
20
|
+
match do
|
|
21
|
+
Success() { |_| bind(proc || block) }
|
|
22
|
+
Failure() { |_| self }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
alias :>> :map
|
|
27
|
+
alias :and_then :map
|
|
28
|
+
|
|
29
|
+
def map_err(proc = nil, &block)
|
|
30
|
+
match do
|
|
31
|
+
Success() { |_| self }
|
|
32
|
+
Failure() { |_| bind(proc || block) }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
alias :or_else :map_err
|
|
37
|
+
|
|
38
|
+
def pipe(proc = nil, &block)
|
|
39
|
+
(proc || block).call(self)
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
alias :<< :pipe
|
|
44
|
+
|
|
45
|
+
def success?
|
|
46
|
+
is_a? Result::Success
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def failure?
|
|
50
|
+
is_a? Result::Failure
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def or(other)
|
|
54
|
+
unless other.is_a? Result
|
|
55
|
+
msg = "Expected #{other.inspect} to be a Result"
|
|
56
|
+
raise FunctionalLightService::Monad::NotMonadError, msg
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
match do
|
|
60
|
+
Success() { |_| self }
|
|
61
|
+
Failure() { |_| other }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def and(other)
|
|
66
|
+
unless other.is_a? Result
|
|
67
|
+
msg = "Expected #{other.inspect} to be a Result"
|
|
68
|
+
raise FunctionalLightService::Monad::NotMonadError, msg
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
match do
|
|
72
|
+
Success() { |_| other }
|
|
73
|
+
Failure() { |_| self }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def +(other)
|
|
78
|
+
unless other.is_a? Result
|
|
79
|
+
msg = "Expected #{other.inspect} to be a Result"
|
|
80
|
+
raise FunctionalLightService::Monad::NotMonadError, msg
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
match do
|
|
84
|
+
Success(where { other.success? }) { |s| Result::Success.new(s + other.value) }
|
|
85
|
+
Failure(where { other.failure? }) { |f| Result::Failure.new(f + other.value) }
|
|
86
|
+
Success() { |_| other } # implied other.failure?
|
|
87
|
+
Failure() { |_| self } # implied other.success?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def try(proc = nil, &block)
|
|
92
|
+
map(proc, &block)
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
Result::Failure.new(e)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
alias :>= :try
|
|
98
|
+
end
|
|
99
|
+
# rubocop:enable Metrics/BlockLength
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
module FunctionalLightService
|
|
103
|
+
module Prelude
|
|
104
|
+
module Result
|
|
105
|
+
# rubocop:disable Naming/MethodName
|
|
106
|
+
def try!(&block)
|
|
107
|
+
FunctionalLightService::Result.try!(&block)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def Success(s)
|
|
111
|
+
FunctionalLightService::Result::Success.new(s)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def Failure(f)
|
|
115
|
+
FunctionalLightService::Result::Failure.new(f)
|
|
116
|
+
end
|
|
117
|
+
# rubocop:enable Naming/MethodName
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
include Result
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module FunctionalLightService
|
|
2
|
+
class LocalizationAdapter
|
|
3
|
+
def failure(message_or_key, action_class, i18n_options = {})
|
|
4
|
+
find_translated_message(message_or_key,
|
|
5
|
+
action_class,
|
|
6
|
+
i18n_options,
|
|
7
|
+
:type => :failure)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def success(message_or_key, action_class, i18n_options = {})
|
|
11
|
+
find_translated_message(message_or_key,
|
|
12
|
+
action_class,
|
|
13
|
+
i18n_options,
|
|
14
|
+
:type => :success)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def find_translated_message(message_or_key,
|
|
20
|
+
action_class,
|
|
21
|
+
i18n_options,
|
|
22
|
+
type)
|
|
23
|
+
if message_or_key.is_a?(Symbol)
|
|
24
|
+
i18n_options.merge!(type)
|
|
25
|
+
translate(message_or_key, action_class, i18n_options)
|
|
26
|
+
else
|
|
27
|
+
message_or_key
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def translate(key, action_class, options = {})
|
|
32
|
+
type = options.delete(:type)
|
|
33
|
+
|
|
34
|
+
scope = i18n_scope_from_class(action_class, type)
|
|
35
|
+
options[:scope] = scope
|
|
36
|
+
|
|
37
|
+
I18n.t(key, options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def i18n_scope_from_class(action_class, type)
|
|
41
|
+
"#{action_class.name.underscore}.light_service.#{type.to_s.pluralize}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module FunctionalLightService
|
|
2
|
+
module Organizer
|
|
3
|
+
class Iterate
|
|
4
|
+
extend ScopedReducable
|
|
5
|
+
|
|
6
|
+
def self.run(organizer, collection_key, steps)
|
|
7
|
+
->(ctx) do
|
|
8
|
+
return ctx if ctx.stop_processing?
|
|
9
|
+
|
|
10
|
+
collection = ctx[collection_key]
|
|
11
|
+
item_key = collection_key.to_s.singularize.to_sym
|
|
12
|
+
collection.each do |item|
|
|
13
|
+
ctx[item_key] = item
|
|
14
|
+
ctx = scoped_reduce(organizer, ctx, steps)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
ctx
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|