fear 1.0.0 → 2.0.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +27 -0
  3. data/.github/workflows/rubocop.yml +39 -0
  4. data/.github/workflows/spec.yml +42 -0
  5. data/.rubocop.yml +4 -60
  6. data/.simplecov +17 -0
  7. data/CHANGELOG.md +29 -1
  8. data/Gemfile +5 -5
  9. data/Gemfile.lock +86 -50
  10. data/README.md +240 -209
  11. data/Rakefile +72 -65
  12. data/examples/pattern_extracting.rb +10 -8
  13. data/examples/pattern_matching_binary_tree_set.rb +7 -2
  14. data/examples/pattern_matching_number_in_words.rb +48 -42
  15. data/fear.gemspec +33 -34
  16. data/lib/dry/types/fear/option.rb +125 -0
  17. data/lib/dry/types/fear.rb +8 -0
  18. data/lib/fear/await.rb +33 -0
  19. data/lib/fear/awaitable.rb +28 -0
  20. data/lib/fear/either.rb +15 -4
  21. data/lib/fear/either_api.rb +4 -0
  22. data/lib/fear/either_pattern_match.rb +9 -5
  23. data/lib/fear/empty_partial_function.rb +3 -1
  24. data/lib/fear/failure.rb +7 -7
  25. data/lib/fear/failure_pattern_match.rb +4 -0
  26. data/lib/fear/for.rb +4 -2
  27. data/lib/fear/for_api.rb +5 -1
  28. data/lib/fear/future.rb +157 -82
  29. data/lib/fear/future_api.rb +17 -4
  30. data/lib/fear/left.rb +3 -9
  31. data/lib/fear/left_pattern_match.rb +2 -0
  32. data/lib/fear/none.rb +28 -10
  33. data/lib/fear/none_pattern_match.rb +2 -0
  34. data/lib/fear/option.rb +30 -2
  35. data/lib/fear/option_api.rb +4 -0
  36. data/lib/fear/option_pattern_match.rb +8 -3
  37. data/lib/fear/partial_function/and_then.rb +4 -2
  38. data/lib/fear/partial_function/any.rb +2 -0
  39. data/lib/fear/partial_function/combined.rb +3 -1
  40. data/lib/fear/partial_function/empty.rb +6 -0
  41. data/lib/fear/partial_function/guard/and.rb +2 -0
  42. data/lib/fear/partial_function/guard/and3.rb +2 -0
  43. data/lib/fear/partial_function/guard/or.rb +2 -0
  44. data/lib/fear/partial_function/guard.rb +8 -6
  45. data/lib/fear/partial_function/lifted.rb +2 -0
  46. data/lib/fear/partial_function/or_else.rb +5 -1
  47. data/lib/fear/partial_function.rb +18 -9
  48. data/lib/fear/partial_function_class.rb +3 -1
  49. data/lib/fear/pattern_match.rb +3 -11
  50. data/lib/fear/pattern_matching_api.rb +6 -28
  51. data/lib/fear/promise.rb +7 -5
  52. data/lib/fear/right.rb +3 -9
  53. data/lib/fear/right_biased.rb +5 -3
  54. data/lib/fear/right_pattern_match.rb +4 -0
  55. data/lib/fear/some.rb +35 -8
  56. data/lib/fear/some_pattern_match.rb +2 -0
  57. data/lib/fear/struct.rb +237 -0
  58. data/lib/fear/success.rb +7 -8
  59. data/lib/fear/success_pattern_match.rb +4 -0
  60. data/lib/fear/try.rb +8 -2
  61. data/lib/fear/try_api.rb +4 -0
  62. data/lib/fear/try_pattern_match.rb +9 -5
  63. data/lib/fear/unit.rb +6 -2
  64. data/lib/fear/utils.rb +14 -2
  65. data/lib/fear/version.rb +4 -1
  66. data/lib/fear.rb +26 -44
  67. data/spec/dry/types/fear/option/constrained_spec.rb +22 -0
  68. data/spec/dry/types/fear/option/core_spec.rb +77 -0
  69. data/spec/dry/types/fear/option/default_spec.rb +21 -0
  70. data/spec/dry/types/fear/option/hash_spec.rb +58 -0
  71. data/spec/dry/types/fear/option/option_spec.rb +97 -0
  72. data/spec/fear/awaitable_spec.rb +19 -0
  73. data/spec/fear/done_spec.rb +7 -5
  74. data/spec/fear/either/mixin_spec.rb +4 -2
  75. data/spec/fear/either_pattern_match_spec.rb +10 -8
  76. data/spec/fear/either_pattern_matching_spec.rb +28 -0
  77. data/spec/fear/either_spec.rb +26 -0
  78. data/spec/fear/failure_spec.rb +57 -70
  79. data/spec/fear/for/mixin_spec.rb +15 -0
  80. data/spec/fear/for_spec.rb +19 -17
  81. data/spec/fear/future_spec.rb +477 -237
  82. data/spec/fear/guard_spec.rb +136 -24
  83. data/spec/fear/left_spec.rb +57 -70
  84. data/spec/fear/none_spec.rb +39 -43
  85. data/spec/fear/option/mixin_spec.rb +9 -7
  86. data/spec/fear/option_pattern_match_spec.rb +10 -8
  87. data/spec/fear/option_pattern_matching_spec.rb +34 -0
  88. data/spec/fear/option_spec.rb +142 -0
  89. data/spec/fear/partial_function/any_spec.rb +25 -0
  90. data/spec/fear/partial_function/empty_spec.rb +12 -10
  91. data/spec/fear/partial_function_and_then_spec.rb +39 -37
  92. data/spec/fear/partial_function_composition_spec.rb +46 -44
  93. data/spec/fear/partial_function_or_else_spec.rb +92 -90
  94. data/spec/fear/partial_function_spec.rb +91 -61
  95. data/spec/fear/pattern_match_spec.rb +19 -51
  96. data/spec/fear/pattern_matching_api_spec.rb +31 -0
  97. data/spec/fear/promise_spec.rb +23 -23
  98. data/spec/fear/right_biased/left.rb +28 -26
  99. data/spec/fear/right_biased/right.rb +51 -49
  100. data/spec/fear/right_spec.rb +48 -68
  101. data/spec/fear/some_spec.rb +30 -40
  102. data/spec/fear/success_spec.rb +40 -60
  103. data/spec/fear/try/mixin_spec.rb +19 -3
  104. data/spec/fear/try_api_spec.rb +23 -0
  105. data/spec/fear/try_pattern_match_spec.rb +10 -8
  106. data/spec/fear/try_pattern_matching_spec.rb +34 -0
  107. data/spec/fear/utils_spec.rb +16 -14
  108. data/spec/spec_helper.rb +13 -7
  109. data/spec/struct_pattern_matching_spec.rb +36 -0
  110. data/spec/struct_spec.rb +194 -0
  111. data/spec/support/dry_types.rb +6 -0
  112. metadata +128 -87
  113. data/.travis.yml +0 -13
  114. data/lib/fear/extractor/anonymous_array_splat_matcher.rb +0 -8
  115. data/lib/fear/extractor/any_matcher.rb +0 -15
  116. data/lib/fear/extractor/array_head_matcher.rb +0 -34
  117. data/lib/fear/extractor/array_matcher.rb +0 -38
  118. data/lib/fear/extractor/array_splat_matcher.rb +0 -14
  119. data/lib/fear/extractor/empty_list_matcher.rb +0 -18
  120. data/lib/fear/extractor/extractor_matcher.rb +0 -42
  121. data/lib/fear/extractor/grammar.rb +0 -201
  122. data/lib/fear/extractor/grammar.treetop +0 -129
  123. data/lib/fear/extractor/identifier_matcher.rb +0 -16
  124. data/lib/fear/extractor/matcher/and.rb +0 -36
  125. data/lib/fear/extractor/matcher.rb +0 -54
  126. data/lib/fear/extractor/named_array_splat_matcher.rb +0 -15
  127. data/lib/fear/extractor/pattern.rb +0 -55
  128. data/lib/fear/extractor/typed_identifier_matcher.rb +0 -24
  129. data/lib/fear/extractor/value_matcher.rb +0 -17
  130. data/lib/fear/extractor.rb +0 -108
  131. data/lib/fear/extractor_api.rb +0 -33
  132. data/spec/fear/extractor/array_matcher_spec.rb +0 -228
  133. data/spec/fear/extractor/extractor_matcher_spec.rb +0 -151
  134. data/spec/fear/extractor/grammar_array_spec.rb +0 -23
  135. data/spec/fear/extractor/identified_matcher_spec.rb +0 -47
  136. data/spec/fear/extractor/identifier_matcher_spec.rb +0 -66
  137. data/spec/fear/extractor/pattern_spec.rb +0 -32
  138. data/spec/fear/extractor/typed_identifier_matcher_spec.rb +0 -62
  139. data/spec/fear/extractor/value_matcher_number_spec.rb +0 -77
  140. data/spec/fear/extractor/value_matcher_string_spec.rb +0 -86
  141. data/spec/fear/extractor/value_matcher_symbol_spec.rb +0 -69
  142. data/spec/fear/extractor_api_spec.rb +0 -113
  143. data/spec/fear/extractor_spec.rb +0 -59
@@ -1,6 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fear/empty_partial_function"
4
+
1
5
  module Fear
2
6
  module PartialFunction
3
7
  EMPTY = EmptyPartialFunction.new
4
8
  EMPTY.freeze
9
+
10
+ public_constant :EMPTY
5
11
  end
6
12
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  class Guard
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  class Guard
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  class Guard
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fear/partial_function/guard/or"
4
+ require "fear/partial_function/guard/and"
5
+ require "fear/partial_function/guard/and3"
6
+
1
7
  module Fear
2
8
  module PartialFunction
3
9
  # Guard represents PartialFunction guardian
4
10
  #
5
11
  # @api private
6
12
  class Guard
7
- autoload :And, 'fear/partial_function/guard/and'
8
- autoload :And3, 'fear/partial_function/guard/and3'
9
- autoload :Or, 'fear/partial_function/guard/or'
10
-
11
13
  class << self
12
14
  # Optimized version for combination of two guardians
13
15
  # Two guarding is a very common situation. For example checking for Some, and checking
@@ -35,7 +37,7 @@ module Fear
35
37
  when 0 then Any
36
38
  else
37
39
  head, *tail = conditions
38
- tail.inject(new(head)) { |acc, condition| acc.and(new(condition)) }
40
+ tail.reduce(new(head)) { |acc, condition| acc.and(new(condition)) }
39
41
  end
40
42
  end
41
43
 
@@ -45,7 +47,7 @@ module Fear
45
47
  return Any if conditions.empty?
46
48
 
47
49
  head, *tail = conditions
48
- tail.inject(new(head)) { |acc, condition| acc.or(new(condition)) }
50
+ tail.reduce(new(head)) { |acc, condition| acc.or(new(condition)) }
49
51
  end
50
52
  end
51
53
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  # @api private
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fear/utils"
4
+
1
5
  module Fear
2
6
  module PartialFunction
3
7
  # Composite function produced by +PartialFunction#or_else+ method
@@ -36,7 +40,7 @@ module Fear
36
40
 
37
41
  # @see Fear::PartialFunction#and_then
38
42
  def and_then(other = Utils::UNDEFINED, &block)
39
- Utils.with_block_or_argument('Fear::PartialFunction::OrElse#and_then', other, block) do |fun|
43
+ Utils.with_block_or_argument("Fear::PartialFunction::OrElse#and_then", other, block) do |fun|
40
44
  OrElse.new(f1.and_then(&fun), f2.and_then(&fun))
41
45
  end
42
46
  end
@@ -1,3 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fear/partial_function/and_then"
4
+ require "fear/partial_function/any"
5
+ require "fear/partial_function/combined"
6
+ require "fear/partial_function/empty"
7
+ require "fear/partial_function/guard"
8
+ require "fear/partial_function/lifted"
9
+ require "fear/partial_function/or_else"
10
+ require "fear/partial_function_class"
11
+
1
12
  module Fear
2
13
  # A partial function is a unary function defined on subset of all possible inputs.
3
14
  # The method +defined_at?+ allows to test dynamically if an arg is in
@@ -43,14 +54,6 @@ module Fear
43
54
  # @return [#call]
44
55
  # @abstract
45
56
  module PartialFunction
46
- autoload :AndThen, 'fear/partial_function/and_then'
47
- autoload :Any, 'fear/partial_function/any'
48
- autoload :Combined, 'fear/partial_function/combined'
49
- autoload :EMPTY, 'fear/partial_function/empty'
50
- autoload :Guard, 'fear/partial_function/guard'
51
- autoload :Lifted, 'fear/partial_function/lifted'
52
- autoload :OrElse, 'fear/partial_function/or_else'
53
-
54
57
  # Checks if a value is contained in the function's domain.
55
58
  #
56
59
  # @param arg [any]
@@ -97,6 +100,12 @@ module Fear
97
100
  # @param other [PartialFunction]
98
101
  # @return [PartialFunction] a partial function which has as domain the union of the domains
99
102
  # of this partial function and +other+.
103
+ # @example
104
+ # handle_even = Fear.case(:even?.to_proc) { |x| "#{x} is even" }
105
+ # handle_odd = Fear.case(:odd?.to_proc) { |x| "#{x} is odd" }
106
+ # handle_even_or_odd = handle_even.or_else(odd)
107
+ # handle_even_or_odd.(42) #=> 42 is even
108
+ # handle_even_or_odd.(42) #=> 21 is odd
100
109
  def or_else(other)
101
110
  OrElse.new(self, other)
102
111
  end
@@ -125,7 +134,7 @@ module Fear
125
134
  # @return [Fear::PartialFunction]
126
135
  #
127
136
  def and_then(other = Utils::UNDEFINED, &block)
128
- Utils.with_block_or_argument('Fear::PartialFunction#and_then', other, block) do |fun|
137
+ Utils.with_block_or_argument("Fear::PartialFunction#and_then", other, block) do |fun|
129
138
  if fun.is_a?(Fear::PartialFunction)
130
139
  Combined.new(self, fun)
131
140
  else
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # @api private
3
5
  class PartialFunctionClass
@@ -25,7 +27,7 @@ module Fear
25
27
  # @yield [arg] if function not defined
26
28
  def call_or_else(arg)
27
29
  if defined_at?(arg)
28
- function.call(arg)
30
+ function.(arg)
29
31
  else
30
32
  yield arg
31
33
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # Pattern match builder. Provides DSL for building pattern matcher
3
5
  # Pattern match is just a combination of partial functions
@@ -59,7 +61,7 @@ module Fear
59
61
 
60
62
  Module.new do
61
63
  define_method(as) do |&matchers|
62
- matcher_class.new(&matchers).call(self)
64
+ matcher_class.new(&matchers).(self)
63
65
  end
64
66
  end
65
67
  end
@@ -93,16 +95,6 @@ module Fear
93
95
  or_else(Fear.case(*guards, &effect))
94
96
  end
95
97
 
96
- # @param pattern [String]
97
- # @param guards [<#===>]
98
- # @param effect [Proc]
99
- # @return [Fear::PartialFunction]
100
- # @see #case for details
101
- # @see Fear.xcase for details
102
- def xcase(pattern, *guards, &effect)
103
- or_else(Fear.xcase(pattern, *guards, &effect))
104
- end
105
-
106
98
  # @see Fear::PartialFunction#or_else
107
99
  def or_else(other)
108
100
  self.result = result.or_else(other)
@@ -1,3 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fear/partial_function"
4
+ require "fear/pattern_match"
5
+
1
6
  module Fear
2
7
  # @api private
3
8
  module PatternMatchingApi
@@ -42,12 +47,6 @@ module Fear
42
47
  # m.case(Integer, :even?.to_proc) { |x| ... }
43
48
  # m.case(Integer, :odd?.to_proc) { |x| ... }
44
49
  #
45
- # If you want to perform pattern destruction, use +#xcase+ method
46
- #
47
- # m.xcase('Date(year, 12, 31)') { |year:| "Last day of the year #{year}" }
48
- #
49
- # The pattern above ensures that it's 31 of December and extracts year to block named parameter
50
- #
51
50
  # Since matcher returns +Fear::PartialFunction+, you can combine matchers using
52
51
  # partial function API:
53
52
  #
@@ -83,7 +82,7 @@ module Fear
83
82
  # @yieldparam matcher [Fear::PartialFunction]
84
83
  # @return [any]
85
84
  def match(value, &block)
86
- matcher(&block).call(value)
85
+ matcher(&block).(value)
87
86
  end
88
87
 
89
88
  # Creates partial function defined on domain described with guards
@@ -109,26 +108,5 @@ module Fear
109
108
  def case(*guards, &function)
110
109
  PartialFunction.and(*guards, &function)
111
110
  end
112
-
113
- # Creates partial function defined on domain described with guard
114
- # and perform pattern extraction.
115
- #
116
- # @param pattern [String] pattern to match against
117
- # @param guards [<#===>] other guards against extracted pattern
118
- # @yieldparam hash [{Symbol => any}]
119
- # @return [Fear::PartialFunction]
120
- #
121
- # @example
122
- # pf = Fear.xcase('['ok', Some(body)]') { |body:| ... }
123
- # pf.defined_at?(['ok', Fear.some(body)]) #=> true
124
- # pf.defined_at?(['err', Fear.none]) #=> false
125
- #
126
- # @example pattern and guards. It matches against non-empty body
127
- #
128
- # pf = Fear.xcase('['ok', Some(body)]', ->(body:) { !body.empty? }) { }
129
- #
130
- def xcase(pattern, *guards, &function)
131
- Fear[pattern].and_then(self.case(*guards, &function))
132
- end
133
111
  end
134
112
  end
data/lib/fear/promise.rb CHANGED
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # @api private
3
5
  class Promise < Concurrent::IVar
4
- # @param options [Hash] options passed to underlying +Concurrent::Future+
5
- def initialize(options = {})
6
+ # @param options [Hash] options passed to underlying +Concurrent::Promise+
7
+ def initialize(*_, **options)
6
8
  super()
7
9
  @options = options
8
10
  @promise = Concurrent::Promise.new(options) do
@@ -19,7 +21,7 @@ module Fear
19
21
 
20
22
  # @return [Fear::Future]
21
23
  def to_future
22
- Future.new(promise, options)
24
+ Future.new(promise, **options)
23
25
  end
24
26
 
25
27
  # Complete this promise with successful result
@@ -30,7 +32,7 @@ module Fear
30
32
  complete(Fear.success(value))
31
33
  end
32
34
 
33
- # Complete this promise with failure
35
+ # Complete this promise with value
34
36
  # @param value [any]
35
37
  # @return [self]
36
38
  # @raise [IllegalStateException]
@@ -64,7 +66,7 @@ module Fear
64
66
  if complete(result)
65
67
  self
66
68
  else
67
- raise IllegalStateException, 'Promise already completed.'
69
+ raise IllegalStateException, "Promise already completed."
68
70
  end
69
71
  end
70
72
 
data/lib/fear/right.rb CHANGED
@@ -1,17 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  class Right
3
5
  include Either
4
6
  include RightBiased::Right
5
7
  include RightPatternMatch.mixin
6
8
 
7
- EXTRACTOR = proc do |either|
8
- if Fear::Right === either
9
- Fear.some([either.right_value])
10
- else
11
- Fear.none
12
- end
13
- end
14
-
15
9
  # @api private
16
10
  def right_value
17
11
  value
@@ -65,7 +59,7 @@ module Fear
65
59
  # @param reduce_right [Proc]
66
60
  # @return [any]
67
61
  def reduce(_reduce_left, reduce_right)
68
- reduce_right.call(value)
62
+ reduce_right.(value)
69
63
  end
70
64
 
71
65
  # @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
@@ -109,7 +111,7 @@ module Fear
109
111
 
110
112
  module Left
111
113
  prepend Interface
112
- include Utils
114
+
113
115
  # @!method get_or_else(default)
114
116
  # @param default [any]
115
117
  # @return [any] default value
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fear/either_pattern_match"
4
+
1
5
  module Fear
2
6
  # @api private
3
7
  class RightPatternMatch < EitherPatternMatch
data/lib/fear/some.rb CHANGED
@@ -1,17 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  class Some
3
5
  include Option
4
6
  include RightBiased::Right
5
7
  include SomePatternMatch.mixin
6
8
 
7
- EXTRACTOR = proc do |option|
8
- if Fear::Some === option
9
- Fear.some([option.get])
10
- else
11
- Fear.none
12
- end
13
- end
14
-
15
9
  attr_reader :value
16
10
  protected :value
17
11
 
@@ -34,6 +28,13 @@ module Fear
34
28
  false
35
29
  end
36
30
 
31
+ alias :blank? :empty?
32
+
33
+ # @return [true]
34
+ def present?
35
+ true
36
+ end
37
+
37
38
  # @return [Option]
38
39
  def select
39
40
  if yield(value)
@@ -65,5 +66,31 @@ module Fear
65
66
 
66
67
  # @return [String]
67
68
  alias to_s inspect
69
+
70
+ # @param other [Fear::Option]
71
+ # @return [Fear::Option]
72
+ def zip(other)
73
+ if other.is_a?(Option)
74
+ other.map do |x|
75
+ if block_given?
76
+ yield(value, x)
77
+ else
78
+ [value, x]
79
+ end
80
+ end
81
+ else
82
+ raise TypeError, "can't zip with #{other.class}"
83
+ end
84
+ end
85
+
86
+ # @return [RightBiased::Left, RightBiased::Right]
87
+ def filter_map(&filter)
88
+ map(&filter).select(&:itself)
89
+ end
90
+
91
+ # @return [Array<any>]
92
+ def deconstruct
93
+ [get]
94
+ end
68
95
  end
69
96
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # @api private
3
5
  class SomePatternMatch < OptionPatternMatch
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fear/pattern_match"
4
+
5
+ module Fear
6
+ # Structs are like regular classes and good for modeling immutable data.
7
+ #
8
+ # A minimal struct requires just a list of attributes:
9
+ #
10
+ # User = Fear::Struct.with_attributes(:id, :email, :admin)
11
+ # john = User.new(id: 2, email: 'john@example.com', admin: false)
12
+ #
13
+ # john.email #=> 'john@example.com'
14
+ #
15
+ # Instead of `.with_attributes` factory method you can use classic inheritance:
16
+ #
17
+ # class User < Fear::Struct
18
+ # attribute :id
19
+ # attribute :email
20
+ # attribute :admin
21
+ # end
22
+ #
23
+ # Since structs are immutable, you are not allowed to reassign their attributes
24
+ #
25
+ # john.email = ''john.doe@example.com'' #=> raises NoMethodError
26
+ #
27
+ # Two structs of the same type with the same attributes are equal
28
+ #
29
+ # john1 = User.new(id: 2, email: 'john@example.com', admin: false)
30
+ # john2 = User.new(id: 2, admin: false, email: 'john@example.com')
31
+ # john1 == john2 #=> true
32
+ #
33
+ # You can create a shallow copy of a +Struct+ by using copy method optionally changing its attributes.
34
+ #
35
+ # john = User.new(id: 2, email: 'john@example.com', admin: false)
36
+ # admin_john = john.copy(admin: true)
37
+ #
38
+ # john.admin #=> false
39
+ # admin_john.admin #=> true
40
+ #
41
+ class Struct
42
+ include PatternMatch.mixin
43
+
44
+ @attributes = [].freeze
45
+
46
+ class << self
47
+ # @param base [Fear::Struct]
48
+ # @api private
49
+ def inherited(base)
50
+ base.instance_variable_set(:@attributes, attributes)
51
+ end
52
+
53
+ # Defines attribute
54
+ #
55
+ # @param name [Symbol]
56
+ # @return [Symbol] attribute name
57
+ #
58
+ # @example
59
+ # class User < Fear::Struct
60
+ # attribute :id
61
+ # attribute :email
62
+ # end
63
+ #
64
+ def attribute(name)
65
+ name.to_sym.tap do |symbolized_name|
66
+ @attributes << symbolized_name
67
+ attr_reader symbolized_name
68
+ end
69
+ end
70
+
71
+ # Members of this struct
72
+ #
73
+ # @return [<Symbol>]
74
+ def attributes
75
+ @attributes.dup
76
+ end
77
+
78
+ # Creates new struct with given attributes
79
+ # @param members [<Symbol>]
80
+ # @return [Fear::Struct]
81
+ #
82
+ # @example
83
+ # User = Fear::Struct.with_attributes(:id, :email, :admin) do
84
+ # def admin?
85
+ # @admin
86
+ # end
87
+ # end
88
+ #
89
+ def with_attributes(*members, &block)
90
+ members = members
91
+ block = block
92
+
93
+ Class.new(self) do
94
+ members.each { |member| attribute(member) }
95
+ class_eval(&block) if block
96
+ end
97
+ end
98
+ end
99
+
100
+ # @param attributes [{Symbol => any}]
101
+ def initialize(**attributes)
102
+ _check_missing_attributes!(attributes)
103
+ _check_unknown_attributes!(attributes)
104
+
105
+ @values = members.each_with_object([]) do |name, values|
106
+ attributes.fetch(name).tap do |value|
107
+ _set_attribute(name, value)
108
+ values << value
109
+ end
110
+ end
111
+ end
112
+
113
+ # Creates a shallow copy of this struct optionally changing the attributes arguments.
114
+ # @param attributes [{Symbol => any}]
115
+ #
116
+ # @example
117
+ # User = Fear::Struct.new(:id, :email, :admin)
118
+ # john = User.new(id: 2, email: 'john@example.com', admin: false)
119
+ # john.admin #=> false
120
+ # admin_john = john.copy(admin: true)
121
+ # admin_john.admin #=> true
122
+ #
123
+ def copy(**attributes)
124
+ self.class.new(**to_h.merge(attributes))
125
+ end
126
+
127
+ # Returns the struct attributes as an array of symbols
128
+ # @return [<Symbol>]
129
+ #
130
+ # @example
131
+ # User = Fear::Struct.new(:id, :email, :admin)
132
+ # john = User.new(email: 'john@example.com', admin: false, id: 2)
133
+ # john.attributes #=> [:id, :email, :admin]
134
+ #
135
+ def members
136
+ self.class.attributes
137
+ end
138
+
139
+ # Returns the values for this struct as an Array.
140
+ # @return [Array]
141
+ #
142
+ # @example
143
+ # User = Fear::Struct.new(:id, :email, :admin)
144
+ # john = User.new(email: 'john@example.com', admin: false, id: 2)
145
+ # john.to_a #=> [2, 'john@example.com', false]
146
+ #
147
+ def to_a
148
+ @values.dup
149
+ end
150
+
151
+ # @overload to_h()
152
+ # Returns a Hash containing the names and values for the struct's attributes
153
+ # @return [{Symbol => any}]
154
+ #
155
+ # @overload to_h(&block)
156
+ # Applies block to pairs of name name and value and use them to construct hash
157
+ # @yieldparam pair [<Symbol, any>] yields pair of name name and value
158
+ # @return [{Symbol => any}]
159
+ #
160
+ # @example
161
+ # User = Fear::Struct.new(:id, :email, :admin)
162
+ # john = User.new(email: 'john@example.com', admin: false, id: 2)
163
+ # john.to_h #=> {id: 2, email: 'john@example.com', admin: false}
164
+ # john.to_h do |key, value|
165
+ # [key.to_s, value]
166
+ # end #=> {'id' => 2, 'email' => 'john@example.com', 'admin' => false}
167
+ #
168
+ def to_h(&block)
169
+ pairs = members.zip(@values)
170
+ if block_given?
171
+ Hash[pairs.map(&block)]
172
+ else
173
+ Hash[pairs]
174
+ end
175
+ end
176
+
177
+ # @param other [any]
178
+ # @return [Boolean]
179
+ def ==(other)
180
+ other.is_a?(other.class) && to_h == other.to_h
181
+ end
182
+
183
+ INSPECT_TEMPLATE = "<#Fear::Struct %{class_name} %{attributes}>"
184
+ private_constant :INSPECT_TEMPLATE
185
+
186
+ # @return [String]
187
+ #
188
+ # @example
189
+ # User = Fear::Struct.with_attributes(:id, :email)
190
+ # user = User.new(id: 2, email: 'john@exmaple.com')
191
+ # user.inspect #=> "<#Fear::Struct User id=2, email=>'john@exmaple.com'>"
192
+ #
193
+ def inspect
194
+ attributes = to_h.map { |key, value| "#{key}=#{value.inspect}" }.join(", ")
195
+
196
+ format(INSPECT_TEMPLATE, class_name: self.class.name, attributes: attributes)
197
+ end
198
+ alias to_s inspect
199
+
200
+ MISSING_KEYWORDS_ERROR = "missing keywords: %{keywords}"
201
+ private_constant :MISSING_KEYWORDS_ERROR
202
+
203
+ private def _check_missing_attributes!(provided_attributes)
204
+ missing_attributes = members - provided_attributes.keys
205
+
206
+ unless missing_attributes.empty?
207
+ raise ArgumentError, format(MISSING_KEYWORDS_ERROR, keywords: missing_attributes.join(", "))
208
+ end
209
+ end
210
+
211
+ UNKNOWN_KEYWORDS_ERROR = "unknown keywords: %{keywords}"
212
+ private_constant :UNKNOWN_KEYWORDS_ERROR
213
+
214
+ private def _check_unknown_attributes!(provided_attributes)
215
+ unknown_attributes = provided_attributes.keys - members
216
+
217
+ unless unknown_attributes.empty?
218
+ raise ArgumentError, format(UNKNOWN_KEYWORDS_ERROR, keywords: unknown_attributes.join(", "))
219
+ end
220
+ end
221
+
222
+ # @return [void]
223
+ private def _set_attribute(name, value)
224
+ instance_variable_set(:"@#{name}", value)
225
+ end
226
+
227
+ # @param keys [Hash, nil]
228
+ # @return [Hash]
229
+ def deconstruct_keys(keys)
230
+ if keys
231
+ to_h.slice(*(self.class.attributes & keys))
232
+ else
233
+ to_h
234
+ end
235
+ end
236
+ end
237
+ end