mona-result 0.1.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/.rubocop.yml +20 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +82 -0
- data/LICENSE.txt +21 -0
- data/README.md +33 -0
- data/Rakefile +20 -0
- data/Steepfile +24 -0
- data/lib/mona/result/action.rb +84 -0
- data/lib/mona/result/dict.rb +57 -0
- data/lib/mona/result/err.rb +48 -0
- data/lib/mona/result/error.rb +18 -0
- data/lib/mona/result/match.rb +53 -0
- data/lib/mona/result/ok.rb +42 -0
- data/lib/mona/result/sequence.rb +33 -0
- data/lib/mona/result/version.rb +7 -0
- data/lib/mona/result.rb +37 -0
- data/sig/mona/result.rbs +177 -0
- metadata +65 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 170acae8e3dab4e7c9dec3b88b17cb8668a678290aafa5eb363da54261a5eab9
|
|
4
|
+
data.tar.gz: 0b327151acf8487019f87f5b859b1f940d766e67c84479c29ef1a70a4d26aeb1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 45ca05476a2621a4d6e76a190b37852880b9c69d43470ec77ed5dc9dabf7ec9f6cbe9771777adc369f69ce421e8297f2e5c5ee66df8bf43b12c62d1e0ce3c53d
|
|
7
|
+
data.tar.gz: 59684cbea28ce2392a1755edb70c9756786b1bf730900e5aaf0829a3a4a068a0742c6db907e3720d4533ff17ff57a2c7b5a72e7dd5d1364c4acfd4f6a4bafd80
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.1
|
|
3
|
+
NewCops: enable
|
|
4
|
+
|
|
5
|
+
Style/StringLiterals:
|
|
6
|
+
Enabled: true
|
|
7
|
+
EnforcedStyle: double_quotes
|
|
8
|
+
|
|
9
|
+
Style/StringLiteralsInInterpolation:
|
|
10
|
+
Enabled: true
|
|
11
|
+
EnforcedStyle: double_quotes
|
|
12
|
+
|
|
13
|
+
Style/NumberedParametersLimit:
|
|
14
|
+
Max: 2
|
|
15
|
+
|
|
16
|
+
Naming/MethodParameterName:
|
|
17
|
+
MinNameLength: 2
|
|
18
|
+
|
|
19
|
+
Layout/LineLength:
|
|
20
|
+
Max: 120
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
mona-result (0.1.0)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
activesupport (7.0.3.1)
|
|
10
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
11
|
+
i18n (>= 1.6, < 2)
|
|
12
|
+
minitest (>= 5.1)
|
|
13
|
+
tzinfo (~> 2.0)
|
|
14
|
+
ast (2.4.2)
|
|
15
|
+
concurrent-ruby (1.1.10)
|
|
16
|
+
ffi (1.15.5)
|
|
17
|
+
i18n (1.12.0)
|
|
18
|
+
concurrent-ruby (~> 1.0)
|
|
19
|
+
json (2.6.2)
|
|
20
|
+
language_server-protocol (3.16.0.3)
|
|
21
|
+
listen (3.7.1)
|
|
22
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
23
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
|
24
|
+
minitest (5.16.2)
|
|
25
|
+
parallel (1.22.1)
|
|
26
|
+
parser (3.1.2.1)
|
|
27
|
+
ast (~> 2.4.1)
|
|
28
|
+
rainbow (3.1.1)
|
|
29
|
+
rake (13.0.6)
|
|
30
|
+
rb-fsevent (0.11.1)
|
|
31
|
+
rb-inotify (0.10.1)
|
|
32
|
+
ffi (~> 1.0)
|
|
33
|
+
rbs (2.6.0)
|
|
34
|
+
regexp_parser (2.5.0)
|
|
35
|
+
rexml (3.2.5)
|
|
36
|
+
rubocop (1.33.0)
|
|
37
|
+
json (~> 2.3)
|
|
38
|
+
parallel (~> 1.10)
|
|
39
|
+
parser (>= 3.1.0.0)
|
|
40
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
41
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
42
|
+
rexml (>= 3.2.5, < 4.0)
|
|
43
|
+
rubocop-ast (>= 1.19.1, < 2.0)
|
|
44
|
+
ruby-progressbar (~> 1.7)
|
|
45
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
|
46
|
+
rubocop-ast (1.21.0)
|
|
47
|
+
parser (>= 3.1.1.0)
|
|
48
|
+
rubocop-minitest (0.21.0)
|
|
49
|
+
rubocop (>= 0.90, < 2.0)
|
|
50
|
+
rubocop-rake (0.6.0)
|
|
51
|
+
rubocop (~> 1.0)
|
|
52
|
+
ruby-progressbar (1.11.0)
|
|
53
|
+
steep (1.1.1)
|
|
54
|
+
activesupport (>= 5.1)
|
|
55
|
+
language_server-protocol (>= 3.15, < 4.0)
|
|
56
|
+
listen (~> 3.0)
|
|
57
|
+
parallel (>= 1.0.0)
|
|
58
|
+
parser (>= 3.1)
|
|
59
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
60
|
+
rbs (>= 2.3.2)
|
|
61
|
+
terminal-table (>= 2, < 4)
|
|
62
|
+
terminal-table (3.0.2)
|
|
63
|
+
unicode-display_width (>= 1.1.1, < 3)
|
|
64
|
+
tzinfo (2.0.5)
|
|
65
|
+
concurrent-ruby (~> 1.0)
|
|
66
|
+
unicode-display_width (2.2.0)
|
|
67
|
+
|
|
68
|
+
PLATFORMS
|
|
69
|
+
x86_64-darwin-21
|
|
70
|
+
x86_64-linux
|
|
71
|
+
|
|
72
|
+
DEPENDENCIES
|
|
73
|
+
minitest
|
|
74
|
+
mona-result!
|
|
75
|
+
rake
|
|
76
|
+
rubocop
|
|
77
|
+
rubocop-minitest
|
|
78
|
+
rubocop-rake
|
|
79
|
+
steep
|
|
80
|
+
|
|
81
|
+
BUNDLED WITH
|
|
82
|
+
2.3.19
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Ian White
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Mona::Result
|
|
2
|
+
|
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/mona/result`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
4
|
+
|
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
10
|
+
|
|
11
|
+
$ bundle add mona-result
|
|
12
|
+
|
|
13
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
14
|
+
|
|
15
|
+
$ gem install mona-result
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
TODO: Write usage instructions here
|
|
20
|
+
|
|
21
|
+
## Development
|
|
22
|
+
|
|
23
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
24
|
+
|
|
25
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
26
|
+
|
|
27
|
+
## Contributing
|
|
28
|
+
|
|
29
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/mona-result.
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
task :steep do
|
|
7
|
+
system "steep check" or raise "Steep type checking failed"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
|
11
|
+
t.libs << "test"
|
|
12
|
+
t.libs << "lib"
|
|
13
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require "rubocop/rake_task"
|
|
17
|
+
|
|
18
|
+
RuboCop::RakeTask.new
|
|
19
|
+
|
|
20
|
+
task default: %i[steep test rubocop]
|
data/Steepfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
D = Steep::Diagnostic
|
|
4
|
+
|
|
5
|
+
target :lib do
|
|
6
|
+
signature "sig"
|
|
7
|
+
|
|
8
|
+
check "lib"
|
|
9
|
+
|
|
10
|
+
configure_code_diagnostics(D::Ruby.default)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
target :test do
|
|
14
|
+
signature "sig"
|
|
15
|
+
|
|
16
|
+
check "test"
|
|
17
|
+
|
|
18
|
+
library "minitest", "mutex_m"
|
|
19
|
+
|
|
20
|
+
configure_code_diagnostics(D::Ruby.strict)
|
|
21
|
+
configure_code_diagnostics do |h|
|
|
22
|
+
h[D::Ruby::UnsupportedSyntax] = :information
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mona
|
|
4
|
+
module Result
|
|
5
|
+
# mixin for objects that return Result::Dict results
|
|
6
|
+
#
|
|
7
|
+
# define #perform to do the work, use #set to add results to the Result::Dict, the first failure will abort the
|
|
8
|
+
# rest of the #perform method and the result will be returned. This works like monadic 'do' notation.
|
|
9
|
+
#
|
|
10
|
+
# After a result is set at a key, it can be accessed via its key name as a method.
|
|
11
|
+
#
|
|
12
|
+
# use the Action with #call
|
|
13
|
+
#
|
|
14
|
+
# example:
|
|
15
|
+
#
|
|
16
|
+
# class UpdateUser
|
|
17
|
+
# inlcude Mona::Result::Action
|
|
18
|
+
# include Auditing # for example, adds #audit(model, input) method
|
|
19
|
+
#
|
|
20
|
+
# def perform(user_id, attributes)
|
|
21
|
+
# set :user, UserRepo.find(user_id) # if find is Err, the block exits with failure of :user
|
|
22
|
+
# set :input, UserInput.valid(attributes) # likewise if valid is Err, the block exits
|
|
23
|
+
# set [:user, :input], UserRepo.update(user.id, input) # note that #input and #user are available methods
|
|
24
|
+
# # if update is Err it is set on the :input key
|
|
25
|
+
# audit(user, input) # this only runs if all of the above set successful results
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# UpdateUser.new.call(user_id, attributes) # => Result
|
|
30
|
+
module Action
|
|
31
|
+
# You can create an Ephemeral Action as follows:
|
|
32
|
+
#
|
|
33
|
+
# Example:
|
|
34
|
+
# compute = Mona::Result::Action::Ephemeral.new do |x, y|
|
|
35
|
+
# set :numerator, x
|
|
36
|
+
# puts "set numerator: #{numerator}"
|
|
37
|
+
# set :denominator, y.zero? ? Result.failure(y, :zero) : y
|
|
38
|
+
# puts "set denominator: #{denominator}"
|
|
39
|
+
# set :answer, numerator / denominator
|
|
40
|
+
# puts "answer: #{answer}"
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# > compute.call(10,2)
|
|
44
|
+
# set numerator: 10
|
|
45
|
+
# set denominator: 2
|
|
46
|
+
# answer: 5
|
|
47
|
+
# => #<Result success: {:numerator=>10, :denominator=>2, :answer=>5}>
|
|
48
|
+
#
|
|
49
|
+
# > compute.call(10,0)
|
|
50
|
+
# set numerator: 10
|
|
51
|
+
# => #<Result failure: {:numerator=>10, :denominator=>0}, error: {:error=>:zero, :on=>:denominator}>
|
|
52
|
+
#
|
|
53
|
+
# Mona::Result.action is a shortcut for this
|
|
54
|
+
class Ephemeral
|
|
55
|
+
include Action
|
|
56
|
+
|
|
57
|
+
def initialize(&perform)
|
|
58
|
+
@perform = perform
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def perform(*args, **kwargs) = instance_exec(*args, **kwargs, &@perform)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def call(*args, **kwargs)
|
|
65
|
+
@sequence = Sequence.new
|
|
66
|
+
@sequence.call { |_sequence| perform(*args, **kwargs) }
|
|
67
|
+
ensure
|
|
68
|
+
remove_instance_variable :@sequence
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def set(key, value) = @sequence.set(key, value)
|
|
74
|
+
|
|
75
|
+
def respond_to_missing?(key, _include_private = false) = @sequence.key?(key)
|
|
76
|
+
|
|
77
|
+
def method_missing(key, *args)
|
|
78
|
+
return @sequence.fetch(key) if args.empty? && @sequence.key?(key)
|
|
79
|
+
|
|
80
|
+
raise NoMethodError, "no method `#{key}' for #{self}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mona
|
|
4
|
+
module Result
|
|
5
|
+
# Represents a dictionary of results, it is successful if all results are successful, and a failure if one
|
|
6
|
+
# is a failure. A Result::Dict can only contain one failure.
|
|
7
|
+
class Dict
|
|
8
|
+
# factory method that returns {Dict::Success} or {Dict::Failure}
|
|
9
|
+
def self.new(initial = {}, &block)
|
|
10
|
+
result = Dict::EMPTY
|
|
11
|
+
initial.each { |k, v| result = result.set(k, v) }
|
|
12
|
+
result = result.sequence(&block) if block
|
|
13
|
+
result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Dict read interface
|
|
17
|
+
module ReadInterface
|
|
18
|
+
def [](key) = to_h[key]
|
|
19
|
+
|
|
20
|
+
def key?(key) = to_h.key?(key)
|
|
21
|
+
|
|
22
|
+
def fetch(key) = to_h.fetch(key)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# OK dict result
|
|
26
|
+
class OK < Result::OK
|
|
27
|
+
include ReadInterface
|
|
28
|
+
|
|
29
|
+
def set(key, to_result)
|
|
30
|
+
key, failure_key = key if key.is_a?(Array)
|
|
31
|
+
failure_key ||= key
|
|
32
|
+
|
|
33
|
+
Result[to_result].either \
|
|
34
|
+
->(value) { OK.new to_h.merge(key => value) },
|
|
35
|
+
->(failure, reason, **m) { Err.new to_h.merge(failure_key => failure), reason, **m, key: failure_key }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def sequence(&) = Sequence.new(self).call(&)
|
|
39
|
+
|
|
40
|
+
def to_h = @value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Err dict result
|
|
44
|
+
class Err < Result::Err
|
|
45
|
+
include ReadInterface
|
|
46
|
+
|
|
47
|
+
def set(_key, _val) = raise(Error, "cannot #set on #{self}")
|
|
48
|
+
|
|
49
|
+
def sequence(&) = self
|
|
50
|
+
|
|
51
|
+
def to_h = @failure
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
EMPTY = OK.new({}).freeze
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mona
|
|
4
|
+
module Result
|
|
5
|
+
# A Error or failure result, with optional reason, and metadata
|
|
6
|
+
class Err
|
|
7
|
+
def initialize(failure, reason = nil, **meta)
|
|
8
|
+
raise ArgumentError, "meta can't contain :reason key" if meta.key?(:reason)
|
|
9
|
+
|
|
10
|
+
@failure = failure.freeze
|
|
11
|
+
@reason = reason
|
|
12
|
+
@meta = meta
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :failure, :reason, :meta
|
|
16
|
+
|
|
17
|
+
def ok? = false
|
|
18
|
+
|
|
19
|
+
def err? = true
|
|
20
|
+
|
|
21
|
+
def value_or(&) = yield
|
|
22
|
+
|
|
23
|
+
def ok(&) = nil
|
|
24
|
+
|
|
25
|
+
def err(&) = yield @failure, @reason, **@meta
|
|
26
|
+
|
|
27
|
+
def either(_ok, err) = err.call(@failure, @reason, **@meta)
|
|
28
|
+
|
|
29
|
+
def and_then(&) = self
|
|
30
|
+
|
|
31
|
+
def and_tap(&) = self
|
|
32
|
+
|
|
33
|
+
def or_else(&) = Result[yield @failure, @reason, **@meta]
|
|
34
|
+
|
|
35
|
+
def deconstruct = [:err, @failure, @reason, @meta]
|
|
36
|
+
|
|
37
|
+
def deconstruct_keys(_keys = nil) = { err: @failure, reason: @reason, **@meta }
|
|
38
|
+
|
|
39
|
+
def to_result = self
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
"#<Err #{@failure.inspect} #{{ reason: @reason, **@meta }.map{ "#{_1}: #{_2.inspect}" }.join(", ")}>"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
alias inspect to_s
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mona
|
|
4
|
+
module Result
|
|
5
|
+
# Base error for Result
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# raised when Result::Match does not match the result
|
|
9
|
+
class NoMatchError < Error
|
|
10
|
+
attr_reader :result
|
|
11
|
+
|
|
12
|
+
def initialize(result)
|
|
13
|
+
@result = result
|
|
14
|
+
super("No match found for #{result}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mona
|
|
4
|
+
module Result
|
|
5
|
+
# Use Match.call to respond to result success or failure
|
|
6
|
+
#
|
|
7
|
+
# Result::Match.call(result) do |r|
|
|
8
|
+
# r.ok { |value| ... }
|
|
9
|
+
# r.err(error, **meta) { |failure, reason, **meta| ... }
|
|
10
|
+
# r.err(error) { |failure, reason, **meta| ... }
|
|
11
|
+
# r.err { |failure, reason, **meta| ... }
|
|
12
|
+
# end
|
|
13
|
+
class Match
|
|
14
|
+
def self.call(result, &) = new(result).call(&)
|
|
15
|
+
|
|
16
|
+
def initialize(result)
|
|
17
|
+
@throw = Object.new
|
|
18
|
+
@result = result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
catch @throw do
|
|
23
|
+
yield self
|
|
24
|
+
raise NoMatchError, @result
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ok
|
|
29
|
+
@result.and_then do |value|
|
|
30
|
+
throw @throw, yield(value)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def err(match_reason = nil, **match_meta)
|
|
35
|
+
@result.or_else do |failure, reason, **meta|
|
|
36
|
+
if match_reason?(match_reason, reason) && match_meta?(match_meta, meta)
|
|
37
|
+
throw @throw, yield(failure, reason, **meta)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def match_reason?(match_reason, reason)
|
|
45
|
+
match_reason.nil? || match_reason === reason # rubocop:disable Style/CaseEquality
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def match_meta?(match_meta, meta)
|
|
49
|
+
match_meta.all? { |key, val| val.nil? || val === meta[key] } # rubocop:disable Style/CaseEquality
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mona
|
|
4
|
+
module Result
|
|
5
|
+
# A Successful (OK) result
|
|
6
|
+
class OK
|
|
7
|
+
def initialize(value)
|
|
8
|
+
@value = value.freeze
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :value
|
|
12
|
+
|
|
13
|
+
def value_or(&) = @value
|
|
14
|
+
|
|
15
|
+
def ok? = true
|
|
16
|
+
|
|
17
|
+
def err? = false
|
|
18
|
+
|
|
19
|
+
def ok(&) = yield @value
|
|
20
|
+
|
|
21
|
+
def err(&) = nil
|
|
22
|
+
|
|
23
|
+
def either(ok, _err) = ok.call(@value)
|
|
24
|
+
|
|
25
|
+
def and_then(&) = Result[yield @value]
|
|
26
|
+
|
|
27
|
+
def and_tap(&) = tap { yield @value }
|
|
28
|
+
|
|
29
|
+
def or_else(&) = self
|
|
30
|
+
|
|
31
|
+
def deconstruct = [:ok, @value]
|
|
32
|
+
|
|
33
|
+
def deconstruct_keys(_keys = nil) = { ok: @value }
|
|
34
|
+
|
|
35
|
+
def to_result = self
|
|
36
|
+
|
|
37
|
+
def to_s = "#<OK #{@value.inspect}>"
|
|
38
|
+
|
|
39
|
+
alias inspect to_s
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mona
|
|
4
|
+
module Result
|
|
5
|
+
# Sequence.call { ... } allows monadic 'do' notation for Result::Dict, where #set-ing the first failure skips the
|
|
6
|
+
# remainder of the block and returns the Result::Dict
|
|
7
|
+
class Sequence
|
|
8
|
+
include Dict::ReadInterface
|
|
9
|
+
|
|
10
|
+
def self.call(result = Dict::EMPTY, &) = new(result).call(&)
|
|
11
|
+
|
|
12
|
+
def initialize(result = Dict::EMPTY)
|
|
13
|
+
@result = result
|
|
14
|
+
@throw = Object.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
catch(@throw) do
|
|
19
|
+
yield self
|
|
20
|
+
@result
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def set(key, value)
|
|
25
|
+
@result = @result.set(key, value)
|
|
26
|
+
ensure
|
|
27
|
+
throw @throw, @result if @result.err?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h = @result.to_h
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/mona/result.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "result/version"
|
|
4
|
+
require_relative "result/error"
|
|
5
|
+
|
|
6
|
+
module Mona
|
|
7
|
+
# Monadic result
|
|
8
|
+
#
|
|
9
|
+
# @author Ian White
|
|
10
|
+
# @since 0.1.0
|
|
11
|
+
module Result
|
|
12
|
+
autoload :OK, "mona/result/ok.rb"
|
|
13
|
+
autoload :Err, "mona/result/err.rb"
|
|
14
|
+
autoload :Match, "mona/result/match.rb"
|
|
15
|
+
autoload :Dict, "mona/result/dict.rb"
|
|
16
|
+
autoload :Sequence, "mona/result/sequence.rb"
|
|
17
|
+
autoload :Action, "mona/result/action.rb"
|
|
18
|
+
|
|
19
|
+
def self.[](obj) = obj.respond_to?(:to_result) ? obj.to_result : OK.new(obj)
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def to_result(obj) = Result[obj]
|
|
24
|
+
|
|
25
|
+
def ok(value) = OK.new(value)
|
|
26
|
+
|
|
27
|
+
def err(failure, reason = nil, **meta) = Err.new(failure, reason, **meta)
|
|
28
|
+
|
|
29
|
+
def on_result(result, &) = Match.call(result, &)
|
|
30
|
+
|
|
31
|
+
def on_ok(result, &) = Match.call(result) { _1.ok(&) }
|
|
32
|
+
|
|
33
|
+
def dict(initial = {}, &) = Dict.new(initial, &)
|
|
34
|
+
|
|
35
|
+
def action(&) = Action::Ephemeral.new(&)
|
|
36
|
+
end
|
|
37
|
+
end
|
data/sig/mona/result.rbs
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
module Mona
|
|
2
|
+
module Result
|
|
3
|
+
VERSION: String
|
|
4
|
+
|
|
5
|
+
type dict = Hash[Symbol, untyped]
|
|
6
|
+
type keys = Array[Symbol]
|
|
7
|
+
type result[T] = (_OK[T] | _Err[T])
|
|
8
|
+
type dict_result = result[dict] & _Dict
|
|
9
|
+
type set_key = (Symbol | [Symbol, Symbol])
|
|
10
|
+
|
|
11
|
+
def self.[]: (untyped) -> result[untyped]
|
|
12
|
+
def self?.to_result: (untyped) -> result[untyped]
|
|
13
|
+
def self?.ok: [T] (T) -> _OK[T]
|
|
14
|
+
def self?.err: [T] (T, ?untyped, **untyped) -> _Err[T]
|
|
15
|
+
def self?.dict: (?dict) ?{ (Sequence) -> void } -> dict_result
|
|
16
|
+
def self?.on_result: (result[untyped]) { (Match) -> void } -> untyped
|
|
17
|
+
def self?.on_ok: (result[untyped]) { (untyped) -> void } -> untyped
|
|
18
|
+
def self?.action: () { (*untyped) -> void } -> Action::Ephemeral
|
|
19
|
+
|
|
20
|
+
interface _OK[T]
|
|
21
|
+
def initialize: (T) -> void
|
|
22
|
+
def value: -> T
|
|
23
|
+
def ok?: -> true
|
|
24
|
+
def err?: -> false
|
|
25
|
+
def ok: [R] () { (T) -> R } -> R
|
|
26
|
+
def err: () { (T, untyped, **untyped) -> void } -> nil
|
|
27
|
+
def either: [R] (^(T) -> R, ^(T, untyped, **untyped) -> void) -> R
|
|
28
|
+
def value_or: () { -> void } -> T
|
|
29
|
+
def and_then: () { (T) -> untyped } -> result[untyped]
|
|
30
|
+
def and_tap: () { (T) -> void } -> self
|
|
31
|
+
def or_else: () { (T, ?untyped, **untyped) -> untyped } -> self
|
|
32
|
+
def deconstruct: -> [:ok, T]
|
|
33
|
+
def deconstruct_keys: (?keys?) -> { ok: T }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
interface _Err[T]
|
|
37
|
+
def initialize: (T, ?untyped, **untyped) -> void
|
|
38
|
+
def failure: -> T
|
|
39
|
+
def reason: -> untyped
|
|
40
|
+
def meta: -> dict
|
|
41
|
+
def ok?: -> false
|
|
42
|
+
def err?: -> true
|
|
43
|
+
def ok: () { (untyped) -> void } -> nil
|
|
44
|
+
def err: [R] () { (T, untyped, **untyped) -> R } -> R
|
|
45
|
+
def either: [R] (^(T) -> void, ^(T, untyped, **untyped) -> R) -> R
|
|
46
|
+
def value_or: [R] () { -> R } -> R
|
|
47
|
+
def and_then: () { (T) -> untyped } -> self
|
|
48
|
+
def and_tap: () { (T) -> void } -> self
|
|
49
|
+
def or_else: () { (T, ?untyped, **untyped) -> untyped } -> result[untyped]
|
|
50
|
+
def deconstruct: -> [:err, T, untyped, dict]
|
|
51
|
+
def deconstruct_keys: (?keys?) -> dict
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
interface _ToH
|
|
55
|
+
def to_h: -> dict
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
interface _DictRead
|
|
59
|
+
def key? : (Symbol) -> bool
|
|
60
|
+
def fetch : (Symbol) -> untyped
|
|
61
|
+
def [] : (Symbol) -> untyped
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
interface _Dict
|
|
65
|
+
include _DictRead
|
|
66
|
+
def set: (set_key, untyped) -> dict_result
|
|
67
|
+
def sequence: () { (Sequence) -> void } -> dict_result
|
|
68
|
+
def to_h: -> dict
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
interface _Perform
|
|
72
|
+
def perform: (*untyped, **untyped) -> void
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class Error < StandardError
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class NoMatchError < Error
|
|
79
|
+
attr_reader result: _Err[untyped]
|
|
80
|
+
def initialize: (_Err[untyped]) -> void
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class OK
|
|
84
|
+
include _OK[untyped]
|
|
85
|
+
|
|
86
|
+
@value: untyped
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class Err
|
|
90
|
+
include _Err[untyped]
|
|
91
|
+
|
|
92
|
+
@failure: untyped
|
|
93
|
+
@reason: untyped
|
|
94
|
+
@meta: dict
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class Dict
|
|
98
|
+
EMPTY: dict_result
|
|
99
|
+
|
|
100
|
+
module ReadInterface : _ToH
|
|
101
|
+
include _DictRead
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class OK < Result::OK
|
|
105
|
+
include ReadInterface
|
|
106
|
+
|
|
107
|
+
include _OK[dict]
|
|
108
|
+
include _Dict
|
|
109
|
+
|
|
110
|
+
@value: dict
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class Err < Result::Err
|
|
114
|
+
include ReadInterface
|
|
115
|
+
|
|
116
|
+
include _Err[dict]
|
|
117
|
+
include _Dict
|
|
118
|
+
|
|
119
|
+
@failure: dict
|
|
120
|
+
@reason: untyped
|
|
121
|
+
@meta: dict
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.new: (?dict) ?{ (Sequence) -> void } -> dict_result
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class Match
|
|
128
|
+
@throw: Object
|
|
129
|
+
@result: result[untyped]
|
|
130
|
+
|
|
131
|
+
def self.call: (result[untyped]) { (Match) -> void } -> untyped
|
|
132
|
+
def initialize: (result[untyped]) -> void
|
|
133
|
+
def call: () { (Match) -> void } -> untyped
|
|
134
|
+
def ok: () { (untyped) -> void } -> void
|
|
135
|
+
def err: (?untyped, **untyped) { (untyped, ?untyped, **untyped) -> void } -> void
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def match_reason?: (untyped, untyped) -> bool
|
|
140
|
+
def match_meta?: (untyped, untyped) -> bool
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class Sequence
|
|
144
|
+
include Dict::ReadInterface
|
|
145
|
+
|
|
146
|
+
@throw: Object
|
|
147
|
+
@result: dict_result
|
|
148
|
+
|
|
149
|
+
def self.call: (?dict_result) { (Sequence) -> void } -> dict_result
|
|
150
|
+
def initialize: (?dict_result) -> void
|
|
151
|
+
def call: () { (Sequence) -> void } -> dict_result
|
|
152
|
+
def set: (set_key, untyped) -> void
|
|
153
|
+
def to_h: -> dict
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
module Action : _Perform
|
|
157
|
+
class Ephemeral
|
|
158
|
+
include Action
|
|
159
|
+
|
|
160
|
+
@perform: ^(*untyped, **untyped) -> void
|
|
161
|
+
|
|
162
|
+
def initialize: () { (*untyped, **untyped) -> void } -> void
|
|
163
|
+
def perform: (*untyped, **untyped) -> void
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
@sequence: Sequence
|
|
167
|
+
|
|
168
|
+
def call: (*untyped, **untyped) -> dict_result
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
def set: (set_key, untyped) -> void
|
|
173
|
+
def respond_to_missing?: (Symbol, ?bool) -> bool
|
|
174
|
+
def method_missing: (Symbol, *untyped) -> untyped
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mona-result
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ian White
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2022-08-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Mona::Result provides a result monad, and dict_result monad
|
|
14
|
+
email:
|
|
15
|
+
- ian.w.white@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- ".rubocop.yml"
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- Gemfile
|
|
23
|
+
- Gemfile.lock
|
|
24
|
+
- LICENSE.txt
|
|
25
|
+
- README.md
|
|
26
|
+
- Rakefile
|
|
27
|
+
- Steepfile
|
|
28
|
+
- lib/mona/result.rb
|
|
29
|
+
- lib/mona/result/action.rb
|
|
30
|
+
- lib/mona/result/dict.rb
|
|
31
|
+
- lib/mona/result/err.rb
|
|
32
|
+
- lib/mona/result/error.rb
|
|
33
|
+
- lib/mona/result/match.rb
|
|
34
|
+
- lib/mona/result/ok.rb
|
|
35
|
+
- lib/mona/result/sequence.rb
|
|
36
|
+
- lib/mona/result/version.rb
|
|
37
|
+
- sig/mona/result.rbs
|
|
38
|
+
homepage: https://github.com/mona/mona-result
|
|
39
|
+
licenses:
|
|
40
|
+
- MIT
|
|
41
|
+
metadata:
|
|
42
|
+
rubygems_mfa_required: 'true'
|
|
43
|
+
homepage_uri: https://github.com/mona/mona-result
|
|
44
|
+
source_code_uri: https://github.com/mona/mona-result
|
|
45
|
+
changelog_uri: https://github.com/mona/mona-result/CHANGELOG.md
|
|
46
|
+
post_install_message:
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 3.1.0
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 3.3.7
|
|
62
|
+
signing_key:
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Mona::Result is a result Monad for ruby
|
|
65
|
+
test_files: []
|