dry-monads 1.3.0 → 1.3.5
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 +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
|