File: base.rb

package info (click to toggle)
ruby-process-executer 4.0.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 408 kB
  • sloc: ruby: 873; makefile: 4
file content (276 lines) | stat: -rw-r--r-- 8,996 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# frozen_string_literal: true

require_relative 'option_definition'

module ProcessExecuter
  module Options
    # Defines, validates, and holds a set of option values
    #
    # Options are defined by subclasses by overriding the `define_options` method.
    #
    # @example Define an options class with two options
    #   class MyOptions < ProcessExecuter::Options::Base
    #     def define_options
    #       # Call super to include options defined in the parent class
    #       [
    #         *super,
    #         ProcessExecuter::Options::OptionDefinition.new(
    #           :option1, default: '', validator: method(:assert_is_string)
    #           ),
    #         ProcessExecuter::Options::OptionDefinition.new(
    #           :option2, default: '', validator: method(:assert_is_string)
    #         ),
    #         ProcessExecuter::Options::OptionDefinition.new(
    #           :option3, default: '', validator: method(:assert_is_string)
    #         )
    #       ]
    #     end
    #     def assert_is_string(key, value)
    #       return if value.is_a?(String)
    #       errors << "#{key} must be a String but was #{value}"
    #     end
    #   end
    #   options = MyOptions.new(option1: 'value1', option2: 'value2')
    #   options.option1 # => 'value1'
    #   options.option2 # => 'value2'
    #
    # @example invalid option values
    #   begin
    #     options = MyOptions.new(option1: 1, option2: 2)
    #   rescue ProcessExecuter::ArgumentError => e
    #     e.message #=> "option1 must be a String but was 1\noption2 must be a String but was 2"
    #   end
    #
    # @api public
    class Base
      # Create a new Options object
      #
      # Normally you would use a subclass instead of instantiating this class
      # directly.
      #
      # @example
      #   options = MyOptions.new(option1: 'value1', option2: 'value2')
      #
      # @example with invalid option values
      #   begin
      #     options = MyOptions.new(option1: 1, option2: 2)
      #   rescue ProcessExecuter::ArgumentError => e
      #     e.message #=> "option1 must be a String but was 1\noption2 must be a String but was 2"
      #   end
      #
      # @param options_hash [Hash] a hash of options
      #
      def initialize(**options_hash)
        @options_hash = allowed_options.transform_values(&:default).merge(options_hash)
        @errors = []
        assert_no_unknown_options
        define_accessor_methods
        validate_options
      end

      # All the allowed options as a hash whose keys are the option names
      #
      # The returned hash what is returned from `define_options` but with the option
      # names as keys. The values are instances of `OptionDefinition`.
      #
      # The returned hash is frozen and cannot be modified.
      #
      # @example
      #   options = MyOptions.new(option1: 'value1', option2: 'value2')
      #   options.allowed_options # => {
      #     option1: #<OptionDefinition>,
      #     option2: #<OptionDefinition>
      #   }
      #
      # @return [Hash<Symbol, ProcessExecuter::Options::OptionDefinition>] A hash
      #   where keys are option names and values are their definitions.
      #
      def allowed_options
        @allowed_options ||=
          define_options.each_with_object({}) do |option, hash|
            hash[option.name] = option
          end.freeze
      end

      # A string representation of the object that includes the options
      #
      # @example
      #   options = MyOptions.new(option1: 'value1', option2: 'value2')
      #   options.to_s # => #<MyOptions option1: "value1", option2: "value2">'
      #
      # @return [String]
      #
      def to_s
        "#{super.to_s[0..-2]} #{inspect}>"
      end

      # A string representation of the options
      #
      # @example
      #   options = MyOptions.new(option1: 'value1', option2: 'value2')
      #   options.inspect # => '{:option1=>"value1", :option2=>"value2"}'
      #
      # @return [String]
      #
      def inspect
        options_hash.inspect
      end

      # A hash representation of the options
      #
      # @example
      #   options = MyOptions.new(option1: 'value1', option2: 'value2')
      #   options.to_h # => { option1: "value1", option2: "value2" }
      #
      # @return [Hash]
      #
      def to_h
        options_hash.dup
      end

      # Iterate over each option with an object
      #
      # @example
      #   options = MyOptions.new(option1: 'value1', option2: 'value2')
      #   options.each_with_object({}) { |(option_key, option_value), obj| obj[option_key] = option_value }
      #   # => { option1: "value1", option2: "value2" }
      #
      # @yield [key_value, obj]
      #
      # @yieldparam key_value [Array<Object, Object>] An array containing the option key and its value
      #
      # @yieldparam obj [Object] The object passed to the block.
      #
      # @return [Object] the obj passed to the block
      #
      def each_with_object(obj, &)
        options_hash.each_with_object(obj, &)
      end

      # Merge the given options into the current options object
      #
      # Subsequent hashes' values overwrite earlier ones for the same key.
      #
      # @example
      #   options = MyOptions.new(option1: 'value1', option2: 'value2')
      #   h1 = { option2: 'new_value2' }
      #   h2 = { option3: 'value3' }
      #   options.merge!(h1, h2) => {option1: "value1", option2: "new_value2", option3: "value3"}
      #
      # @param other_options_hashes [Array<Hash>] zero of more hashes to merge into the current options
      #
      # @return [self] the current options object with the merged options
      #
      # @api public
      #
      def merge!(*other_options_hashes)
        options_hash.merge!(*other_options_hashes)
      end

      # Returns a new options object formed by merging self with each of other_hashes
      #
      # @example
      #   options = MyOptions.new(option1: 'value1', option2: 'value2')
      #   options.object_id # => 1025
      #   h1 = { option2: 'new_value2' }
      #   h2 = { option3: 'value3' }
      #   merged_options = options.merge(h1, h2)
      #   merged_options.object_id # => 1059
      #
      # @param other_options_hashes [Array<Hash>] the options to merge into the current options
      #
      # @return [self.class]
      #
      def merge(*other_options_hashes)
        merged_options = other_options_hashes.reduce(options_hash, :merge)
        self.class.new(**merged_options)
      end

      protected

      # An array of OptionDefinition objects that define the allowed options
      #
      # Subclasses MUST override this method to define the allowed options.
      #
      # @return [Array<OptionDefinition>]
      #
      # @api private
      #
      def define_options
        [].freeze
      end

      # Determine if the given option is a valid option
      #
      # May be overridden by subclasses to add additional validation.
      #
      # @param option [Symbol] the option to be tested
      # @return [Boolean] true if the given option is a valid option
      # @api private
      def valid_option?(option)
        allowed_options.keys.include?(option)
      end

      private

      # The list of validation errors
      #
      # Validators should add error messages to this array.
      #
      # @return [Array<String>]
      #
      # @api private
      #
      attr_reader :errors

      # @!attribute [r]
      #
      # A hash of all options keyed by the option name
      #
      # @return [Hash<Object, Object>]
      #
      # @api private
      #
      attr_reader :options_hash

      # Raise an argument error for invalid option values
      # @return [void]
      # @raise [ProcessExecuter::ArgumentError] if any invalid option values are found
      # @api private
      def validate_options
        options_hash.each_key do |option_key|
          validator = allowed_options[option_key]&.validator
          instance_exec(option_key, send(option_key), &validator.to_proc) unless validator.nil?
        end

        raise ProcessExecuter::ArgumentError, errors.join("\n") unless errors.empty?
      end

      # Define accessor methods for each option
      # @return [void]
      # @api private
      def define_accessor_methods
        allowed_options.each_key do |option|
          define_singleton_method(option) do
            options_hash[option]
          end
        end
      end

      # Determine if the options hash contains any unknown options
      # @return [void]
      # @raise [ProcessExecuter::ArgumentError] if the options hash contains any unknown options
      # @api private
      def assert_no_unknown_options
        unknown_options = options_hash.keys.reject { |key| valid_option?(key) }

        return if unknown_options.empty?

        raise(
          ArgumentError,
          "Unknown option#{'s' if unknown_options.count > 1}: #{unknown_options.join(', ')}"
        )
      end
    end
  end
end