aspera-cli 4.24.2 → 4.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1070 -758
  4. data/CONTRIBUTING.md +130 -115
  5. data/README.md +961 -623
  6. data/lib/aspera/agent/direct.rb +14 -12
  7. data/lib/aspera/agent/factory.rb +9 -6
  8. data/lib/aspera/agent/transferd.rb +8 -8
  9. data/lib/aspera/api/aoc.rb +104 -67
  10. data/lib/aspera/api/ats.rb +1 -0
  11. data/lib/aspera/api/cos_node.rb +3 -2
  12. data/lib/aspera/api/faspex.rb +17 -10
  13. data/lib/aspera/api/node.rb +10 -12
  14. data/lib/aspera/ascmd.rb +2 -3
  15. data/lib/aspera/ascp/installation.rb +60 -46
  16. data/lib/aspera/ascp/management.rb +9 -5
  17. data/lib/aspera/assert.rb +28 -6
  18. data/lib/aspera/cli/error.rb +4 -2
  19. data/lib/aspera/cli/extended_value.rb +94 -62
  20. data/lib/aspera/cli/formatter.rb +44 -58
  21. data/lib/aspera/cli/main.rb +21 -14
  22. data/lib/aspera/cli/manager.rb +317 -250
  23. data/lib/aspera/cli/plugins/alee.rb +3 -3
  24. data/lib/aspera/cli/plugins/aoc.rb +139 -78
  25. data/lib/aspera/cli/plugins/ats.rb +30 -36
  26. data/lib/aspera/cli/plugins/base.rb +68 -55
  27. data/lib/aspera/cli/plugins/config.rb +90 -100
  28. data/lib/aspera/cli/plugins/console.rb +15 -9
  29. data/lib/aspera/cli/plugins/cos.rb +1 -1
  30. data/lib/aspera/cli/plugins/faspex.rb +39 -30
  31. data/lib/aspera/cli/plugins/faspex5.rb +57 -52
  32. data/lib/aspera/cli/plugins/faspio.rb +10 -7
  33. data/lib/aspera/cli/plugins/httpgw.rb +3 -2
  34. data/lib/aspera/cli/plugins/node.rb +140 -125
  35. data/lib/aspera/cli/plugins/oauth.rb +13 -12
  36. data/lib/aspera/cli/plugins/orchestrator.rb +116 -33
  37. data/lib/aspera/cli/plugins/preview.rb +28 -48
  38. data/lib/aspera/cli/plugins/server.rb +9 -10
  39. data/lib/aspera/cli/plugins/shares.rb +77 -43
  40. data/lib/aspera/cli/sync_actions.rb +49 -38
  41. data/lib/aspera/cli/transfer_agent.rb +16 -35
  42. data/lib/aspera/cli/version.rb +1 -1
  43. data/lib/aspera/cli/wizard.rb +8 -5
  44. data/lib/aspera/command_line_builder.rb +24 -21
  45. data/lib/aspera/coverage.rb +6 -2
  46. data/lib/aspera/dot_container.rb +108 -0
  47. data/lib/aspera/environment.rb +71 -84
  48. data/lib/aspera/faspex_gw.rb +1 -1
  49. data/lib/aspera/faspex_postproc.rb +1 -1
  50. data/lib/aspera/id_generator.rb +7 -10
  51. data/lib/aspera/keychain/factory.rb +1 -2
  52. data/lib/aspera/keychain/macos_security.rb +2 -2
  53. data/lib/aspera/log.rb +2 -1
  54. data/lib/aspera/markdown.rb +31 -0
  55. data/lib/aspera/nagios.rb +6 -5
  56. data/lib/aspera/oauth/base.rb +41 -64
  57. data/lib/aspera/oauth/factory.rb +6 -7
  58. data/lib/aspera/oauth/generic.rb +1 -1
  59. data/lib/aspera/oauth/jwt.rb +1 -1
  60. data/lib/aspera/oauth/url_json.rb +6 -4
  61. data/lib/aspera/oauth/web.rb +2 -2
  62. data/lib/aspera/preview/file_types.rb +24 -38
  63. data/lib/aspera/preview/terminal.rb +95 -29
  64. data/lib/aspera/preview/utils.rb +6 -5
  65. data/lib/aspera/products/connect.rb +3 -3
  66. data/lib/aspera/rest.rb +54 -39
  67. data/lib/aspera/rest_error_analyzer.rb +4 -4
  68. data/lib/aspera/ssh.rb +10 -6
  69. data/lib/aspera/ssl.rb +41 -0
  70. data/lib/aspera/sync/conf.schema.yaml +184 -36
  71. data/lib/aspera/sync/database.rb +2 -1
  72. data/lib/aspera/sync/operations.rb +128 -72
  73. data/lib/aspera/transfer/parameters.rb +9 -10
  74. data/lib/aspera/transfer/spec.rb +2 -3
  75. data/lib/aspera/transfer/spec.schema.yaml +52 -22
  76. data/lib/aspera/transfer/spec_doc.rb +20 -30
  77. data/lib/aspera/uri_reader.rb +18 -4
  78. data/lib/transferd_pb.rb +2 -2
  79. data.tar.gz.sig +0 -0
  80. metadata +34 -6
  81. metadata.gz.sig +0 -0
@@ -33,25 +33,12 @@ module Aspera
33
33
  class Installation
34
34
  include Singleton
35
35
 
36
- DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
37
- <?xml version='1.0' encoding='UTF-8'?>
38
- <CONF version="2">
39
- <default>
40
- <file_system>
41
- <resume_suffix>.aspera-ckpt</resume_suffix>
42
- <partial_file_suffix>.partial</partial_file_suffix>
43
- </file_system>
44
- </default>
45
- </CONF>
46
- END_OF_CONFIG_FILE
47
- # all executable files from SDK
48
- EXE_FILES = %i[ascp ascp4 async transferd].freeze
49
- SDK_FILES = %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
50
- TRANSFERD_ARCHIVE_LOCATION_URL = 'https://ibm.biz/sdk_location'
51
- # filename for ascp with optional extension (Windows)
52
- private_constant :DEFAULT_ASPERA_CONF, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
53
36
  # options for SSH client private key
54
37
  CLIENT_SSH_KEY_OPTIONS = %i{dsa_rsa rsa per_client}.freeze
38
+ # prefix
39
+ USE_PRODUCT_PREFIX = 'product:'
40
+ # policy for product selection
41
+ FIRST_FOUND = 'FIRST'
55
42
 
56
43
  # Loads YAML from cloud with locations of SDK archives for all platforms
57
44
  # @return location structure
@@ -66,12 +53,13 @@ module Aspera
66
53
  end
67
54
  end
68
55
 
69
- # set ascp executable path
56
+ # set ascp executable "location"
70
57
  def ascp_path=(v)
71
58
  Aspera.assert_type(v, String)
72
- Aspera.assert(!v.empty?){'ascp path cannot be empty: check your config file'}
73
- Aspera.assert(File.exist?(v)){"No such file: [#{v}]"}
74
- @path_to_ascp = v
59
+ Aspera.assert(!v.empty?){'ascp location cannot be empty: check your config file'}
60
+ @ascp_location = v
61
+ @ascp_path = nil
62
+ return
75
63
  end
76
64
 
77
65
  def ascp_path
@@ -93,8 +81,7 @@ module Aspera
93
81
  pl = installed_products.find{ |i| i[:name].eql?(product_name)}
94
82
  raise "No such product installed: #{product_name}" if pl.nil?
95
83
  end
96
- self.ascp_path = pl[:ascp_path]
97
- Log.log.debug{"ascp_path=#{@path_to_ascp}"}
84
+ @ascp_path = pl[:ascp_path]
98
85
  end
99
86
 
100
87
  # @return [Hash] with key = file name (String), and value = path to file
@@ -111,7 +98,9 @@ module Aspera
111
98
  end
112
99
  end
113
100
 
101
+ # TODO: if using another product than SDK, should use files from there
114
102
  def check_or_create_sdk_file(filename, force: false, &block)
103
+ FileUtils.mkdir_p(Products::Transferd.sdk_directory)
115
104
  return Environment.write_file_restricted(File.join(Products::Transferd.sdk_directory, filename), force: force, mode: 0o644, &block)
116
105
  end
117
106
 
@@ -127,10 +116,18 @@ module Aspera
127
116
  file = if k.eql?(:transferd)
128
117
  Products::Transferd.transferd_path
129
118
  else
130
- # ensure at least ascp is found
131
- use_ascp_from_product(FIRST_FOUND) if @path_to_ascp.nil?
119
+ # find ascp when needed
120
+ if @ascp_path.nil?
121
+ if @ascp_location.start_with?(USE_PRODUCT_PREFIX)
122
+ use_ascp_from_product(@ascp_location[USE_PRODUCT_PREFIX.length..-1])
123
+ else
124
+ @ascp_path = @ascp_location
125
+ end
126
+ Aspera.assert(File.exist?(@ascp_path)){"No such file: [#{@ascp_path}]"}
127
+ Log.log.debug{"ascp_path=#{@ascp_path}"}
128
+ end
132
129
  # NOTE: that there might be a .exe at the end
133
- @path_to_ascp.gsub('ascp', k.to_s)
130
+ @ascp_path.gsub('ascp', k.to_s)
134
131
  end
135
132
  when :ssh_private_dsa, :ssh_private_rsa
136
133
  # assume last 3 letters are type
@@ -195,7 +192,9 @@ module Aspera
195
192
  return exe_version
196
193
  end
197
194
 
198
- def ascp_pvcl_info
195
+ # Extract some stings from ascp logs
196
+ # Folder, PVCL, version, license information
197
+ def ascp_info_from_log
199
198
  data = {}
200
199
  # read PATHs from ascp directly, and pvcl modules as well
201
200
  Open3.popen3(ascp_path, '-DDL-') do |_stdin, _stdout, stderr, thread|
@@ -227,8 +226,9 @@ module Aspera
227
226
  return data
228
227
  end
229
228
 
230
- # extract some stings from ascp binary
231
- def ascp_ssl_info
229
+ # Extract some stings from ascp binary
230
+ # Openssl information
231
+ def ascp_info_from_file
232
232
  data = {}
233
233
  File.binread(ascp_path).scan(/[\x20-\x7E]{10,}/) do |bin_string|
234
234
  if (m = bin_string.match(/OPENSSLDIR.*"(.*)"/))
@@ -243,17 +243,17 @@ module Aspera
243
243
  # information for `ascp info`
244
244
  def ascp_info
245
245
  ascp_data = file_paths
246
- ascp_data.merge!(ascp_pvcl_info)
247
- ascp_data.merge!(ascp_ssl_info)
246
+ ascp_data.merge!(ascp_info_from_log)
247
+ ascp_data.merge!(ascp_info_from_file)
248
248
  return ascp_data
249
249
  end
250
250
 
251
251
  # @return the url for download of SDK archive for the given platform and version
252
252
  def sdk_url_for_platform(platform: nil, version: nil)
253
- locations = sdk_locations
253
+ all_locations = sdk_locations
254
254
  platform = Environment.instance.architecture if platform.nil?
255
- locations = locations.select{ |l| l['platform'].eql?(platform)}
256
- raise "No SDK for platform: #{platform}" if locations.empty?
255
+ locations = all_locations.select{ |l| l['platform'].eql?(platform)}
256
+ raise "No SDK for platform: #{platform}, available: #{all_locations.map{ |i| i['platform']}.uniq}" if locations.empty?
257
257
  version = locations.max_by{ |entry| Gem::Version.new(entry['version'])}['version'] if version.nil?
258
258
  info = locations.select{ |entry| entry['version'].eql?(version)}
259
259
  raise "No such version: #{version} for #{platform}" if info.empty?
@@ -294,12 +294,12 @@ module Aspera
294
294
  end
295
295
 
296
296
  # Retrieves ascp binary for current system architecture from URL or file
297
- # @param url [String] URL to SDK archive, or SpecialValues::DEF
298
- # @param folder [String] Destination folder path
299
- # @param backup [Bool] If destination folder exists, then rename
300
- # @param with_exe [Bool] If false, only retrieves files, but do not generate or restrict access
301
- # @param &block [Proc] A lambda that receives a file path from archive and tells destination sub folder(end with /) or file, or nil to not extract
302
- # @return ascp version (from execution)
297
+ # @param url [String] URL to SDK archive, or SpecialValues::DEF
298
+ # @param folder [String] Destination folder path
299
+ # @param backup [Boolean] If destination folder exists, then rename
300
+ # @param with_exe [Boolean] If false, only retrieves files, but do not generate or restrict access
301
+ # @param &block [Proc] A lambda that receives a file path from archive and tells destination sub folder(end with /) or file, or nil to not extract
302
+ # @return [Array] name, ascp version (from execution), folder
303
303
  def install_sdk(url: nil, version: nil, folder: nil, backup: true, with_exe: true, &block)
304
304
  url = sdk_url_for_platform(version: version) if url.nil? || url.eql?('DEF')
305
305
  folder = Products::Transferd.sdk_directory if folder.nil?
@@ -348,20 +348,34 @@ module Aspera
348
348
  sdk_name = 'IBM Aspera Transfer SDK'
349
349
  sdk_version = transferd_version || sdk_ascp_version
350
350
  File.write(File.join(folder, Products::Other::INFO_META_FILE), "<product><name>#{sdk_name}</name><version>#{sdk_version}</version></product>")
351
- return sdk_name, sdk_version
351
+ return sdk_name, sdk_version, folder
352
352
  end
353
353
 
354
354
  attr_accessor :transferd_urls
355
355
 
356
356
  private
357
357
 
358
- # policy for product selection
359
- FIRST_FOUND = 'FIRST'
360
-
361
- private_constant :FIRST_FOUND
358
+ DEFAULT_ASPERA_CONF = <<~END_OF_CONFIG_FILE
359
+ <?xml version='1.0' encoding='UTF-8'?>
360
+ <CONF version="2">
361
+ <default>
362
+ <file_system>
363
+ <resume_suffix>.aspera-ckpt</resume_suffix>
364
+ <partial_file_suffix>.partial</partial_file_suffix>
365
+ </file_system>
366
+ </default>
367
+ </CONF>
368
+ END_OF_CONFIG_FILE
369
+ # all executable files from SDK
370
+ EXE_FILES = %i[ascp ascp4 async transferd].freeze
371
+ SDK_FILES = %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key].unshift(*EXE_FILES).freeze
372
+ TRANSFERD_ARCHIVE_LOCATION_URL = 'https://ibm.biz/sdk_location'
373
+ # filename for ascp with optional extension (Windows)
374
+ private_constant :DEFAULT_ASPERA_CONF, :EXE_FILES, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URL
362
375
 
363
376
  def initialize
364
- @path_to_ascp = nil
377
+ @ascp_path = nil
378
+ @ascp_location = nil
365
379
  @sdk_dir = nil
366
380
  @found_products = nil
367
381
  @transferd_urls = TRANSFERD_ARCHIVE_LOCATION_URL
@@ -5,8 +5,12 @@ require 'aspera/assert'
5
5
  module Aspera
6
6
  module Ascp
7
7
  # processing of ascp management port events
8
+ # Reference: `mgmtmess.c`
8
9
  class Management
9
- # from https://www.google.com/search?q=FASP+error+codes
10
+ # References:
11
+ # https://www.google.com/search?q=FASP+error+codes
12
+ # https://www.ibm.com/support/pages/error-code-reference-tables
13
+ # mgmtmess.c : as_mgmt_err_is_retryable
10
14
  # Note that the fact that an error is retry-able is not internally defined by protocol, it's client-side responsibility
11
15
  # rubocop:disable Layout/FirstHashElementLineBreak
12
16
  ERRORS = {
@@ -289,7 +293,7 @@ module Aspera
289
293
  # cspell: enable
290
294
 
291
295
  class << self
292
- # translate native event name to snake case
296
+ # Translate native event name to snake case
293
297
  def field_native_to_snake(name)
294
298
  case name
295
299
  when 'Elapsedusec' then 'elapsed_usec'
@@ -298,7 +302,7 @@ module Aspera
298
302
  end
299
303
  end
300
304
 
301
- # translate snake case event name to native
305
+ # Translate snake case event name to native
302
306
  # @param name [String] Field name
303
307
  def field_snake_to_native(name)
304
308
  field = name.delete('_')
@@ -320,7 +324,7 @@ module Aspera
320
324
  end
321
325
 
322
326
  # Build command to send on management port
323
- # @param data [Hash] e.g. {'type'=>'START','source'=>_path_,'destination'=>_path_}
327
+ # @param data [Hash] keys are snake case: e.g. {'type'=>'START','source'=>_path_,'destination'=>_path_}
324
328
  # @return [String] frame to send on management port
325
329
  def command_to_stream(data)
326
330
  data
@@ -339,7 +343,7 @@ module Aspera
339
343
  end
340
344
  attr_reader :last_event
341
345
 
342
- # process line of mgt port event
346
+ # Process line of mgt port event
343
347
  # @param line [String] line of mgt port event
344
348
  # @return [Hash] event hash or nil if event is not yet complete
345
349
  def process_line(line)
data/lib/aspera/assert.rb CHANGED
@@ -31,7 +31,7 @@ module Aspera
31
31
  end
32
32
 
33
33
  # Assert that a condition is true, else raise exception
34
- # @param assertion [Bool] Must be true
34
+ # @param assertion [TrueClass, FalseClass] Must be true
35
35
  # @param info [String,nil] Fixed message in case assert fails, else use `block`
36
36
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
37
37
  # @param block [Proc] Produces a string that describes the problem for complex messages
@@ -41,8 +41,8 @@ module Aspera
41
41
  return if assertion
42
42
  message = 'assertion failed'
43
43
  info = yield if block_given?
44
- message = "#{message}: #{info}" if info
45
- message = "#{message}: #{caller.find{ |call| !call.start_with?(__FILE__)}}"
44
+ message = type.eql?(AssertError) ? "#{message}: #{info}" : info if info
45
+ # message = "#{message}: #{caller.find{ |call| !call.start_with?(__FILE__)}}"
46
46
  report_error(type, message)
47
47
  end
48
48
 
@@ -52,11 +52,33 @@ module Aspera
52
52
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
53
53
  # @param block [Proc] Additional description in front of message
54
54
  def assert_type(value, *classes, type: AssertError)
55
- assert(classes.any?{ |k| value.is_a?(k)}, type: type){"#{"#{yield}: " if block_given?}expecting #{classes.join(', ')}, but have #{value.inspect}"}
55
+ assert(classes.any?{ |k| value.is_a?(k)}, type: type){"#{"#{yield}: " if block_given?}expecting #{classes.join(', ')}, but have (#{value.class})#{value.inspect}"}
56
+ end
57
+
58
+ # Assert that all value of array are of the same specified type
59
+ # @param array [Array] The array to check
60
+ # @param klass [Class] The expected type of elements
61
+ # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
62
+ # @param block [Proc] Additional description in front of message
63
+ def assert_array_all(array, klass, type: AssertError)
64
+ assert_type(array, Array, type: type)
65
+ assert(array.all?(klass), type: type){"#{"#{yield}: " if block_given?}expecting all as #{klass}, but have #{array.map(&:class).uniq}"}
66
+ end
67
+
68
+ # Assert value is Hash, keys have type, and Values have type
69
+ # @param hash [Hash] The hash to check
70
+ # @param key_class [Class] The expected type of keys (or nil)
71
+ # @param value_class [Class] The expected type of values (or nil)
72
+ # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
73
+ # @param block [Proc] Additional description in front of message
74
+ def assert_hash_all(hash, key_class, value_class, type: AssertError)
75
+ assert_type(hash, Hash, type: type)
76
+ assert_array_all(hash.keys, key_class, type: AssertError){"#{"#{yield}: " if block_given?}keys"} unless key_class.nil?
77
+ assert_array_all(hash.values, value_class, type: AssertError){"#{"#{yield}: " if block_given?}values"} unless value_class.nil?
56
78
  end
57
79
 
58
80
  # Assert that value is one of the given values
59
- # @param value [any] Value to check
81
+ # @param value [Object] Value to check
60
82
  # @param values [Array] Accepted values
61
83
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
62
84
  # @param block [Proc] Additional description in front of message
@@ -69,7 +91,7 @@ module Aspera
69
91
  end
70
92
 
71
93
  # The value is not one of the expected values
72
- # @param value [any] The wrong value
94
+ # @param value [Object] The wrong value
73
95
  # @param type [Exception,Symbol] Exception to raise, or Symbol for Log.log
74
96
  # @param block [Proc] Additional description in front of message
75
97
  def error_unexpected_value(value, type: InternalError)
@@ -6,11 +6,13 @@ module Aspera
6
6
  class Error < StandardError; end
7
7
  # Raised when an unexpected argument is provided.
8
8
  class BadArgument < Error; end
9
+ class MissingArgument < Error; end
9
10
  class NoSuchElement < Error; end
10
11
 
11
12
  class BadIdentifier < Error
12
- def initialize(res_type, res_id)
13
- super("#{res_type} with identifier #{res_id} not found")
13
+ def initialize(res_type, res_id, field: 'identifier', count: 0)
14
+ msg = count.eql?(0) ? 'not found' : "found #{count}"
15
+ super("#{res_type} with #{field}=#{res_id}: #{msg}")
14
16
  end
15
17
  end
16
18
  end
@@ -17,16 +17,8 @@ module Aspera
17
17
  class ExtendedValue
18
18
  include Singleton
19
19
 
20
- MARKER_START = '@'
21
- MARKER_END = ':'
22
- MARKER_IN_END = '@'
23
-
24
- # Special handlers stop processing of handlers on right
25
- # :extend includes processing of other handlers in itself
26
- # :val keeps the value intact
27
- SPECIAL_HANDLERS = %i[extend val].freeze
28
-
29
- private_constant :MARKER_START, :MARKER_END, :MARKER_IN_END, :SPECIAL_HANDLERS
20
+ # First is default
21
+ DEFAULT_DECODERS = %i[none json ruby yaml]
30
22
 
31
23
  class << self
32
24
  # Decode comma separated table text
@@ -45,8 +37,41 @@ module Aspera
45
37
  return hash_array
46
38
  end
47
39
 
48
- def assert_no_value(value, what)
49
- raise "no value allowed for extended value type: #{what}" unless value.empty?
40
+ # JSON Parser, with more information on error location
41
+ # extract a context: 10 chars before and after the error on the given line and display a pointer "^"
42
+ # :reek:UncommunicativeMethodName
43
+ def JSON_parse(value) # rubocop:disable Naming/MethodName
44
+ JSON.parse(value)
45
+ rescue JSON::ParserError => e
46
+ m = /at line (\d+) column (\d+)/.match(e.message)
47
+ raise if m.nil?
48
+ line = m[1].to_i - 1
49
+ column = m[2].to_i - 1
50
+ lines = value.lines
51
+ raise if line >= lines.size
52
+ error_line = lines[line].chomp
53
+ context_col_beg = [column - 10, 0].max
54
+ context_col_end = [column + 10, error_line.length].min
55
+ context = error_line[context_col_beg...context_col_end]
56
+ cursor_pos = column - context_col_beg
57
+ pointer = ' ' * cursor_pos + '^'.blink
58
+ raise BadArgument, "#{e.message}\n#{context}\n#{pointer}"
59
+ end
60
+
61
+ # The value must be empty
62
+ # @param value [String] The value as parameter
63
+ # @param ext_type [Symbol] The method of extended value
64
+ def assert_no_value(value, ext_type)
65
+ Aspera.assert(value.empty?, type: BadArgument){"no value allowed for extended value type: #{ext_type}"}
66
+ end
67
+
68
+ def read_stdin(mode)
69
+ case mode
70
+ when '' then $stdin.read
71
+ when 'bin' then $stdin.binmode.read
72
+ when 'chomp' then $stdin.chomp
73
+ else raise BadArgument, "`stdin` supports only: '', 'bin' or 'chomp'"
74
+ end
50
75
  end
51
76
  end
52
77
 
@@ -54,7 +79,8 @@ module Aspera
54
79
 
55
80
  def initialize
56
81
  # Base handlers
57
- # Other handlers can be set using set_handler, e.g. `preset` is reader in config plugin
82
+ # Other handlers can be set using `on`
83
+ # e.g. `preset` is reader in config plugin
58
84
  @handlers = {
59
85
  val: lambda{ |i| i},
60
86
  base64: lambda{ |i| Base64.decode64(i)},
@@ -62,7 +88,7 @@ module Aspera
62
88
  env: lambda{ |i| ENV.fetch(i, nil)},
63
89
  file: lambda{ |i| File.read(File.expand_path(i))},
64
90
  uri: lambda{ |i| UriReader.read(i)},
65
- json: lambda{ |i| JSON_parse(i)},
91
+ json: lambda{ |i| ExtendedValue.JSON_parse(i)},
66
92
  lines: lambda{ |i| i.split("\n")},
67
93
  list: lambda{ |i| i[1..-1].split(i[0])},
68
94
  none: lambda{ |i| ExtendedValue.assert_no_value(i, :none); nil}, # rubocop:disable Style/Semicolon
@@ -70,67 +96,63 @@ module Aspera
70
96
  re: lambda{ |i| Regexp.new(i, Regexp::MULTILINE)},
71
97
  ruby: lambda{ |i| Environment.secure_eval(i, __FILE__, __LINE__)},
72
98
  secret: lambda{ |i| prompt = i.empty? ? 'secret' : i; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
73
- stdin: lambda{ |i| ExtendedValue.assert_no_value(i, :stdin); $stdin.read}, # rubocop:disable Style/Semicolon
74
- stdbin: lambda{ |i| ExtendedValue.assert_no_value(i, :stdbin); $stdin.binmode.read}, # rubocop:disable Style/Semicolon
99
+ stdin: lambda{ |i| ExtendedValue.read_stdin(i)},
75
100
  yaml: lambda{ |i| YAML.load(i)},
76
101
  zlib: lambda{ |i| Zlib::Inflate.inflate(i)},
77
- extend: lambda{ |i| ExtendedValue.instance.evaluate_all(i)}
102
+ extend: lambda{ |i| ExtendedValue.instance.evaluate_extend(i)}
78
103
  }
104
+ @regex_single = nil
105
+ @regex_extend = nil
79
106
  @default_decoder = nil
107
+ update_regex
80
108
  end
81
109
 
82
- # Regex to match an extended value
83
- def handler_regex_string
84
- "#{MARKER_START}(#{modifiers.join('|')})#{MARKER_END}"
85
- end
86
-
87
- # JSON Parser, with more information on error location
88
- # :reek:UncommunicativeMethodName
89
- def JSON_parse(value) # rubocop:disable Naming/MethodName
90
- JSON.parse(value)
91
- rescue JSON::ParserError => e
92
- m = /at line (\d+) column (\d+)/.match(e.message)
93
- raise if m.nil?
94
- line = m[1].to_i - 1
95
- column = m[2].to_i - 1
96
- lines = value.lines
97
- raise if line >= lines.size
98
- error_line = lines[line].chomp
99
- context_col_beg = [column - 10, 0].max
100
- context_col_end = [column + 10, error_line.length].min
101
- context = error_line[context_col_beg...context_col_end]
102
- cursor_pos = column - context_col_beg
103
- pointer = ' ' * cursor_pos + '^'.blink
104
- raise BadArgument, "#{e.message}\n#{context}\n#{pointer}"
110
+ # Update the Regex to match an extended value based on @handlers
111
+ def update_regex
112
+ handler_regex = "#{MARKER_START}(#{modifiers.join('|')})#{MARKER_END}"
113
+ @regex_single = Regexp.new("^#{handler_regex}(.*)$", Regexp::MULTILINE)
114
+ @regex_extend = Regexp.new("^(.*)#{handler_regex}([^#{MARKER_IN_END}]*)#{MARKER_IN_END}(.*)$", Regexp::MULTILINE)
105
115
  end
106
116
 
107
117
  public
108
118
 
119
+ attr_reader :default_decoder
120
+
109
121
  def default_decoder=(value)
110
122
  Log.log.debug{"Setting default decoder to (#{value.class}) #{value}"}
111
- Aspera.assert(value.nil? || @handlers.key?(value))
123
+ Aspera.assert_values(value, DEFAULT_DECODERS)
124
+ value = nil if value.eql?(:none)
112
125
  @default_decoder = value
113
126
  end
114
127
 
128
+ # List of Extended Value methods
115
129
  def modifiers; @handlers.keys; end
116
130
 
117
131
  # Add a new handler
118
- def set_handler(name, method)
119
- Log.log.debug{"setting handler for #{name}"}
132
+ def on(name, &block)
120
133
  Aspera.assert_type(name, Symbol){'name'}
121
- @handlers[name] = method
134
+ Aspera.assert(block)
135
+ Log.log.debug{"Setting handler for #{name}"}
136
+ @handlers[name] = block
137
+ update_regex
122
138
  end
123
139
 
124
- # Parse an string value to extended value, if it is a String using supported extended value modifiers
125
- # Other value types are returned as is
126
- # @param value [String] the value to parse
127
- # @param expect [Class,Array] one or a list of expected types
128
- def evaluate(value)
140
+ # Parses a `String` value to extended value.
141
+ # If it is a String using supported extended value modifiers, then evaluate them.
142
+ # Other value types are returned as is.
143
+ # @param value [String] the value to parse
144
+ # @param context [String] Context in which evaluation is done
145
+ # @param allowed [Array<Class>,NilClass] Expected types
146
+ # @return [Object] Evaluated value
147
+ def evaluate(value, context:, allowed: nil)
129
148
  return value unless value.is_a?(String)
130
- regex = Regexp.new("^#{handler_regex_string}(.*)$", Regexp::MULTILINE)
149
+ Aspera.assert_array_all(allowed, Class) unless allowed.nil?
150
+ # use default decoder if not an extended value and expect complex types
151
+ using_default_decoder = allowed&.all?{ |t| DEFAULT_PARSER_TYPES.include?(t)} && !@regex_single.match?(value) && !@default_decoder.nil?
152
+ value = [MARKER_START, @default_decoder, MARKER_END, value].join if using_default_decoder
131
153
  # First determine decoders, in reversed order
132
154
  handlers_reversed = []
133
- while (m = value.match(regex))
155
+ while (m = value.match(@regex_single))
134
156
  handler = m[1].to_sym
135
157
  handlers_reversed.unshift(handler)
136
158
  value = m[2]
@@ -139,27 +161,37 @@ module Aspera
139
161
  Log.log.trace1{"evaluating: #{handlers_reversed}, value: #{value}"}
140
162
  handlers_reversed.each do |handler|
141
163
  value = @handlers[handler].call(value)
164
+ rescue => e
165
+ raise BadArgument, "Evaluation of #{handler} for #{context}: #{e.message}"
142
166
  end
143
167
  return value
144
168
  end
145
169
 
146
- # Parse string value as extended value
147
- # Use default decoder if none is specified
148
- def evaluate_with_default(value)
149
- value = [MARKER_START, @default_decoder, MARKER_END, value].join if value.is_a?(String) && value.match(/^#{handler_regex_string}.*$/).nil? && !@default_decoder.nil?
150
- return evaluate(value)
151
- end
152
-
153
170
  # Find inner extended values
154
- def evaluate_all(value)
155
- regex = Regexp.new("^(.*)#{handler_regex_string}([^#{MARKER_IN_END}]*)#{MARKER_IN_END}(.*)$", Regexp::MULTILINE)
156
- while (m = value.match(regex))
171
+ # Only used in above lambda
172
+ def evaluate_extend(value)
173
+ while (m = value.match(@regex_extend))
157
174
  sub_value = "@#{m[2]}:#{m[3]}"
158
175
  Log.log.debug{"evaluating #{sub_value}"}
159
- value = "#{m[1]}#{evaluate(sub_value)}#{m[4]}"
176
+ value = "#{m[1]}#{evaluate(sub_value, context: 'composite extended value')}#{m[4]}"
160
177
  end
161
178
  return value
162
179
  end
180
+ # marker "@"
181
+ MARKER_START = '@'
182
+ # marker ":"
183
+ MARKER_END = ':'
184
+ # marker "@"
185
+ MARKER_IN_END = '@'
186
+
187
+ # Special handlers stop processing of handlers on right
188
+ # :extend includes processing of other handlers in itself
189
+ # :val keeps the value intact
190
+ SPECIAL_HANDLERS = %i[extend val].freeze
191
+
192
+ # Array and Hash types:
193
+ DEFAULT_PARSER_TYPES = [Array, Hash].freeze
194
+ private_constant :MARKER_START, :MARKER_END, :MARKER_IN_END, :SPECIAL_HANDLERS, :DEFAULT_PARSER_TYPES
163
195
  end
164
196
  end
165
197
  end