fear 1.1.0 → 1.2.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubocop.yml +39 -0
  3. data/.github/workflows/spec.yml +42 -0
  4. data/.rubocop.yml +1 -1
  5. data/.simplecov +17 -0
  6. data/CHANGELOG.md +12 -3
  7. data/Gemfile +0 -2
  8. data/Gemfile.lock +80 -39
  9. data/README.md +158 -9
  10. data/Rakefile +27 -0
  11. data/examples/pattern_extracting.rb +1 -1
  12. data/examples/pattern_extracting_ruby2.7.rb +15 -0
  13. data/examples/pattern_matching_binary_tree_set.rb +3 -0
  14. data/examples/pattern_matching_number_in_words.rb +2 -0
  15. data/fear.gemspec +7 -8
  16. data/lib/dry/types/fear.rb +8 -0
  17. data/lib/dry/types/fear/option.rb +125 -0
  18. data/lib/fear.rb +9 -0
  19. data/lib/fear/awaitable.rb +2 -2
  20. data/lib/fear/either.rb +5 -0
  21. data/lib/fear/either_pattern_match.rb +3 -0
  22. data/lib/fear/extractor.rb +2 -0
  23. data/lib/fear/extractor/any_matcher.rb +1 -1
  24. data/lib/fear/extractor/array_splat_matcher.rb +1 -1
  25. data/lib/fear/extractor/empty_list_matcher.rb +1 -1
  26. data/lib/fear/extractor/matcher.rb +0 -3
  27. data/lib/fear/extractor/pattern.rb +1 -0
  28. data/lib/fear/extractor/value_matcher.rb +1 -1
  29. data/lib/fear/failure.rb +7 -0
  30. data/lib/fear/future.rb +12 -6
  31. data/lib/fear/future_api.rb +2 -2
  32. data/lib/fear/left.rb +1 -0
  33. data/lib/fear/none.rb +18 -0
  34. data/lib/fear/option.rb +22 -0
  35. data/lib/fear/option_pattern_match.rb +1 -0
  36. data/lib/fear/partial_function.rb +6 -0
  37. data/lib/fear/partial_function/empty.rb +2 -0
  38. data/lib/fear/pattern_matching_api.rb +1 -0
  39. data/lib/fear/right.rb +2 -0
  40. data/lib/fear/some.rb +28 -0
  41. data/lib/fear/struct.rb +13 -0
  42. data/lib/fear/success.rb +6 -0
  43. data/lib/fear/try_pattern_match.rb +3 -0
  44. data/lib/fear/utils.rb +13 -0
  45. data/lib/fear/version.rb +1 -1
  46. data/spec/dry/types/fear/option/constrained_spec.rb +22 -0
  47. data/spec/dry/types/fear/option/core_spec.rb +77 -0
  48. data/spec/dry/types/fear/option/default_spec.rb +21 -0
  49. data/spec/dry/types/fear/option/hash_spec.rb +58 -0
  50. data/spec/dry/types/fear/option/option_spec.rb +97 -0
  51. data/spec/fear/awaitable_spec.rb +17 -0
  52. data/spec/fear/either_pattern_matching_spec.rb +28 -0
  53. data/spec/fear/future_spec.rb +11 -2
  54. data/spec/fear/none_spec.rb +1 -1
  55. data/spec/fear/option_pattern_matching_spec.rb +34 -0
  56. data/spec/fear/option_spec.rb +128 -0
  57. data/spec/fear/partial_function_spec.rb +50 -0
  58. data/spec/fear/pattern_matching_api_spec.rb +31 -0
  59. data/spec/fear/try_pattern_matching_spec.rb +34 -0
  60. data/spec/spec_helper.rb +6 -2
  61. data/spec/struct_pattern_matching_spec.rb +36 -0
  62. data/spec/struct_spec.rb +2 -2
  63. data/spec/support/dry_types.rb +6 -0
  64. metadata +110 -12
  65. data/.travis.yml +0 -17
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fear
4
- # rubocop: disable Metrics/LineLength
4
+ # rubocop: disable Layout/LineLength
5
5
  module FutureApi
6
6
  # Asynchronously evaluates the block
7
7
  # @param options [Hash] options will be passed directly to underlying +Concurrent::Promise+
@@ -17,5 +17,5 @@ module Fear
17
17
  Future.new(options, &block)
18
18
  end
19
19
  end
20
- # rubocop: enable Metrics/LineLength
20
+ # rubocop: enable Layout/LineLength
21
21
  end
@@ -13,6 +13,7 @@ module Fear
13
13
  Fear.none
14
14
  end
15
15
  end
16
+ public_constant :EXTRACTOR
16
17
 
17
18
  # @api private
18
19
  def left_value
@@ -14,6 +14,7 @@ module Fear
14
14
  Fear.none
15
15
  end
16
16
  end
17
+ public_constant :EXTRACTOR
17
18
 
18
19
  # @raise [NoSuchElementError]
19
20
  def get
@@ -59,12 +60,29 @@ module Fear
59
60
  def ===(other)
60
61
  self == other
61
62
  end
63
+
64
+ # @param other [Fear::Option]
65
+ # @return [Fear::Option]
66
+ def zip(other)
67
+ if other.is_a?(Option)
68
+ Fear.none
69
+ else
70
+ raise TypeError, "can't zip with #{other.class}"
71
+ end
72
+ end
73
+
74
+ # @return [RightBiased::Left]
75
+ def filter_map
76
+ self
77
+ end
62
78
  end
63
79
 
64
80
  private_constant(:NoneClass)
65
81
 
66
82
  # The only instance of NoneClass
83
+ # @api private
67
84
  None = NoneClass.new.freeze
85
+ public_constant :None
68
86
 
69
87
  class << NoneClass
70
88
  def new
@@ -82,6 +82,17 @@ module Fear
82
82
  # Fear.some(42).map { |v| v/2 } #=> Fear.some(21)
83
83
  # Fear.none.map { |v| v/2 } #=> None
84
84
  #
85
+ # @!method filter_map(&block)
86
+ # Returns a new +Some+ of truthy results (everything except +false+ or +nil+) of
87
+ # running the block or +None+ otherwise.
88
+ # @yieldparam [any] value
89
+ # @yieldreturn [any]
90
+ # @example
91
+ # Fear.some(42).filter_map { |v| v/2 if v.even? } #=> Fear.some(21)
92
+ # Fear.some(42).filter_map { |v| v/2 if v.odd? } #=> Fear.none
93
+ # Fear.some(42).filter_map { |v| false } #=> Fear.none
94
+ # Fear.none.filter_map { |v| v/2 } #=> Fear.none
95
+ #
85
96
  # @!method flat_map(&block)
86
97
  # Returns the given block applied to the value from this +Some+
87
98
  # or returns this if this is a +None+
@@ -153,6 +164,17 @@ module Fear
153
164
  # m.else { 'error '}
154
165
  # end
155
166
  #
167
+ # @!method zip(other)
168
+ # @param other [Fear::Option]
169
+ # @return [Fear::Option] a +Fear::Some+ formed from this option and another option by
170
+ # combining the corresponding elements in an array.
171
+ #
172
+ # @example
173
+ # Fear.some("foo").zip(Fear.some("bar")) #=> Fear.some(["foo", "bar"])
174
+ # Fear.some("foo").zip(Fear.some("bar")) { |x, y| x + y } #=> Fear.some("foobar")
175
+ # Fear.some("foo").zip(Fear.none) #=> Fear.none
176
+ # Fear.none.zip(Fear.some("bar")) #=> Fear.none
177
+ #
156
178
  # @see https://github.com/scala/scala/blob/2.11.x/src/library/scala/Option.scala
157
179
  #
158
180
  module Option
@@ -25,6 +25,7 @@ module Fear
25
25
  # @api private
26
26
  class OptionPatternMatch < Fear::PatternMatch
27
27
  GET_METHOD = :get.to_proc
28
+ private_constant :GET_METHOD
28
29
 
29
30
  # Match against Some
30
31
  #
@@ -99,6 +99,12 @@ module Fear
99
99
  # @param other [PartialFunction]
100
100
  # @return [PartialFunction] a partial function which has as domain the union of the domains
101
101
  # of this partial function and +other+.
102
+ # @example
103
+ # handle_even = Fear.case(:even?.to_proc) { |x| "#{x} is even" }
104
+ # handle_odd = Fear.case(:odd?.to_proc) { |x| "#{x} is odd" }
105
+ # handle_even_or_odd = handle_even.or_else(odd)
106
+ # handle_even_or_odd.(42) #=> 42 is even
107
+ # handle_even_or_odd.(42) #=> 21 is odd
102
108
  def or_else(other)
103
109
  OrElse.new(self, other)
104
110
  end
@@ -4,5 +4,7 @@ module Fear
4
4
  module PartialFunction
5
5
  EMPTY = EmptyPartialFunction.new
6
6
  EMPTY.freeze
7
+
8
+ public_constant :EMPTY
7
9
  end
8
10
  end
@@ -130,6 +130,7 @@ module Fear
130
130
  # pf = Fear.xcase('['ok', Some(body)]', ->(body:) { !body.empty? }) { }
131
131
  #
132
132
  def xcase(pattern, *guards, &function)
133
+ warn "NOTE: Fear.xcase is deprecated and will be removed in a future version. Use `case .. in ..` instead."
133
134
  Fear[pattern].and_then(self.case(*guards, &function))
134
135
  end
135
136
  end
@@ -14,6 +14,8 @@ module Fear
14
14
  end
15
15
  end
16
16
 
17
+ public_constant :EXTRACTOR
18
+
17
19
  # @api private
18
20
  def right_value
19
21
  value
@@ -14,6 +14,8 @@ module Fear
14
14
  end
15
15
  end
16
16
 
17
+ public_constant :EXTRACTOR
18
+
17
19
  attr_reader :value
18
20
  protected :value
19
21
 
@@ -67,5 +69,31 @@ module Fear
67
69
 
68
70
  # @return [String]
69
71
  alias to_s inspect
72
+
73
+ # @param other [Fear::Option]
74
+ # @return [Fear::Option]
75
+ def zip(other)
76
+ if other.is_a?(Option)
77
+ other.map do |x|
78
+ if block_given?
79
+ yield(value, x)
80
+ else
81
+ [value, x]
82
+ end
83
+ end
84
+ else
85
+ raise TypeError, "can't zip with #{other.class}"
86
+ end
87
+ end
88
+
89
+ # @return [RightBiased::Left, RightBiased::Right]
90
+ def filter_map(&filter)
91
+ map(&filter).select(&:itself)
92
+ end
93
+
94
+ # @return [Array<any>]
95
+ def deconstruct
96
+ [get]
97
+ end
70
98
  end
71
99
  end
@@ -192,6 +192,7 @@ module Fear
192
192
  end
193
193
 
194
194
  INSPECT_TEMPLATE = "<#Fear::Struct %{class_name} %{attributes}>"
195
+ private_constant :INSPECT_TEMPLATE
195
196
 
196
197
  # @return [String]
197
198
  #
@@ -208,6 +209,7 @@ module Fear
208
209
  alias to_s inspect
209
210
 
210
211
  MISSING_KEYWORDS_ERROR = "missing keywords: %{keywords}"
212
+ private_constant :MISSING_KEYWORDS_ERROR
211
213
 
212
214
  private def _check_missing_attributes!(provided_attributes)
213
215
  missing_attributes = members - provided_attributes.keys
@@ -218,6 +220,7 @@ module Fear
218
220
  end
219
221
 
220
222
  UNKNOWN_KEYWORDS_ERROR = "unknown keywords: %{keywords}"
223
+ private_constant :UNKNOWN_KEYWORDS_ERROR
221
224
 
222
225
  private def _check_unknown_attributes!(provided_attributes)
223
226
  unknown_attributes = provided_attributes.keys - members
@@ -231,5 +234,15 @@ module Fear
231
234
  private def _set_attribute(name, value)
232
235
  instance_variable_set(:"@#{name}", value)
233
236
  end
237
+
238
+ # @param keys [Hash, nil]
239
+ # @return [Hash]
240
+ def deconstruct_keys(keys)
241
+ if keys
242
+ to_h.slice(*(self.class.attributes & keys))
243
+ else
244
+ to_h
245
+ end
246
+ end
234
247
  end
235
248
  end
@@ -13,6 +13,7 @@ module Fear
13
13
  Fear.none
14
14
  end
15
15
  end
16
+ public_constant :EXTRACTOR
16
17
 
17
18
  attr_reader :value
18
19
  protected :value
@@ -106,5 +107,10 @@ module Fear
106
107
 
107
108
  # @return [String]
108
109
  alias to_s inspect
110
+
111
+ # @return [<any>]
112
+ def deconstruct
113
+ [value]
114
+ end
109
115
  end
110
116
  end
@@ -7,7 +7,10 @@ module Fear
7
7
  # @api private
8
8
  class TryPatternMatch < Fear::PatternMatch
9
9
  SUCCESS_EXTRACTOR = :get.to_proc
10
+ private_constant :SUCCESS_EXTRACTOR
11
+
10
12
  FAILURE_EXTRACTOR = :exception.to_proc
13
+ private_constant :FAILURE_EXTRACTOR
11
14
 
12
15
  # Match against +Fear::Success+
13
16
  #
@@ -3,7 +3,20 @@
3
3
  module Fear
4
4
  # @private
5
5
  module Utils
6
+ EMPTY_STRING = ""
7
+ public_constant :EMPTY_STRING
8
+
9
+ IDENTITY = :itself.to_proc
10
+ public_constant :IDENTITY
11
+
6
12
  UNDEFINED = Object.new.freeze
13
+ public_constant :UNDEFINED
14
+
15
+ EMPTY_HASH = {}.freeze
16
+ public_constant :EMPTY_HASH
17
+
18
+ EMPTY_ARRAY = [].freeze
19
+ public_constant :EMPTY_ARRAY
7
20
 
8
21
  class << self
9
22
  def assert_arg_or_block!(method_name, *args)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fear
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  public_constant :VERSION
6
6
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "support/dry_types"
4
+
5
+ RSpec.describe Dry::Types::Constrained, :option do
6
+ context "with a option type" do
7
+ subject(:type) do
8
+ Dry::Types["nominal.string"].constrained(size: 4).option
9
+ end
10
+
11
+ it_behaves_like "Dry::Types::Nominal without primitive"
12
+
13
+ it "passes when constraints are not violated" do
14
+ expect(type[nil]).to be_none
15
+ expect(type["hell"]).to be_some_of("hell")
16
+ end
17
+
18
+ it "raises when a given constraint is violated" do
19
+ expect { type["hel"] }.to raise_error(Dry::Types::ConstraintError, /hel/)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "support/dry_types"
4
+
5
+ RSpec.describe Dry::Types::Nominal, :option do
6
+ describe "with opt-in option types" do
7
+ context "with strict string" do
8
+ let(:string) { Dry::Types["option.strict.string"] }
9
+
10
+ it_behaves_like "Dry::Types::Nominal without primitive" do
11
+ let(:type) { string }
12
+ end
13
+
14
+ it "accepts nil" do
15
+ expect(string[nil]).to be_none
16
+ end
17
+
18
+ it "accepts a string" do
19
+ expect(string["something"]).to be_some_of("something")
20
+ end
21
+ end
22
+
23
+ context "with coercible string" do
24
+ let(:string) { Dry::Types["option.coercible.string"] }
25
+
26
+ it_behaves_like "Dry::Types::Nominal without primitive" do
27
+ let(:type) { string }
28
+ end
29
+
30
+ it "accepts nil" do
31
+ expect(string[nil]).to be_none
32
+ end
33
+
34
+ it "accepts a string" do
35
+ expect(string[:something]).to be_some_of("something")
36
+ end
37
+ end
38
+ end
39
+
40
+ describe "defining coercible Option String" do
41
+ let(:option_string) { Dry::Types["coercible.string"].option }
42
+
43
+ it_behaves_like "Dry::Types::Nominal without primitive" do
44
+ let(:type) { option_string }
45
+ end
46
+
47
+ it "accepts nil" do
48
+ expect(option_string[nil]).to be_none
49
+ end
50
+
51
+ it "accepts an object coercible to a string" do
52
+ expect(option_string[123]).to be_some_of("123")
53
+ end
54
+ end
55
+
56
+ describe "defining Option String" do
57
+ let(:option_string) { Dry::Types["strict.string"].option }
58
+
59
+ it_behaves_like "Dry::Types::Nominal without primitive" do
60
+ let(:type) { option_string }
61
+ end
62
+
63
+ it "accepts nil and returns None instance" do
64
+ value = option_string[nil]
65
+
66
+ expect(value).to be_none
67
+ expect(value.map(&:downcase).map(&:upcase)).to be_none
68
+ end
69
+
70
+ it "accepts a string and returns Some instance" do
71
+ value = option_string["SomeThing"]
72
+
73
+ expect(value).to be_some_of("SomeThing")
74
+ expect(value.map(&:downcase).map(&:upcase)).to be_some_of("SOMETHING")
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "support/dry_types"
4
+
5
+ RSpec.describe Dry::Types::Nominal, "#default", :option do
6
+ context "with a maybe" do
7
+ subject(:type) { Dry::Types["strict.integer"].option }
8
+
9
+ it_behaves_like "Dry::Types::Nominal without primitive" do
10
+ let(:type) { Dry::Types["strict.integer"].option.default(0) }
11
+ end
12
+
13
+ it "does not allow nil" do
14
+ expect { type.default(nil) }.to raise_error(ArgumentError, /nil/)
15
+ end
16
+
17
+ it "accepts a non-nil value" do
18
+ expect(type.default(0)[0]).to be_some_of(0)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "support/dry_types"
4
+
5
+ RSpec.describe Dry::Types::Hash, :option do
6
+ let(:email) { Dry::Types["option.strict.string"] }
7
+
8
+ context "Symbolized constructor" do
9
+ subject(:hash) do
10
+ Dry::Types["nominal.hash"].schema(
11
+ name: "string",
12
+ email: email,
13
+ ).with_key_transform(&:to_sym)
14
+ end
15
+
16
+ describe "#[]" do
17
+ it "sets None as a default value for option" do
18
+ result = hash["name" => "Jane"]
19
+
20
+ expect(result[:email]).to be_none
21
+ end
22
+ end
23
+ end
24
+
25
+ context "Schema constructor" do
26
+ subject(:hash) do
27
+ Dry::Types["nominal.hash"].schema(
28
+ name: "string",
29
+ email: email,
30
+ )
31
+ end
32
+
33
+ describe "#[]" do
34
+ it "sets None as a default value for option types" do
35
+ result = hash[name: "Jane"]
36
+
37
+ expect(result[:email]).to be_none
38
+ end
39
+ end
40
+ end
41
+
42
+ context "Strict with defaults" do
43
+ subject(:hash) do
44
+ Dry::Types["nominal.hash"].schema(
45
+ name: "string",
46
+ email: email,
47
+ )
48
+ end
49
+
50
+ describe "#[]" do
51
+ it "sets None as a default value for option types" do
52
+ result = hash[name: "Jane"]
53
+
54
+ expect(result[:email]).to be_none
55
+ end
56
+ end
57
+ end
58
+ end