fear 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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