Class: Aspera::Cli::ExtendedValue

Inherits:
Object
  • Object
show all
Includes:
Singleton
Defined in:
lib/aspera/cli/extended_value.rb

Overview

Command line extended values

Constant Summary collapse

DEFAULT_DECODERS =

First is default

%i[none json ruby yaml]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#default_decoderObject

Returns the value of attribute default_decoder.



125
126
127
# File 'lib/aspera/cli/extended_value.rb', line 125

def default_decoder
  @default_decoder
end

Class Method Details

.assert_no_value(value, ext_type) ⇒ Object

The value must be empty

Parameters:

  • value (String)

    The value as parameter

  • ext_type (Symbol)

    The method of extended value



69
70
71
# File 'lib/aspera/cli/extended_value.rb', line 69

def assert_no_value(value, ext_type)
  Aspera.assert(value.empty?, type: BadArgument){"no value allowed for extended value type: #{ext_type}"}
end

.decode_csvt(value) ⇒ Object

Decode comma separated table text



30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/aspera/cli/extended_value.rb', line 30

def decode_csvt(value)
  col_titles = nil
  hash_array = []
  CSV.parse(value).each do |values|
    next if values.empty?
    if col_titles.nil?
      col_titles = values
    else
      hash_array.push(col_titles.zip(values).to_h)
    end
  end
  Log.log.warn('Titled CSV file without any row') if hash_array.empty?
  return hash_array
end

.instanceExtendedValue

Returns the singleton instance of ExtendedValue

Returns:



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

class ExtendedValue
  include Singleton

  # First is default
  DEFAULT_DECODERS = %i[none json ruby yaml]

  class << self
    # Decode comma separated table text
    def decode_csvt(value)
      col_titles = nil
      hash_array = []
      CSV.parse(value).each do |values|
        next if values.empty?
        if col_titles.nil?
          col_titles = values
        else
          hash_array.push(col_titles.zip(values).to_h)
        end
      end
      Log.log.warn('Titled CSV file without any row') if hash_array.empty?
      return hash_array
    end

    # JSON Parser, with more information on error location
    # extract a context: 10 chars before and after the error on the given line and display a pointer "^"
    # :reek:UncommunicativeMethodName
    def JSON_parse(value) # rubocop:disable Naming/MethodName
      JSON.parse(value)
    rescue JSON::ParserError => e
      m = /at line (\d+) column (\d+)/.match(e.message)
      raise if m.nil?
      line = m[1].to_i - 1
      column = m[2].to_i - 1
      lines = value.lines
      raise if line >= lines.size
      error_line = lines[line].chomp
      context_col_beg = [column - 10, 0].max
      context_col_end = [column + 10, error_line.length].min
      context = error_line[context_col_beg...context_col_end]
      cursor_pos = column - context_col_beg
      pointer = ' ' * cursor_pos + '^'.blink
      raise BadArgument, "#{e.message}\n#{context}\n#{pointer}"
    end

    # The value must be empty
    # @param value [String] The value as parameter
    # @param ext_type [Symbol] The method of extended value
    def assert_no_value(value, ext_type)
      Aspera.assert(value.empty?, type: BadArgument){"no value allowed for extended value type: #{ext_type}"}
    end

    def read_stdin(mode)
      case mode
      when '' then $stdin.read
      when 'bin' then $stdin.binmode.read
      when 'chomp' then $stdin.chomp
      else raise BadArgument, "`stdin` supports only: '', 'bin' or 'chomp'"
      end
    end
  end

  private

  def initialize
    # Base handlers
    # Other handlers can be set using `on`
    # e.g. `preset` is reader in config plugin
    @handlers = {
      val:    lambda{ |i| i},
      base64: lambda{ |i| Base64.decode64(i)},
      csvt:   lambda{ |i| ExtendedValue.decode_csvt(i)},
      env:    lambda{ |i| ENV.fetch(i, nil)},
      file:   lambda{ |i| File.read(File.expand_path(i))},
      uri:    lambda{ |i| UriReader.read(i)},
      json:   lambda{ |i| ExtendedValue.JSON_parse(i)},
      lines:  lambda{ |i| i.split("\n")},
      list:   lambda{ |i| i[1..].split(i[0])},
      none:   lambda{ |i| ExtendedValue.assert_no_value(i, :none); nil}, # rubocop:disable Style/Semicolon
      path:   lambda{ |i| File.expand_path(i)},
      re:     lambda{ |i| Regexp.new(i, Regexp::MULTILINE)},
      ruby:   lambda{ |i| Environment.secure_eval(i, __FILE__, __LINE__)},
      s:      lambda{ |i| i.to_s},
      secret: lambda{ |i| prompt = i.empty? ? 'secret' : i; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
      stdin:  lambda{ |i| ExtendedValue.read_stdin(i)},
      yaml:   lambda{ |i| YAML.load(i)},
      zlib:   lambda{ |i| Zlib::Inflate.inflate(i)},
      extend: lambda{ |i| ExtendedValue.instance.evaluate_extend(i)}
    }
    @regex_single = nil
    @regex_extend = nil
    @default_decoder = nil
    update_regex
  end

  # Update the Regex to match an extended value based on @handlers
  def update_regex
    handler_regex = "#{MARKER_START}(#{modifiers.join('|')})#{MARKER_END}"
    @regex_single = Regexp.new("^#{handler_regex}(.*)$", Regexp::MULTILINE)
    @regex_extend = Regexp.new("^(.*)#{handler_regex}([^#{MARKER_IN_END}]*)#{MARKER_IN_END}(.*)$", Regexp::MULTILINE)
  end

  public

  attr_reader :default_decoder

  def default_decoder=(value)
    Log.log.debug{"Setting default decoder to (#{value.class}) #{value}"}
    Aspera.assert_values(value, DEFAULT_DECODERS)
    value = nil if value.eql?(:none)
    @default_decoder = value
  end

  # List of Extended Value methods
  def modifiers; @handlers.keys; end

  # Add a new handler
  def on(name, &block)
    Aspera.assert_type(name, Symbol){'name'}
    Aspera.assert(block)
    Log.log.debug{"Setting handler for #{name}"}
    @handlers[name] = block
    update_regex
  end

  # Parses a `String` value to extended value.
  # If it is a String using supported extended value modifiers, then evaluate them.
  # Other value types are returned as is.
  # @param value   [String] the value to parse
  # @param context [String] Context in which evaluation is done
  # @param allowed [Array<Class>,NilClass] Expected types
  # @return [String, Integer, Array, Hash, Boolean] Evaluated value
  def evaluate(value, context:, allowed: nil)
    return value unless value.is_a?(String)
    Aspera.assert_array_all(allowed, Class) unless allowed.nil?
    # use default decoder if not an extended value and expect complex types
    using_default_decoder = allowed&.all?{ |t| DEFAULT_PARSER_TYPES.include?(t)} && !@regex_single.match?(value) && !@default_decoder.nil?
    value = [MARKER_START, @default_decoder, MARKER_END, value].join if using_default_decoder
    # First determine decoders, in reversed order
    handlers_reversed = []
    while (m = value.match(@regex_single))
      handler = m[1].to_sym
      handlers_reversed.unshift(handler)
      value = m[2]
      break if SPECIAL_HANDLERS.include?(handler)
    end
    Log.log.trace1{"evaluating: #{handlers_reversed}, value: #{value}"}
    handlers_reversed.each do |handler|
      value = @handlers[handler].call(value)
    rescue => e
      raise BadArgument, "Evaluation of #{handler} for #{context}: #{e.message}"
    end
    return value
  end

  # Find inner extended values
  # Only used in above lambda
  def evaluate_extend(value)
    while (m = value.match(@regex_extend))
      sub_value = "@#{m[2]}:#{m[3]}"
      Log.log.debug{"evaluating #{sub_value}"}
      value = "#{m[1]}#{evaluate(sub_value, context: 'composite extended value')}#{m[4]}"
    end
    return value
  end
  # marker "@"
  MARKER_START = '@'
  # marker ":"
  MARKER_END = ':'
  # marker "@"
  MARKER_IN_END = '@'

  # Special handlers stop processing of handlers on right
  # :extend includes processing of other handlers in itself
  # :val keeps the value intact
  SPECIAL_HANDLERS = %i[extend val].freeze

  # Array and Hash types:
  DEFAULT_PARSER_TYPES = [Array, Hash].freeze
  private_constant :MARKER_START, :MARKER_END, :MARKER_IN_END, :SPECIAL_HANDLERS, :DEFAULT_PARSER_TYPES
end

.JSON_parse(value) ⇒ Object

JSON Parser, with more information on error location extract a context: 10 chars before and after the error on the given line and display a pointer “^” :reek:UncommunicativeMethodName



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/aspera/cli/extended_value.rb', line 48

def JSON_parse(value) # rubocop:disable Naming/MethodName
  JSON.parse(value)
rescue JSON::ParserError => e
  m = /at line (\d+) column (\d+)/.match(e.message)
  raise if m.nil?
  line = m[1].to_i - 1
  column = m[2].to_i - 1
  lines = value.lines
  raise if line >= lines.size
  error_line = lines[line].chomp
  context_col_beg = [column - 10, 0].max
  context_col_end = [column + 10, error_line.length].min
  context = error_line[context_col_beg...context_col_end]
  cursor_pos = column - context_col_beg
  pointer = ' ' * cursor_pos + '^'.blink
  raise BadArgument, "#{e.message}\n#{context}\n#{pointer}"
end

.read_stdin(mode) ⇒ Object



73
74
75
76
77
78
79
80
# File 'lib/aspera/cli/extended_value.rb', line 73

def read_stdin(mode)
  case mode
  when '' then $stdin.read
  when 'bin' then $stdin.binmode.read
  when 'chomp' then $stdin.chomp
  else raise BadArgument, "`stdin` supports only: '', 'bin' or 'chomp'"
  end
end

Instance Method Details

#evaluate(value, context:, allowed: nil) ⇒ String, ...

Parses a ‘String` value to extended value. If it is a String using supported extended value modifiers, then evaluate them. Other value types are returned as is.

Parameters:

  • value (String)

    the value to parse

  • context (String)

    Context in which evaluation is done

  • allowed (Array<Class>, NilClass) (defaults to: nil)

    Expected types

Returns:

  • (String, Integer, Array, Hash, Boolean)

    Evaluated value



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/aspera/cli/extended_value.rb', line 153

def evaluate(value, context:, allowed: nil)
  return value unless value.is_a?(String)
  Aspera.assert_array_all(allowed, Class) unless allowed.nil?
  # use default decoder if not an extended value and expect complex types
  using_default_decoder = allowed&.all?{ |t| DEFAULT_PARSER_TYPES.include?(t)} && !@regex_single.match?(value) && !@default_decoder.nil?
  value = [MARKER_START, @default_decoder, MARKER_END, value].join if using_default_decoder
  # First determine decoders, in reversed order
  handlers_reversed = []
  while (m = value.match(@regex_single))
    handler = m[1].to_sym
    handlers_reversed.unshift(handler)
    value = m[2]
    break if SPECIAL_HANDLERS.include?(handler)
  end
  Log.log.trace1{"evaluating: #{handlers_reversed}, value: #{value}"}
  handlers_reversed.each do |handler|
    value = @handlers[handler].call(value)
  rescue => e
    raise BadArgument, "Evaluation of #{handler} for #{context}: #{e.message}"
  end
  return value
end

#evaluate_extend(value) ⇒ Object

Find inner extended values Only used in above lambda



178
179
180
181
182
183
184
185
# File 'lib/aspera/cli/extended_value.rb', line 178

def evaluate_extend(value)
  while (m = value.match(@regex_extend))
    sub_value = "@#{m[2]}:#{m[3]}"
    Log.log.debug{"evaluating #{sub_value}"}
    value = "#{m[1]}#{evaluate(sub_value, context: 'composite extended value')}#{m[4]}"
  end
  return value
end

#modifiersObject

List of Extended Value methods



135
# File 'lib/aspera/cli/extended_value.rb', line 135

def modifiers; @handlers.keys; end

#on(name, &block) ⇒ Object

Add a new handler



138
139
140
141
142
143
144
# File 'lib/aspera/cli/extended_value.rb', line 138

def on(name, &block)
  Aspera.assert_type(name, Symbol){'name'}
  Aspera.assert(block)
  Log.log.debug{"Setting handler for #{name}"}
  @handlers[name] = block
  update_regex
end