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.
Files changed (155) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/rubocop.yml +39 -0
  3. data/.github/workflows/spec.yml +42 -0
  4. data/.gitignore +0 -1
  5. data/.rubocop.yml +4 -12
  6. data/.simplecov +17 -0
  7. data/CHANGELOG.md +40 -0
  8. data/Gemfile +5 -2
  9. data/Gemfile.lock +130 -0
  10. data/LICENSE.txt +1 -1
  11. data/README.md +1293 -97
  12. data/Rakefile +369 -1
  13. data/benchmarks/README.md +1 -0
  14. data/benchmarks/dry_do_vs_fear_for.txt +11 -0
  15. data/benchmarks/dry_some_fmap_vs_fear_some_map.txt +11 -0
  16. data/benchmarks/factorial.txt +16 -0
  17. data/benchmarks/fear_gaurd_and1_vs_new.txt +13 -0
  18. data/benchmarks/fear_gaurd_and2_vs_and.txt +13 -0
  19. data/benchmarks/fear_gaurd_and3_vs_and_and.txt +13 -0
  20. data/benchmarks/fear_pattern_extracting_with_vs_without_cache.txt +11 -0
  21. data/benchmarks/fear_pattern_matching_construction_vs_execution.txt +13 -0
  22. data/benchmarks/pattern_matching_dry_vs_qo_vs_fear_try.txt +14 -0
  23. data/benchmarks/pattern_matching_qo_vs_fear_pattern_extraction.txt +11 -0
  24. data/benchmarks/pattern_matching_qo_vs_fear_try_execution.txt +11 -0
  25. data/examples/pattern_extracting.rb +17 -0
  26. data/examples/pattern_extracting_ruby2.7.rb +15 -0
  27. data/examples/pattern_matching_binary_tree_set.rb +101 -0
  28. data/examples/pattern_matching_number_in_words.rb +60 -0
  29. data/fear.gemspec +34 -23
  30. data/lib/dry/types/fear.rb +8 -0
  31. data/lib/dry/types/fear/option.rb +125 -0
  32. data/lib/fear.rb +65 -15
  33. data/lib/fear/await.rb +33 -0
  34. data/lib/fear/awaitable.rb +28 -0
  35. data/lib/fear/either.rb +131 -71
  36. data/lib/fear/either_api.rb +23 -0
  37. data/lib/fear/either_pattern_match.rb +53 -0
  38. data/lib/fear/empty_partial_function.rb +38 -0
  39. data/lib/fear/extractor.rb +112 -0
  40. data/lib/fear/extractor/anonymous_array_splat_matcher.rb +10 -0
  41. data/lib/fear/extractor/any_matcher.rb +17 -0
  42. data/lib/fear/extractor/array_head_matcher.rb +36 -0
  43. data/lib/fear/extractor/array_matcher.rb +40 -0
  44. data/lib/fear/extractor/array_splat_matcher.rb +16 -0
  45. data/lib/fear/extractor/empty_list_matcher.rb +20 -0
  46. data/lib/fear/extractor/extractor_matcher.rb +44 -0
  47. data/lib/fear/extractor/grammar.rb +203 -0
  48. data/lib/fear/extractor/grammar.treetop +129 -0
  49. data/lib/fear/extractor/identifier_matcher.rb +18 -0
  50. data/lib/fear/extractor/matcher.rb +53 -0
  51. data/lib/fear/extractor/matcher/and.rb +38 -0
  52. data/lib/fear/extractor/named_array_splat_matcher.rb +17 -0
  53. data/lib/fear/extractor/pattern.rb +58 -0
  54. data/lib/fear/extractor/typed_identifier_matcher.rb +26 -0
  55. data/lib/fear/extractor/value_matcher.rb +19 -0
  56. data/lib/fear/extractor_api.rb +35 -0
  57. data/lib/fear/failure.rb +46 -14
  58. data/lib/fear/failure_pattern_match.rb +10 -0
  59. data/lib/fear/for.rb +37 -95
  60. data/lib/fear/for_api.rb +68 -0
  61. data/lib/fear/future.rb +497 -0
  62. data/lib/fear/future_api.rb +21 -0
  63. data/lib/fear/left.rb +19 -2
  64. data/lib/fear/left_pattern_match.rb +11 -0
  65. data/lib/fear/none.rb +67 -3
  66. data/lib/fear/none_pattern_match.rb +14 -0
  67. data/lib/fear/option.rb +120 -56
  68. data/lib/fear/option_api.rb +40 -0
  69. data/lib/fear/option_pattern_match.rb +48 -0
  70. data/lib/fear/partial_function.rb +176 -0
  71. data/lib/fear/partial_function/and_then.rb +50 -0
  72. data/lib/fear/partial_function/any.rb +28 -0
  73. data/lib/fear/partial_function/combined.rb +53 -0
  74. data/lib/fear/partial_function/empty.rb +10 -0
  75. data/lib/fear/partial_function/guard.rb +80 -0
  76. data/lib/fear/partial_function/guard/and.rb +38 -0
  77. data/lib/fear/partial_function/guard/and3.rb +41 -0
  78. data/lib/fear/partial_function/guard/or.rb +38 -0
  79. data/lib/fear/partial_function/lifted.rb +23 -0
  80. data/lib/fear/partial_function/or_else.rb +64 -0
  81. data/lib/fear/partial_function_class.rb +38 -0
  82. data/lib/fear/pattern_match.rb +114 -0
  83. data/lib/fear/pattern_matching_api.rb +137 -0
  84. data/lib/fear/promise.rb +95 -0
  85. data/lib/fear/right.rb +20 -2
  86. data/lib/fear/right_biased.rb +6 -14
  87. data/lib/fear/right_pattern_match.rb +11 -0
  88. data/lib/fear/some.rb +55 -3
  89. data/lib/fear/some_pattern_match.rb +13 -0
  90. data/lib/fear/struct.rb +248 -0
  91. data/lib/fear/success.rb +35 -5
  92. data/lib/fear/success_pattern_match.rb +12 -0
  93. data/lib/fear/try.rb +136 -79
  94. data/lib/fear/try_api.rb +33 -0
  95. data/lib/fear/try_pattern_match.rb +33 -0
  96. data/lib/fear/unit.rb +32 -0
  97. data/lib/fear/utils.rb +39 -14
  98. data/lib/fear/version.rb +4 -1
  99. data/spec/dry/types/fear/option/constrained_spec.rb +22 -0
  100. data/spec/dry/types/fear/option/core_spec.rb +77 -0
  101. data/spec/dry/types/fear/option/default_spec.rb +21 -0
  102. data/spec/dry/types/fear/option/hash_spec.rb +58 -0
  103. data/spec/dry/types/fear/option/option_spec.rb +97 -0
  104. data/spec/fear/awaitable_spec.rb +17 -0
  105. data/spec/fear/done_spec.rb +8 -6
  106. data/spec/fear/either/mixin_spec.rb +17 -0
  107. data/spec/fear/either_pattern_match_spec.rb +37 -0
  108. data/spec/fear/either_pattern_matching_spec.rb +28 -0
  109. data/spec/fear/extractor/array_matcher_spec.rb +230 -0
  110. data/spec/fear/extractor/extractor_matcher_spec.rb +153 -0
  111. data/spec/fear/extractor/grammar_array_spec.rb +25 -0
  112. data/spec/fear/extractor/identified_matcher_spec.rb +49 -0
  113. data/spec/fear/extractor/identifier_matcher_spec.rb +68 -0
  114. data/spec/fear/extractor/pattern_spec.rb +34 -0
  115. data/spec/fear/extractor/typed_identifier_matcher_spec.rb +64 -0
  116. data/spec/fear/extractor/value_matcher_number_spec.rb +79 -0
  117. data/spec/fear/extractor/value_matcher_string_spec.rb +88 -0
  118. data/spec/fear/extractor/value_matcher_symbol_spec.rb +71 -0
  119. data/spec/fear/extractor_api_spec.rb +115 -0
  120. data/spec/fear/extractor_spec.rb +61 -0
  121. data/spec/fear/failure_spec.rb +145 -45
  122. data/spec/fear/for_spec.rb +57 -67
  123. data/spec/fear/future_spec.rb +691 -0
  124. data/spec/fear/guard_spec.rb +103 -0
  125. data/spec/fear/left_spec.rb +112 -46
  126. data/spec/fear/none_spec.rb +114 -16
  127. data/spec/fear/option/mixin_spec.rb +39 -0
  128. data/spec/fear/option_pattern_match_spec.rb +35 -0
  129. data/spec/fear/option_pattern_matching_spec.rb +34 -0
  130. data/spec/fear/option_spec.rb +121 -8
  131. data/spec/fear/partial_function/empty_spec.rb +38 -0
  132. data/spec/fear/partial_function_and_then_spec.rb +147 -0
  133. data/spec/fear/partial_function_composition_spec.rb +82 -0
  134. data/spec/fear/partial_function_or_else_spec.rb +276 -0
  135. data/spec/fear/partial_function_spec.rb +239 -0
  136. data/spec/fear/pattern_match_spec.rb +93 -0
  137. data/spec/fear/pattern_matching_api_spec.rb +31 -0
  138. data/spec/fear/promise_spec.rb +96 -0
  139. data/spec/fear/right_biased/left.rb +29 -32
  140. data/spec/fear/right_biased/right.rb +51 -54
  141. data/spec/fear/right_spec.rb +109 -41
  142. data/spec/fear/some_spec.rb +80 -15
  143. data/spec/fear/success_spec.rb +99 -32
  144. data/spec/fear/try/mixin_spec.rb +19 -0
  145. data/spec/fear/try_pattern_match_spec.rb +37 -0
  146. data/spec/fear/try_pattern_matching_spec.rb +34 -0
  147. data/spec/fear/utils_spec.rb +16 -14
  148. data/spec/spec_helper.rb +13 -7
  149. data/spec/struct_pattern_matching_spec.rb +36 -0
  150. data/spec/struct_spec.rb +226 -0
  151. data/spec/support/dry_types.rb +6 -0
  152. metadata +320 -29
  153. data/.travis.yml +0 -9
  154. data/lib/fear/done.rb +0 -22
  155. data/lib/fear/for/evaluation_context.rb +0 -91
@@ -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
@@ -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(_, reduce_right)
54
- reduce_right.call(value)
71
+ def reduce(_reduce_left, reduce_right)
72
+ reduce_right.(value)
55
73
  end
56
74
 
57
75
  # @return [Either]
@@ -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!('get_or_else', *args, &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!('or_else', *args, &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.new
166
+ None
175
167
  end
176
168
 
177
169
  # @return [false]
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fear
4
+ # @api private
5
+ class RightPatternMatch < EitherPatternMatch
6
+ def left(*)
7
+ self
8
+ end
9
+ alias failure left
10
+ end
11
+ end
@@ -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.new
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.new
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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fear
4
+ # @api private
5
+ class SomePatternMatch < OptionPatternMatch
6
+ # @return [Fear::OptionPatternMatch]
7
+ def none
8
+ self
9
+ end
10
+ end
11
+
12
+ private_constant :SomePatternMatch
13
+ end
@@ -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