fear 0.9.0 → 1.2.0

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