mona-result 0.2.0 → 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: 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