mona-result 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11b851820d5a4a5558b4b9fca9ed4d473a4c7f41f76296e8304de95f319b4eac
4
- data.tar.gz: e02cccae0cc5ddf02a7a8fd99d1f509be1148c9d41f39f28d545409da7906e05
3
+ metadata.gz: b53dbabf6036e587ba320df3ffde82737a8d692a354d38ea2093d5c1046eedd8
4
+ data.tar.gz: ffd6a0f736e3407fb4c1cbab320572b399bc4091de6c90644520695162d382d7
5
5
  SHA512:
6
- metadata.gz: 4b01676a3ec1acaa3541a3e60eeaacd82cfacdc81889bd7073e0824ee987ec89c01415b28ba1a72413a32d78d687d0ac1f0a5521a0933b4a1edae8fe0af6ce46
7
- data.tar.gz: 66738db3e3571dd6c53982081acf787f634b5f7964a19a58649b513eeb5c3c169ac89bf25aa7fd9aaf951e25c83f9646314eab0d5b7b474f08efe3fe96adbe91
6
+ metadata.gz: cd2b54c044638818a3359239c43adeb631b19254bf94153a99127008c58774d2813ce4aa1363dc92aa107a269585d8fee1739ae98ff53643afdbd446fa5c0549
7
+ data.tar.gz: d9947684fd66ee1394811c1bf2160215699dd51650ed0b91fd04f22415155fd1af48e85ed35a9a68bfc4283a258172fcd0689929613f574a30afb28e0081703f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
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
+
1
6
  ## [0.2.0] - 2022-08-23
2
7
 
3
8
  - Adds Mona::Resultable module which implements the Result interface
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mona-result (0.2.0)
4
+ mona-result (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -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,41 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "result/error"
4
- require_relative "resultable"
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
- VERSION = "0.2.0"
8
+ VERSION = "0.3.0"
13
9
 
14
- autoload :OK, "mona/result/ok.rb"
15
- autoload :Err, "mona/result/err.rb"
16
- autoload :Match, "mona/result/match.rb"
17
- autoload :Dict, "mona/result/dict.rb"
18
- autoload :Sequence, "mona/result/sequence.rb"
19
- autoload :Action, "mona/result/action.rb"
10
+ def self.[](obj) = obj.is_a?(Result) ? obj : OK[obj]
20
11
 
21
- def self.[](obj) = obj.is_a?(Resultable) ? obj : OK.new(obj)
12
+ def either(_ok, _err) = raise(NotImplementedError, "implement #either")
22
13
 
23
- module_function
14
+ def value_or(&block) = either -> { _1 }, ->(*_args) { block.call }
24
15
 
25
- # @dynamic self.to_result, self.ok, self.err, self.on_result, self.on_ok, self.dict, self.action
16
+ def ok? = either ->(_) { true }, ->(*_args) { false }
26
17
 
27
- def to_result(obj) = Result[obj]
18
+ def err? = either ->(_) { false }, ->(*_args) { true }
28
19
 
29
- def ok(value) = OK.new(value)
20
+ def ok(&block) = either block, ->(*_args) {}
30
21
 
31
- def err(failure, reason = nil, **meta) = Err.new(failure, reason, **meta)
22
+ def err(&block) = either ->(_) {}, block
32
23
 
33
- def on_result(result, &) = Match.call(result, &)
24
+ def and_tap(&block) = tap { either block, ->(*_args) {} }
34
25
 
35
- def on_ok(result, &) = Match.call(result) { _1.ok(&) }
26
+ def and_then(&block) = either -> { Result[block.call _1] }, ->(*_args) { self }
36
27
 
37
- def dict(initial = {}, &) = Dict.new(initial, &)
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
38
32
 
39
- def action(&) = Action::Ephemeral.new(&)
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
40
44
  end
41
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 CHANGED
@@ -1,126 +1,84 @@
1
1
  module Mona
2
+ Result::VERSION: String
3
+
2
4
  type dict = Hash[Symbol, untyped]
3
- type result[T] = Resultable[T]
4
- type dict_result = (Result::Dict::OK | Result::Dict::Err)
5
+ type result = Result[untyped, untyped]
6
+ type dict_result = DictResult[untyped]
5
7
 
6
- module Resultable[T] : _Either[T]
7
- interface _Either[T]
8
- def either: [OKR, ErrR] (^(T) -> OKR, ^(T, ?untyped, **untyped) -> ErrR) -> (OKR | ErrR)
9
- end
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
10
18
 
19
+ def either: [OKR, ErrR] (^(T) -> OKR, ^(T, ?ReasonT?, **untyped) -> ErrR) -> (OKR | ErrR)
11
20
  def ok?: -> bool
12
21
  def err?: -> bool
13
22
  def ok: [R] () { (T) -> R } -> (R | nil)
14
- def err: [R] () { (T, ?untyped, **untyped) -> R } -> (R | nil)
23
+ def err: [R] () { (T, ?ReasonT?, **untyped) -> R } -> (R | nil)
15
24
  def value_or: [R] { () -> R } -> (R | T)
16
- def and_then: [R] () { (T) -> R } -> result[untyped]
25
+ def and_then: [R] () { (T) -> R } -> result
17
26
  def and_tap: () { (T) -> void } -> self
18
- def or_else: [R] () { (T, ?untyped, **untyped) -> R } -> result[untyped]
27
+ def or_else: [R] () { (T, ?ReasonT?, **untyped) -> R } -> result
19
28
  def deconstruct: -> Array[untyped]
20
29
  def deconstruct_keys: (?Array[Symbol]?) -> dict
21
30
  end
22
31
 
23
- module Result
24
- VERSION: String
25
-
26
- def self.[]: (untyped) -> result[untyped]
27
- def self?.to_result: (untyped) -> result[untyped]
28
- def self?.ok: [ValueT] (ValueT) -> OK[ValueT]
29
- def self?.err: [FailureT] (FailureT, ?untyped, **untyped) -> Err[FailureT]
30
- def self?.dict: (?dict) ?{ (Sequence) -> void } -> dict_result
31
- def self?.on_result: (result[untyped]) { (Match) -> void } -> untyped
32
- def self?.on_ok: (result[untyped]) { (untyped) -> void } -> untyped
33
- def self?.action: () { (*untyped) -> void } -> Action::Ephemeral
34
-
35
- class Error < StandardError
36
- end
37
-
38
- class NoMatchError < Error
39
- attr_reader result: result[untyped]
40
- def initialize: (result[untyped]) -> void
41
- end
32
+ class OK[T]
33
+ include Result[T, nil]
42
34
 
43
- class OK[ValueT]
44
- include Resultable[ValueT]
35
+ attr_reader value: T
45
36
 
46
- attr_reader value: ValueT
47
-
48
- def initialize: (ValueT) -> void
49
- def either: [OKR, ErrR] (^(ValueT) -> OKR, ^(untyped, ?untyped, **untyped) -> ErrR) -> OKR
50
- def inspect: -> String
51
- alias to_s inspect
52
- end
37
+ def self.[]: [T] (T) -> OK[T]
53
38
 
54
- class Err[FailureT]
55
- include Resultable[FailureT]
56
-
57
- attr_reader failure: FailureT
58
- attr_reader reason: untyped
59
- attr_reader meta: dict
60
-
61
- def initialize: (FailureT, ?untyped, **untyped) -> void
62
- def either: [OKR, ErrR] (^(untyped) -> OKR, ^(FailureT, ?untyped, **untyped) -> ErrR) -> ErrR
63
- def inspect: -> String
64
- alias to_s inspect
65
- end
66
-
67
- class Dict
68
- EMPTY: dict_result
69
-
70
- module Read : _ToH
71
- interface _ToH
72
- def to_h: -> dict
73
- end
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
74
44
 
75
- def key? : (Symbol) -> bool
76
- def fetch : (Symbol) -> untyped
77
- def [] : (Symbol) -> untyped
78
- end
45
+ class Err[T, ReasonT]
46
+ include Result[T, ReasonT]
79
47
 
80
- interface _Interface
81
- def set: (Symbol | [Symbol, Symbol], untyped) -> dict_result
82
- def sequence: () { (Sequence) -> void } -> dict_result
83
- def to_h: -> dict
84
- end
48
+ attr_reader failure: T
49
+ attr_reader reason: ReasonT
50
+ attr_reader meta: dict
85
51
 
86
- class OK < Result::OK[dict]
87
- include Read
52
+ def self.[]: [T, ReasonT] (T, ?ReasonT?, **untyped) -> Err[T, ReasonT?]
88
53
 
89
- include _Interface
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
90
59
 
91
- @value: dict
92
- end
60
+ module DictResult[ReasonT]
61
+ include Result[dict, ReasonT]
93
62
 
94
- class Err < Result::Err[dict]
95
- include Read
63
+ EMPTY: dict_result
96
64
 
97
- include _Interface
65
+ def self.[]: (?dict) ?{ (Sequence) -> void } -> dict_result
98
66
 
99
- @failure: dict
100
- end
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
101
72
 
102
- def self.new: (?dict) ?{ (Sequence) -> void } -> dict_result
73
+ class OK < Mona::OK[dict]
74
+ include DictResult[nil]
103
75
  end
104
76
 
105
- class Match
106
- @throw: Object
107
- @result: result[untyped]
108
-
109
- def self.call: (result[untyped]) { (Match) -> void } -> untyped
110
- def initialize: (result[untyped]) -> void
111
- def call: () { (Match) -> void } -> untyped
112
- def ok: () { (untyped) -> void } -> void
113
- def err: (?untyped, **untyped) { (untyped, ?untyped, **untyped) -> void } -> void
114
-
115
- private
116
-
117
- def match_reason?: (untyped, untyped) -> bool
118
- def match_meta?: (untyped, untyped) -> bool
77
+ class Err[ReasonT] < Mona::Err[dict, ReasonT]
78
+ include DictResult[ReasonT]
119
79
  end
120
80
 
121
81
  class Sequence
122
- include Dict::Read
123
-
124
82
  @throw: Object
125
83
  @result: dict_result
126
84
 
@@ -128,30 +86,56 @@ module Mona
128
86
  def initialize: (?dict_result) -> void
129
87
  def call: () { (Sequence) -> void } -> dict_result
130
88
  def set: (Symbol | [Symbol,Symbol], untyped) -> void
131
- def to_h: -> dict
89
+ def get: (Symbol) -> untyped
90
+ def key?: (Symbol) -> bool
132
91
  end
92
+ end
133
93
 
134
- module Action
135
- class Ephemeral
136
- include Action
94
+ class Error < StandardError
95
+ end
96
+
97
+ class NoMatchError < Error
98
+ attr_reader result: result
99
+ def initialize: (result) -> void
100
+ end
137
101
 
138
- @perform: ^(*untyped, **untyped) -> void
102
+ class ResultMatch
103
+ @throw: Object
104
+ @result: result
139
105
 
140
- def initialize: () { (*untyped, **untyped) -> void } -> void
141
- def perform: (*untyped, **untyped) -> void
142
- end
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
143
111
 
144
- @sequence: Sequence
112
+ private
145
113
 
146
- def call: (*untyped, **untyped) -> dict_result
147
- def perform: (*untyped, **untyped) -> void
114
+ def match_reason?: (untyped, untyped) -> bool
115
+ def match_meta?: (untyped, untyped) -> bool
116
+ end
148
117
 
149
- private
118
+ module ResultAction
119
+ class Ephemeral
120
+ include ResultAction
150
121
 
151
- def get: (Symbol) -> untyped
152
- def set: (Symbol | [Symbol,Symbol], untyped) -> void
153
- def respond_to_missing?: (Symbol, ?bool) -> bool
154
- def method_missing: (Symbol, *untyped) -> untyped
122
+ @perform: ^(*untyped, **untyped) -> void
123
+
124
+ def initialize: () { (*untyped, **untyped) -> void } -> void
125
+ def perform: (*untyped, **untyped) -> void
155
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
156
140
  end
157
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.2.0
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-23 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,15 +25,17 @@ 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/resultable.rb
37
+ - lib/mona/result_action.rb
38
+ - lib/mona/result_match.rb
37
39
  - sig/mona.rbs
38
40
  homepage: https://github.com/mona-rb/mona-result
39
41
  licenses:
@@ -1,88 +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
- def perform(*args, **kwargs) = raise(NotImplementedError, "implement `perform'")
72
-
73
- private
74
-
75
- def get(key) = @sequence.fetch(key)
76
-
77
- def set(key, value) = @sequence.set(key, value)
78
-
79
- def respond_to_missing?(key, _include_private = false) = @sequence.key?(key)
80
-
81
- def method_missing(key, *args)
82
- return get(key) if args.empty? && @sequence.key?(key)
83
-
84
- raise NoMethodError, "no method `#{key}' for #{self}"
85
- end
86
- end
87
- end
88
- end
@@ -1,58 +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::OK} or {Dict::Err}
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 Read
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 Read
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
- # @type var meta: Hash[Symbol, untyped]
36
- ->(failure, reason, **meta) { Err.new to_h.merge(failure_key => failure), reason, **meta, key: failure_key }
37
- end
38
-
39
- def sequence(&) = Sequence.new(self).call(&)
40
-
41
- def to_h = @value
42
- end
43
-
44
- # Err dict result
45
- class Err < Result::Err
46
- include Read
47
-
48
- def set(_key, _val) = raise(Error, "cannot #set on #{self}")
49
-
50
- def sequence(&) = self
51
-
52
- def to_h = @failure
53
- end
54
-
55
- EMPTY = OK.new({}).freeze
56
- end
57
- end
58
- end
@@ -1,28 +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
- include Resultable
8
-
9
- def initialize(failure, reason = nil, **meta)
10
- raise ArgumentError, "meta can't contain :reason or err: key" if meta.key?(:reason) || meta.key?(:err)
11
-
12
- @failure = failure
13
- @reason = reason
14
- @meta = meta
15
- end
16
-
17
- # @dynamic failure, reason, meta
18
- attr_reader :failure, :reason, :meta
19
-
20
- def either(_ok, err) = err.call(@failure, @reason, **@meta)
21
-
22
- def inspect = "#<Err #{@failure} #{{ reason: @reason, **@meta }.map { "#{_1}: #{_2}" }.join(", ")}>"
23
-
24
- # @dynamic to_s
25
- alias to_s inspect
26
- end
27
- end
28
- end
@@ -1,19 +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
- # @dynamic result
11
- attr_reader :result
12
-
13
- def initialize(result)
14
- @result = result
15
- super("No match found for #{result}")
16
- end
17
- end
18
- end
19
- end
@@ -1,55 +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(reason, **meta) { |failure, reason, **meta| ... }
10
- # r.err(**meta) { |failure, reason, **meta| ... }
11
- # r.err(reason) { |failure, reason, **meta| ... }
12
- # r.err { |failure, reason, **meta| ... }
13
- # end
14
- class Match
15
- def self.call(result, &) = new(result).call(&)
16
-
17
- def initialize(result)
18
- @throw = Object.new
19
- @result = result
20
- end
21
-
22
- def call
23
- catch @throw do
24
- yield self
25
- raise NoMatchError, @result
26
- end
27
- end
28
-
29
- def ok
30
- @result.and_then do |value|
31
- throw @throw, yield(value)
32
- end
33
- end
34
-
35
- def err(match_reason = nil, **match_meta)
36
- @result.or_else do |failure, reason, **meta|
37
- # @type var meta: Hash[Symbol, untyped]
38
- if match_reason?(match_reason, reason) && match_meta?(match_meta, meta)
39
- throw @throw, yield(failure, reason, **meta)
40
- end
41
- end
42
- end
43
-
44
- private
45
-
46
- def match_reason?(match_reason, reason)
47
- match_reason.nil? || match_reason === reason # rubocop:disable Style/CaseEquality
48
- end
49
-
50
- def match_meta?(match_meta, meta)
51
- match_meta.all? { |key, val| val.nil? || val === meta[key] } # rubocop:disable Style/CaseEquality
52
- end
53
- end
54
- end
55
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mona
4
- module Result
5
- # A Successful (OK) result
6
- class OK
7
- include Resultable
8
-
9
- def initialize(value)
10
- @value = value
11
- end
12
-
13
- # @dynamic value
14
- attr_reader :value
15
-
16
- def either(ok, _err) = ok.call(@value)
17
-
18
- def inspect = "#<OK #{@value}>"
19
-
20
- # @dynamic to_s
21
- alias to_s inspect
22
- end
23
- end
24
- 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::Read
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,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mona
4
- # Provides the result interface
5
- # The including class must implement #either(ok, err)
6
- module Resultable
7
- def value_or(&block)
8
- either ->(value) { value },
9
- ->(_failure, _reason = nil, **_meta) { block.call }
10
- end
11
-
12
- def ok?
13
- either ->(_value) { true },
14
- ->(_failure, _reason = nil, **_meta) { false }
15
- end
16
-
17
- def err?
18
- either ->(_value) { false },
19
- ->(_failure, _reason = nil, **_meta) { true }
20
- end
21
-
22
- def ok(&block)
23
- either ->(value) { block.call(value) },
24
- ->(_failure, _reason = nil, **_meta) {}
25
- end
26
-
27
- def err(&block)
28
- either ->(_value) {},
29
- # @type var meta: Hash[Symbol, untyped]
30
- ->(failure, reason = nil, **meta) { block.call(failure, reason, **meta) }
31
- end
32
-
33
- def and_tap(&block)
34
- tap do
35
- either ->(value) { block.call(value) },
36
- ->(_failure, _reason = nil, **_meta) {}
37
- end
38
- end
39
-
40
- def and_then(&block)
41
- either ->(value) { Result[block.call(value)] },
42
- ->(_failure, _reason, **_meta) { self }
43
- end
44
-
45
- def or_else(&block)
46
- either ->(_value) { self },
47
- # @type var meta: Hash[Symbol, untyped]
48
- ->(failure, reason, **meta) { Result[block.call(failure, reason, **meta)] }
49
- end
50
-
51
- def deconstruct
52
- either ->(value) { [:ok, value] },
53
- # @type var meta: Hash[Symbol, untyped]
54
- ->(failure, reason, **meta) { [:err, failure, reason, meta] }
55
- end
56
-
57
- def deconstruct_keys(_keys = nil)
58
- either ->(value) { { ok: value } },
59
- # @type var meta: Hash[Symbol, untyped]
60
- ->(failure, reason, **meta) { { err: failure, reason:, **meta } }
61
- end
62
- end
63
- end