fear 0.9.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.
- checksums.yaml +5 -5
- data/.github/workflows/rubocop.yml +39 -0
- data/.github/workflows/spec.yml +42 -0
- data/.gitignore +0 -1
- data/.rubocop.yml +4 -12
- data/.simplecov +17 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile +5 -2
- data/Gemfile.lock +130 -0
- data/LICENSE.txt +1 -1
- data/README.md +1293 -97
- data/Rakefile +369 -1
- data/benchmarks/README.md +1 -0
- data/benchmarks/dry_do_vs_fear_for.txt +11 -0
- data/benchmarks/dry_some_fmap_vs_fear_some_map.txt +11 -0
- data/benchmarks/factorial.txt +16 -0
- data/benchmarks/fear_gaurd_and1_vs_new.txt +13 -0
- data/benchmarks/fear_gaurd_and2_vs_and.txt +13 -0
- data/benchmarks/fear_gaurd_and3_vs_and_and.txt +13 -0
- data/benchmarks/fear_pattern_extracting_with_vs_without_cache.txt +11 -0
- data/benchmarks/fear_pattern_matching_construction_vs_execution.txt +13 -0
- data/benchmarks/pattern_matching_dry_vs_qo_vs_fear_try.txt +14 -0
- data/benchmarks/pattern_matching_qo_vs_fear_pattern_extraction.txt +11 -0
- data/benchmarks/pattern_matching_qo_vs_fear_try_execution.txt +11 -0
- data/examples/pattern_extracting.rb +17 -0
- data/examples/pattern_extracting_ruby2.7.rb +15 -0
- data/examples/pattern_matching_binary_tree_set.rb +101 -0
- data/examples/pattern_matching_number_in_words.rb +60 -0
- data/fear.gemspec +34 -23
- data/lib/dry/types/fear.rb +8 -0
- data/lib/dry/types/fear/option.rb +125 -0
- data/lib/fear.rb +65 -15
- data/lib/fear/await.rb +33 -0
- data/lib/fear/awaitable.rb +28 -0
- data/lib/fear/either.rb +131 -71
- data/lib/fear/either_api.rb +23 -0
- data/lib/fear/either_pattern_match.rb +53 -0
- data/lib/fear/empty_partial_function.rb +38 -0
- data/lib/fear/extractor.rb +112 -0
- data/lib/fear/extractor/anonymous_array_splat_matcher.rb +10 -0
- data/lib/fear/extractor/any_matcher.rb +17 -0
- data/lib/fear/extractor/array_head_matcher.rb +36 -0
- data/lib/fear/extractor/array_matcher.rb +40 -0
- data/lib/fear/extractor/array_splat_matcher.rb +16 -0
- data/lib/fear/extractor/empty_list_matcher.rb +20 -0
- data/lib/fear/extractor/extractor_matcher.rb +44 -0
- data/lib/fear/extractor/grammar.rb +203 -0
- data/lib/fear/extractor/grammar.treetop +129 -0
- data/lib/fear/extractor/identifier_matcher.rb +18 -0
- data/lib/fear/extractor/matcher.rb +53 -0
- data/lib/fear/extractor/matcher/and.rb +38 -0
- data/lib/fear/extractor/named_array_splat_matcher.rb +17 -0
- data/lib/fear/extractor/pattern.rb +58 -0
- data/lib/fear/extractor/typed_identifier_matcher.rb +26 -0
- data/lib/fear/extractor/value_matcher.rb +19 -0
- data/lib/fear/extractor_api.rb +35 -0
- data/lib/fear/failure.rb +46 -14
- data/lib/fear/failure_pattern_match.rb +10 -0
- data/lib/fear/for.rb +37 -95
- data/lib/fear/for_api.rb +68 -0
- data/lib/fear/future.rb +497 -0
- data/lib/fear/future_api.rb +21 -0
- data/lib/fear/left.rb +19 -2
- data/lib/fear/left_pattern_match.rb +11 -0
- data/lib/fear/none.rb +67 -3
- data/lib/fear/none_pattern_match.rb +14 -0
- data/lib/fear/option.rb +120 -56
- data/lib/fear/option_api.rb +40 -0
- data/lib/fear/option_pattern_match.rb +48 -0
- data/lib/fear/partial_function.rb +176 -0
- data/lib/fear/partial_function/and_then.rb +50 -0
- data/lib/fear/partial_function/any.rb +28 -0
- data/lib/fear/partial_function/combined.rb +53 -0
- data/lib/fear/partial_function/empty.rb +10 -0
- data/lib/fear/partial_function/guard.rb +80 -0
- data/lib/fear/partial_function/guard/and.rb +38 -0
- data/lib/fear/partial_function/guard/and3.rb +41 -0
- data/lib/fear/partial_function/guard/or.rb +38 -0
- data/lib/fear/partial_function/lifted.rb +23 -0
- data/lib/fear/partial_function/or_else.rb +64 -0
- data/lib/fear/partial_function_class.rb +38 -0
- data/lib/fear/pattern_match.rb +114 -0
- data/lib/fear/pattern_matching_api.rb +137 -0
- data/lib/fear/promise.rb +95 -0
- data/lib/fear/right.rb +20 -2
- data/lib/fear/right_biased.rb +6 -14
- data/lib/fear/right_pattern_match.rb +11 -0
- data/lib/fear/some.rb +55 -3
- data/lib/fear/some_pattern_match.rb +13 -0
- data/lib/fear/struct.rb +248 -0
- data/lib/fear/success.rb +35 -5
- data/lib/fear/success_pattern_match.rb +12 -0
- data/lib/fear/try.rb +136 -79
- data/lib/fear/try_api.rb +33 -0
- data/lib/fear/try_pattern_match.rb +33 -0
- data/lib/fear/unit.rb +32 -0
- data/lib/fear/utils.rb +39 -14
- data/lib/fear/version.rb +4 -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/done_spec.rb +8 -6
- data/spec/fear/either/mixin_spec.rb +17 -0
- data/spec/fear/either_pattern_match_spec.rb +37 -0
- data/spec/fear/either_pattern_matching_spec.rb +28 -0
- data/spec/fear/extractor/array_matcher_spec.rb +230 -0
- data/spec/fear/extractor/extractor_matcher_spec.rb +153 -0
- data/spec/fear/extractor/grammar_array_spec.rb +25 -0
- data/spec/fear/extractor/identified_matcher_spec.rb +49 -0
- data/spec/fear/extractor/identifier_matcher_spec.rb +68 -0
- data/spec/fear/extractor/pattern_spec.rb +34 -0
- data/spec/fear/extractor/typed_identifier_matcher_spec.rb +64 -0
- data/spec/fear/extractor/value_matcher_number_spec.rb +79 -0
- data/spec/fear/extractor/value_matcher_string_spec.rb +88 -0
- data/spec/fear/extractor/value_matcher_symbol_spec.rb +71 -0
- data/spec/fear/extractor_api_spec.rb +115 -0
- data/spec/fear/extractor_spec.rb +61 -0
- data/spec/fear/failure_spec.rb +145 -45
- data/spec/fear/for_spec.rb +57 -67
- data/spec/fear/future_spec.rb +691 -0
- data/spec/fear/guard_spec.rb +103 -0
- data/spec/fear/left_spec.rb +112 -46
- data/spec/fear/none_spec.rb +114 -16
- data/spec/fear/option/mixin_spec.rb +39 -0
- data/spec/fear/option_pattern_match_spec.rb +35 -0
- data/spec/fear/option_pattern_matching_spec.rb +34 -0
- data/spec/fear/option_spec.rb +121 -8
- data/spec/fear/partial_function/empty_spec.rb +38 -0
- data/spec/fear/partial_function_and_then_spec.rb +147 -0
- data/spec/fear/partial_function_composition_spec.rb +82 -0
- data/spec/fear/partial_function_or_else_spec.rb +276 -0
- data/spec/fear/partial_function_spec.rb +239 -0
- data/spec/fear/pattern_match_spec.rb +93 -0
- data/spec/fear/pattern_matching_api_spec.rb +31 -0
- data/spec/fear/promise_spec.rb +96 -0
- data/spec/fear/right_biased/left.rb +29 -32
- data/spec/fear/right_biased/right.rb +51 -54
- data/spec/fear/right_spec.rb +109 -41
- data/spec/fear/some_spec.rb +80 -15
- data/spec/fear/success_spec.rb +99 -32
- data/spec/fear/try/mixin_spec.rb +19 -0
- data/spec/fear/try_pattern_match_spec.rb +37 -0
- data/spec/fear/try_pattern_matching_spec.rb +34 -0
- data/spec/fear/utils_spec.rb +16 -14
- data/spec/spec_helper.rb +13 -7
- data/spec/struct_pattern_matching_spec.rb +36 -0
- data/spec/struct_spec.rb +226 -0
- data/spec/support/dry_types.rb +6 -0
- metadata +320 -29
- data/.travis.yml +0 -9
- data/lib/fear/done.rb +0 -22
- data/lib/fear/for/evaluation_context.rb +0 -91
data/lib/fear/promise.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "concurrent"
|
5
|
+
rescue LoadError
|
6
|
+
puts "You must add 'concurrent-ruby' to your Gemfile in order to use Fear::Future"
|
7
|
+
end
|
8
|
+
|
9
|
+
module Fear
|
10
|
+
# @api private
|
11
|
+
class Promise < Concurrent::IVar
|
12
|
+
# @param options [Hash] options passed to underlying +Concurrent::Promise+
|
13
|
+
def initialize(**options)
|
14
|
+
super()
|
15
|
+
@options = options
|
16
|
+
@promise = Concurrent::Promise.new(options) do
|
17
|
+
Fear.try { value }.flatten
|
18
|
+
end
|
19
|
+
end
|
20
|
+
attr_reader :promise, :options
|
21
|
+
private :promise
|
22
|
+
private :options
|
23
|
+
|
24
|
+
def completed?
|
25
|
+
complete?
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Fear::Future]
|
29
|
+
def to_future
|
30
|
+
Future.new(promise, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Complete this promise with successful result
|
34
|
+
# @param value [any]
|
35
|
+
# @return [Boolean]
|
36
|
+
# @see #complete
|
37
|
+
def success(value)
|
38
|
+
complete(Fear.success(value))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Complete this promise with failure
|
42
|
+
# @param value [any]
|
43
|
+
# @return [self]
|
44
|
+
# @raise [IllegalStateException]
|
45
|
+
# @see #complete!
|
46
|
+
def success!(value)
|
47
|
+
complete!(Fear.success(value))
|
48
|
+
end
|
49
|
+
|
50
|
+
# Complete this promise with failure
|
51
|
+
# @param error [StandardError]
|
52
|
+
# @return [Boolean]
|
53
|
+
# @see #complete
|
54
|
+
def failure(error)
|
55
|
+
complete(Fear.failure(error))
|
56
|
+
end
|
57
|
+
|
58
|
+
# Complete this promise with failure
|
59
|
+
# @param error [StandardError]
|
60
|
+
# @return [self]
|
61
|
+
# @raise [IllegalStateException]
|
62
|
+
# @see #complete!
|
63
|
+
def failure!(error)
|
64
|
+
complete!(Fear.failure(error))
|
65
|
+
end
|
66
|
+
|
67
|
+
# Complete this promise with result
|
68
|
+
# @param result [Fear::Try]
|
69
|
+
# @return [self]
|
70
|
+
# @raise [IllegalStateException] if promise already completed
|
71
|
+
def complete!(result)
|
72
|
+
if complete(result)
|
73
|
+
self
|
74
|
+
else
|
75
|
+
raise IllegalStateException, "Promise already completed."
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Complete this promise with result
|
80
|
+
# @param result [Fear::Try]
|
81
|
+
# @return [Boolean] If the promise has already been completed returns
|
82
|
+
# `false`, or `true` otherwise.
|
83
|
+
# @raise [IllegalStateException] if promise already completed
|
84
|
+
#
|
85
|
+
def complete(result)
|
86
|
+
if completed?
|
87
|
+
false
|
88
|
+
else
|
89
|
+
set result
|
90
|
+
promise.execute
|
91
|
+
true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/fear/right.rb
CHANGED
@@ -1,7 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Fear
|
2
4
|
class Right
|
3
5
|
include Either
|
4
6
|
include RightBiased::Right
|
7
|
+
include RightPatternMatch.mixin
|
8
|
+
|
9
|
+
EXTRACTOR = proc do |either|
|
10
|
+
if Fear::Right === either
|
11
|
+
Fear.some([either.right_value])
|
12
|
+
else
|
13
|
+
Fear.none
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
public_constant :EXTRACTOR
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
def right_value
|
21
|
+
value
|
22
|
+
end
|
5
23
|
|
6
24
|
# @return [true]
|
7
25
|
def right?
|
@@ -50,8 +68,8 @@ module Fear
|
|
50
68
|
|
51
69
|
# @param reduce_right [Proc]
|
52
70
|
# @return [any]
|
53
|
-
def reduce(
|
54
|
-
reduce_right.
|
71
|
+
def reduce(_reduce_left, reduce_right)
|
72
|
+
reduce_right.(value)
|
55
73
|
end
|
56
74
|
|
57
75
|
# @return [Either]
|
data/lib/fear/right_biased.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Fear
|
2
4
|
# @private
|
3
5
|
module RightBiased
|
@@ -7,14 +9,14 @@ module Fear
|
|
7
9
|
# Returns the value from this `RightBiased::Right` or the given argument if
|
8
10
|
# this is a `RightBiased::Left`.
|
9
11
|
def get_or_else(*args, &block)
|
10
|
-
Utils.assert_arg_or_block!(
|
12
|
+
Utils.assert_arg_or_block!("get_or_else", *args, &block)
|
11
13
|
super
|
12
14
|
end
|
13
15
|
|
14
16
|
# Returns this `RightBiased::Right` or the given alternative if
|
15
17
|
# this is a `RightBiased::Left`.
|
16
18
|
def or_else(*args, &block)
|
17
|
-
Utils.assert_arg_or_block!(
|
19
|
+
Utils.assert_arg_or_block!("or_else", *args, &block)
|
18
20
|
super.tap do |result|
|
19
21
|
Utils.assert_type!(result, left_class, right_class)
|
20
22
|
end
|
@@ -85,11 +87,6 @@ module Fear
|
|
85
87
|
yield(value)
|
86
88
|
end
|
87
89
|
|
88
|
-
# @return [Array] containing value
|
89
|
-
def to_a
|
90
|
-
[value]
|
91
|
-
end
|
92
|
-
|
93
90
|
# @return [Option] containing value
|
94
91
|
def to_option
|
95
92
|
Some.new(value)
|
@@ -136,7 +133,7 @@ module Fear
|
|
136
133
|
# @param [any]
|
137
134
|
# @return [false]
|
138
135
|
#
|
139
|
-
def include?(
|
136
|
+
def include?(_value)
|
140
137
|
false
|
141
138
|
end
|
142
139
|
|
@@ -164,14 +161,9 @@ module Fear
|
|
164
161
|
self
|
165
162
|
end
|
166
163
|
|
167
|
-
# @return [Array] empty array
|
168
|
-
def to_a
|
169
|
-
[]
|
170
|
-
end
|
171
|
-
|
172
164
|
# @return [None]
|
173
165
|
def to_option
|
174
|
-
None
|
166
|
+
None
|
175
167
|
end
|
176
168
|
|
177
169
|
# @return [false]
|
data/lib/fear/some.rb
CHANGED
@@ -1,8 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Fear
|
2
4
|
class Some
|
3
5
|
include Option
|
4
|
-
include Dry::Equalizer(:get)
|
5
6
|
include RightBiased::Right
|
7
|
+
include SomePatternMatch.mixin
|
8
|
+
|
9
|
+
EXTRACTOR = proc do |option|
|
10
|
+
if Fear::Some === option
|
11
|
+
Fear.some([option.get])
|
12
|
+
else
|
13
|
+
Fear.none
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
public_constant :EXTRACTOR
|
6
18
|
|
7
19
|
attr_reader :value
|
8
20
|
protected :value
|
@@ -31,17 +43,57 @@ module Fear
|
|
31
43
|
if yield(value)
|
32
44
|
self
|
33
45
|
else
|
34
|
-
None
|
46
|
+
None
|
35
47
|
end
|
36
48
|
end
|
37
49
|
|
38
50
|
# @return [Option]
|
39
51
|
def reject
|
40
52
|
if yield(value)
|
41
|
-
None
|
53
|
+
None
|
42
54
|
else
|
43
55
|
self
|
44
56
|
end
|
45
57
|
end
|
58
|
+
|
59
|
+
# @param other [Any]
|
60
|
+
# @return [Boolean]
|
61
|
+
def ==(other)
|
62
|
+
other.is_a?(Some) && get == other.get
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [String]
|
66
|
+
def inspect
|
67
|
+
"#<Fear::Some get=#{value.inspect}>"
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [String]
|
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
|
46
98
|
end
|
47
99
|
end
|
data/lib/fear/struct.rb
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fear
|
4
|
+
# Structs are like regular classes and good for modeling immutable data.
|
5
|
+
#
|
6
|
+
# A minimal struct requires just a list of attributes:
|
7
|
+
#
|
8
|
+
# User = Fear::Struct.with_attributes(:id, :email, :admin)
|
9
|
+
# john = User.new(id: 2, email: 'john@example.com', admin: false)
|
10
|
+
#
|
11
|
+
# john.email #=> 'john@example.com'
|
12
|
+
#
|
13
|
+
# Instead of `.with_attributes` factory method you can use classic inheritance:
|
14
|
+
#
|
15
|
+
# class User < Fear::Struct
|
16
|
+
# attribute :id
|
17
|
+
# attribute :email
|
18
|
+
# attribute :admin
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# Since structs are immutable, you are not allowed to reassign their attributes
|
22
|
+
#
|
23
|
+
# john.email = ''john.doe@example.com'' #=> raises NoMethodError
|
24
|
+
#
|
25
|
+
# Two structs of the same type with the same attributes are equal
|
26
|
+
#
|
27
|
+
# john1 = User.new(id: 2, email: 'john@example.com', admin: false)
|
28
|
+
# john2 = User.new(id: 2, admin: false, email: 'john@example.com')
|
29
|
+
# john1 == john2 #=> true
|
30
|
+
#
|
31
|
+
# You can create a shallow copy of a +Struct+ by using copy method optionally changing its attributes.
|
32
|
+
#
|
33
|
+
# john = User.new(id: 2, email: 'john@example.com', admin: false)
|
34
|
+
# admin_john = john.copy(admin: true)
|
35
|
+
#
|
36
|
+
# john.admin #=> false
|
37
|
+
# admin_john.admin #=> true
|
38
|
+
#
|
39
|
+
# It's possible to match against struct attributes. The following example extracts email from
|
40
|
+
# user only if user is admin
|
41
|
+
#
|
42
|
+
# john = User.new(id: 2, email: 'john@example.com', admin: false)
|
43
|
+
# john.match |m|
|
44
|
+
# m.xcase('Fear::Struct(_, email, true)') do |email|
|
45
|
+
# email
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# Note, parameters got extracted in order they was defined.
|
50
|
+
#
|
51
|
+
class Struct
|
52
|
+
include PatternMatch.mixin
|
53
|
+
|
54
|
+
@attributes = [].freeze
|
55
|
+
|
56
|
+
class << self
|
57
|
+
# @param base [Fear::Struct]
|
58
|
+
# @api private
|
59
|
+
def inherited(base)
|
60
|
+
base.instance_variable_set(:@attributes, attributes)
|
61
|
+
Fear.register_extractor(base, Fear.case(base, &:to_a).lift)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Defines attribute
|
65
|
+
#
|
66
|
+
# @param name [Symbol]
|
67
|
+
# @return [Symbol] attribute name
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# class User < Fear::Struct
|
71
|
+
# attribute :id
|
72
|
+
# attribute :email
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
def attribute(name)
|
76
|
+
name.to_sym.tap do |symbolized_name|
|
77
|
+
@attributes << symbolized_name
|
78
|
+
attr_reader symbolized_name
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Members of this struct
|
83
|
+
#
|
84
|
+
# @return [<Symbol>]
|
85
|
+
def attributes
|
86
|
+
@attributes.dup
|
87
|
+
end
|
88
|
+
|
89
|
+
# Creates new struct with given attributes
|
90
|
+
# @param members [<Symbol>]
|
91
|
+
# @return [Fear::Struct]
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# User = Fear::Struct.with_attributes(:id, :email, :admin) do
|
95
|
+
# def admin?
|
96
|
+
# @admin
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
def with_attributes(*members, &block)
|
101
|
+
members = members
|
102
|
+
block = block
|
103
|
+
|
104
|
+
Class.new(self) do
|
105
|
+
members.each { |member| attribute(member) }
|
106
|
+
class_eval(&block) if block
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# @param attributes [{Symbol => any}]
|
112
|
+
def initialize(**attributes)
|
113
|
+
_check_missing_attributes!(attributes)
|
114
|
+
_check_unknown_attributes!(attributes)
|
115
|
+
|
116
|
+
@values = members.each_with_object([]) do |name, values|
|
117
|
+
attributes.fetch(name).tap do |value|
|
118
|
+
_set_attribute(name, value)
|
119
|
+
values << value
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Creates a shallow copy of this struct optionally changing the attributes arguments.
|
125
|
+
# @param attributes [{Symbol => any}]
|
126
|
+
#
|
127
|
+
# @example
|
128
|
+
# User = Fear::Struct.new(:id, :email, :admin)
|
129
|
+
# john = User.new(id: 2, email: 'john@example.com', admin: false)
|
130
|
+
# john.admin #=> false
|
131
|
+
# admin_john = john.copy(admin: true)
|
132
|
+
# admin_john.admin #=> true
|
133
|
+
#
|
134
|
+
def copy(**attributes)
|
135
|
+
self.class.new(to_h.merge(attributes))
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns the struct attributes as an array of symbols
|
139
|
+
# @return [<Symbol>]
|
140
|
+
#
|
141
|
+
# @example
|
142
|
+
# User = Fear::Struct.new(:id, :email, :admin)
|
143
|
+
# john = User.new(email: 'john@example.com', admin: false, id: 2)
|
144
|
+
# john.attributes #=> [:id, :email, :admin]
|
145
|
+
#
|
146
|
+
def members
|
147
|
+
self.class.attributes
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns the values for this struct as an Array.
|
151
|
+
# @return [Array]
|
152
|
+
#
|
153
|
+
# @example
|
154
|
+
# User = Fear::Struct.new(:id, :email, :admin)
|
155
|
+
# john = User.new(email: 'john@example.com', admin: false, id: 2)
|
156
|
+
# john.to_a #=> [2, 'john@example.com', false]
|
157
|
+
#
|
158
|
+
def to_a
|
159
|
+
@values.dup
|
160
|
+
end
|
161
|
+
|
162
|
+
# @overload to_h()
|
163
|
+
# Returns a Hash containing the names and values for the struct's attributes
|
164
|
+
# @return [{Symbol => any}]
|
165
|
+
#
|
166
|
+
# @overload to_h(&block)
|
167
|
+
# Applies block to pairs of name name and value and use them to construct hash
|
168
|
+
# @yieldparam pair [<Symbol, any>] yields pair of name name and value
|
169
|
+
# @return [{Symbol => any}]
|
170
|
+
#
|
171
|
+
# @example
|
172
|
+
# User = Fear::Struct.new(:id, :email, :admin)
|
173
|
+
# john = User.new(email: 'john@example.com', admin: false, id: 2)
|
174
|
+
# john.to_h #=> {id: 2, email: 'john@example.com', admin: false}
|
175
|
+
# john.to_h do |key, value|
|
176
|
+
# [key.to_s, value]
|
177
|
+
# end #=> {'id' => 2, 'email' => 'john@example.com', 'admin' => false}
|
178
|
+
#
|
179
|
+
def to_h(&block)
|
180
|
+
pairs = members.zip(@values)
|
181
|
+
if block_given?
|
182
|
+
Hash[pairs.map(&block)]
|
183
|
+
else
|
184
|
+
Hash[pairs]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# @param other [any]
|
189
|
+
# @return [Boolean]
|
190
|
+
def ==(other)
|
191
|
+
other.is_a?(other.class) && to_h == other.to_h
|
192
|
+
end
|
193
|
+
|
194
|
+
INSPECT_TEMPLATE = "<#Fear::Struct %{class_name} %{attributes}>"
|
195
|
+
private_constant :INSPECT_TEMPLATE
|
196
|
+
|
197
|
+
# @return [String]
|
198
|
+
#
|
199
|
+
# @example
|
200
|
+
# User = Fear::Struct.with_attributes(:id, :email)
|
201
|
+
# user = User.new(id: 2, email: 'john@exmaple.com')
|
202
|
+
# user.inspect #=> "<#Fear::Struct User id=2, email=>'john@exmaple.com'>"
|
203
|
+
#
|
204
|
+
def inspect
|
205
|
+
attributes = to_h.map { |key, value| "#{key}=#{value.inspect}" }.join(", ")
|
206
|
+
|
207
|
+
format(INSPECT_TEMPLATE, class_name: self.class.name, attributes: attributes)
|
208
|
+
end
|
209
|
+
alias to_s inspect
|
210
|
+
|
211
|
+
MISSING_KEYWORDS_ERROR = "missing keywords: %{keywords}"
|
212
|
+
private_constant :MISSING_KEYWORDS_ERROR
|
213
|
+
|
214
|
+
private def _check_missing_attributes!(provided_attributes)
|
215
|
+
missing_attributes = members - provided_attributes.keys
|
216
|
+
|
217
|
+
unless missing_attributes.empty?
|
218
|
+
raise ArgumentError, format(MISSING_KEYWORDS_ERROR, keywords: missing_attributes.join(", "))
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
UNKNOWN_KEYWORDS_ERROR = "unknown keywords: %{keywords}"
|
223
|
+
private_constant :UNKNOWN_KEYWORDS_ERROR
|
224
|
+
|
225
|
+
private def _check_unknown_attributes!(provided_attributes)
|
226
|
+
unknown_attributes = provided_attributes.keys - members
|
227
|
+
|
228
|
+
unless unknown_attributes.empty?
|
229
|
+
raise ArgumentError, format(UNKNOWN_KEYWORDS_ERROR, keywords: unknown_attributes.join(", "))
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# @return [void]
|
234
|
+
private def _set_attribute(name, value)
|
235
|
+
instance_variable_set(:"@#{name}", value)
|
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
|
247
|
+
end
|
248
|
+
end
|