fear 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rubocop.yml +39 -0
- data/.github/workflows/spec.yml +42 -0
- data/.rubocop.yml +1 -1
- data/.simplecov +17 -0
- data/CHANGELOG.md +12 -3
- data/Gemfile +0 -2
- data/Gemfile.lock +80 -39
- data/README.md +158 -9
- data/Rakefile +27 -0
- data/examples/pattern_extracting.rb +1 -1
- data/examples/pattern_extracting_ruby2.7.rb +15 -0
- data/examples/pattern_matching_binary_tree_set.rb +3 -0
- data/examples/pattern_matching_number_in_words.rb +2 -0
- data/fear.gemspec +7 -8
- data/lib/dry/types/fear.rb +8 -0
- data/lib/dry/types/fear/option.rb +125 -0
- data/lib/fear.rb +9 -0
- data/lib/fear/awaitable.rb +2 -2
- data/lib/fear/either.rb +5 -0
- data/lib/fear/either_pattern_match.rb +3 -0
- data/lib/fear/extractor.rb +2 -0
- data/lib/fear/extractor/any_matcher.rb +1 -1
- data/lib/fear/extractor/array_splat_matcher.rb +1 -1
- data/lib/fear/extractor/empty_list_matcher.rb +1 -1
- data/lib/fear/extractor/matcher.rb +0 -3
- data/lib/fear/extractor/pattern.rb +1 -0
- data/lib/fear/extractor/value_matcher.rb +1 -1
- data/lib/fear/failure.rb +7 -0
- data/lib/fear/future.rb +12 -6
- data/lib/fear/future_api.rb +2 -2
- data/lib/fear/left.rb +1 -0
- data/lib/fear/none.rb +18 -0
- data/lib/fear/option.rb +22 -0
- data/lib/fear/option_pattern_match.rb +1 -0
- data/lib/fear/partial_function.rb +6 -0
- data/lib/fear/partial_function/empty.rb +2 -0
- data/lib/fear/pattern_matching_api.rb +1 -0
- data/lib/fear/right.rb +2 -0
- data/lib/fear/some.rb +28 -0
- data/lib/fear/struct.rb +13 -0
- data/lib/fear/success.rb +6 -0
- data/lib/fear/try_pattern_match.rb +3 -0
- data/lib/fear/utils.rb +13 -0
- data/lib/fear/version.rb +1 -1
- data/spec/dry/types/fear/option/constrained_spec.rb +22 -0
- data/spec/dry/types/fear/option/core_spec.rb +77 -0
- data/spec/dry/types/fear/option/default_spec.rb +21 -0
- data/spec/dry/types/fear/option/hash_spec.rb +58 -0
- data/spec/dry/types/fear/option/option_spec.rb +97 -0
- data/spec/fear/awaitable_spec.rb +17 -0
- data/spec/fear/either_pattern_matching_spec.rb +28 -0
- data/spec/fear/future_spec.rb +11 -2
- data/spec/fear/none_spec.rb +1 -1
- data/spec/fear/option_pattern_matching_spec.rb +34 -0
- data/spec/fear/option_spec.rb +128 -0
- data/spec/fear/partial_function_spec.rb +50 -0
- data/spec/fear/pattern_matching_api_spec.rb +31 -0
- data/spec/fear/try_pattern_matching_spec.rb +34 -0
- data/spec/spec_helper.rb +6 -2
- data/spec/struct_pattern_matching_spec.rb +36 -0
- data/spec/struct_spec.rb +2 -2
- data/spec/support/dry_types.rb +6 -0
- metadata +110 -12
- data/.travis.yml +0 -17
data/lib/fear/future_api.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Fear
|
4
|
-
# rubocop: disable
|
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
|
20
|
+
# rubocop: enable Layout/LineLength
|
21
21
|
end
|
data/lib/fear/left.rb
CHANGED
data/lib/fear/none.rb
CHANGED
@@ -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
|
data/lib/fear/option.rb
CHANGED
@@ -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
|
@@ -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
|
@@ -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
|
data/lib/fear/right.rb
CHANGED
data/lib/fear/some.rb
CHANGED
@@ -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
|
data/lib/fear/struct.rb
CHANGED
@@ -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
|
data/lib/fear/success.rb
CHANGED
@@ -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
|
#
|
data/lib/fear/utils.rb
CHANGED
@@ -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)
|
data/lib/fear/version.rb
CHANGED
@@ -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
|