fear 1.0.0 → 1.1.0

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