aspera-cli 4.20.0 → 4.21.2
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +41 -3
- data/CONTRIBUTING.md +69 -142
- data/README.md +687 -461
- data/bin/ascli +5 -14
- data/bin/asession +3 -5
- data/examples/get_proto_file.rb +4 -3
- data/examples/proxy.pac +20 -20
- data/lib/aspera/agent/base.rb +2 -0
- data/lib/aspera/agent/connect.rb +20 -2
- data/lib/aspera/agent/{alpha.rb → desktop.rb} +12 -18
- data/lib/aspera/agent/direct.rb +30 -31
- data/lib/aspera/agent/node.rb +1 -11
- data/lib/aspera/agent/{trsdk.rb → transferd.rb} +37 -51
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +13 -8
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/node.rb +49 -32
- data/lib/aspera/ascp/installation.rb +98 -77
- data/lib/aspera/ascp/management.rb +27 -6
- data/lib/aspera/cli/extended_value.rb +9 -3
- data/lib/aspera/cli/formatter.rb +155 -154
- data/lib/aspera/cli/info.rb +2 -1
- data/lib/aspera/cli/main.rb +12 -0
- data/lib/aspera/cli/manager.rb +4 -4
- data/lib/aspera/cli/plugin.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +134 -73
- data/lib/aspera/cli/plugins/config.rb +114 -83
- data/lib/aspera/cli/plugins/cos.rb +1 -0
- data/lib/aspera/cli/plugins/faspex.rb +4 -2
- data/lib/aspera/cli/plugins/faspex5.rb +29 -14
- data/lib/aspera/cli/plugins/node.rb +51 -41
- data/lib/aspera/cli/transfer_progress.rb +2 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +1 -1
- data/lib/aspera/coverage.rb +5 -3
- data/lib/aspera/environment.rb +59 -16
- data/lib/aspera/faspex_postproc.rb +3 -5
- data/lib/aspera/hash_ext.rb +2 -12
- data/lib/aspera/node_simulator.rb +230 -112
- data/lib/aspera/oauth/base.rb +40 -48
- data/lib/aspera/oauth/factory.rb +41 -2
- data/lib/aspera/oauth/jwt.rb +4 -1
- data/lib/aspera/persistency_action_once.rb +1 -1
- data/lib/aspera/persistency_folder.rb +20 -2
- data/lib/aspera/preview/generator.rb +13 -10
- data/lib/aspera/preview/options.rb +2 -2
- data/lib/aspera/preview/terminal.rb +1 -1
- data/lib/aspera/preview/utils.rb +11 -6
- data/lib/aspera/products/connect.rb +82 -0
- data/lib/aspera/products/desktop.rb +30 -0
- data/lib/aspera/products/other.rb +82 -0
- data/lib/aspera/products/transferd.rb +61 -0
- data/lib/aspera/rest.rb +22 -17
- data/lib/aspera/secret_hider.rb +9 -2
- data/lib/aspera/ssh.rb +31 -24
- data/lib/aspera/temp_file_manager.rb +5 -4
- data/lib/aspera/transfer/parameters.rb +2 -1
- data/lib/aspera/transfer/spec.yaml +22 -20
- data/lib/aspera/transfer/sync.rb +1 -5
- data/lib/aspera/transfer/uri.rb +2 -2
- data/lib/aspera/uri_reader.rb +18 -1
- data/lib/transferd_pb.rb +86 -0
- data/lib/transferd_services_pb.rb +84 -0
- data.tar.gz.sig +0 -0
- metadata +13 -166
- metadata.gz.sig +0 -0
- data/examples/build_exec +0 -74
- data/examples/build_exec_rubyc +0 -40
- data/lib/aspera/ascp/products.rb +0 -168
- data/lib/transfer_pb.rb +0 -84
- data/lib/transfer_services_pb.rb +0 -82
@@ -127,10 +127,13 @@ module Aspera
|
|
127
127
|
# commands for execute_command_gen4
|
128
128
|
COMMANDS_GEN4 = %i[mkdir rename delete upload download sync http_node_download show modify permission thumbnail v3].concat(NODE4_READ_ACTIONS).freeze
|
129
129
|
|
130
|
+
# commands supported in ATS for COS
|
130
131
|
COMMANDS_COS = %i[upload download info access_keys api_details transfer].freeze
|
131
132
|
COMMANDS_SHARES = (BASE_ACTIONS - %i[search]).freeze
|
132
133
|
COMMANDS_FASPEX = COMMON_ACTIONS
|
133
134
|
|
135
|
+
GEN4_LS_FIELDS = %w[name type recursive_size size modified_time access_level].freeze
|
136
|
+
|
134
137
|
def initialize(api: nil, **env)
|
135
138
|
super(**env, basic_options: api.nil?)
|
136
139
|
Node.declare_options(options) if api.nil?
|
@@ -474,7 +477,7 @@ module Aspera
|
|
474
477
|
result[:password] = apifid[:api].auth_params[:password]
|
475
478
|
when :oauth2
|
476
479
|
result[:username] = apifid[:api].params[:headers][Api::Node::HEADER_X_ASPERA_ACCESS_KEY]
|
477
|
-
result[:password] = apifid[:api].oauth.
|
480
|
+
result[:password] = apifid[:api].oauth.authorization
|
478
481
|
else Aspera.error_unreachable_line
|
479
482
|
end
|
480
483
|
return {type: :single_object, data: result} if command_repo.eql?(:node_info)
|
@@ -484,22 +487,15 @@ module Aspera
|
|
484
487
|
when :browse
|
485
488
|
apifid = apifid_from_next_arg(top_file_id)
|
486
489
|
file_info = apifid[:api].read_with_cache("files/#{apifid[:file_id]}")
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
subpath: "files/#{apifid[:file_id]}/files",
|
491
|
-
headers: Api::Node.cache_control_headers,
|
492
|
-
query: query_read_delete)
|
493
|
-
items = result[:data]
|
494
|
-
formatter.display_item_count(result[:data].length, result[:http]['X-Total-Count'])
|
495
|
-
else
|
496
|
-
items = [file_info]
|
490
|
+
unless file_info['type'].eql?('folder')
|
491
|
+
# a single file
|
492
|
+
return {type: :object_list, data: [file_info], fields: GEN4_LS_FIELDS}
|
497
493
|
end
|
498
|
-
return {type: :object_list, data:
|
494
|
+
return {type: :object_list, data: apifid[:api].list_files(apifid[:file_id]), fields: GEN4_LS_FIELDS}
|
499
495
|
when :find
|
500
496
|
apifid = apifid_from_next_arg(top_file_id)
|
501
|
-
|
502
|
-
return {type: :object_list, data: @api_node.find_files(apifid[:file_id],
|
497
|
+
find_lambda = Api::Node.file_matcher_from_argument(options)
|
498
|
+
return {type: :object_list, data: @api_node.find_files(apifid[:file_id], find_lambda), fields: ['path']}
|
503
499
|
when :mkdir
|
504
500
|
containing_folder_path = options.get_next_argument('path').split(Api::Node::PATH_SEPARATOR)
|
505
501
|
new_folder = containing_folder_path.pop
|
@@ -605,16 +601,19 @@ module Aspera
|
|
605
601
|
return Main.result_image(result[:http].body, formatter: formatter)
|
606
602
|
when :permission
|
607
603
|
apifid = apifid_from_next_arg(top_file_id)
|
608
|
-
command_perm = options.get_next_command(%i[list create delete])
|
604
|
+
command_perm = options.get_next_command(%i[list show create delete])
|
609
605
|
case command_perm
|
610
606
|
when :list
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
items = apifid[:api].read('permissions',
|
607
|
+
list_query = query_read_delete(default: {'include' => Rest.array_params(%w[access_level permission_count])})
|
608
|
+
# specify file to get permissions for unless not specified
|
609
|
+
list_query['file_id'] = apifid[:file_id] unless apifid[:file_id].to_s.empty?
|
610
|
+
list_query['inherited'] = false if list_query.key?('file_id') && !list_query.key?('inherited')
|
611
|
+
# NOTE: supports per_page and page and header X-Total-Count
|
612
|
+
items = apifid[:api].read('permissions', list_query)
|
617
613
|
return {type: :object_list, data: items}
|
614
|
+
when :show
|
615
|
+
perm_id = instance_identifier
|
616
|
+
return Main.result_single_object(apifid[:api].read("permissions/#{perm_id}"))
|
618
617
|
when :delete
|
619
618
|
return do_bulk_operation(command: command_perm, descr: 'identifier', values: :identifier) do |one_id|
|
620
619
|
apifid[:api].delete("permissions/#{one_id}")
|
@@ -802,42 +801,53 @@ module Aspera
|
|
802
801
|
end
|
803
802
|
case command
|
804
803
|
when :list
|
804
|
+
transfer_filter = query_read_delete(default: {})
|
805
|
+
last_iteration_token = nil
|
805
806
|
iteration_persistency = nil
|
806
|
-
iteration_data = []
|
807
807
|
if options.get_option(:once_only, mandatory: true)
|
808
808
|
iteration_persistency = PersistencyActionOnce.new(
|
809
809
|
manager: persistency,
|
810
|
-
data:
|
810
|
+
data: [],
|
811
811
|
id: IdGenerator.from_list([
|
812
812
|
'node_transfers',
|
813
813
|
options.get_option(:url, mandatory: true),
|
814
814
|
options.get_option(:username, mandatory: true)
|
815
815
|
]))
|
816
|
+
if transfer_filter.delete('reset')
|
817
|
+
iteration_persistency.data.clear
|
818
|
+
iteration_persistency.save
|
819
|
+
return Main.result_status('Persistency reset')
|
820
|
+
end
|
821
|
+
last_iteration_token = iteration_persistency.data.first
|
816
822
|
end
|
817
|
-
|
818
|
-
if transfer_filter.delete('reset')
|
819
|
-
iteration_data.clear
|
820
|
-
iteration_persistency&.save
|
821
|
-
return Main.result_status('Persistency reset')
|
822
|
-
end
|
823
|
+
raise 'reset only with once_only' if transfer_filter.key?('reset') && iteration_persistency.nil?
|
823
824
|
max_items = transfer_filter.delete(MAX_ITEMS)
|
824
|
-
transfer_filter['iteration_token'] = iteration_persistency.data[0] unless iteration_data.empty?
|
825
825
|
transfers_data = []
|
826
826
|
loop do
|
827
|
+
transfer_filter['iteration_token'] = last_iteration_token unless last_iteration_token.nil?
|
827
828
|
result = @api_node.call(operation: 'GET', subpath: res_class_path, query: transfer_filter)
|
828
|
-
|
829
|
-
|
830
|
-
|
829
|
+
# no data
|
830
|
+
break if result[:data].empty?
|
831
|
+
# get next iteration token from link
|
832
|
+
next_iteration_token = nil
|
833
|
+
link_info = result[:http]['Link']
|
834
|
+
unless link_info.nil?
|
835
|
+
m = link_info.match(/<([^>]+)>/)
|
836
|
+
raise "Cannot parse iteration in Link: #{link_info}" if m.nil?
|
837
|
+
next_iteration_token = CGI.parse(URI.parse(m[1]).query)['iteration_token']&.first
|
838
|
+
end
|
839
|
+
# same as last iteration: stop
|
840
|
+
break if next_iteration_token&.eql?(last_iteration_token)
|
841
|
+
last_iteration_token = next_iteration_token
|
842
|
+
transfers_data.concat(result[:data])
|
843
|
+
if max_items&.<=(transfers_data.length)
|
844
|
+
# if !max_items.nil? && (transfers_data.length >= max_items)
|
831
845
|
transfers_data = transfers_data.slice(0, max_items)
|
832
846
|
break
|
833
847
|
end
|
834
|
-
|
835
|
-
break if iteration_persistency.nil? || data.empty? || link_info.nil?
|
836
|
-
m = link_info.match(/<([^>]+)>/)
|
837
|
-
raise "Problem with iteration: #{link_info}" if m.nil?
|
838
|
-
iteration_token = CGI.parse(URI.parse(m[1]).query)['iteration_token']&.first
|
839
|
-
iteration_data[0] = transfer_filter['iteration_token'] = iteration_token
|
848
|
+
break if last_iteration_token.nil?
|
840
849
|
end
|
850
|
+
iteration_persistency&.data&.[]=(0, last_iteration_token)
|
841
851
|
iteration_persistency&.save
|
842
852
|
return {
|
843
853
|
type: :object_list,
|
@@ -1006,7 +1016,7 @@ module Aspera
|
|
1006
1016
|
Environment.instance.open_uri("#{options.get_option(:asperabrowserurl)}?goto=#{encoded_params}")
|
1007
1017
|
return Main.result_status('done')
|
1008
1018
|
when :basic_token
|
1009
|
-
return Main.result_status(Rest.
|
1019
|
+
return Main.result_status(Rest.basic_authorization(options.get_option(:username, mandatory: true), options.get_option(:password, mandatory: true)))
|
1010
1020
|
when :bearer_token
|
1011
1021
|
private_key = OpenSSL::PKey::RSA.new(options.get_next_argument('private RSA key PEM value', validation: String))
|
1012
1022
|
token_info = options.get_next_argument('user and group identification', validation: Hash)
|
@@ -1019,7 +1029,7 @@ module Aspera
|
|
1019
1029
|
raise 'Missing key: url' unless parameters.key?(:url)
|
1020
1030
|
uri = URI.parse(parameters[:url])
|
1021
1031
|
server = WebServerSimple.new(uri, certificate: parameters[:certificate])
|
1022
|
-
server.mount(uri.path, NodeSimulatorServlet, parameters[:credentials],
|
1032
|
+
server.mount(uri.path, NodeSimulatorServlet, parameters[:credentials], NodeSimulator.new)
|
1023
1033
|
server.start
|
1024
1034
|
return Main.result_status('Simulator terminated')
|
1025
1035
|
end
|
@@ -79,6 +79,8 @@ module Aspera
|
|
79
79
|
new_title = @sessions.length < 2 ? @title.to_s : "[#{@sessions.length}] #{@title}"
|
80
80
|
@progress_bar.title = new_title unless @progress_bar.title.eql?(new_title)
|
81
81
|
@progress_bar.increment if !progress_provided && !@completed
|
82
|
+
rescue ProgressBar::InvalidProgressError => e
|
83
|
+
Log.log.error{"Progress error: #{e}"}
|
82
84
|
end
|
83
85
|
|
84
86
|
private
|
data/lib/aspera/cli/version.rb
CHANGED
@@ -77,7 +77,7 @@ module Aspera
|
|
77
77
|
@param_hash.each_pair{|key, val|Log.log.warn{"unrecognized parameter: #{key} = \"#{val}\""} if !@used_param_names.include?(key)}
|
78
78
|
# set result
|
79
79
|
env_args[:env].merge!(@result[:env])
|
80
|
-
env_args[:args].
|
80
|
+
env_args[:args].concat(@result[:args])
|
81
81
|
return nil
|
82
82
|
end
|
83
83
|
|
data/lib/aspera/coverage.rb
CHANGED
@@ -20,14 +20,16 @@ if ENV.key?('ENABLE_COVERAGE')
|
|
20
20
|
# lines with those words are ignored from coverage
|
21
21
|
no_cov_functions = %w[error_unreachable_line error_unexpected_value Log.log.trace].freeze
|
22
22
|
SimpleCov.start do
|
23
|
-
|
24
|
-
add_filter 'lib/aspera/node_simulator.rb'
|
25
|
-
add_filter 'lib/aspera/keychain/macos_security.rb'
|
23
|
+
# assert usually do not trigger
|
26
24
|
add_filter do |source_file|
|
27
25
|
source_file.lines.each do |line|
|
28
26
|
line.skipped! if no_cov_functions.any?{|i|line.src.include?(i)}
|
29
27
|
end
|
30
28
|
false
|
31
29
|
end
|
30
|
+
# no coverage test in those
|
31
|
+
add_filter 'lib/aspera/cli/plugins/faspex.rb'
|
32
|
+
add_filter 'lib/aspera/node_simulator.rb'
|
33
|
+
add_filter 'lib/aspera/keychain/macos_security.rb'
|
32
34
|
end
|
33
35
|
end
|
data/lib/aspera/environment.rb
CHANGED
@@ -46,8 +46,7 @@ module Aspera
|
|
46
46
|
return OS_LINUX
|
47
47
|
when /aix/
|
48
48
|
return OS_AIX
|
49
|
-
else
|
50
|
-
raise "Unknown OS: #{RbConfig::CONFIG['host_os']}"
|
49
|
+
else Aspera.error_unexpected_value(RbConfig::CONFIG['host_os']){'host_os'}
|
51
50
|
end
|
52
51
|
end
|
53
52
|
|
@@ -62,8 +61,8 @@ module Aspera
|
|
62
61
|
return CPU_S390
|
63
62
|
when /arm/, /aarch64/
|
64
63
|
return CPU_ARM64
|
64
|
+
else Aspera.error_unexpected_value(RbConfig::CONFIG['host_cpu']){'host_cpu'}
|
65
65
|
end
|
66
|
-
raise "Unknown CPU: #{RbConfig::CONFIG['host_cpu']}"
|
67
66
|
end
|
68
67
|
|
69
68
|
# normalized architecture name
|
@@ -73,9 +72,9 @@ module Aspera
|
|
73
72
|
end
|
74
73
|
|
75
74
|
# executable file extension for current OS
|
76
|
-
def
|
77
|
-
return
|
78
|
-
return
|
75
|
+
def exe_file(name='')
|
76
|
+
return "#{name}.exe" if os.eql?(OS_WINDOWS)
|
77
|
+
return name
|
79
78
|
end
|
80
79
|
|
81
80
|
# on Windows, the env var %USERPROFILE% provides the path to user's home more reliably than %HOMEDRIVE%%HOMEPATH%
|
@@ -96,34 +95,78 @@ module Aspera
|
|
96
95
|
Kernel.send('lave'.reverse, code, empty_binding, file, line)
|
97
96
|
end
|
98
97
|
|
99
|
-
|
98
|
+
# Generate log line for external program with arguments
|
99
|
+
# @param env [Hash, nil] environment variables
|
100
|
+
# @param exec [String] path to executable
|
101
|
+
# @param args [Array, nil] arguments
|
102
|
+
# @return [String] log line with environment, program and arguments
|
103
|
+
def log_spawn(exec:, args: nil, env: nil)
|
100
104
|
[
|
101
105
|
'execute:'.red,
|
102
|
-
env
|
106
|
+
env&.map{|k, v| "#{k}=#{Shellwords.shellescape(v)}"},
|
103
107
|
Shellwords.shellescape(exec),
|
104
|
-
args
|
105
|
-
].flatten.join(' ')
|
108
|
+
args&.map{|a|Shellwords.shellescape(a)}
|
109
|
+
].compact.flatten.join(' ')
|
106
110
|
end
|
107
111
|
|
108
|
-
#
|
112
|
+
# Start process in background
|
109
113
|
# caller can call Process.wait on returned value
|
110
|
-
|
111
|
-
|
114
|
+
# @param exec [String] path to executable
|
115
|
+
# @param args [Array, nil] arguments for executable
|
116
|
+
# @param env [Hash, nil] environment variables
|
117
|
+
# @param options [Hash, nil] spawn options
|
118
|
+
# @return [String] PID of process
|
119
|
+
# @raise [Exception] if problem
|
120
|
+
def secure_spawn(exec:, args: nil, env: nil, **options)
|
121
|
+
Aspera.assert_type(exec, String)
|
122
|
+
Aspera.assert_type(args, Array) unless args.nil?
|
123
|
+
Aspera.assert_type(env, Hash) unless env.nil?
|
124
|
+
Aspera.assert_type(options, Hash) unless options.nil?
|
125
|
+
Log.log.debug {log_spawn(exec: exec, args: args, env: env)}
|
112
126
|
# start ascp in separate process
|
113
|
-
|
127
|
+
spawn_args = []
|
128
|
+
spawn_args.push(env) unless env.nil?
|
129
|
+
spawn_args.push([exec, exec])
|
130
|
+
spawn_args.concat(args) unless args.nil?
|
131
|
+
opts = {close_others: true}
|
132
|
+
opts.merge!(options) unless options.nil?
|
133
|
+
ascp_pid = Process.spawn(*spawn_args, **opts)
|
114
134
|
Log.log.debug{"pid: #{ascp_pid}"}
|
115
135
|
return ascp_pid
|
116
136
|
end
|
117
137
|
|
138
|
+
# start process and wait for completion
|
139
|
+
# @param env [Hash, nil] environment variables
|
140
|
+
# @param exec [String] path to executable
|
141
|
+
# @param args [Array, nil] arguments
|
142
|
+
# @return [String] PID of process
|
143
|
+
def secure_execute(exec:, args: nil, env: nil, **system_args)
|
144
|
+
Aspera.assert_type(exec, String)
|
145
|
+
Aspera.assert_type(args, Array) unless args.nil?
|
146
|
+
Aspera.assert_type(env, Hash) unless env.nil?
|
147
|
+
Log.log.debug {log_spawn(exec: exec, args: args, env: env)}
|
148
|
+
# start in separate process
|
149
|
+
spawn_args = []
|
150
|
+
spawn_args.push(env) unless env.nil?
|
151
|
+
# ensure no shell expansion
|
152
|
+
spawn_args.push([exec, exec])
|
153
|
+
spawn_args.concat(args) unless args.nil?
|
154
|
+
kwargs = {exception: true}
|
155
|
+
kwargs.merge!(system_args)
|
156
|
+
Kernel.system(*spawn_args, **kwargs)
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
|
160
|
+
# Execute process and capture stdout
|
118
161
|
# @param exec [String] path to executable
|
119
162
|
# @param args [Array] arguments to executable
|
120
163
|
# @param opts [Hash] options to capture3
|
121
|
-
# @return stdout of executable or raise
|
164
|
+
# @return stdout of executable or raise exception
|
122
165
|
def secure_capture(exec:, args: [], **opts)
|
123
166
|
Aspera.assert_type(exec, String)
|
124
167
|
Aspera.assert_type(args, Array)
|
125
168
|
Aspera.assert_type(opts, Hash)
|
126
|
-
Log.log.debug {log_spawn(
|
169
|
+
Log.log.debug {log_spawn(exec: exec, args: args)}
|
127
170
|
stdout, stderr, status = Open3.capture3(exec, *args, **opts)
|
128
171
|
Log.log.debug{"status=#{status}, stderr=#{stderr}"}
|
129
172
|
Log.log.trace1{"stdout=#{stdout}"}
|
@@ -39,7 +39,7 @@ module Aspera
|
|
39
39
|
response.body = {status: 'error', message: 'Empty request'}.to_json
|
40
40
|
return
|
41
41
|
end
|
42
|
-
# build script path by removing domain
|
42
|
+
# build script path by removing domain and adding script folder
|
43
43
|
script_file = request.path[@parameters[:root].size..]
|
44
44
|
Log.log.debug{"script file=#{script_file}"}
|
45
45
|
script_path = File.join(@parameters[:script_folder], script_file)
|
@@ -48,11 +48,9 @@ module Aspera
|
|
48
48
|
Log.log.debug{Log.dump(:webhook_parameters, webhook_parameters)}
|
49
49
|
# env expects only strings
|
50
50
|
environment = webhook_parameters.each_with_object({}) { |(k, v), h| h[k] = v.to_s }
|
51
|
-
post_proc_pid =
|
52
|
-
Log.log.debug{"pid=#{post_proc_pid}"}
|
53
|
-
raise 'no pid' if post_proc_pid.nil?
|
54
|
-
# "wait" for process to avoid zombie
|
51
|
+
post_proc_pid = Environment.secure_spawn(env: environment, exec: script_path)
|
55
52
|
Timeout.timeout(@parameters[:timeout_seconds]) do
|
53
|
+
# "wait" for process to avoid zombie
|
56
54
|
Process.wait(post_proc_pid)
|
57
55
|
post_proc_pid = nil
|
58
56
|
end
|
data/lib/aspera/hash_ext.rb
CHANGED
@@ -20,17 +20,7 @@ class ::Hash
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
# in
|
24
|
-
unless Hash.method_defined?(:transform_keys)
|
25
|
-
class Hash
|
26
|
-
def transform_keys
|
27
|
-
raise 'missing block' unless block_given?
|
28
|
-
return each_with_object({}){|(k, v), memo|memo[yield(k)] = v}
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
# rails
|
23
|
+
# Exists in Rails
|
34
24
|
unless Hash.method_defined?(:symbolize_keys)
|
35
25
|
class Hash
|
36
26
|
def symbolize_keys
|
@@ -39,7 +29,7 @@ unless Hash.method_defined?(:symbolize_keys)
|
|
39
29
|
end
|
40
30
|
end
|
41
31
|
|
42
|
-
#
|
32
|
+
# Exists in Rails
|
43
33
|
unless Hash.method_defined?(:stringify_keys)
|
44
34
|
class Hash
|
45
35
|
def stringify_keys
|