command_mapper 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,6 +5,11 @@ require 'command_mapper/option'
5
5
  require 'shellwords'
6
6
 
7
7
  module CommandMapper
8
+ #
9
+ # Base class for all mapped commands.
10
+ #
11
+ # @api public
12
+ #
8
13
  class Command
9
14
 
10
15
  include Types
@@ -24,16 +29,6 @@ module CommandMapper
24
29
  # @return [Hash{String => String}]
25
30
  attr_reader :command_env
26
31
 
27
- # The option values to execute the command with.
28
- #
29
- # @return [Hash{String => Object}]
30
- attr_reader :command_options
31
-
32
- # The argument values to execute the command with.
33
- #
34
- # @return [Hash{String => Object}]
35
- attr_reader :command_arguments
36
-
37
32
  # The subcommand's options and arguments.
38
33
  #
39
34
  # @return [Command, nil]
@@ -51,7 +46,7 @@ module CommandMapper
51
46
  # @param [String, nil] command_path
52
47
  # Overrides the command with a custom path to the command.
53
48
  #
54
- # @param [Hash{String => String}] env
49
+ # @param [Hash{String => String}] command_env
55
50
  # Custom environment variables to pass to the command.
56
51
  #
57
52
  # @param [Hash{Symbol => Object}] kwargs
@@ -94,12 +89,16 @@ module CommandMapper
94
89
  # Initializes and runs the command.
95
90
  #
96
91
  # @param [Hash{Symbol => Object}] params
97
- # The option values.
92
+ # The option and argument values.
98
93
  #
99
- # @yield [self]
94
+ # @param [Hash{Symbol => Object}] kwargs
95
+ # Additional keywords arguments. These will be used to populate
96
+ # {#options} and {#arguments}, along with `params`.
97
+ #
98
+ # @yield [command]
100
99
  # The newly initialized command.
101
100
  #
102
- # @yieldparam [Command] self
101
+ # @yieldparam [Command] command
103
102
  #
104
103
  # @return [Boolean, nil]
105
104
  #
@@ -109,15 +108,49 @@ module CommandMapper
109
108
  end
110
109
 
111
110
  #
112
- # Runs the command in a shell and captures all stdout output.
111
+ # Initializes and spawns the command as a separate process, returning the
112
+ # PID of the process.
113
113
  #
114
114
  # @param [Hash{Symbol => Object}] params
115
- # The option values.
115
+ # The option and argument values.
116
116
  #
117
- # @yield [self]
117
+ # @param [Hash{Symbol => Object}] kwargs
118
+ # Additional keywords arguments. These will be used to populate
119
+ # {#options} and {#arguments}, along with `params`.
120
+ #
121
+ # @yield [command]
118
122
  # The newly initialized command.
119
123
  #
120
- # @yieldparam [Command] self
124
+ # @yieldparam [Command] command
125
+ #
126
+ # @return [Integer]
127
+ # The PID of the new command process.
128
+ #
129
+ # @raise [Errno::ENOENT]
130
+ # The command could not be found.
131
+ #
132
+ # @since 0.2.0
133
+ #
134
+ def self.spawn(params={},**kwargs,&block)
135
+ command = new(params,**kwargs,&block)
136
+ command.spawn_command
137
+ end
138
+
139
+ #
140
+ # Initializes and runs the command in a shell and captures all stdout
141
+ # output.
142
+ #
143
+ # @param [Hash{Symbol => Object}] params
144
+ # The option and argument values.
145
+ #
146
+ # @param [Hash{Symbol => Object}] kwargs
147
+ # Additional keywords arguments. These will be used to populate
148
+ # {#options} and {#arguments}, along with `params`.
149
+ #
150
+ # @yield [command]
151
+ # The newly initialized command.
152
+ #
153
+ # @yieldparam [Command] command
121
154
  #
122
155
  # @return [String]
123
156
  # The stdout output of the command.
@@ -128,15 +161,22 @@ module CommandMapper
128
161
  end
129
162
 
130
163
  #
131
- # Executes the command and returns an IO object to it.
164
+ # Initializes and executes the command and returns an IO object to it.
132
165
  #
133
166
  # @param [Hash{Symbol => Object}] params
134
- # The option values.
167
+ # The option and argument values.
135
168
  #
136
- # @yield [self]
169
+ # @param [String] mode
170
+ # The IO "mode" to open the IO pipe in.
171
+ #
172
+ # @param [Hash{Symbol => Object}] kwargs
173
+ # Additional keywords arguments. These will be used to populate
174
+ # {#options} and {#arguments}, along with `params`.
175
+ #
176
+ # @yield [command]
137
177
  # The newly initialized command.
138
178
  #
139
- # @yieldparam [Command] self
179
+ # @yieldparam [Command] command
140
180
  #
141
181
  # @return [IO]
142
182
  #
@@ -146,18 +186,18 @@ module CommandMapper
146
186
  end
147
187
 
148
188
  #
149
- # Initializes and runs the command through sudo.
189
+ # Initializes and runs the command through `sudo`.
150
190
  #
151
191
  # @param [Hash{Symbol => Object}] params
152
- # The option values.
192
+ # The option and argument values.
153
193
  #
154
194
  # @param [Hash{Symbol => Object}] kwargs
155
195
  # Additional keyword arguments for {#initialize}.
156
196
  #
157
- # @yield [self]
197
+ # @yield [command]
158
198
  # The newly initialized command.
159
199
  #
160
- # @yieldparam [Command] self
200
+ # @yieldparam [Command] command
161
201
  #
162
202
  # @return [Boolean, nil]
163
203
  #
@@ -181,7 +221,11 @@ module CommandMapper
181
221
  # @api semipublic
182
222
  #
183
223
  def self.command_name
184
- @command_name || raise(NotImplementedError,"#{self} did not call command(...)")
224
+ @command_name || if superclass < Command
225
+ superclass.command_name
226
+ else
227
+ raise(NotImplementedError,"#{self} did not call command(...)")
228
+ end
185
229
  end
186
230
 
187
231
  #
@@ -221,6 +265,23 @@ module CommandMapper
221
265
  end
222
266
  end
223
267
 
268
+ #
269
+ # Determines if an option with the given name has been defined.
270
+ #
271
+ # @param [Symbol] name
272
+ # The given name.
273
+ #
274
+ # @return [Boolean]
275
+ # Specifies whether an option with the given name has been defined.
276
+ #
277
+ # @api semipublic
278
+ #
279
+ # @since 0.2.0
280
+ #
281
+ def self.has_option?(name)
282
+ options.has_key?(name)
283
+ end
284
+
224
285
  #
225
286
  # Defines an option for the command.
226
287
  #
@@ -230,10 +291,6 @@ module CommandMapper
230
291
  # @param [Symbol, nil] name
231
292
  # The option's name.
232
293
  #
233
- # @param [Boolean] equals
234
- # Specifies whether the option's flag and value should be separated with a
235
- # `=` character.
236
- #
237
294
  # @param [Hash, nil] value
238
295
  # The option's value.
239
296
  #
@@ -246,6 +303,14 @@ module CommandMapper
246
303
  # @param [Boolean] repeats
247
304
  # Specifies whether the option can be given multiple times.
248
305
  #
306
+ # @param [Boolean] equals
307
+ # Specifies whether the option's flag and value should be separated with a
308
+ # `=` character.
309
+ #
310
+ # @param [Boolean] value_in_flag
311
+ # Specifies that the value should be appended to the option's flag
312
+ # (ex: `-Fvalue`).
313
+ #
249
314
  # @api public
250
315
  #
251
316
  # @example Defining an option:
@@ -260,6 +325,9 @@ module CommandMapper
260
325
  # @example Defining an option who's value is optional:
261
326
  # option '--file', value: {required: false}
262
327
  #
328
+ # @example Defining an `-Fvalue` option:
329
+ # option '--foo', value: true, value_in_flag: true
330
+ #
263
331
  # @example Defining an `--opt=value` option:
264
332
  # option '--foo', equals: true, value: true
265
333
  #
@@ -270,25 +338,35 @@ module CommandMapper
270
338
  # option '--list', value: List.new
271
339
  #
272
340
  # @raise [ArgumentError]
273
- # The option flag conflicts with a pre-existing internal method.
274
- #
275
- def self.option(flag, name: nil, equals: nil, value: nil, repeats: false, &block)
341
+ # The option flag conflicts with a pre-existing internal method, or
342
+ # another argument or subcommand.
343
+ #
344
+ def self.option(flag, name: nil, value: nil, repeats: false,
345
+ # formatting options
346
+ equals: nil,
347
+ value_in_flag: nil,
348
+ &block)
276
349
  option = Option.new(flag, name: name,
277
- equals: equals,
278
350
  value: value,
279
351
  repeats: repeats,
352
+ # formatting options
353
+ equals: equals,
354
+ value_in_flag: value_in_flag,
280
355
  &block)
281
356
 
282
- self.options[option.name] = option
283
-
284
357
  if is_internal_method?(option.name)
285
358
  if name
286
359
  raise(ArgumentError,"option #{flag.inspect} with name #{name.inspect} cannot override the internal method with same name: ##{option.name}")
287
360
  else
288
361
  raise(ArgumentError,"option #{flag.inspect} maps to method name ##{option.name} and cannot override the internal method with same name: ##{option.name}")
289
362
  end
363
+ elsif has_argument?(option.name)
364
+ raise(ArgumentError,"option #{flag.inspect} with name #{option.name.inspect} conflicts with another argument with the same name")
365
+ elsif has_subcommand?(option.name)
366
+ raise(ArgumentError,"option #{flag.inspect} with name #{option.name.inspect} conflicts with another subcommand with the same name")
290
367
  end
291
368
 
369
+ self.options[option.name] = option
292
370
  attr_accessor option.name
293
371
  end
294
372
 
@@ -296,6 +374,7 @@ module CommandMapper
296
374
  # All defined options.
297
375
  #
298
376
  # @return [Hash{Symbol => Argument}]
377
+ # The mapping of argument names and {Argument} objects.
299
378
  #
300
379
  # @api semipublic
301
380
  #
@@ -307,6 +386,23 @@ module CommandMapper
307
386
  end
308
387
  end
309
388
 
389
+ #
390
+ # Determines if an argument with the given name has been defined.
391
+ #
392
+ # @param [Symbol] name
393
+ # The given name.
394
+ #
395
+ # @return [Boolean]
396
+ # Specifies whether an argument with the given name has been defined.
397
+ #
398
+ # @api semipublic
399
+ #
400
+ # @since 0.2.0
401
+ #
402
+ def self.has_argument?(name)
403
+ arguments.has_key?(name)
404
+ end
405
+
310
406
  #
311
407
  # Defines an option for the command.
312
408
  #
@@ -333,7 +429,8 @@ module CommandMapper
333
429
  # argument :file, required: false
334
430
  #
335
431
  # @raise [ArgumentError]
336
- # The argument name conflicts with a pre-existing internal method.
432
+ # The argument name conflicts with a pre-existing internal method, or
433
+ # another option or subcommand.
337
434
  #
338
435
  def self.argument(name, required: true, type: Str.new, repeats: false)
339
436
  name = name.to_sym
@@ -341,19 +438,23 @@ module CommandMapper
341
438
  type: type,
342
439
  repeats: repeats)
343
440
 
344
- self.arguments[argument.name] = argument
345
-
346
441
  if is_internal_method?(argument.name)
347
442
  raise(ArgumentError,"argument #{name.inspect} cannot override internal method with same name: ##{argument.name}")
443
+ elsif has_option?(argument.name)
444
+ raise(ArgumentError,"argument #{name.inspect} conflicts with another option with the same name")
445
+ elsif has_subcommand?(argument.name)
446
+ raise(ArgumentError,"argument #{name.inspect} conflicts with another subcommand with the same name")
348
447
  end
349
448
 
449
+ self.arguments[argument.name] = argument
350
450
  attr_accessor name
351
451
  end
352
452
 
353
453
  #
354
454
  # All defined subcommands.
355
455
  #
356
- # @return [Hash{Symbol => Command}]
456
+ # @return [Hash{Symbol => Command.class}]
457
+ # The mapping of subcommand names and subcommand classes.
357
458
  #
358
459
  # @api semipublic
359
460
  #
@@ -365,6 +466,23 @@ module CommandMapper
365
466
  end
366
467
  end
367
468
 
469
+ #
470
+ # Determines if a subcommand with the given name has been defined.
471
+ #
472
+ # @param [Symbol] name
473
+ # The given name.
474
+ #
475
+ # @return [Boolean]
476
+ # Specifies whether a subcommand with the given name has been defined.
477
+ #
478
+ # @api semipublic
479
+ #
480
+ # @since 0.2.0
481
+ #
482
+ def self.has_subcommand?(name)
483
+ subcommands.has_key?(name)
484
+ end
485
+
368
486
  #
369
487
  # Defines a subcommand.
370
488
  #
@@ -392,25 +510,30 @@ module CommandMapper
392
510
  # end
393
511
  #
394
512
  # @raise [ArgumentError]
395
- # The subcommand name conflicts with a pre-existing internal method.
513
+ # The subcommand name conflicts with a pre-existing internal method, or
514
+ # another option or argument.
396
515
  #
397
516
  def self.subcommand(name,&block)
398
- name = name.to_s
517
+ name = name.to_s
518
+ method_name = name.tr('-','_')
519
+ class_name = name.split(/[_-]+/).map(&:capitalize).join
520
+ subcommand_name = method_name.to_sym
521
+
522
+ if is_internal_method?(method_name)
523
+ raise(ArgumentError,"subcommand #{name.inspect} maps to method name ##{method_name} and cannot override the internal method with same name: ##{method_name}")
524
+ elsif has_option?(subcommand_name)
525
+ raise(ArgumentError,"subcommand #{name.inspect} conflicts with another option with the same name")
526
+ elsif has_argument?(subcommand_name)
527
+ raise(ArgumentError,"subcommand #{name.inspect} conflicts with another argument with the same name")
528
+ end
399
529
 
400
530
  subcommand_class = Class.new(Command)
401
531
  subcommand_class.command(name)
402
532
  subcommand_class.class_eval(&block)
403
533
 
404
- method_name = name.tr('-','_')
405
- class_name = name.split(/[_-]+/).map(&:capitalize).join
406
-
407
- self.subcommands[method_name.to_sym] = subcommand_class
534
+ self.subcommands[subcommand_name] = subcommand_class
408
535
  const_set(class_name,subcommand_class)
409
536
 
410
- if is_internal_method?(method_name)
411
- raise(ArgumentError,"subcommand #{name.inspect} maps to method name ##{method_name} and cannot override the internal method with same name: ##{method_name}")
412
- end
413
-
414
537
  define_method(method_name) do |&block|
415
538
  if block then @command_subcommand = subcommand_class.new(&block)
416
539
  else @command_subcommand
@@ -428,8 +551,10 @@ module CommandMapper
428
551
  # Gets the value of an option or an argument.
429
552
  #
430
553
  # @param [Symbol] name
554
+ # The name of the option, argument, or subcommand.
431
555
  #
432
556
  # @return [Object]
557
+ # The value of the option, argument, or subcommand.
433
558
  #
434
559
  # @raise [ArgumentError]
435
560
  # The given name was not match any option or argument.
@@ -448,10 +573,13 @@ module CommandMapper
448
573
  # Sets an option or an argument with the given name.
449
574
  #
450
575
  # @param [Symbol] name
576
+ # The name of the option, argument, or subcommand.
451
577
  #
452
578
  # @param [Object] value
579
+ # The new value for the option, argument, or subcommand.
453
580
  #
454
581
  # @return [Object]
582
+ # The new value for the option, argument, or subcommand.
455
583
  #
456
584
  # @raise [ArgumentError]
457
585
  # The given name was not match any option or argument.
@@ -468,6 +596,7 @@ module CommandMapper
468
596
  # Returns an Array of command-line arguments for the command.
469
597
  #
470
598
  # @return [Array<String>]
599
+ # The formatted command-line arguments.
471
600
  #
472
601
  # @raise [ArgumentReqired]
473
602
  # A required argument was not set.
@@ -490,8 +619,10 @@ module CommandMapper
490
619
  self.class.arguments.each do |name,argument|
491
620
  value = self[name]
492
621
 
493
- if value.nil? && argument.required?
494
- raise(ArgumentRequired,"argument #{name} is required")
622
+ if value.nil?
623
+ if argument.required?
624
+ raise(ArgumentRequired,"argument #{name} is required")
625
+ end
495
626
  else
496
627
  argument.argv(additional_args,value)
497
628
  end
@@ -529,12 +660,30 @@ module CommandMapper
529
660
  end
530
661
 
531
662
  #
532
- # Initializes and runs the command.
663
+ # Runs the command.
533
664
  #
534
665
  # @return [Boolean, nil]
666
+ # Indicates whether the command exited successfully or not.
667
+ # `nil` indicates the command could not be found.
535
668
  #
536
669
  def run_command
537
- system(@command_env,*command_argv)
670
+ Kernel.system(@command_env,*command_argv)
671
+ end
672
+
673
+ #
674
+ # Spawns the command as a separate process, returning the PID of the
675
+ # process.
676
+ #
677
+ # @return [Integer]
678
+ # The PID of the new command process.
679
+ #
680
+ # @raise [Errno::ENOENT]
681
+ # The command could not be found.
682
+ #
683
+ # @since 0.2.0
684
+ #
685
+ def spawn_command
686
+ Process.spawn(@command_env,*command_argv)
538
687
  end
539
688
 
540
689
  #
@@ -551,6 +700,7 @@ module CommandMapper
551
700
  # Executes the command and returns an IO object to it.
552
701
  #
553
702
  # @return [IO]
703
+ # The IO object for the command's `STDIN`.
554
704
  #
555
705
  def popen_command(mode=nil)
556
706
  if mode then IO.popen(@command_env,command_argv,mode)
@@ -559,12 +709,14 @@ module CommandMapper
559
709
  end
560
710
 
561
711
  #
562
- # Initializes and runs the command through sudo.
712
+ # Runs the command through `sudo`.
563
713
  #
564
714
  # @param [Hash{Symbol => Object}] sudo_params
565
715
  # Additional keyword arguments for {Sudo#initialize}.
566
716
  #
567
717
  # @return [Boolean, nil]
718
+ # Indicates whether the command exited successfully or not.
719
+ # `nil` indicates the command could not be found.
568
720
  #
569
721
  def sudo_command(**sudo_kwargs,&block)
570
722
  sudo_params = sudo_kwargs.merge(command: command_argv)
@@ -595,6 +747,7 @@ module CommandMapper
595
747
  # The method name.
596
748
  #
597
749
  # @return [Boolean]
750
+ # Indicates that the method name is also an intenral method name.
598
751
  #
599
752
  def self.is_internal_method?(name)
600
753
  Command.instance_methods(false).include?(name.to_sym)
@@ -7,12 +7,18 @@ module CommandMapper
7
7
  #
8
8
  class Option
9
9
 
10
+ # The option's flag (ex: `-o` or `--output`).
11
+ #
10
12
  # @return [String]
11
13
  attr_reader :flag
12
14
 
15
+ # The option's name.
16
+ #
13
17
  # @return [Symbol]
14
18
  attr_reader :name
15
19
 
20
+ # Describes the option's value.
21
+ #
16
22
  # @return [OptionValue, nil]
17
23
  attr_reader :value
18
24
 
@@ -25,10 +31,6 @@ module CommandMapper
25
31
  # @param [Symbol, nil] name
26
32
  # The option's name.
27
33
  #
28
- # @param [Boolean] equals
29
- # Specifies whether the option's flag and value should be separated with a
30
- # `=` character.
31
- #
32
34
  # @param [Hash, nil] value
33
35
  # The option's value.
34
36
  #
@@ -41,15 +43,31 @@ module CommandMapper
41
43
  # @param [Boolean] repeats
42
44
  # Specifies whether the option can be given multiple times.
43
45
  #
44
- def initialize(flag, name: nil, equals: nil, value: nil, repeats: false)
46
+ # @param [Boolean] equals
47
+ # Specifies whether the option's flag and value should be separated with a
48
+ # `=` character.
49
+ #
50
+ # @param [Boolean] value_in_flag
51
+ # Specifies that the value should be appended to the option's flag
52
+ # (ex: `-Fvalue`).
53
+ #
54
+ # @api private
55
+ #
56
+ def initialize(flag, name: nil, value: nil, repeats: false,
57
+ # formatting options
58
+ equals: nil,
59
+ value_in_flag: nil)
45
60
  @flag = flag
46
61
  @name = name || self.class.infer_name_from_flag(flag)
47
- @equals = equals
48
62
  @value = case value
49
63
  when Hash then OptionValue.new(**value)
50
64
  when true then OptionValue.new
51
65
  end
52
66
  @repeats = repeats
67
+
68
+ # formatting options
69
+ @equals = equals
70
+ @value_in_flag = value_in_flag
53
71
  end
54
72
 
55
73
  #
@@ -65,6 +83,8 @@ module CommandMapper
65
83
  # Could not infer the name from the given option flag or was not given a
66
84
  # valid option flag.
67
85
  #
86
+ # @api private
87
+ #
68
88
  def self.infer_name_from_flag(flag)
69
89
  if flag.start_with?('--')
70
90
  name = flag[2..-1]
@@ -90,6 +110,15 @@ module CommandMapper
90
110
  !@value.nil?
91
111
  end
92
112
 
113
+ #
114
+ # Determines whether the option can be given multiple times.
115
+ #
116
+ # @return [Boolean]
117
+ #
118
+ def repeats?
119
+ @repeats
120
+ end
121
+
93
122
  #
94
123
  # Indicates whether the option flag and value should be separated with a
95
124
  # `=` character.
@@ -101,12 +130,14 @@ module CommandMapper
101
130
  end
102
131
 
103
132
  #
104
- # Determines whether the option can be given multiple times.
133
+ # Indicates whether the value will be appended to the option's flag.
105
134
  #
106
135
  # @return [Boolean]
107
136
  #
108
- def repeats?
109
- @repeats
137
+ # @since 0.2.0
138
+ #
139
+ def value_in_flag?
140
+ @value_in_flag
110
141
  end
111
142
 
112
143
  #
@@ -119,6 +150,8 @@ module CommandMapper
119
150
  # Returns true if the value is valid, or `false` and a validation error
120
151
  # message if the value is not compatible.
121
152
  #
153
+ # @api semipublic
154
+ #
122
155
  def validate(value)
123
156
  if accepts_value?
124
157
  if repeats?
@@ -146,6 +179,8 @@ module CommandMapper
146
179
  # @raise [ArgumentError]
147
180
  # The given value was incompatible with the option.
148
181
  #
182
+ # @api semipublic
183
+ #
149
184
  def argv(argv=[],value)
150
185
  valid, message = validate(value)
151
186
 
@@ -269,13 +304,15 @@ module CommandMapper
269
304
  else
270
305
  string = @value.format(value)
271
306
 
272
- if string.start_with?('-')
273
- raise(ValidationError,"option #{@name} formatted value (#{string.inspect}) cannot start with a '-'")
274
- end
275
-
276
307
  if equals?
277
308
  argv << "#{@flag}=#{string}"
309
+ elsif value_in_flag?
310
+ argv << "#{@flag}#{string}"
278
311
  else
312
+ if string.start_with?('-')
313
+ raise(ValidationError,"option #{@name} formatted value (#{string.inspect}) cannot start with a '-'")
314
+ end
315
+
279
316
  argv << @flag << string
280
317
  end
281
318
  end
@@ -6,6 +6,26 @@ module CommandMapper
6
6
  #
7
7
  class OptionValue < Arg
8
8
 
9
+ #
10
+ # Validates whether a given value is compatible with the option {#type}.
11
+ #
12
+ # @param [Object] value
13
+ # The given value to validate.
14
+ #
15
+ # @return [true, (false, String)]
16
+ # Returns true if the value is valid, or `false` and a validation error
17
+ # message if the value is not compatible.
18
+ #
19
+ # @api semipublic
20
+ #
21
+ def validate(value)
22
+ if !required? && value == true
23
+ return true
24
+ else
25
+ super(value)
26
+ end
27
+ end
28
+
9
29
  #
10
30
  # Formats a value using the options {#type}.
11
31
  #
@@ -15,6 +35,8 @@ module CommandMapper
15
35
  # @return [String]
16
36
  # The formatted value.
17
37
  #
38
+ # @api semipublic
39
+ #
18
40
  def format(value)
19
41
  @type.format(value)
20
42
  end
@@ -2,9 +2,17 @@ require 'command_mapper/types/map'
2
2
 
3
3
  module CommandMapper
4
4
  module Types
5
+ #
6
+ # Represents a mapping of Ruby values to their String equivalents.
7
+ #
5
8
  class Enum < Map
6
9
 
10
+ # The values of the enum.
11
+ #
7
12
  # @return [Array<Object>]
13
+ #
14
+ # @api semipublic
15
+ #
8
16
  attr_reader :values
9
17
 
10
18
  #