dry-monads 1.3.0 → 1.3.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +10 -39
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/ci.yml +52 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +56 -0
- data/.rspec +1 -0
- data/.rubocop.yml +101 -0
- data/CHANGELOG.md +130 -62
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +4 -4
- data/Gemfile +7 -8
- data/Gemfile.devtools +14 -0
- data/LICENSE +17 -17
- data/README.md +17 -38
- data/Rakefile +2 -0
- data/bin/.gitkeep +0 -0
- data/bin/console +1 -0
- data/docsite/source/case-equality.html.md +42 -0
- data/docsite/source/do-notation.html.md +207 -0
- data/docsite/source/getting-started.html.md +142 -0
- data/docsite/source/index.html.md +179 -0
- data/docsite/source/list.html.md +87 -0
- data/docsite/source/maybe.html.md +146 -0
- data/docsite/source/pattern-matching.html.md +68 -0
- data/docsite/source/result.html.md +190 -0
- data/docsite/source/task.html.md +126 -0
- data/docsite/source/tracing-failures.html.md +32 -0
- data/docsite/source/try.html.md +76 -0
- data/docsite/source/unit.html.md +36 -0
- data/docsite/source/validated.html.md +88 -0
- data/dry-monads.gemspec +7 -5
- data/lib/dry-monads.rb +2 -0
- data/lib/dry/monads.rb +3 -0
- data/lib/dry/monads/all.rb +2 -0
- data/lib/dry/monads/{undefined.rb → constants.rb} +3 -1
- data/lib/dry/monads/conversion_stubs.rb +2 -0
- data/lib/dry/monads/curry.rb +2 -0
- data/lib/dry/monads/do.rb +7 -2
- data/lib/dry/monads/do/all.rb +5 -2
- data/lib/dry/monads/do/mixin.rb +4 -3
- data/lib/dry/monads/either.rb +2 -0
- data/lib/dry/monads/errors.rb +4 -2
- data/lib/dry/monads/lazy.rb +4 -2
- data/lib/dry/monads/list.rb +10 -9
- data/lib/dry/monads/maybe.rb +9 -4
- data/lib/dry/monads/registry.rb +5 -2
- data/lib/dry/monads/result.rb +5 -4
- data/lib/dry/monads/result/fixed.rb +4 -2
- data/lib/dry/monads/right_biased.rb +55 -8
- data/lib/dry/monads/task.rb +6 -3
- data/lib/dry/monads/transformer.rb +2 -0
- data/lib/dry/monads/traverse.rb +2 -0
- data/lib/dry/monads/try.rb +6 -1
- data/lib/dry/monads/validated.rb +12 -9
- data/lib/dry/monads/version.rb +3 -1
- data/lib/json/add/dry/monads/maybe.rb +1 -0
- data/project.yml +2 -0
- metadata +45 -22
- data/.travis.yml +0 -41
@@ -0,0 +1,36 @@
|
|
1
|
+
---
|
2
|
+
title: Unit
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-monads
|
5
|
+
---
|
6
|
+
|
7
|
+
Some constructors do not require you to pass a value. As a default they use `Unit`, a special singleton value:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
extend Dry::Monads[:result]
|
11
|
+
|
12
|
+
Success().value! # => Unit
|
13
|
+
```
|
14
|
+
|
15
|
+
`Unit` doesn't have any special properties or methods, it's similar to `nil` except for it is not i.e. `if Unit` passes.
|
16
|
+
|
17
|
+
`Unit` is usually excluded from the output:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
extend Dry::Monads[:result]
|
21
|
+
|
22
|
+
# Outputs as "Success()" but technically it's "Success(Unit)"
|
23
|
+
Success()
|
24
|
+
```
|
25
|
+
|
26
|
+
### Discarding values
|
27
|
+
|
28
|
+
When the outcome of an operation is not a caller's concern, call `.discard`, it will map the wrapped value to `Unit`:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
extend Dry::Monads[:result]
|
32
|
+
|
33
|
+
result = create_user # returns Success(#<User...>) or Failure(...)
|
34
|
+
|
35
|
+
result.discard # => Maps Success(#<User ...>) to Success() but lefts Failure(...) intact
|
36
|
+
```
|
@@ -0,0 +1,88 @@
|
|
1
|
+
---
|
2
|
+
title: Validated
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-monads
|
5
|
+
---
|
6
|
+
|
7
|
+
Suppose you've got a form to validate. If you are using `Result` combined with `Do` your code might look like this:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require 'dry/monads'
|
11
|
+
|
12
|
+
class CreateAccount
|
13
|
+
include Dry::Monads[:result, :do]
|
14
|
+
|
15
|
+
def call(form)
|
16
|
+
name = yield validate_name(form)
|
17
|
+
email = yield validate_email(form)
|
18
|
+
password = yield validate_password(form)
|
19
|
+
|
20
|
+
user = repo.create_user(
|
21
|
+
name: name,
|
22
|
+
email: email,
|
23
|
+
password: password
|
24
|
+
)
|
25
|
+
|
26
|
+
Success(user)
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_name(form)
|
30
|
+
# Success(name) or Failure(:invalid_name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_email(form)
|
34
|
+
# Success(email) or Failure(:invalid_email)
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_password(form)
|
38
|
+
# Success(password) or Failure(:invalid_password)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
If any of the validation steps fails the user will see an error. The problem is if `name` is not valid the user won't see errors about invalid `email` and `password`, if any. `Validated` circumvents this particular problem.
|
44
|
+
|
45
|
+
`Validated` is actually not a monad but an applicative functor. This means you can't call `bind` on it. Instead, it can accumulate values in combination with `List`:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
require 'dry/monads'
|
49
|
+
|
50
|
+
class CreateAccount
|
51
|
+
include Dry::Monads[:list, :result, :validated, :do]
|
52
|
+
|
53
|
+
def call(form)
|
54
|
+
name, email, password = yield List::Validated[
|
55
|
+
validate_name(form),
|
56
|
+
validate_email(form),
|
57
|
+
validate_password(form)
|
58
|
+
].traverse.to_result
|
59
|
+
|
60
|
+
user = repo.create_user(
|
61
|
+
name: name,
|
62
|
+
email: email,
|
63
|
+
password: password
|
64
|
+
)
|
65
|
+
|
66
|
+
Success(user)
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_name(form)
|
70
|
+
# Valid(name) or Invalid(:invalid_name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def validate_email(form)
|
74
|
+
# Valid(email) or Invalid(:invalid_email)
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_password(form)
|
78
|
+
# Valid(password) or Invalid(:invalid_password)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
Here all validations will be processed at once, if any of them fails the result will be converted to a `Failure` wrapping the `List` of errors:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
create_account.(form)
|
87
|
+
# => Failure(List[:invalid_name, :invalid_email])
|
88
|
+
```
|
data/dry-monads.gemspec
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
2
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
5
|
require 'dry/monads/version'
|
4
6
|
|
@@ -25,13 +27,13 @@ Gem::Specification.new do |spec|
|
|
25
27
|
spec.bindir = 'exe'
|
26
28
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
29
|
spec.require_paths = ['lib']
|
28
|
-
spec.required_ruby_version =
|
29
|
-
spec.add_dependency 'dry-equalizer'
|
30
|
-
spec.add_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
|
30
|
+
spec.required_ruby_version = '>= 2.4.0'
|
31
31
|
spec.add_dependency 'concurrent-ruby', '~> 1.0'
|
32
|
+
spec.add_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
|
33
|
+
spec.add_dependency 'dry-equalizer'
|
32
34
|
|
33
35
|
spec.add_development_dependency 'bundler'
|
36
|
+
spec.add_development_dependency 'dry-types', '>= 0.12'
|
34
37
|
spec.add_development_dependency 'rake'
|
35
38
|
spec.add_development_dependency 'rspec'
|
36
|
-
spec.add_development_dependency 'dry-types', '>= 0.12'
|
37
39
|
end
|
data/lib/dry-monads.rb
CHANGED
data/lib/dry/monads.rb
CHANGED
data/lib/dry/monads/all.rb
CHANGED
data/lib/dry/monads/curry.rb
CHANGED
data/lib/dry/monads/do.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/monads/list'
|
2
4
|
require 'dry/monads/do/mixin'
|
5
|
+
require 'dry/monads/constants'
|
3
6
|
|
4
7
|
module Dry
|
5
8
|
module Monads
|
@@ -9,6 +12,8 @@ module Dry
|
|
9
12
|
module Do
|
10
13
|
extend Mixin
|
11
14
|
|
15
|
+
DELEGATE = ::RUBY_VERSION < '2.7' ? '*' : '...'
|
16
|
+
|
12
17
|
# @api private
|
13
18
|
class Halt < StandardError
|
14
19
|
# @api private
|
@@ -102,7 +107,7 @@ module Dry
|
|
102
107
|
# @api private
|
103
108
|
def wrap_method(target, method_name)
|
104
109
|
target.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
105
|
-
def #{method_name}(
|
110
|
+
def #{method_name}(#{DELEGATE})
|
106
111
|
if block_given?
|
107
112
|
super
|
108
113
|
else
|
@@ -130,7 +135,7 @@ module Dry
|
|
130
135
|
|
131
136
|
# @api private
|
132
137
|
def halt(result)
|
133
|
-
raise Halt.new(result)
|
138
|
+
raise Halt.new(result), EMPTY_STRING, []
|
134
139
|
end
|
135
140
|
end
|
136
141
|
end
|
data/lib/dry/monads/do/all.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/monads/do'
|
2
4
|
|
3
5
|
module Dry
|
@@ -56,7 +58,7 @@ module Dry
|
|
56
58
|
#
|
57
59
|
module All
|
58
60
|
# @private
|
59
|
-
class MethodTracker < Module
|
61
|
+
class MethodTracker < ::Module
|
60
62
|
attr_reader :wrappers
|
61
63
|
|
62
64
|
def initialize(wrappers)
|
@@ -80,7 +82,7 @@ module Dry
|
|
80
82
|
|
81
83
|
def included(base)
|
82
84
|
super
|
83
|
-
|
85
|
+
All.included(base)
|
84
86
|
end
|
85
87
|
end
|
86
88
|
end
|
@@ -119,6 +121,7 @@ module Dry
|
|
119
121
|
super(method)
|
120
122
|
|
121
123
|
next if method.equal?(:singleton_method_added)
|
124
|
+
|
122
125
|
Do.wrap_method(wrapper, method)
|
123
126
|
end
|
124
127
|
object.singleton_class.instance_methods(false).each do |m|
|
data/lib/dry/monads/do/mixin.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Monads
|
3
5
|
module Do
|
@@ -43,14 +45,13 @@ module Dry
|
|
43
45
|
# @api public
|
44
46
|
def bind(monads)
|
45
47
|
monads = Do.coerce_to_monad(Array(monads))
|
46
|
-
unwrapped = monads.map
|
48
|
+
unwrapped = monads.map do |result|
|
47
49
|
monad = result.to_monad
|
48
50
|
monad.or { Do.halt(monad) }.value!
|
49
|
-
|
51
|
+
end
|
50
52
|
monads.size == 1 ? unwrapped[0] : unwrapped
|
51
53
|
end
|
52
54
|
end
|
53
55
|
end
|
54
56
|
end
|
55
57
|
end
|
56
|
-
|
data/lib/dry/monads/either.rb
CHANGED
data/lib/dry/monads/errors.rb
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Monads
|
3
5
|
# An unsuccessful result of extracting a value from a monad.
|
4
6
|
class UnwrapError < StandardError
|
5
7
|
def initialize(ctx)
|
6
|
-
super("value! was called on #{
|
8
|
+
super("value! was called on #{ctx.inspect}")
|
7
9
|
end
|
8
10
|
end
|
9
11
|
|
10
12
|
# An error thrown on returning a Failure of unknown type.
|
11
13
|
class InvalidFailureTypeError < StandardError
|
12
14
|
def initialize(failure)
|
13
|
-
super("Cannot create Failure from #{
|
15
|
+
super("Cannot create Failure from #{failure.inspect}, it doesn't meet the constraints")
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
data/lib/dry/monads/lazy.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'concurrent/promise'
|
2
4
|
|
3
5
|
require 'dry/monads/task'
|
@@ -45,12 +47,12 @@ module Dry
|
|
45
47
|
when :fulfilled
|
46
48
|
value!.inspect
|
47
49
|
when :rejected
|
48
|
-
"!#{
|
50
|
+
"!#{promise.reason.inspect}"
|
49
51
|
else
|
50
52
|
'?'
|
51
53
|
end
|
52
54
|
|
53
|
-
"Lazy(#{
|
55
|
+
"Lazy(#{state})"
|
54
56
|
end
|
55
57
|
alias_method :inspect, :to_s
|
56
58
|
|
data/lib/dry/monads/list.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/equalizer'
|
2
4
|
|
3
5
|
require 'dry/monads/maybe'
|
@@ -163,8 +165,8 @@ module Dry
|
|
163
165
|
#
|
164
166
|
# @return [String]
|
165
167
|
def inspect
|
166
|
-
type_ann = typed? ? "<#{
|
167
|
-
"List#{
|
168
|
+
type_ann = typed? ? "<#{type.name.split('::').last}>" : ''
|
169
|
+
"List#{type_ann}#{value.inspect}"
|
168
170
|
end
|
169
171
|
alias_method :to_s, :inspect
|
170
172
|
|
@@ -263,10 +265,10 @@ module Dry
|
|
263
265
|
def typed(type = nil)
|
264
266
|
if type.nil?
|
265
267
|
if size.zero?
|
266
|
-
raise ArgumentError,
|
268
|
+
raise ArgumentError, 'Cannot infer a monad for an empty list'
|
267
269
|
else
|
268
270
|
self.class.warn(
|
269
|
-
|
271
|
+
'Automatic monad inference is deprecated, pass a type explicitly '\
|
270
272
|
"or use a predefined constant, e.g. List::Result\n"\
|
271
273
|
"#{caller.find { |l| l !~ %r{(lib/dry/monads)|(gems)} }}"
|
272
274
|
)
|
@@ -296,16 +298,16 @@ module Dry
|
|
296
298
|
# @return [Monad] Result is a monadic value
|
297
299
|
def traverse(proc = nil, &block)
|
298
300
|
unless typed?
|
299
|
-
raise StandardError,
|
301
|
+
raise StandardError, 'Cannot traverse an untyped list'
|
300
302
|
end
|
301
303
|
|
302
304
|
cons = type.pure { |list, i| list + List.pure(i) }
|
303
305
|
with = proc || block || Traverse[type]
|
304
306
|
|
305
307
|
foldl(type.pure(EMPTY)) do |acc, el|
|
306
|
-
cons
|
307
|
-
apply(acc)
|
308
|
-
apply { with.(el) }
|
308
|
+
cons
|
309
|
+
.apply(acc)
|
310
|
+
.apply { with.(el) }
|
309
311
|
end
|
310
312
|
end
|
311
313
|
|
@@ -433,7 +435,6 @@ module Dry
|
|
433
435
|
#
|
434
436
|
# @api public
|
435
437
|
module Mixin
|
436
|
-
|
437
438
|
# @see Dry::Monads::List
|
438
439
|
List = List
|
439
440
|
|
data/lib/dry/monads/maybe.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/equalizer'
|
2
|
-
require 'dry/core/constants'
|
3
4
|
require 'dry/core/deprecations'
|
4
5
|
|
5
6
|
require 'dry/monads/right_biased'
|
6
7
|
require 'dry/monads/transformer'
|
7
8
|
require 'dry/monads/unit'
|
8
|
-
require 'dry/monads/
|
9
|
+
require 'dry/monads/constants'
|
9
10
|
|
10
11
|
module Dry
|
11
12
|
module Monads
|
@@ -16,7 +17,7 @@ module Dry
|
|
16
17
|
include Transformer
|
17
18
|
|
18
19
|
class << self
|
19
|
-
extend
|
20
|
+
extend Core::Deprecations[:'dry-monads']
|
20
21
|
|
21
22
|
# Wraps the given value with into a Maybe object.
|
22
23
|
#
|
@@ -105,6 +106,7 @@ module Dry
|
|
105
106
|
|
106
107
|
def initialize(value = Undefined)
|
107
108
|
raise ArgumentError, 'nil cannot be some' if value.nil?
|
109
|
+
|
108
110
|
@value = Undefined.default(value, Unit)
|
109
111
|
end
|
110
112
|
|
@@ -139,7 +141,6 @@ module Dry
|
|
139
141
|
# @api public
|
140
142
|
class None < Maybe
|
141
143
|
include RightBiased::Left
|
142
|
-
include Core::Constants
|
143
144
|
|
144
145
|
@instance = new.freeze
|
145
146
|
singleton_class.send(:attr_reader, :instance)
|
@@ -163,6 +164,10 @@ module Dry
|
|
163
164
|
@trace = trace
|
164
165
|
end
|
165
166
|
|
167
|
+
# @!method maybe(*args, &block)
|
168
|
+
# Alias of fmap, returns self back
|
169
|
+
alias_method :maybe, :fmap
|
170
|
+
|
166
171
|
# If a block is given passes internal value to it and returns the result,
|
167
172
|
# otherwise simply returns the parameter val.
|
168
173
|
#
|
data/lib/dry/monads/registry.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'concurrent/map'
|
2
4
|
|
3
5
|
module Dry
|
@@ -36,8 +38,9 @@ module Dry
|
|
36
38
|
# @private
|
37
39
|
def register_mixin(name, mod)
|
38
40
|
if registry.key?(name)
|
39
|
-
raise ArgumentError, "#{
|
41
|
+
raise ArgumentError, "#{name.inspect} is already registered"
|
40
42
|
end
|
43
|
+
|
41
44
|
self.registry = registry.merge(name => mod)
|
42
45
|
end
|
43
46
|
|
@@ -49,7 +52,7 @@ module Dry
|
|
49
52
|
# @private
|
50
53
|
def load_monad(name)
|
51
54
|
path = @paths.fetch(name) {
|
52
|
-
raise ArgumentError, "#{
|
55
|
+
raise ArgumentError, "#{name.inspect} is not a known monad"
|
53
56
|
}
|
54
57
|
Array(path).each { |p| require p }
|
55
58
|
end
|