Class: Aspera::Cli::Plugin

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

Overview

Base class for plugins

Constant Summary collapse

GLOBAL_OPS =

operations without id

%i[create list].freeze
INSTANCE_OPS =

operations with id

%i[modify delete show].freeze
ALL_OPS =

all standard operations

[GLOBAL_OPS, INSTANCE_OPS].flatten.freeze
MAX_ITEMS =

special query parameter: max number of items for list command

'max'
MAX_PAGES =

special query parameter: max number of pages for list command

'pmax'
REGEX_LOOKUP_ID_BY_FIELD =

special identifier format: look for this name to find where supported

/^%([^:]+):(.*)$/.freeze
INIT_PARAMS =

instance variables, also constructor parameters

%i[options transfer config formatter persistency only_manual].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options:, transfer:, config:, formatter:, persistency:, only_manual:) ⇒ Plugin

Returns a new instance of Plugin.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/aspera/cli/plugin.rb', line 39

def initialize(options:, transfer:, config:, formatter:, persistency:, only_manual:)
  @options = options
  @transfer = transfer
  @config = config
  @formatter = formatter
  @persistency = persistency
  @only_manual = only_manual
  # check presence in descendant of mandatory method and constant
  Aspera.assert(respond_to?(:execute_action)){"Missing method 'execute_action' in #{self.class}"}
  Aspera.assert(self.class.constants.include?(:ACTIONS)){'ACTIONS shall be redefined by subclass'}
  # manual header for all plugins
  options.parser.separator('')
  options.parser.separator("COMMAND: #{self.class.name.split('::').last.downcase}")
  options.parser.separator("SUBCOMMANDS: #{self.class.const_get(:ACTIONS).map(&:to_s).sort.join(' ')}")
  options.parser.separator('OPTIONS:')
end

Class Method Details

.declare_generic_options(options) ⇒ Object



26
27
28
29
30
31
32
33
34
# File 'lib/aspera/cli/plugin.rb', line 26

def declare_generic_options(options)
  options.declare(:query, 'Additional filter for for some commands (list/delete)', types: Hash)
  options.declare(
    :value, 'Value for create, update, list filter', types: Hash,
    deprecation: '(4.14) Use positional value for create/modify or option: query for list/delete')
  options.declare(:property, 'Name of property to set (modify operation)')
  options.declare(:bulk, 'Bulk operation (only some)', values: :bool, default: :no)
  options.declare(:bfail, 'Bulk operation error handling', values: :bool, default: :yes)
end

Instance Method Details

#do_bulk_operation(command:, descr:, values: Hash, id_result: 'id', fields: :default) ⇒ Object

For create and delete operations: execute one actin or multiple if bulk is yes

Parameters:

  • command (Symbol)

    operation: :create, :delete, …

  • descr (String)

    description of the value

  • values (Object) (defaults to: Hash)

    the value(s), or the type of value to get from user

  • id_result (String) (defaults to: 'id')

    key in result hash to use as identifier

  • fields (Array) (defaults to: :default)

    fields to display



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

def do_bulk_operation(command:, descr:, values: Hash, id_result: 'id', fields: :default)
  Aspera.assert(block_given?){'missing block'}
  is_bulk = options.get_option(:bulk)
  case values
  when :identifier
    values = instance_identifier
  when Class
    values = value_create_modify(command: command, type: values, bulk: is_bulk)
  end
  # if not bulk, there is a single value
  params = is_bulk ? values : [values]
  Log.log.warn('Empty list given for bulk operation') if params.empty?
  Log.log.debug{Log.dump(:bulk_operation, params)}
  result_list = []
  params.each do |param|
    # init for delete
    result = {id_result => param}
    begin
      # execute custom code
      res = yield(param)
      # if block returns a hash, let's use this (create)
      result = res if res.is_a?(Hash)
      # TODO: remove when faspio gw api fixes this
      result = res.first if res.is_a?(Array) && res.first.is_a?(Hash)
      # create -> created
      result['status'] = "#{command}#{'e' unless command.to_s.end_with?('e')}d".gsub(/yed$/, 'ied')
    rescue StandardError => e
      raise e if options.get_option(:bfail)
      result['status'] = e.to_s
    end
    result_list.push(result)
  end
  display_fields = [id_result, 'status']
  if is_bulk
    return {type: :object_list, data: result_list, fields: display_fields}
  else
    display_fields = fields unless fields.eql?(:default)
    return {type: :single_object, data: result_list.first, fields: display_fields}
  end
end

#entity_action(rest_api, res_class_path, **opts, &block) ⇒ Object

implement generic rest operations on given resource path



211
212
213
214
215
# File 'lib/aspera/cli/plugin.rb', line 211

def entity_action(rest_api, res_class_path, **opts, &block)
  # res_name=res_class_path.gsub(%r{^.*/},'').gsub(%r{s$},'').gsub('_',' ')
  command = options.get_next_command(ALL_OPS)
  return entity_command(command, rest_api, res_class_path, **opts, &block)
end

#entity_command(command, rest_api, res_class_path, display_fields: nil, item_list_key: false, id_as_arg: false, is_singleton: false, delete_style: nil, &block) ⇒ Object

Returns result suitable for CLI result.

Parameters:

  • command (Symbol)

    command to execute: create show list modify delete

  • rest_api (Rest)

    api to use

  • res_class_path (String)

    sub path in URL to resource relative to base url

  • display_fields (Array) (defaults to: nil)

    fields to display by default

  • item_list_key (String) (defaults to: false)

    result is in a sub key of the json

  • id_as_arg (String) (defaults to: false)

    if set, the id is provided as url argument ?<id_as_arg>=<id>

  • is_singleton (Boolean) (defaults to: false)

    if true, res_class_path is the full path to the resource

  • delete_style (String) (defaults to: nil)

    if set, the delete operation by array in payload

  • block (Proc)

    block to search for identifier based on attribute value

Returns:

  • result suitable for CLI result



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
202
203
204
205
206
207
208
# File 'lib/aspera/cli/plugin.rb', line 140

def entity_command(command, rest_api, res_class_path,
  display_fields: nil,
  item_list_key: false,
  id_as_arg: false,
  is_singleton: false,
  delete_style: nil,
  &block)
  if is_singleton
    one_res_path = res_class_path
  elsif INSTANCE_OPS.include?(command)
    one_res_id = instance_identifier(&block)
    one_res_path = "#{res_class_path}/#{one_res_id}"
    one_res_path = "#{res_class_path}?#{id_as_arg}=#{one_res_id}" if id_as_arg
  end

  case command
  when :create
    raise 'cannot create singleton' if is_singleton
    return do_bulk_operation(command: command, descr: 'data', fields: display_fields) do |params|
      rest_api.create(res_class_path, params)[:data]
    end
  when :delete
    raise 'cannot delete singleton' if is_singleton
    if !delete_style.nil?
      one_res_id = [one_res_id] unless one_res_id.is_a?(Array)
      Aspera.assert_type(one_res_id, Array, exception_class: Cli::BadArgument)
      rest_api.call(operation: 'DELETE', subpath: res_class_path, headers: {'Accept' => 'application/json'}, body: {delete_style => one_res_id}, body_type: :json)
      return Main.result_status('deleted')
    end
    return do_bulk_operation(command: command, descr: 'identifier', values: one_res_id) do |one_id|
      rest_api.delete("#{res_class_path}/#{one_id}", query_read_delete)
      {'id' => one_id}
    end
  when :show
    return {type: :single_object, data: rest_api.read(one_res_path)[:data], fields: display_fields}
  when :list
    resp = rest_api.read(res_class_path, query_read_delete)
    return Main.result_empty if resp[:http].code == '204'
    data = resp[:data]
    # TODO: not generic : which application is this for ?
    if resp[:http]['Content-Type'].start_with?('application/vnd.api+json')
      Log.log.debug{'is vnd.api'}
      data = data[res_class_path]
    end
    if item_list_key
      item_list = data[item_list_key]
      total_count = data['total_count']
      formatter.display_item_count(item_list.length, total_count) unless total_count.nil?
      data = item_list
    end
    case data
    when Hash
      return {type: :single_object, data: data, fields: display_fields}
    when Array
      return {type: :object_list, data: data, fields: display_fields} if data.empty? || data.first.is_a?(Hash)
      return {type: :value_list, data: data, name: 'id'}
    else
      raise "An error occurred: unexpected result type for list: #{data.class}"
    end
  when :modify
    parameters = value_create_modify(command: command)
    property = options.get_option(:property)
    parameters = {property => parameters} unless property.nil?
    rest_api.update(one_res_path, parameters)
    return Main.result_status('modified')
  else
    raise "unknown action: #{command}"
  end
end

#init_paramsObject

Returns a hash of instance variables.

Returns:

  • a hash of instance variables



57
58
59
# File 'lib/aspera/cli/plugin.rb', line 57

def init_params
  INIT_PARAMS.map{|p| [p, instance_variable_get("@#{p}".to_sym)]}.to_h
end

#instance_identifier(description: 'identifier', as_option: nil, &block) ⇒ String, Array

must be called AFTER the instance action, … folder browse <call instance_identifier>

Parameters:

  • description (String) (defaults to: 'identifier')

    description of the identifier

  • as_option (Symbol) (defaults to: nil)

    option name to use if identifier is an option

  • block (Proc)

    block to search for identifier based on attribute value

Returns:

  • (String, Array)

    identifier or list of ids



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/aspera/cli/plugin.rb', line 66

def instance_identifier(description: 'identifier', as_option: nil, &block)
  if as_option.nil?
    res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
  else
    res_id = options.get_option(as_option)
  end
  # can be an Array
  if res_id.is_a?(String) && (m = res_id.match(REGEX_LOOKUP_ID_BY_FIELD))
    if block
      res_id = yield(m[1], ExtendedValue.instance.evaluate(m[2]))
    else
      raise Cli::BadArgument, "Percent syntax for #{description} not supported in this context"
    end
  end
  return res_id
end

#query_option(mandatory: false, default: nil) ⇒ Object

TODO: when deprecation of ‘value` is completed: remove this method, replace with options.get_option(:query) deprecation: 4.14



234
235
236
237
238
239
240
241
242
# File 'lib/aspera/cli/plugin.rb', line 234

def query_option(mandatory: false, default: nil)
  option = :value
  value = options.get_option(option, mandatory: false)
  if value.nil?
    option = :query
    value = options.get_option(option, mandatory: mandatory, default: default)
  end
  return value
end

#query_read_delete(default: nil) ⇒ Object

query parameters in URL suitable for REST list/GET and delete/DELETE



218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/aspera/cli/plugin.rb', line 218

def query_read_delete(default: nil)
  query = options.get_option(:query)
  # dup default, as it could be frozen
  query = default.dup if query.nil?
  Log.log.debug{"Query=#{query}".bg_red}
  begin
    # check it is suitable
    URI.encode_www_form(query) unless query.nil?
  rescue StandardError => e
    raise Cli::BadArgument, "Query must be an extended value which can be encoded with URI.encode_www_form. Refer to manual. (#{e.message})"
  end
  return query
end

#value_create_modify(command:, description: nil, type: Hash, bulk: false, default: nil) ⇒ Object

Retrieves an extended value from command line, used for creation or modification of entities TODO: when deprecation of ‘value` is completed: remove line with :value

Parameters:

  • command (Symbol)

    command name for error message

  • type (Class) (defaults to: Hash)

    expected type of value, either a Class, an Array of Class

  • bulk (Boolean) (defaults to: false)

    if true, value must be an Array of <type>

  • default (Object) (defaults to: nil)

    default value if not provided



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/aspera/cli/plugin.rb', line 250

def value_create_modify(command:, description: nil, type: Hash, bulk: false, default: nil)
  value = options.get_option(:value)
  Log.log.warn("option `value` is deprecated. Use positional parameter for #{command}") unless value.nil?
  value = options.get_next_argument(
    "parameters for #{command}#{description.nil? ? '' : " (#{description})"}", mandatory: default.nil?,
    validation: bulk ? Array : type) if value.nil?
  value = default if value.nil?
  unless type.nil?
    type = [type] unless type.is_a?(Array)
    Aspera.assert(type.all?(Class)){"check types must be a Class, not #{type.map(&:class).join(',')}"}
    if bulk
      Aspera.assert_type(value, Array, exception_class: Cli::BadArgument)
      value.each do |v|
        Aspera.assert_values(v.class, type, exception_class: Cli::BadArgument)
      end
    else
      Aspera.assert_values(value.class, type, exception_class: Cli::BadArgument)
    end
  end
  return value
end