mona-result 0.1.2 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74a4d1f256317d8a2675c58dc8c5b52822987f3d7f05473042ce617828c9a78d
4
- data.tar.gz: 278922b075a71ab08d07756e8b46c8cdafa2b11c390e489c9875d482039664d9
3
+ metadata.gz: b53dbabf6036e587ba320df3ffde82737a8d692a354d38ea2093d5c1046eedd8
4
+ data.tar.gz: ffd6a0f736e3407fb4c1cbab320572b399bc4091de6c90644520695162d382d7
5
5
  SHA512:
6
- metadata.gz: 8319264ee1391c3244626af638c33b575156b17db884984214d140069cf3ae1f5f28bcbf20b5e222537d24b895a36b75b65e656d74645999d01a6b16153e52d2
7
- data.tar.gz: 8457dbf1772ac1b536b1026cb1cb241456a2fdb27fbbf22e178937d01816921289e825cb1a0f4172629c305ce23ec4f1c89dcbb16f0d9e71fcaf39d0d85588af
6
+ metadata.gz: cd2b54c044638818a3359239c43adeb631b19254bf94153a99127008c58774d2813ce4aa1363dc92aa107a269585d8fee1739ae98ff53643afdbd446fa5c0549
7
+ data.tar.gz: d9947684fd66ee1394811c1bf2160215699dd51650ed0b91fd04f22415155fd1af48e85ed35a9a68bfc4283a258172fcd0689929613f574a30afb28e0081703f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [0.3.0] - 2022-08-25
2
+
3
+ - Bring utility methods into Mona namespace
4
+ - Reorganisation to make Result and DictResult interface modules
5
+
6
+ ## [0.2.0] - 2022-08-23
7
+
8
+ - Adds Mona::Resultable module which implements the Result interface
9
+ - Simplify and improve RBS
10
+ - Steep checks tests in a stricter fashion
11
+ - Improve docs, tests, and rubocop
12
+
13
+ ## [0.1.3] - 2022-08-09
14
+
15
+ - Fix CHANGELOG rubygems link
16
+
1
17
  ## [0.1.2] - 2022-08-09
2
18
 
3
19
  - Fix gemspec links
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mona-result (0.1.2)
4
+ mona-result (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -21,7 +21,7 @@ GEM
21
21
  listen (3.7.1)
22
22
  rb-fsevent (~> 0.10, >= 0.10.3)
23
23
  rb-inotify (~> 0.9, >= 0.9.10)
24
- minitest (5.16.2)
24
+ minitest (5.16.3)
25
25
  parallel (1.22.1)
26
26
  parser (3.1.2.1)
27
27
  ast (~> 2.4.1)
@@ -33,14 +33,14 @@ GEM
33
33
  rbs (2.6.0)
34
34
  regexp_parser (2.5.0)
35
35
  rexml (3.2.5)
36
- rubocop (1.34.0)
36
+ rubocop (1.35.1)
37
37
  json (~> 2.3)
38
38
  parallel (~> 1.10)
39
39
  parser (>= 3.1.2.1)
40
40
  rainbow (>= 2.2.2, < 4.0)
41
41
  regexp_parser (>= 1.8, < 3.0)
42
42
  rexml (>= 3.2.5, < 4.0)
43
- rubocop-ast (>= 1.20.0, < 2.0)
43
+ rubocop-ast (>= 1.20.1, < 2.0)
44
44
  ruby-progressbar (~> 1.7)
45
45
  unicode-display_width (>= 1.4.0, < 3.0)
46
46
  rubocop-ast (1.21.0)
data/README.md CHANGED
@@ -24,7 +24,10 @@ success = Result.ok(1) # => #<OK 1>
24
24
  success.ok? # => true
25
25
  success.err? # => false
26
26
  success.value # => 1
27
- success.and_tap { puts "it is #{_1}" }.and_then { _1 * 5 }.value_or { :nope }
27
+
28
+ success.and_tap { puts "it is #{_1}" }
29
+ .and_then { _1 * 5 }
30
+ .value_or { :nope }
28
31
  # OUT: it is 1
29
32
  # => 5
30
33
 
@@ -34,7 +37,11 @@ failure.ok? # => false
34
37
  failure.err? # => true
35
38
  failure.failure # => 4
36
39
  failure.reason # => :not_prime
37
- failure.and_tap { puts "it is #{_1}" }.and_then { _1 * 5 }.value_or { :nope } # => :nope
40
+
41
+ failure.and_tap { puts "it is #{_1}" }
42
+ .and_then { _1 * 5 }
43
+ .value_or { :nope }
44
+ # => :nope
38
45
 
39
46
  # Dict with sequence, example using a fictional repository.
40
47
  # #set can take a [Symbol, Symbol] key argument where left is OK key, right is Err key
data/Steepfile CHANGED
@@ -6,18 +6,15 @@ target :lib do
6
6
  signature "sig"
7
7
 
8
8
  check "lib"
9
-
10
- configure_code_diagnostics(D::Ruby.default)
11
9
  end
12
10
 
13
11
  target :test do
14
- signature "sig"
12
+ signature "sig", "test/sig"
15
13
 
16
14
  check "test"
17
15
 
18
16
  library "minitest", "mutex_m"
19
17
 
20
- configure_code_diagnostics(D::Ruby.strict)
21
18
  configure_code_diagnostics do |h|
22
19
  h[D::Ruby::UnsupportedSyntax] = :information
23
20
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mona
4
+ module DictResult
5
+ # Err DictResult
6
+ class Err < Mona::Err
7
+ include DictResult
8
+
9
+ def set(_key, _val) = raise(Error, "cannot #set on #{self}")
10
+
11
+ def to_h = @failure
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mona
4
+ module DictResult
5
+ # OK DictResult
6
+ class OK < Mona::OK
7
+ include DictResult
8
+
9
+ def set(key, val)
10
+ key, failure_key = key if key.is_a?(Array)
11
+ failure_key ||= key
12
+
13
+ # @type var meta: Hash[Symbol, untyped]
14
+ Result[val].either \
15
+ ->(value) { OK.new to_h.merge(key => value) },
16
+ ->(failure, reason, **meta) { Err.new to_h.merge(failure_key => failure), reason, **meta, key: failure_key }
17
+ end
18
+
19
+ def to_h = @value
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mona
4
+ module DictResult
5
+ # Sequence.call { ... } allows monadic 'do' notation for DictResult, where #set-ing the first failure skips the
6
+ # remainder of the block and returns the DictResult
7
+ class Sequence
8
+ def self.call(result = DictResult::EMPTY, &) = new(result).call(&)
9
+
10
+ def initialize(result = DictResult::EMPTY)
11
+ @result = result
12
+ @throw = Object.new
13
+ end
14
+
15
+ def call
16
+ catch(@throw) do
17
+ yield self
18
+ @result
19
+ end
20
+ end
21
+
22
+ def set(key, value)
23
+ @result = @result.set(key, value)
24
+ ensure
25
+ throw @throw, @result if @result.err?
26
+ end
27
+
28
+ def get(key) = @result.get(key)
29
+
30
+ def key?(key) = @result.key?(key)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dict_result/ok"
4
+ require_relative "dict_result/err"
5
+ require_relative "dict_result/sequence"
6
+
7
+ module Mona
8
+ # Represents a dictionary of results, it is successful if all results are successful, and a failure if one
9
+ # is a failure. A DictResult can only contain one failure.
10
+ module DictResult
11
+ include Result
12
+
13
+ # factory method that returns DictResult::OK or DictResult::Err
14
+ def self.[](initial = {}, &block)
15
+ result = EMPTY
16
+ initial.each { |k, v| result = result.set(k, v) }
17
+ result = result.sequence(&block) if block
18
+ result
19
+ end
20
+
21
+ def key?(key) = to_h.key?(key)
22
+
23
+ def get(key) = to_h.fetch(key)
24
+
25
+ def set(_key, _val) = raise(NotImplementedError, "implement #to_h")
26
+
27
+ def to_h = raise(NotImplementedError, "implement #to_h")
28
+
29
+ def sequence(&) = Sequence.new(self).call(&)
30
+
31
+ EMPTY = DictResult::OK.new({}).freeze
32
+ end
33
+ end
data/lib/mona/err.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mona
4
+ # An Err (failure) result, with optional reason, and metadata
5
+ class Err
6
+ include Result
7
+
8
+ def self.[](failure, reason = nil, **meta) = new(failure, reason, **meta)
9
+
10
+ def initialize(failure, reason, **meta)
11
+ raise ArgumentError, "meta can't contain :reason or err: key" if meta.key?(:reason) || meta.key?(:err)
12
+
13
+ @failure = failure
14
+ @reason = reason
15
+ @meta = meta
16
+ end
17
+
18
+ # @dynamic failure, reason, meta
19
+ attr_reader :failure, :reason, :meta
20
+
21
+ def either(_ok, err) = err.call(@failure, @reason, **@meta)
22
+
23
+ def inspect = "Err(#{[failure, *reason, *meta.map { "#{_1}: #{_2}" }].join(", ")})"
24
+
25
+ # @dynamic to_s
26
+ alias to_s inspect
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mona
4
+ # raised by ResultMatch.call when no match is found
5
+ class NoMatchError < Mona::Error
6
+ # @dynamic result
7
+ attr_reader :result
8
+
9
+ def initialize(result)
10
+ @result = result
11
+ super("No match found for #{@result}")
12
+ end
13
+ end
14
+ end
data/lib/mona/ok.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mona
4
+ # A Successful (OK) result
5
+ class OK
6
+ include Result
7
+
8
+ def self.[](value) = new(value)
9
+
10
+ def initialize(value)
11
+ @value = value
12
+ end
13
+
14
+ # @dynamic value
15
+ attr_reader :value
16
+
17
+ def either(ok, _err) = ok.call(@value)
18
+
19
+ def inspect = "OK(#{@value})"
20
+
21
+ # @dynamic to_s
22
+ alias to_s inspect
23
+ end
24
+ end
data/lib/mona/result.rb CHANGED
@@ -1,37 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "result/version"
4
- require_relative "result/error"
3
+ require_relative "../mona"
5
4
 
6
5
  module Mona
7
- # Monadic result
8
- #
9
- # @author Ian White
10
- # @since 0.1.0
6
+ # Provides the result interface, including class must implement #either(on_ok, on_err)
11
7
  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"
8
+ VERSION = "0.3.0"
18
9
 
19
- def self.[](obj) = obj.respond_to?(:to_result) ? obj.to_result : OK.new(obj)
10
+ def self.[](obj) = obj.is_a?(Result) ? obj : OK[obj]
20
11
 
21
- module_function
12
+ def either(_ok, _err) = raise(NotImplementedError, "implement #either")
22
13
 
23
- def to_result(obj) = Result[obj]
14
+ def value_or(&block) = either -> { _1 }, ->(*_args) { block.call }
24
15
 
25
- def ok(value) = OK.new(value)
16
+ def ok? = either ->(_) { true }, ->(*_args) { false }
26
17
 
27
- def err(failure, reason = nil, **meta) = Err.new(failure, reason, **meta)
18
+ def err? = either ->(_) { false }, ->(*_args) { true }
28
19
 
29
- def on_result(result, &) = Match.call(result, &)
20
+ def ok(&block) = either block, ->(*_args) {}
30
21
 
31
- def on_ok(result, &) = Match.call(result) { _1.ok(&) }
22
+ def err(&block) = either ->(_) {}, block
32
23
 
33
- def dict(initial = {}, &) = Dict.new(initial, &)
24
+ def and_tap(&block) = tap { either block, ->(*_args) {} }
34
25
 
35
- def action(&) = Action::Ephemeral.new(&)
26
+ def and_then(&block) = either -> { Result[block.call _1] }, ->(*_args) { self }
27
+
28
+ def or_else(&block)
29
+ # @type var meta: Hash[Symbol, untyped]
30
+ either ->(_) { self }, ->(failure, reason, **meta) { Result[block.call failure, reason, **meta] }
31
+ end
32
+
33
+ def deconstruct
34
+ # @type var meta: Hash[Symbol, untyped]
35
+ either ->(value) { [:ok, value] }, ->(failure, reason, **meta) { [:err, failure, reason, meta] }
36
+ end
37
+
38
+ def deconstruct_keys(_keys = nil)
39
+ # @type var meta: Hash[Symbol, untyped]
40
+ either ->(ok) { { ok: } }, ->(err, reason, **meta) { meta.merge(err:, reason:) }
41
+ end
42
+
43
+ def ==(other) = deconstruct == other.deconstruct
36
44
  end
37
45
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mona
4
+ # mixin for objects that return Result::Dict results
5
+ #
6
+ # define #perform to do the work, use #set to add results to the Result::Dict, the first failure will abort the
7
+ # rest of the #perform method and the result will be returned. This works like monadic 'do' notation.
8
+ #
9
+ # After a result is set at a key, it can be accessed via its key name as a method.
10
+ #
11
+ # use the Action with #call
12
+ #
13
+ # example:
14
+ #
15
+ # class UpdateUser
16
+ # inlcude Mona::ResultAction
17
+ # include Auditing # for example, adds #audit(model, input) method
18
+ #
19
+ # def perform(user_id, attributes)
20
+ # set :user, UserRepo.find(user_id) # if find is Err, the block exits with failure of :user
21
+ # set :input, UserInput.valid(attributes) # likewise if valid is Err, the block exits
22
+ # set [:user, :input], UserRepo.update(user.id, input) # note that #input and #user are available methods
23
+ # # if update is Err it is set on the :input key
24
+ # audit(user, input) # this only runs if all of the above set successful results
25
+ # end
26
+ # end
27
+ #
28
+ # UpdateUser.new.call(user_id, attributes) # => Result
29
+ module ResultAction
30
+ # You can create an Ephemeral Action as follows:
31
+ #
32
+ # Example:
33
+ # compute = Mona::ResultAction::Ephemeral.new do |x, y|
34
+ # set :numerator, x
35
+ # puts "set numerator: #{numerator}"
36
+ # set :denominator, y.zero? ? Result.failure(y, :zero) : y
37
+ # puts "set denominator: #{denominator}"
38
+ # set :answer, numerator / denominator
39
+ # puts "answer: #{answer}"
40
+ # end
41
+ #
42
+ # > compute.call(10,2)
43
+ # set numerator: 10
44
+ # set denominator: 2
45
+ # answer: 5
46
+ # => #<Result success: {:numerator=>10, :denominator=>2, :answer=>5}>
47
+ #
48
+ # > compute.call(10,0)
49
+ # set numerator: 10
50
+ # => #<Result failure: {:numerator=>10, :denominator=>0}, error: {:error=>:zero, :on=>:denominator}>
51
+ #
52
+ # Mona::ResultAction() is a shortcut for this
53
+ class Ephemeral
54
+ include ResultAction
55
+
56
+ def initialize(&perform)
57
+ @perform = perform
58
+ end
59
+
60
+ def perform(*args, **kwargs) = instance_exec(*args, **kwargs, &@perform)
61
+ end
62
+
63
+ def call(*args, **kwargs)
64
+ @sequence = DictResult::Sequence.new
65
+ @sequence.call { |_sequence| perform(*args, **kwargs) }
66
+ ensure
67
+ remove_instance_variable :@sequence
68
+ end
69
+
70
+ def perform(*args, **kwargs) = raise(NotImplementedError, "implement `perform'")
71
+
72
+ private
73
+
74
+ def get(key) = @sequence.get(key)
75
+
76
+ def set(key, value) = @sequence.set(key, value)
77
+
78
+ def key?(key) = @sequence.key?(key)
79
+
80
+ def respond_to_missing?(key, _include_private = false) = key?(key)
81
+
82
+ def method_missing(key, *args)
83
+ return get(key) if args.empty? && key?(key)
84
+
85
+ raise NoMethodError, "no method `#{key}' for #{self}"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mona
4
+ # Use ResultMatch.call to respond to result success or failure
5
+ #
6
+ # ResultMatch.call(result) do |r|
7
+ # r.ok { |value| ... }
8
+ # r.err(reason, **meta) { |failure, reason, **meta| ... }
9
+ # r.err(**meta) { |failure, reason, **meta| ... }
10
+ # r.err(reason) { |failure, reason, **meta| ... }
11
+ # r.err { |failure, reason, **meta| ... }
12
+ # end
13
+ class ResultMatch
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
+ # @type var meta: Hash[Symbol, untyped]
37
+ if match_reason?(match_reason, reason) && match_meta?(match_meta, meta)
38
+ throw @throw, yield(failure, reason, **meta)
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def match_reason?(match_reason, reason)
46
+ match_reason.nil? || match_reason === reason # rubocop:disable Style/CaseEquality
47
+ end
48
+
49
+ def match_meta?(match_meta, meta)
50
+ match_meta.all? { |key, val| val.nil? || val === meta[key] } # rubocop:disable Style/CaseEquality
51
+ end
52
+ end
53
+ end
data/lib/mona.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mona top level module contains utility methods
4
+ module Mona
5
+ autoload :OK, "mona/ok.rb"
6
+ autoload :Err, "mona/err.rb"
7
+ autoload :DictResult, "mona/dict_result.rb"
8
+ autoload :ResultMatch, "mona/result_match.rb"
9
+ autoload :ResultAction, "mona/result_action.rb"
10
+ autoload :NoMatchError, "mona/no_match_error.rb"
11
+
12
+ class Error < StandardError; end
13
+
14
+ module_function
15
+
16
+ # @dynamic self.result, self.ok, self.err, self.dict_result, self.on_result, self.on_ok, self.result_action
17
+
18
+ def result(obj) = obj.is_a?(Result) ? obj : OK[obj]
19
+
20
+ def ok(value) = OK[value]
21
+
22
+ def err(failure, reason = nil, **meta) = Err[failure, reason, **meta]
23
+
24
+ def dict_result(initial = {}, &) = DictResult[initial, &]
25
+
26
+ def on_result(result, &) = ResultMatch.call(result, &)
27
+
28
+ def on_ok(result, &) = ResultMatch.call(result) { _1.ok(&) }
29
+
30
+ def result_action(&) = ResultAction::Ephemeral.new(&)
31
+ end
data/sig/mona.rbs ADDED
@@ -0,0 +1,141 @@
1
+ module Mona
2
+ Result::VERSION: String
3
+
4
+ type dict = Hash[Symbol, untyped]
5
+ type result = Result[untyped, untyped]
6
+ type dict_result = DictResult[untyped]
7
+
8
+ def self?.result: (untyped) -> result
9
+ def self?.ok: [T] (T) -> OK[T]
10
+ def self?.err: [T, ReasonT] (T, ?ReasonT?, **untyped) -> Err[T, ReasonT?]
11
+ def self?.dict_result: (?dict) ?{ (DictResult::Sequence) -> void } -> dict_result
12
+ def self?.on_result: (result) { (ResultMatch) -> void } -> untyped
13
+ def self?.on_ok: (result) { (untyped) -> void } -> untyped
14
+ def self?.result_action: { (*untyped) -> void } -> ResultAction::Ephemeral
15
+
16
+ module Result[T, ReasonT]
17
+ def self.[]: (untyped) -> result
18
+
19
+ def either: [OKR, ErrR] (^(T) -> OKR, ^(T, ?ReasonT?, **untyped) -> ErrR) -> (OKR | ErrR)
20
+ def ok?: -> bool
21
+ def err?: -> bool
22
+ def ok: [R] () { (T) -> R } -> (R | nil)
23
+ def err: [R] () { (T, ?ReasonT?, **untyped) -> R } -> (R | nil)
24
+ def value_or: [R] { () -> R } -> (R | T)
25
+ def and_then: [R] () { (T) -> R } -> result
26
+ def and_tap: () { (T) -> void } -> self
27
+ def or_else: [R] () { (T, ?ReasonT?, **untyped) -> R } -> result
28
+ def deconstruct: -> Array[untyped]
29
+ def deconstruct_keys: (?Array[Symbol]?) -> dict
30
+ end
31
+
32
+ class OK[T]
33
+ include Result[T, nil]
34
+
35
+ attr_reader value: T
36
+
37
+ def self.[]: [T] (T) -> OK[T]
38
+
39
+ def initialize: (T) -> void
40
+ def either: [OKR] (^(T) -> OKR, ^(T, ?untyped?, **untyped) -> void) -> OKR
41
+ def inspect: -> String
42
+ alias to_s inspect
43
+ end
44
+
45
+ class Err[T, ReasonT]
46
+ include Result[T, ReasonT]
47
+
48
+ attr_reader failure: T
49
+ attr_reader reason: ReasonT
50
+ attr_reader meta: dict
51
+
52
+ def self.[]: [T, ReasonT] (T, ?ReasonT?, **untyped) -> Err[T, ReasonT?]
53
+
54
+ def initialize: (T, ReasonT, **untyped) -> void
55
+ def either: [ErrR] (^(T) -> void, ^(T, ?ReasonT?, **untyped) -> ErrR) -> ErrR
56
+ def inspect: -> String
57
+ alias to_s inspect
58
+ end
59
+
60
+ module DictResult[ReasonT]
61
+ include Result[dict, ReasonT]
62
+
63
+ EMPTY: dict_result
64
+
65
+ def self.[]: (?dict) ?{ (Sequence) -> void } -> dict_result
66
+
67
+ def key? : (Symbol) -> bool
68
+ def get : (Symbol) -> untyped
69
+ def set: (Symbol | [Symbol, Symbol], untyped) -> dict_result
70
+ def sequence: () { (Sequence) -> void } -> dict_result
71
+ def to_h: -> dict
72
+
73
+ class OK < Mona::OK[dict]
74
+ include DictResult[nil]
75
+ end
76
+
77
+ class Err[ReasonT] < Mona::Err[dict, ReasonT]
78
+ include DictResult[ReasonT]
79
+ end
80
+
81
+ class Sequence
82
+ @throw: Object
83
+ @result: dict_result
84
+
85
+ def self.call: (?dict_result) { (Sequence) -> void } -> dict_result
86
+ def initialize: (?dict_result) -> void
87
+ def call: () { (Sequence) -> void } -> dict_result
88
+ def set: (Symbol | [Symbol,Symbol], untyped) -> void
89
+ def get: (Symbol) -> untyped
90
+ def key?: (Symbol) -> bool
91
+ end
92
+ end
93
+
94
+ class Error < StandardError
95
+ end
96
+
97
+ class NoMatchError < Error
98
+ attr_reader result: result
99
+ def initialize: (result) -> void
100
+ end
101
+
102
+ class ResultMatch
103
+ @throw: Object
104
+ @result: result
105
+
106
+ def self.call: (result) { (ResultMatch) -> void } -> untyped
107
+ def initialize: (result) -> void
108
+ def call: () { (ResultMatch) -> void } -> untyped
109
+ def ok: () { (untyped) -> void } -> void
110
+ def err: (?untyped, **untyped) { (untyped, ?untyped, **untyped) -> void } -> void
111
+
112
+ private
113
+
114
+ def match_reason?: (untyped, untyped) -> bool
115
+ def match_meta?: (untyped, untyped) -> bool
116
+ end
117
+
118
+ module ResultAction
119
+ class Ephemeral
120
+ include ResultAction
121
+
122
+ @perform: ^(*untyped, **untyped) -> void
123
+
124
+ def initialize: () { (*untyped, **untyped) -> void } -> void
125
+ def perform: (*untyped, **untyped) -> void
126
+ end
127
+
128
+ @sequence: DictResult::Sequence
129
+
130
+ def call: (*untyped, **untyped) -> dict_result
131
+ def perform: (*untyped, **untyped) -> void
132
+
133
+ private
134
+
135
+ def get: (Symbol) -> untyped
136
+ def set: (Symbol | [Symbol,Symbol], untyped) -> void
137
+ def key?: (Symbol) -> bool
138
+ def respond_to_missing?: (Symbol, ?bool) -> bool
139
+ def method_missing: (Symbol, *untyped) -> untyped
140
+ end
141
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mona-result
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian White
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-09 00:00:00.000000000 Z
11
+ date: 2022-08-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Mona::Result provides a result monad, and dict result monad with do-notation
14
14
  email:
@@ -25,16 +25,18 @@ files:
25
25
  - README.md
26
26
  - Rakefile
27
27
  - Steepfile
28
+ - lib/mona.rb
29
+ - lib/mona/dict_result.rb
30
+ - lib/mona/dict_result/err.rb
31
+ - lib/mona/dict_result/ok.rb
32
+ - lib/mona/dict_result/sequence.rb
33
+ - lib/mona/err.rb
34
+ - lib/mona/no_match_error.rb
35
+ - lib/mona/ok.rb
28
36
  - 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
37
+ - lib/mona/result_action.rb
38
+ - lib/mona/result_match.rb
39
+ - sig/mona.rbs
38
40
  homepage: https://github.com/mona-rb/mona-result
39
41
  licenses:
40
42
  - MIT
@@ -42,7 +44,7 @@ metadata:
42
44
  rubygems_mfa_required: 'true'
43
45
  homepage_uri: https://github.com/mona-rb/mona-result
44
46
  source_code_uri: https://github.com/mona-rb/mona-result
45
- changelog_uri: https://github.com/mona-rb/mona-result/CHANGELOG.md
47
+ changelog_uri: https://github.com/mona-rb/mona-result/blob/main/CHANGELOG.md
46
48
  post_install_message:
47
49
  rdoc_options: []
48
50
  require_paths:
@@ -1,84 +0,0 @@
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
@@ -1,57 +0,0 @@
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
@@ -1,48 +0,0 @@
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
@@ -1,18 +0,0 @@
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
@@ -1,53 +0,0 @@
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
@@ -1,42 +0,0 @@
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
@@ -1,33 +0,0 @@
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
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mona
4
- module Result
5
- VERSION = "0.1.2"
6
- end
7
- end
data/sig/mona/result.rbs DELETED
@@ -1,177 +0,0 @@
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