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