Class: Chieftain::Command

Inherits:
Object
  • Object
show all
Defined in:
lib/chieftain/command.rb

Overview

An implementation of the Command design pattern that aims to take some advantage of Ruby’s enhanced capabilities.

Defined Under Namespace

Classes: Error, Result

Constant Summary collapse

@@convertors =
{self => {}}
@@parameters =
{self => {}}
@@validators =
{self => {}}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parameters = {}) ⇒ Command

Returns a new instance of Command.



50
51
52
53
54
55
56
# File 'lib/chieftain/command.rb', line 50

def initialize(parameters={})
  @convertors = Command.convertors_for(self.class)
  @errors     = []
  @parameters = {}.merge(parameters).inject({}) {|t,v| t[v[0].to_s.to_sym] = v[1]; t}
  @settings   = Command.parameters(self.class)
  @validators = Command.validators_for(self.class)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *arguments, &block) ⇒ Object

An implementation of the #method_missing method for the Command class that checks whether a parameter is being requested and, if so, returns it’s value or delegates handling to the parent class implementation.



178
179
180
181
182
183
184
# File 'lib/chieftain/command.rb', line 178

def method_missing(name, *arguments, &block)
  if expects?(name)
    get_parameter_value(name)
  else
    super
  end
end

Instance Attribute Details

#convertorsObject (readonly)

Returns the value of attribute convertors.



57
58
59
# File 'lib/chieftain/command.rb', line 57

def convertors
  @convertors
end

#errorsObject (readonly)

Returns the value of attribute errors.



57
58
59
# File 'lib/chieftain/command.rb', line 57

def errors
  @errors
end

#parametersObject (readonly)

Returns the value of attribute parameters.



57
58
59
# File 'lib/chieftain/command.rb', line 57

def parameters
  @parameters
end

#settingsObject (readonly)

Returns the value of attribute settings.



57
58
59
# File 'lib/chieftain/command.rb', line 57

def settings
  @settings
end

#validatorsObject (readonly)

Returns the value of attribute validators.



57
58
59
# File 'lib/chieftain/command.rb', line 57

def validators
  @validators
end

Class Method Details

.add_convertor(name, convertor_class) ⇒ Object

Registers a convertor for a Command class. A convertor is any class that can be constructed using a default constructor and responds to the #convertible?() and #convert() methods. Both of these methods take a single parameter which is the value to undergo conversion. The #convertible?() method returns true if it’s possible to convert the value to the convertors output type. The #convert() method performs the actual conversion, returning the result.



297
298
299
300
301
302
303
304
# File 'lib/chieftain/command.rb', line 297

def self.add_convertor(name, convertor_class)
  @@convertors[self] = {} if !@@convertors.include?(self)
  if @@convertors[self].include?(name)
    raise CommandError.new("Duplicate convertor '#{name}' specified for the #{self.name} class.")
  end

  @@convertors[self][name] = convertor_class
end

.add_validator(name, &block) ⇒ Object

Registers a validator for a Command class. A validator has to be registered with a block that will be invoked for the relevant parameters. This block should take 3 parameters. The first is the command object being executed. The second is the name of the parameter being validated. The third is the value of the parameter being validated. Validators can register errors by invoking the #error() method on the command they are passed.



312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/chieftain/command.rb', line 312

def self.add_validator(name, &block)
  @@validators[self] = {} if !@@validators.include?(self)
  if @@validators[self].include?(name)
    raise CommandError.new("Duplicate validator '#{name}' specified for the #{self.name} class.")
  end

  if !block
    raise CommandError.new("No block specified for the '#{name}' validator in the #{self.name} class.")
  end

  @@validators[self][name] = block
end

.convertors_for(command_class) ⇒ Object

This method scans the class hierarchy for a Command instance and assembles a list of the registered convertors for it. Convertors registered in classes lower in the hierarchy (i.e. derived classes) override those registered in parent classes.



329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/chieftain/command.rb', line 329

def self.convertors_for(command_class)
  hierarchy = [command_class]
  while !hierarchy.last.superclass.nil?
    hierarchy << hierarchy.last.superclass
  end

  convertors = {}
  hierarchy.reverse.each do |c|
    convertors.merge!(@@convertors[c]) if @@convertors.include?(c)
  end
  convertors.inject({}) {|list, entry| list[entry[0]] = entry[1].new; list}
end

.optional(name, settings = {}, &block) ⇒ Object

Registers an optional parameter for the command. See the #parameter() method for details of the parameters this method accepts.



344
345
346
# File 'lib/chieftain/command.rb', line 344

def self.optional(name, settings={}, &block)
  parameter(name, {}.merge(settings, {required: false}), &block)
end

.parameter(name, settings = {}, &block) ⇒ Object

Register a new parameter for a Command class. The first method parameter specifies the new parameters name. This can be followed by a Hash of settings value for the parameter. All keys in this Hash should be symbols and the following keys are currently recognised - :required, :types and :validators. You can also register a block for a parameter. This block will be invoked with the raw parameter value and the return value from this block will become the actual parameter value used.



355
356
357
358
359
360
361
# File 'lib/chieftain/command.rb', line 355

def self.parameter(name, settings={}, &block)
  if self.method_defined?(name)
    raise ParameterError.new("The '#{name}' parameter clashes with an existing class method.", name)
  end
  @@parameters[self]       = {} if !@@parameters.include?(self)
  @@parameters[self][name] = OpenStruct.new({}.merge(settings, {name: name, block: block}))
end

.parameters(command_class) ⇒ Object

Fetches the parameter list registered for a specific Command class instance.



365
366
367
# File 'lib/chieftain/command.rb', line 365

def self.parameters(command_class)
  @@parameters[command_class] || {}
end

.required(name, settings = {}, &block) ⇒ Object

Registers an optional parameter for the command. See the #parameter() method for details of the parameters this method accepts.



371
372
373
# File 'lib/chieftain/command.rb', line 371

def self.required(name, settings={}, &block)
  parameter(name, {}.merge(settings, {required: true}), &block)
end

.validate(name, &block) ⇒ Object

A synomym for the #add_validator() method that is intended for use with a validator that matches a parameter name.



377
378
379
# File 'lib/chieftain/command.rb', line 377

def self.validate(name, &block)
  add_validator(name, &block)
end

.validators_for(command_class) ⇒ Object

This method scans the class hierarchy for a Command instance and assembles a list of the registered validators for it. Validators registered in classes lower in the hierarchy (i.e. derived classes) override those registered in parent classes.



385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/chieftain/command.rb', line 385

def self.validators_for(command_class)
  hierarchy = [command_class]
  while !hierarchy.last.superclass.nil?
    hierarchy << hierarchy.last.superclass
  end

  validators = {}
  hierarchy.reverse.each do |c|
    validators.merge!(@@validators[c]) if @@validators.include?(c)
  end
  validators
end

Instance Method Details

#convertible?(name, value) ⇒ Boolean

Test whether a given value is convertible for a named parameter. This will return true if the parameter is expected and either has no type specified or the value given can be converted to the parameters specified type.

Returns:

  • (Boolean)


62
63
64
65
66
67
68
69
70
71
72
# File 'lib/chieftain/command.rb', line 62

def convertible?(name, value)
  result = false
  if expects?(name)
    result   = true
    settings = @settings[name]
    if settings.type
      result = get_convertor(settings.type).convertible?(value)
    end
  end
  result
end

#error(message, code = nil) ⇒ Object

Register an error with the execution of the current Command.



75
76
77
# File 'lib/chieftain/command.rb', line 75

def error(message, code=nil)
  @errors << Error.new(message, code)
end

#executeObject

Invokes the #perform() method if and only if the Command instance tests as valid. This method should be the one invoked to run a Command instance.



81
82
83
84
85
86
# File 'lib/chieftain/command.rb', line 81

def execute
  @errors = []
  value   = nil
  value = perform if valid?
  Result.new(value, errors)
end

#expected_parameter_namesObject

Returns a list of the expected parameters configured for a Command instance.



90
91
92
# File 'lib/chieftain/command.rb', line 90

def expected_parameter_names
  @settings ? @settings.values.map(&:name) : []
end

#expects?(parameter) ⇒ Boolean

Tests whether a parameter name is among the parameters specified for the Command instance.

Returns:

  • (Boolean)


96
97
98
# File 'lib/chieftain/command.rb', line 96

def expects?(parameter)
  expected_parameter_names.include?(parameter)
end

#get_convertor(type) ⇒ Object

Fetches a name convertor from the list for the Command instance, raises an exception if one cannot be found.



153
154
155
156
157
158
# File 'lib/chieftain/command.rb', line 153

def get_convertor(type)
  if !has_convertor?(type)
    raise CommandError.new("Unable to locate the '#{type}' parameter convertor.")
  end
  @convertors[type]
end

#get_parameter_value(name) ⇒ Object

Retrieve the value for a named parameter. The value will be run through an applicable converted prior to being returned. An exception will be raised if conversion fails. If the parameter is optional and has not be specified then conversion will not be attempted and nil will be returned.



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
# File 'lib/chieftain/command.rb', line 104

def get_parameter_value(name)
  if expects?(name)
    settings = settings_for(name)
    if settings[:required] && !provided?(name)
      raise ParameterError.new("A value has not been provided for the '#{name}' parameter.", name)
    end

    if settings[:required]
      raw_value = get_raw_parameter_value(name)
      if settings[:type]
        convertor = get_convertor(settings.type)
        if !convertor.convertible?(raw_value)
          raise ParameterError.new("The value of the '#{name}' parameter cannot be converted to the '#{settings.type}' type.", name)
        end
        convertor.convert(raw_value)
      else
        raw_value
      end

    else
      raw_value = nil
      if provided?(name)
        raw_value = get_raw_parameter_value(name)
        raw_value = settings[:default] if raw_value.nil?
      else
        raw_value = settings[:default]
      end

      if provided?(name)
        if settings[:type]
          convertor = get_convertor(settings.type)
          if !convertor.convertible?(raw_value)
            raise ParameterError.new("The value of the '#{name}' parameter cannot be converted to the '#{settings.type}' type.", name)
          end
          convertor.convert(raw_value)
        else
          raw_value
        end
      else
        raw_value
      end
    end
  else
    raise ParameterError.new("Unknown parameter '#{name}' requested from a '#{self.class.name}' command instance.")
  end
end

#get_raw_parameter_value(name) ⇒ Object

Fetches the raw, unaltered value specified for a name parameter to the Command instance. Returns nil if the specified parameter has not been given an explicit value. Raises an exception if an unknown parameter is specified.

Raises:



164
165
166
167
# File 'lib/chieftain/command.rb', line 164

def get_raw_parameter_value(name)
  raise ParameterError.new("Unknown parameter '#{name}' requested in command.", name) if !expects?(name)
  @parameters[name]
end

#has_convertor?(name) ⇒ Boolean

This method tests whether a named convertor is available to a Command instance.

Returns:

  • (Boolean)


171
172
173
# File 'lib/chieftain/command.rb', line 171

def has_convertor?(name)
  @convertors.include?(name)
end

#optional_parameter_namesObject

Returns a list of the names of the commands optional parameters.



187
188
189
# File 'lib/chieftain/command.rb', line 187

def optional_parameter_names
  settings.values.filter {|p| !p.required}.map(&:name)
end

#parameter_namesObject

Returns a list of the names of the parameters specified to the Command instance.



193
194
195
# File 'lib/chieftain/command.rb', line 193

def parameter_names
  @settings.keys
end

#performObject

Derived command classes should override this method to do the work for the command. This method will only get invoked if the command is valid. This default implementation raises an exception.

Raises:



200
201
202
# File 'lib/chieftain/command.rb', line 200

def perform
  raise CommandError.new("The #{self.class.name} command class has not overridden the #perform() method.")
end

#provided?(name) ⇒ Boolean

This method checks whether a name parameter is among those provided to a Command instance.

Returns:

  • (Boolean)


206
207
208
# File 'lib/chieftain/command.rb', line 206

def provided?(name)
  @parameters.include?(name)
end

#required_parameter_namesObject

Returns a list of the names of the commands required parameters. Note a required parameter must have a value specified for it when the command is executed.



213
214
215
# File 'lib/chieftain/command.rb', line 213

def required_parameter_names
  settings.values.filter {|p| p.required}.map(&:name)
end

#settings_for(name) ⇒ Object

Retrieves the parameter settings for a named parameter. Raises an exception if an unknown parameter is specified.



219
220
221
222
223
# File 'lib/chieftain/command.rb', line 219

def settings_for(name)
  raise ParameterError("Unknown parameter '#{name}' requested in command.", name) if !expects?(name)
  entry = @settings.find {|entry| entry[1].name == name}
  entry ? entry[1] : nil
end

#valid?Boolean

Invokes the validate command and then checks that there are no errors registered for the command.

Returns:

  • (Boolean)


262
263
264
265
266
# File 'lib/chieftain/command.rb', line 262

def valid?
  @errors = []
  validate
  @errors.empty?
end

#validateObject

Performs validation of the parameters passed to a command. Deriving classes should ensure this method is invoked in any custom #validate method their class provides.



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
# File 'lib/chieftain/command.rb', line 228

def validate
  @settings.values.each do |parameter|
    if provided?(parameter.name)
      if parameter.type
        # Check conversion.
        if has_convertor?(parameter.type)
          convertor = get_convertor(parameter.type)
          if !convertor.convertible?(get_raw_parameter_value(parameter.name))
            error("The value of the '#{parameter.name}' parameter cannot be converted to the '#{parameter.type}' type.")
          end
        else
          error("Invalid type '#{parameter.type}' specified for the '#{parameter.name}' parameter.")
        end
      end

      # Run validations.
      if convertible?(parameter.name, get_raw_parameter_value(parameter.name))
        value = get_parameter_value(parameter.name)
        validations_for(parameter.name).each do |validation|
          self.instance_exec(parameter.name, value, &validation)
        end
      else
        error("The value of the '#{parameter.name}' parameter cannot be converted to the '#{parameter.type}' type.")
      end
    else
      if parameter.required
        error("No value specified for the '#{parameter.name}' required parameter.")
      end
    end
  end
end

#validations_for(name) ⇒ Object

Returns a list of the validators that apply to a named parameter. This will be a combination of validators explicitly declared on the parameter and class validators with the same name as the parameter. The method raises an exception if given the name of a parameter that the Command instance does not expect. It can also raise an exception if a parameter has an unknown validator specified for it.



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/chieftain/command.rb', line 274

def validations_for(name)
  if !expects?(name)
    raise ParameterError.new("Validators requested for unknown parameter '#{name}'.", name)
  end
  settings = @settings[name]
  names    = []
  names << name if @validators.include?(name)
  names = names.concat(settings.validations) if settings.validations
  names.uniq.map do |key|
    if !@validators.include?(key)
      raise ParameterError.new("Unknown validation '#{key}' requested for the '#{name}' parameter.", name)
    end
    @validators[key]
  end
end