fear 1.0.0 → 1.1.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -60
  3. data/.travis.yml +8 -4
  4. data/CHANGELOG.md +7 -1
  5. data/Gemfile +5 -3
  6. data/Gemfile.lock +18 -20
  7. data/README.md +28 -14
  8. data/Rakefile +61 -60
  9. data/examples/pattern_extracting.rb +8 -6
  10. data/examples/pattern_matching_binary_tree_set.rb +4 -2
  11. data/examples/pattern_matching_number_in_words.rb +46 -42
  12. data/fear.gemspec +29 -27
  13. data/lib/fear.rb +44 -37
  14. data/lib/fear/await.rb +33 -0
  15. data/lib/fear/awaitable.rb +28 -0
  16. data/lib/fear/either.rb +2 -0
  17. data/lib/fear/either_api.rb +2 -0
  18. data/lib/fear/either_pattern_match.rb +2 -0
  19. data/lib/fear/empty_partial_function.rb +3 -1
  20. data/lib/fear/extractor.rb +30 -28
  21. data/lib/fear/extractor/anonymous_array_splat_matcher.rb +2 -0
  22. data/lib/fear/extractor/any_matcher.rb +2 -0
  23. data/lib/fear/extractor/array_head_matcher.rb +2 -0
  24. data/lib/fear/extractor/array_matcher.rb +2 -0
  25. data/lib/fear/extractor/array_splat_matcher.rb +2 -0
  26. data/lib/fear/extractor/empty_list_matcher.rb +2 -0
  27. data/lib/fear/extractor/extractor_matcher.rb +5 -3
  28. data/lib/fear/extractor/grammar.rb +5 -3
  29. data/lib/fear/extractor/identifier_matcher.rb +2 -0
  30. data/lib/fear/extractor/matcher.rb +5 -3
  31. data/lib/fear/extractor/matcher/and.rb +3 -1
  32. data/lib/fear/extractor/named_array_splat_matcher.rb +2 -0
  33. data/lib/fear/extractor/pattern.rb +7 -5
  34. data/lib/fear/extractor/typed_identifier_matcher.rb +2 -0
  35. data/lib/fear/extractor/value_matcher.rb +2 -0
  36. data/lib/fear/extractor_api.rb +2 -0
  37. data/lib/fear/failure.rb +2 -0
  38. data/lib/fear/failure_pattern_match.rb +2 -0
  39. data/lib/fear/for.rb +4 -2
  40. data/lib/fear/for_api.rb +3 -1
  41. data/lib/fear/future.rb +141 -64
  42. data/lib/fear/future_api.rb +2 -0
  43. data/lib/fear/left.rb +3 -1
  44. data/lib/fear/left_pattern_match.rb +2 -0
  45. data/lib/fear/none.rb +4 -2
  46. data/lib/fear/none_pattern_match.rb +2 -0
  47. data/lib/fear/option.rb +3 -1
  48. data/lib/fear/option_api.rb +2 -0
  49. data/lib/fear/option_pattern_match.rb +2 -0
  50. data/lib/fear/partial_function.rb +10 -8
  51. data/lib/fear/partial_function/and_then.rb +4 -2
  52. data/lib/fear/partial_function/any.rb +2 -0
  53. data/lib/fear/partial_function/combined.rb +3 -1
  54. data/lib/fear/partial_function/empty.rb +2 -0
  55. data/lib/fear/partial_function/guard.rb +7 -5
  56. data/lib/fear/partial_function/guard/and.rb +2 -0
  57. data/lib/fear/partial_function/guard/and3.rb +2 -0
  58. data/lib/fear/partial_function/guard/or.rb +2 -0
  59. data/lib/fear/partial_function/lifted.rb +2 -0
  60. data/lib/fear/partial_function/or_else.rb +3 -1
  61. data/lib/fear/partial_function_class.rb +3 -1
  62. data/lib/fear/pattern_match.rb +3 -1
  63. data/lib/fear/pattern_matching_api.rb +3 -1
  64. data/lib/fear/promise.rb +11 -3
  65. data/lib/fear/right.rb +3 -1
  66. data/lib/fear/right_biased.rb +4 -2
  67. data/lib/fear/right_pattern_match.rb +2 -0
  68. data/lib/fear/some.rb +2 -0
  69. data/lib/fear/some_pattern_match.rb +2 -0
  70. data/lib/fear/struct.rb +235 -0
  71. data/lib/fear/success.rb +2 -0
  72. data/lib/fear/success_pattern_match.rb +2 -0
  73. data/lib/fear/try.rb +2 -0
  74. data/lib/fear/try_api.rb +2 -0
  75. data/lib/fear/try_pattern_match.rb +2 -0
  76. data/lib/fear/unit.rb +6 -2
  77. data/lib/fear/utils.rb +4 -2
  78. data/lib/fear/version.rb +4 -1
  79. data/spec/fear/done_spec.rb +7 -5
  80. data/spec/fear/either/mixin_spec.rb +4 -2
  81. data/spec/fear/either_pattern_match_spec.rb +10 -8
  82. data/spec/fear/extractor/array_matcher_spec.rb +65 -63
  83. data/spec/fear/extractor/extractor_matcher_spec.rb +64 -62
  84. data/spec/fear/extractor/grammar_array_spec.rb +5 -3
  85. data/spec/fear/extractor/identified_matcher_spec.rb +18 -16
  86. data/spec/fear/extractor/identifier_matcher_spec.rb +26 -24
  87. data/spec/fear/extractor/pattern_spec.rb +17 -15
  88. data/spec/fear/extractor/typed_identifier_matcher_spec.rb +23 -21
  89. data/spec/fear/extractor/value_matcher_number_spec.rb +29 -27
  90. data/spec/fear/extractor/value_matcher_string_spec.rb +27 -25
  91. data/spec/fear/extractor/value_matcher_symbol_spec.rb +24 -22
  92. data/spec/fear/extractor_api_spec.rb +70 -68
  93. data/spec/fear/extractor_spec.rb +23 -21
  94. data/spec/fear/failure_spec.rb +59 -57
  95. data/spec/fear/for_spec.rb +19 -17
  96. data/spec/fear/future_spec.rb +456 -240
  97. data/spec/fear/guard_spec.rb +26 -24
  98. data/spec/fear/left_spec.rb +60 -58
  99. data/spec/fear/none_spec.rb +36 -34
  100. data/spec/fear/option/mixin_spec.rb +9 -7
  101. data/spec/fear/option_pattern_match_spec.rb +10 -8
  102. data/spec/fear/partial_function/empty_spec.rb +12 -10
  103. data/spec/fear/partial_function_and_then_spec.rb +39 -37
  104. data/spec/fear/partial_function_composition_spec.rb +46 -44
  105. data/spec/fear/partial_function_or_else_spec.rb +92 -90
  106. data/spec/fear/partial_function_spec.rb +46 -44
  107. data/spec/fear/pattern_match_spec.rb +31 -29
  108. data/spec/fear/promise_spec.rb +19 -17
  109. data/spec/fear/right_biased/left.rb +28 -26
  110. data/spec/fear/right_biased/right.rb +51 -49
  111. data/spec/fear/right_spec.rb +58 -56
  112. data/spec/fear/some_spec.rb +30 -28
  113. data/spec/fear/success_spec.rb +50 -48
  114. data/spec/fear/try/mixin_spec.rb +5 -3
  115. data/spec/fear/try_pattern_match_spec.rb +10 -8
  116. data/spec/fear/utils_spec.rb +16 -14
  117. data/spec/spec_helper.rb +7 -5
  118. data/spec/struct_spec.rb +226 -0
  119. metadata +18 -13
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # rubocop: disable Metrics/LineLength
3
5
  module FutureApi
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  class Left
3
5
  include Either
@@ -52,7 +54,7 @@ module Fear
52
54
  # @param reduce_left [Proc]
53
55
  # @return [any]
54
56
  def reduce(reduce_left, _reduce_right)
55
- reduce_left.call(value)
57
+ reduce_left.(value)
56
58
  end
57
59
 
58
60
  # @return [self]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # @api private
3
5
  class LeftPatternMatch < Fear::EitherPatternMatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # @api private
3
5
  class NoneClass
@@ -40,7 +42,7 @@ module Fear
40
42
 
41
43
  # @return [String]
42
44
  def inspect
43
- '#<Fear::NoneClass>'
45
+ "#<Fear::NoneClass>"
44
46
  end
45
47
 
46
48
  # @return [String]
@@ -70,7 +72,7 @@ module Fear
70
72
  end
71
73
 
72
74
  def inherited(*)
73
- raise 'you are not allowed to inherit from NoneClass, use Fear::None instead'
75
+ raise "you are not allowed to inherit from NoneClass, use Fear::None instead"
74
76
  end
75
77
  end
76
78
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # @api private
3
5
  class NonePatternMatch < OptionPatternMatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # Represents optional values. Instances of +Option+
3
5
  # are either an instance of +Some+ or the object +None+.
@@ -187,7 +189,7 @@ module Fear
187
189
  end
188
190
 
189
191
  def match(value, &block)
190
- matcher(&block).call(value)
192
+ matcher(&block).(value)
191
193
  end
192
194
  end
193
195
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module OptionApi
3
5
  # An +Option+ factory which creates +Some+ if the argument is
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # Option pattern matcher
3
5
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # A partial function is a unary function defined on subset of all possible inputs.
3
5
  # The method +defined_at?+ allows to test dynamically if an arg is in
@@ -43,13 +45,13 @@ module Fear
43
45
  # @return [#call]
44
46
  # @abstract
45
47
  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'
48
+ autoload :AndThen, "fear/partial_function/and_then"
49
+ autoload :Any, "fear/partial_function/any"
50
+ autoload :Combined, "fear/partial_function/combined"
51
+ autoload :EMPTY, "fear/partial_function/empty"
52
+ autoload :Guard, "fear/partial_function/guard"
53
+ autoload :Lifted, "fear/partial_function/lifted"
54
+ autoload :OrElse, "fear/partial_function/or_else"
53
55
 
54
56
  # Checks if a value is contained in the function's domain.
55
57
  #
@@ -125,7 +127,7 @@ module Fear
125
127
  # @return [Fear::PartialFunction]
126
128
  #
127
129
  def and_then(other = Utils::UNDEFINED, &block)
128
- Utils.with_block_or_argument('Fear::PartialFunction#and_then', other, block) do |fun|
130
+ Utils.with_block_or_argument("Fear::PartialFunction#and_then", other, block) do |fun|
129
131
  if fun.is_a?(Fear::PartialFunction)
130
132
  Combined.new(self, fun)
131
133
  else
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  # Composite function produced by +PartialFunction#and_then+ method
@@ -23,7 +25,7 @@ module Fear
23
25
  # @param arg [any]
24
26
  # @return [any ]
25
27
  def call(arg)
26
- function.call(partial_function.call(arg))
28
+ function.(partial_function.(arg))
27
29
  end
28
30
 
29
31
  # @param arg [any]
@@ -39,7 +41,7 @@ module Fear
39
41
  result = partial_function.call_or_else(arg) do
40
42
  return yield(arg)
41
43
  end
42
- function.call(result)
44
+ function.(result)
43
45
  end
44
46
  end
45
47
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  # Any is an object which is always truthy
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  # Composite function produced by +PartialFunction#and_then+ method
@@ -22,7 +24,7 @@ module Fear
22
24
  # @param arg [any]
23
25
  # @return [any ]
24
26
  def call(arg)
25
- f2.call(f1.call(arg))
27
+ f2.(f1.(arg))
26
28
  end
27
29
 
28
30
  alias === call
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  EMPTY = EmptyPartialFunction.new
@@ -1,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  # Guard represents PartialFunction guardian
4
6
  #
5
7
  # @api private
6
8
  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'
9
+ autoload :And, "fear/partial_function/guard/and"
10
+ autoload :And3, "fear/partial_function/guard/and3"
11
+ autoload :Or, "fear/partial_function/guard/or"
10
12
 
11
13
  class << self
12
14
  # Optimized version for combination of two guardians
@@ -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
  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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  # @api private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  module PartialFunction
3
5
  # Composite function produced by +PartialFunction#or_else+ method
@@ -36,7 +38,7 @@ module Fear
36
38
 
37
39
  # @see Fear::PartialFunction#and_then
38
40
  def and_then(other = Utils::UNDEFINED, &block)
39
- Utils.with_block_or_argument('Fear::PartialFunction::OrElse#and_then', other, block) do |fun|
41
+ Utils.with_block_or_argument("Fear::PartialFunction::OrElse#and_then", other, block) do |fun|
40
42
  OrElse.new(f1.and_then(&fun), f2.and_then(&fun))
41
43
  end
42
44
  end
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # @api private
3
5
  module PatternMatchingApi
@@ -83,7 +85,7 @@ module Fear
83
85
  # @yieldparam matcher [Fear::PartialFunction]
84
86
  # @return [any]
85
87
  def match(value, &block)
86
- matcher(&block).call(value)
88
+ matcher(&block).(value)
87
89
  end
88
90
 
89
91
  # Creates partial function defined on domain described with guards
@@ -1,8 +1,16 @@
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
+
1
9
  module Fear
2
10
  # @api private
3
11
  class Promise < Concurrent::IVar
4
- # @param options [Hash] options passed to underlying +Concurrent::Future+
5
- def initialize(options = {})
12
+ # @param options [Hash] options passed to underlying +Concurrent::Promise+
13
+ def initialize(**options)
6
14
  super()
7
15
  @options = options
8
16
  @promise = Concurrent::Promise.new(options) do
@@ -64,7 +72,7 @@ module Fear
64
72
  if complete(result)
65
73
  self
66
74
  else
67
- raise IllegalStateException, 'Promise already completed.'
75
+ raise IllegalStateException, "Promise already completed."
68
76
  end
69
77
  end
70
78
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  class Right
3
5
  include Either
@@ -65,7 +67,7 @@ module Fear
65
67
  # @param reduce_right [Proc]
66
68
  # @return [any]
67
69
  def reduce(_reduce_left, reduce_right)
68
- reduce_right.call(value)
70
+ reduce_right.(value)
69
71
  end
70
72
 
71
73
  # @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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  # @api private
3
5
  class RightPatternMatch < EitherPatternMatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fear
2
4
  class Some
3
5
  include Option
@@ -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,235 @@
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
+
196
+ # @return [String]
197
+ #
198
+ # @example
199
+ # User = Fear::Struct.with_attributes(:id, :email)
200
+ # user = User.new(id: 2, email: 'john@exmaple.com')
201
+ # user.inspect #=> "<#Fear::Struct User id=2, email=>'john@exmaple.com'>"
202
+ #
203
+ def inspect
204
+ attributes = to_h.map { |key, value| "#{key}=#{value.inspect}" }.join(", ")
205
+
206
+ format(INSPECT_TEMPLATE, class_name: self.class.name, attributes: attributes)
207
+ end
208
+ alias to_s inspect
209
+
210
+ MISSING_KEYWORDS_ERROR = "missing keywords: %{keywords}"
211
+
212
+ private def _check_missing_attributes!(provided_attributes)
213
+ missing_attributes = members - provided_attributes.keys
214
+
215
+ unless missing_attributes.empty?
216
+ raise ArgumentError, format(MISSING_KEYWORDS_ERROR, keywords: missing_attributes.join(", "))
217
+ end
218
+ end
219
+
220
+ UNKNOWN_KEYWORDS_ERROR = "unknown keywords: %{keywords}"
221
+
222
+ private def _check_unknown_attributes!(provided_attributes)
223
+ unknown_attributes = provided_attributes.keys - members
224
+
225
+ unless unknown_attributes.empty?
226
+ raise ArgumentError, format(UNKNOWN_KEYWORDS_ERROR, keywords: unknown_attributes.join(", "))
227
+ end
228
+ end
229
+
230
+ # @return [void]
231
+ private def _set_attribute(name, value)
232
+ instance_variable_set(:"@#{name}", value)
233
+ end
234
+ end
235
+ end