Class: Aspera::Cli::OptionValue

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/cli/parser.rb

Overview

Description of option, how to manage

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(option:, description: nil, allowed: Allowed::TYPES_STRING, handler: nil, deprecation: nil, schema: nil) ⇒ OptionValue

allowed:

  • nil No validation, so just a string
  • Class The single allowed Class
  • Array<Class> Multiple allowed classes
  • Array<Symbol> List of allowed values

Parameters:

  • Name of option

  • (defaults to: nil)

    Description for help; if nil, derived from schema

  • (defaults to: Allowed::TYPES_STRING)

    Allowed values

  • (defaults to: nil)

    Accessor: keys: :o(object) and :m(method); nil for local storage

  • (defaults to: nil)

    Deprecation message

  • (defaults to: nil)

    Declaration of schema



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
# File 'lib/aspera/cli/parser.rb', line 106

def initialize(option:, description: nil, allowed: Allowed::TYPES_STRING, handler: nil, deprecation: nil, schema: nil)
  Log.log.trace1{"option: #{option}, allowed: #{allowed}"}
  @option = option
  @description = description
  @group = nil
  @block = nil
  # by default passwords and secrets are sensitive, else specify when declaring the option
  @sensitive = SecretHider.instance.secret?(@option, '')
  @deprecation = deprecation
  @schema = schema
  # Start with local storage; bind_handler wires the delegation if a handler is given.
  @object = nil
  @read_method = nil
  @write_method = nil
  @access = :local
  bind_handler(handler) unless handler.nil?
  @types = nil
  @values = nil
  # Derive allowed type from schema when not explicitly provided
  if (allowed.nil? || allowed.eql?(Allowed::TYPES_STRING)) && schema
    schema_node = Schema::Registry.instance.reader(schema).current rescue nil
    if schema_node
      case schema_node['type']
      when 'object' then allowed = Hash
      when 'array'  then allowed = Array
      end
    end
  end
  if !allowed.nil?
    allowed = [allowed] if allowed.is_a?(Class)
    Aspera.assert_type(allowed, Array)
    if allowed.take(Allowed::TYPES_SYMBOL_ARRAY.length) == Allowed::TYPES_SYMBOL_ARRAY
      # Special case: array of defined symbol values
      @types = Allowed::TYPES_SYMBOL_ARRAY
      @values = allowed[Allowed::TYPES_SYMBOL_ARRAY.length..]
      # Default value for symbol array when no value has been set yet
      assign_value([], where: 'array default', warn_deprecation: false) if value(log: false).nil?
    elsif allowed.all?(Class)
      @types = allowed
      @values = BoolValue::ALL if allowed.eql?(Allowed::TYPES_BOOLEAN)
      # Default value for array/hash when no value has been set yet
      if @types.first.eql?(Array) && !@types.include?(NilClass) && value(log: false).nil?
        assign_value([], where: 'array default', warn_deprecation: false)
      elsif @types.first.eql?(Hash) && !@types.include?(NilClass) && value(log: false).nil?
        assign_value({}, where: 'hash default', warn_deprecation: false)
      end
    elsif allowed.all?(Symbol)
      @types = Allowed::TYPES_ENUM
      @values = allowed
    else
      Aspera.error_unexpected_value(allowed)
    end
  end
  Log.log.trace1{"declare: #{@option}: #{@access} #{@object.class}.#{@read_method}".green}
end

Instance Attribute Details

#blockObject

[Proc, nil] Block to call for flag options (TYPES_NONE)



93
94
95
# File 'lib/aspera/cli/parser.rb', line 93

def block
  @block
end

#deprecationObject (readonly)

[Array(Class)] List of allowed types



87
88
89
# File 'lib/aspera/cli/parser.rb', line 87

def deprecation
  @deprecation
end

#groupObject

[String] Help section group name (set by Parser#group)



91
92
93
# File 'lib/aspera/cli/parser.rb', line 91

def group
  @group
end

#optionObject (readonly)

[Array(Class)] List of allowed types



87
88
89
# File 'lib/aspera/cli/parser.rb', line 87

def option
  @option
end

#schemaObject (readonly)

[Array(Class)] List of allowed types



87
88
89
# File 'lib/aspera/cli/parser.rb', line 87

def schema
  @schema
end

#sensitiveObject (readonly)

[Array(Class)] List of allowed types



87
88
89
# File 'lib/aspera/cli/parser.rb', line 87

def sensitive
  @sensitive
end

#typesObject (readonly)

[Array(Class)] List of allowed types



87
88
89
# File 'lib/aspera/cli/parser.rb', line 87

def types
  @types
end

#valuesObject

[Array] List of allowed values (Symbols and specific values)



89
90
91
# File 'lib/aspera/cli/parser.rb', line 89

def values
  @values
end

Instance Method Details

#assign_value(value, where:, warn_deprecation: true) ⇒ nil

Assign value to option. Value can be a String, then evaluated with ExtendedValue, or directly a value.

Parameters:

  • Value to assign to option

  • Where the value is assigned from

  • (defaults to: true)

    Emit deprecation warning (false for internal transfers)

Returns:



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
# File 'lib/aspera/cli/parser.rb', line 217

def assign_value(value, where:, warn_deprecation: true)
  Aspera.assert(!@deprecation, type: :warn){"Option #{@option} is deprecated: #{@deprecation}"} if warn_deprecation
  new_value = ExtendedValue.instance.evaluate(value, context: "option: #{@option}", allowed: @types)
  Log.log.trace1{"#{where}: #{@option} <- (#{new_value.class})#{new_value}"}
  # Per-type coercion: String input from CLI/env/preset is normalized to the expected type.
  # Centralized here so all sources (CLI dispatch, preset, env) go through the same path.
  case @types
  when Allowed::TYPES_ENUM
    new_value = Parser.get_from_list(new_value, @option, @values) if new_value.is_a?(String)
  when Allowed::TYPES_BOOLEAN
    new_value = Parser.get_from_list(new_value, @option, BoolValue::ALL) if new_value.is_a?(String)
    new_value = BoolValue.true?(new_value)
  when Allowed::TYPES_INTEGER
    new_value = Integer(new_value)
  when Allowed::TYPES_STRING_ARRAY
    new_value = [new_value] if new_value.is_a?(String)
  when Allowed::TYPES_SYMBOL_ARRAY
    new_value = [new_value] if new_value.is_a?(String)
    Aspera.assert_array_all(new_value, String, type: BadArgument)
    new_value = new_value.map{ |v| Parser.get_from_list(v, @option, @values)}
  else
    # nil (setting nil on a Hash/Array option resets to empty container)
    new_value = {} if new_value.nil? && @types&.first.eql?(Hash)
    new_value = [] if new_value.nil? && @types&.first.eql?(Array)
  end
  # Skip type validation for the special 'help' value on Hash options: store it as-is
  # so that get_option(schema:) can raise SchemaRequest with the contextual schema later.
  if new_value.eql?(Parser::HELP) && @types&.include?(Hash) && !@schema
    case @access
    when :local  then @object = new_value
    when :write  then @object.send(@write_method, new_value)
    when :setter then @object.send(@read_method, @option, :set, new_value)
    end
    return
  end
  Aspera.assert_type(new_value, *@types, type: BadArgument){"Option #{@option}"} if @types
  if new_value.is_a?(Hash) || new_value.is_a?(Array)
    current_value = value(log: false)
    new_value = current_value.deep_merge(new_value) if new_value.is_a?(Hash) && current_value.is_a?(Hash) && !current_value.empty?
    new_value = current_value + new_value if new_value.is_a?(Array) && current_value.is_a?(Array) && !current_value.empty?
  end
  case @access
  when :local then @object = new_value
  when :write then @object.send(@write_method, new_value)
  when :setter then @object.send(@read_method, @option, :set, new_value)
  end
  Log.log.trace1{v = value(log: false); "#{@option} <- (#{v.class})#{v}"} # rubocop:disable Style/Semicolon
  nil
end

#bind_handler(handler) ⇒ nil

Wire (or re-wire) the getter/setter delegation for this option. Safe to call after construction - used by Parser#set_handler to bind a composed instance variable that did not exist at class-load time (Category C handlers).

Parameters:

  • Accessor hash with keys :o (object) and :m (method symbol)

Returns:



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/aspera/cli/parser.rb', line 167

def bind_handler(handler)
  Aspera.assert_type(handler, Hash){'handler'}
  # Capture any value already stored locally before switching to delegated storage.
  # This transfers defaults (and any preset values already applied) to the new target.
  pending_value = @access.eql?(:local) ? @object : nil
  @object       = handler[:o]
  @read_method  = handler[:m]
  @write_method = "#{@read_method}=".to_sym
  @access = if @object.respond_to?(@write_method)
    :write
  else
    :setter
  end
  Aspera.assert(@object.respond_to?(@read_method)){"#{@object} does not respond to #{@read_method}"}
  Log.log.trace1{"bind_handler: #{@option}: #{@access} #{@object.class}.#{@read_method}".green}
  # Push the pending local value to the new target if one was stored
  assign_value(pending_value, where: 'bind_handler', warn_deprecation: false) unless pending_value.nil?
  nil
end

#clearObject



196
197
198
# File 'lib/aspera/cli/parser.rb', line 196

def clear
  @object = nil
end

#descriptionString

Returns description of the option: explicit one, or first line of schema description.

Returns:

  • description of the option: explicit one, or first line of schema description



188
189
190
191
192
193
194
# File 'lib/aspera/cli/parser.rb', line 188

def description
  return @description unless @description.nil?
  return if @schema.nil?
  schema_node = Schema::Registry.instance.reader(@schema).current
  first_line = (schema_node['title'] || schema_node['description'].to_s).lines.first.to_s.strip
  first_line.end_with?('.') ? first_line[0..-2] : first_line
end

#value(log: true) ⇒ Object



200
201
202
203
204
205
206
207
208
209
# File 'lib/aspera/cli/parser.rb', line 200

def value(log: true)
  current_value =
    case @access
    when :local then @object
    when :write then @object.send(@read_method)
    when :setter then @object.send(@read_method, @option, :get)
    end
  Log.log.trace1{"#{@option} -> (#{current_value.class})#{current_value}"} if log
  current_value
end