eco-helpers 2.0.16 → 2.0.22
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +125 -6
- data/eco-helpers.gemspec +10 -5
- data/lib/eco-helpers.rb +2 -0
- data/lib/eco/api/common/base_loader.rb +18 -0
- data/lib/eco/api/common/loaders/parser.rb +1 -0
- data/lib/eco/api/common/people/default_parsers.rb +1 -0
- data/lib/eco/api/common/people/default_parsers/date_parser.rb +11 -1
- data/lib/eco/api/common/people/default_parsers/login_providers_parser.rb +1 -1
- data/lib/eco/api/common/people/default_parsers/policy_groups_parser.rb +11 -11
- data/lib/eco/api/common/people/default_parsers/xls_parser.rb +53 -0
- data/lib/eco/api/common/people/entries.rb +1 -0
- data/lib/eco/api/common/people/entry_factory.rb +88 -23
- data/lib/eco/api/common/people/person_entry.rb +5 -2
- data/lib/eco/api/common/people/person_parser.rb +1 -1
- data/lib/eco/api/common/session.rb +1 -0
- data/lib/eco/api/common/session/base_session.rb +2 -0
- data/lib/eco/api/common/session/helpers.rb +30 -0
- data/lib/eco/api/common/session/helpers/prompt_user.rb +34 -0
- data/lib/eco/api/common/session/mailer.rb +0 -1
- data/lib/eco/api/common/session/s3_uploader.rb +0 -1
- data/lib/eco/api/common/session/sftp.rb +0 -1
- data/lib/eco/api/common/version_patches/ecoportal_api/external_person.rb +1 -1
- data/lib/eco/api/common/version_patches/ecoportal_api/internal_person.rb +7 -4
- data/lib/eco/api/common/version_patches/exception.rb +11 -4
- data/lib/eco/api/microcases.rb +3 -1
- data/lib/eco/api/microcases/append_usergroups.rb +0 -1
- data/lib/eco/api/microcases/people_cache.rb +2 -2
- data/lib/eco/api/microcases/people_load.rb +2 -2
- data/lib/eco/api/microcases/people_refresh.rb +2 -2
- data/lib/eco/api/microcases/people_search.rb +6 -6
- data/lib/eco/api/microcases/preserve_default_tag.rb +23 -0
- data/lib/eco/api/microcases/preserve_filter_tags.rb +28 -0
- data/lib/eco/api/microcases/preserve_policy_groups.rb +30 -0
- data/lib/eco/api/microcases/set_account.rb +0 -1
- data/lib/eco/api/microcases/with_each.rb +67 -6
- data/lib/eco/api/microcases/with_each_present.rb +4 -2
- data/lib/eco/api/microcases/with_each_starter.rb +4 -2
- data/lib/eco/api/organization.rb +1 -0
- data/lib/eco/api/organization/people.rb +98 -22
- data/lib/eco/api/organization/people_similarity.rb +272 -0
- data/lib/eco/api/organization/person_schemas.rb +5 -1
- data/lib/eco/api/organization/policy_groups.rb +5 -1
- data/lib/eco/api/organization/presets_factory.rb +22 -83
- data/lib/eco/api/organization/presets_integrity.json +6 -0
- data/lib/eco/api/organization/presets_values.json +5 -4
- data/lib/eco/api/organization/tag_tree.rb +33 -0
- data/lib/eco/api/policies/default_policies/99_user_access_policy.rb +0 -30
- data/lib/eco/api/session.rb +20 -28
- data/lib/eco/api/session/batch.rb +25 -7
- data/lib/eco/api/session/config.rb +0 -10
- data/lib/eco/api/session/config/apis.rb +80 -14
- data/lib/eco/api/session/config/people.rb +1 -17
- data/lib/eco/api/usecases.rb +2 -2
- data/lib/eco/api/usecases/base_case.rb +2 -2
- data/lib/eco/api/usecases/base_io.rb +17 -4
- data/lib/eco/api/usecases/default_cases.rb +2 -1
- data/lib/eco/api/usecases/default_cases/abstract_policygroup_abilities_case.rb +4 -4
- data/lib/eco/api/usecases/default_cases/analyse_people_case.rb +223 -0
- data/lib/eco/api/usecases/default_cases/clean_unknown_tags_case.rb +37 -0
- data/lib/eco/api/usecases/default_cases/codes_to_tags_case.rb +2 -3
- data/lib/eco/api/usecases/default_cases/reset_landing_page_case.rb +11 -1
- data/lib/eco/api/usecases/default_cases/restore_db_case.rb +1 -2
- data/lib/eco/api/usecases/default_cases/supers_cyclic_identify_case.rb +1 -1
- data/lib/eco/api/usecases/default_cases/supers_hierarchy_case.rb +1 -1
- data/lib/eco/api/usecases/default_cases/to_csv_case.rb +132 -29
- data/lib/eco/api/usecases/default_cases/to_csv_detailed_case.rb +61 -36
- data/lib/eco/api/usecases/ooze_samples/ooze_update_case.rb +3 -2
- data/lib/eco/cli/config/default/input.rb +61 -8
- data/lib/eco/cli/config/default/options.rb +48 -17
- data/lib/eco/cli/config/default/people.rb +18 -24
- data/lib/eco/cli/config/default/people_filters.rb +3 -3
- data/lib/eco/cli/config/default/usecases.rb +97 -32
- data/lib/eco/cli/config/default/workflow.rb +22 -13
- data/lib/eco/cli/config/help.rb +1 -0
- data/lib/eco/cli/config/options_set.rb +106 -13
- data/lib/eco/cli/config/use_cases.rb +33 -33
- data/lib/eco/cli/scripting/args_helpers.rb +32 -5
- data/lib/eco/csv.rb +4 -2
- data/lib/eco/csv/table.rb +121 -21
- data/lib/eco/data.rb +1 -0
- data/lib/eco/data/crypto/encryption.rb +3 -3
- data/lib/eco/data/files/helpers.rb +6 -4
- data/lib/eco/data/fuzzy_match.rb +201 -0
- data/lib/eco/data/fuzzy_match/array_helpers.rb +75 -0
- data/lib/eco/data/fuzzy_match/chars_position_score.rb +38 -0
- data/lib/eco/data/fuzzy_match/ngrams_score.rb +82 -0
- data/lib/eco/data/fuzzy_match/pairing.rb +95 -0
- data/lib/eco/data/fuzzy_match/result.rb +87 -0
- data/lib/eco/data/fuzzy_match/results.rb +77 -0
- data/lib/eco/data/fuzzy_match/score.rb +49 -0
- data/lib/eco/data/fuzzy_match/stop_words.rb +35 -0
- data/lib/eco/data/fuzzy_match/string_helpers.rb +82 -0
- data/lib/eco/version.rb +1 -1
- metadata +147 -11
- data/lib/eco/api/microcases/refresh_abilities.rb +0 -19
- data/lib/eco/api/organization/presets_reference.json +0 -59
- data/lib/eco/api/usecases/default_cases/refresh_abilities_case.rb +0 -30
data/lib/eco/cli/config/help.rb
CHANGED
@@ -16,6 +16,7 @@ module Eco
|
|
16
16
|
# Creatas a well aligned line
|
17
17
|
def help_line(key, desc, keys_max_len = key.length, line_len = 100)
|
18
18
|
blanks = keys_max_len + 3 - key.length
|
19
|
+
blanks = blanks < 0 ? 0 : blanks
|
19
20
|
top_line = " #{key}#{" "*blanks} "
|
20
21
|
indent = top_line.length
|
21
22
|
first = true
|
@@ -5,31 +5,53 @@ module Eco
|
|
5
5
|
include Eco::CLI::Config::Help
|
6
6
|
attr_reader :core_config
|
7
7
|
|
8
|
+
class OptConfig < Struct.new(:name, :namespace, :description, :callback)
|
9
|
+
end
|
10
|
+
|
8
11
|
def initialize(core_config:)
|
9
12
|
@core_config = core_config
|
10
|
-
@
|
11
|
-
@description = {}
|
13
|
+
@sets = {}
|
12
14
|
end
|
13
15
|
|
14
16
|
# @return [String] summary of the options.
|
15
17
|
def help
|
18
|
+
indent = 2
|
19
|
+
spaces = any_non_general_space_active? ? active_namespaces : namespaces
|
20
|
+
|
16
21
|
["The following are the available options:"].yield_self do |lines|
|
17
|
-
max_len = keys_max_len(
|
18
|
-
|
19
|
-
|
22
|
+
max_len = keys_max_len(options_args(spaces)) + indent
|
23
|
+
spaces.each do |namespace|
|
24
|
+
is_general = (namespace == :general)
|
25
|
+
str_indent = is_general ? "" : " " * indent
|
26
|
+
lines << help_line(namespace, "", max_len) unless is_general
|
27
|
+
options_set(namespace).each do |arg, option|
|
28
|
+
lines << help_line(" " * indent + "#{option.name}", option.description, max_len)
|
29
|
+
end
|
20
30
|
end
|
21
31
|
lines
|
22
32
|
end.join("\n")
|
23
33
|
end
|
24
34
|
|
25
|
-
# @
|
35
|
+
# @return [Array<String>] all the argument of the options in `namespaces`
|
36
|
+
def options_args(namespaces)
|
37
|
+
namespaces.each_with_object([]) do |space, args|
|
38
|
+
args.concat(options_set(space).keys)
|
39
|
+
end.uniq
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param option [String, Array<String>] the command line option(s).
|
43
|
+
# @param namespace [String] preceding command(s) argument that enables this option.
|
26
44
|
# @param desc [String] description of the option.
|
27
|
-
def add(option, desc = nil)
|
45
|
+
def add(option, desc = nil, namespace: :general)
|
28
46
|
raise "Missing block to define the options builder" unless block_given?
|
29
|
-
|
30
|
-
[option].flatten.compact
|
31
|
-
|
32
|
-
|
47
|
+
|
48
|
+
opts = [option].flatten.compact
|
49
|
+
unless opts.empty?
|
50
|
+
callback = Proc.new
|
51
|
+
opts.each do |opt|
|
52
|
+
puts "Overriding option '#{option}' in '#{namespace}' namespace" if option_exists?(opt, namespace)
|
53
|
+
options_set(namespace)[opt] = OptConfig.new(opt, namespace, desc, callback)
|
54
|
+
end
|
33
55
|
end
|
34
56
|
self
|
35
57
|
end
|
@@ -39,12 +61,83 @@ module Eco
|
|
39
61
|
raise "You need to provide Eco::API::UseCases::BaseIO object. Given: #{io.class}"
|
40
62
|
end
|
41
63
|
|
42
|
-
|
43
|
-
callback.call(io.options, io.session)
|
64
|
+
active_options.each do |option|
|
65
|
+
option.callback.call(io.options, io.session)
|
44
66
|
end
|
67
|
+
|
45
68
|
io.options
|
46
69
|
end
|
47
70
|
|
71
|
+
def active_options
|
72
|
+
@active_options ||= sets.select do |namespace, opts_set|
|
73
|
+
active_namespace?(namespace)
|
74
|
+
end.each_with_object([]) do |(namespace, opts_set), options|
|
75
|
+
opts_set.each do |arg, option|
|
76
|
+
options << option if active_option?(arg, namespace)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def all_options
|
82
|
+
sets.each_with_object([]) do |(namespace, opts_set), options|
|
83
|
+
options << opts_set.values
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def namespaces
|
88
|
+
sets.keys.sort_by do |key|
|
89
|
+
key == :general
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def any_non_general_space_active?
|
94
|
+
(active_namespaces - [:general]).length > 0
|
95
|
+
end
|
96
|
+
|
97
|
+
def active_namespaces
|
98
|
+
@active_namespaces ||= [].tap do |active|
|
99
|
+
active << :general
|
100
|
+
other = (namespaces - [:general]).select {|nm| SCR.arg?(nm)}
|
101
|
+
active.concat(other)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def active_namespace?(namespace)
|
109
|
+
(namespace == :general) || SCR.get_arg(namespace)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Is the option active?
|
113
|
+
# 1. If :general namespace, it does just a direct check
|
114
|
+
# 2. Otherwise, the `namespace` wording should come first in the `cli` or it is considered inactive
|
115
|
+
def active_option?(opt, namespace = :general)
|
116
|
+
if namespace == :general
|
117
|
+
SCR.get_arg(opt)
|
118
|
+
else
|
119
|
+
active_namespace?(namespace) && SCR.arg_order?(namespace, opt) && SCR.get_arg(opt)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def option_exists?(opt, namespace = :general)
|
124
|
+
options_set(namespace).key?(opt)
|
125
|
+
end
|
126
|
+
|
127
|
+
def sets
|
128
|
+
@sets ||= {
|
129
|
+
general: {}
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def namespaces
|
134
|
+
@sets.keys
|
135
|
+
end
|
136
|
+
|
137
|
+
def options_set(namespace = :general)
|
138
|
+
@sets[namespace] ||= {}
|
139
|
+
end
|
140
|
+
|
48
141
|
end
|
49
142
|
end
|
50
143
|
end
|
@@ -5,18 +5,35 @@ module Eco
|
|
5
5
|
include Eco::CLI::Config::Help
|
6
6
|
attr_reader :core_config
|
7
7
|
|
8
|
+
class CaseConfig < Struct.new(:cases_config, :option, :type, :description, :casename, :callback)
|
9
|
+
|
10
|
+
def add_option(arg, desc = nil, &block)
|
11
|
+
core_config.options_set.add(arg, desc, namespace: option, &block)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def core_config
|
18
|
+
cases_config.core_config
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class ActiveCase < Struct.new(:index, :option, :callback)
|
23
|
+
|
24
|
+
end
|
25
|
+
|
8
26
|
def initialize(core_config:)
|
9
27
|
@core_config = core_config
|
10
28
|
@linked_cases = {}
|
11
|
-
@description = {}
|
12
29
|
end
|
13
30
|
|
14
31
|
# @return [String] summary of the use cases.
|
15
32
|
def help
|
16
33
|
["The following are the available use cases:"].yield_self do |lines|
|
17
34
|
max_len = keys_max_len(@linked_cases.keys)
|
18
|
-
@linked_cases.keys.sort.each do |
|
19
|
-
lines << help_line(
|
35
|
+
@linked_cases.keys.sort.each do |option_case|
|
36
|
+
lines << help_line(option_case, @linked_cases[option_case].description, max_len)
|
20
37
|
end
|
21
38
|
lines
|
22
39
|
end.join("\n")
|
@@ -33,18 +50,8 @@ module Eco
|
|
33
50
|
raise "You must specify a valid 'case_name' when no block is provided" unless case_name
|
34
51
|
raise "'case_name' expected to be a String. Given: #{case_name.class}" unless case_name.is_a?(String)
|
35
52
|
end
|
36
|
-
|
37
|
-
@linked_cases[option_case] =
|
38
|
-
type => {
|
39
|
-
option: option_case,
|
40
|
-
type: type,
|
41
|
-
casename: case_name,
|
42
|
-
callback: callback
|
43
|
-
}
|
44
|
-
}
|
45
|
-
@description[option_case] = desc
|
46
|
-
|
47
|
-
self
|
53
|
+
puts "Overriding case config '#{option_case}'" if @linked_cases.key?(option_case)
|
54
|
+
@linked_cases[option_case] = CaseConfig.new(self, option_case, type, desc, case_name, callback)
|
48
55
|
end
|
49
56
|
|
50
57
|
# Scopes/identifies which usecases are being invoked from the command line
|
@@ -55,20 +62,13 @@ module Eco
|
|
55
62
|
def active(io:)
|
56
63
|
validate_io!(io)
|
57
64
|
return @active_cases unless !@active_cases
|
58
|
-
active_cases = {}
|
59
|
-
@linked_cases.each do |option_case, types|
|
65
|
+
@active_cases = @linked_cases.each_with_object({}) do |(option_case, data), active_cases|
|
60
66
|
next nil unless SCR.get_arg(option_case)
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
index: SCR.get_arg_index(option_case),
|
65
|
-
option: option_case,
|
66
|
-
callback: data[:callback]
|
67
|
-
}
|
68
|
-
end
|
67
|
+
if usecase = get_usecase(io: io, data: data)
|
68
|
+
index = SCR.get_arg_index(option_case)
|
69
|
+
active_cases[usecase] = ActiveCase.new(index, option_case, data.callback)
|
69
70
|
end
|
70
|
-
end
|
71
|
-
@active_cases = active_cases.sort_by {|c, d| d[:index]}.to_h
|
71
|
+
end.sort_by {|c, d| d.index}.to_h
|
72
72
|
end
|
73
73
|
|
74
74
|
def process(io:)
|
@@ -79,7 +79,7 @@ module Eco
|
|
79
79
|
processed = true
|
80
80
|
io = case_io(io: io, usecase: usecase)
|
81
81
|
# some usecases have a callback to collect the parameters
|
82
|
-
data
|
82
|
+
data.callback&.call(*io.params)
|
83
83
|
io = usecase.launch(io: io)
|
84
84
|
end
|
85
85
|
processed
|
@@ -100,17 +100,17 @@ module Eco
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def get_usecase(io:, data:)
|
103
|
-
usecase = if case_name = data
|
104
|
-
io.session.usecases.case(case_name, type: data
|
103
|
+
usecase = if case_name = data.casename
|
104
|
+
io.session.usecases.case(case_name, type: data.type)
|
105
105
|
end
|
106
|
-
usecase ||= if callback = data
|
106
|
+
usecase ||= if callback = data.callback
|
107
107
|
# identify/retrieve usecase via callback
|
108
|
-
params = io.params(keyed: true).merge(type: data
|
108
|
+
params = io.params(keyed: true).merge(type: data.type)
|
109
109
|
io = io.new(**params, validate: false)
|
110
110
|
callback.call(*io.params).tap do |usecase|
|
111
111
|
unless usecase.is_a?(Eco::API::UseCases::UseCase)
|
112
112
|
msg = "When adding a usecase, without specifying 'case_name:', "
|
113
|
-
msg += "the block that integrates usecase for cli option '#{data
|
113
|
+
msg += "the block that integrates usecase for cli option '#{data.option}'"
|
114
114
|
msg += " must return an Eco::API::UseCases::UseCase object. It returns #{usecase.class}"
|
115
115
|
raise msg
|
116
116
|
end
|
@@ -3,6 +3,7 @@ module Eco
|
|
3
3
|
class Scripting
|
4
4
|
module ArgsHelpers
|
5
5
|
|
6
|
+
# @return [Array<String] the command line arguments.
|
6
7
|
def argv
|
7
8
|
@argv || ARGV
|
8
9
|
end
|
@@ -11,10 +12,18 @@ module Eco
|
|
11
12
|
Argument.is_modifier?(value)
|
12
13
|
end
|
13
14
|
|
15
|
+
# @return [Arguments] supported known arguments.
|
14
16
|
def arguments
|
15
17
|
@arguments ||= Arguments.new(argv)
|
16
18
|
end
|
17
19
|
|
20
|
+
# Registers an argument as a known one.
|
21
|
+
def known_argument(key, with_param: false)
|
22
|
+
arguments.add(key, with_param: with_param)
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Validation to stop the `script` if among `argv` there's any **unknown** argument.
|
18
27
|
def stop_on_unknown!(exclude: [], only_options: false)
|
19
28
|
# validate only those that are options
|
20
29
|
unknown = arguments.unknown(exclude: exclude)
|
@@ -23,18 +32,35 @@ module Eco
|
|
23
32
|
end
|
24
33
|
|
25
34
|
unless unknown.empty?
|
26
|
-
|
35
|
+
msg = "There are unknown options in your command line arguments:\n"
|
36
|
+
msg += "#{unknown}\n"
|
37
|
+
msg += "Please, remember that use case specific options should come after the use case in the command line.\n"
|
38
|
+
msg += "Use 'ruby main.rb -org [-usecase] --help -options' for more information"
|
39
|
+
raise msg
|
27
40
|
end
|
28
41
|
end
|
29
42
|
|
43
|
+
# @return [Boolean] if `key` is in the command line.
|
44
|
+
def arg?(key)
|
45
|
+
argv.include?(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Integer, nil] the position of `key` in the command line.
|
30
49
|
def get_arg_index(key)
|
31
|
-
return nil if !
|
50
|
+
return nil if !arg?(key)
|
32
51
|
argv.index(key)
|
33
52
|
end
|
34
53
|
|
54
|
+
# @return [Boolean] if `key1` precedes `key2` in the command line.
|
55
|
+
def arg_order?(key1, key2)
|
56
|
+
return false unless (k1 = get_arg_index(key1)) && k2 = get_arg_index(key2)
|
57
|
+
k1 < k2
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [String, Boolean] the argument value if `with_param` or a `Boolean` if not.
|
35
61
|
def get_arg(key, with_param: false, valid: true)
|
36
62
|
# track what a known option looks like
|
37
|
-
|
63
|
+
known_argument(key, with_param: with_param)
|
38
64
|
return nil unless index = get_arg_index(key)
|
39
65
|
value = true
|
40
66
|
if with_param
|
@@ -45,13 +71,14 @@ module Eco
|
|
45
71
|
return value
|
46
72
|
end
|
47
73
|
|
74
|
+
# @return [String] the filename.
|
48
75
|
def get_file(key, required: false, should_exist: true)
|
49
76
|
filename = get_arg(key, with_param: true)
|
50
77
|
if !filename && required
|
51
|
-
puts "You need to specify a file '#{key}
|
78
|
+
puts "You need to specify a file or folder '#{key} file_or_folder'"
|
52
79
|
exit(1)
|
53
80
|
elsif !file_exists?(filename) && should_exist && required
|
54
|
-
puts "This file doesn't exist '#{filename}'"
|
81
|
+
puts "This file/folder doesn't exist '#{filename}'"
|
55
82
|
exit(1)
|
56
83
|
end
|
57
84
|
|
data/lib/eco/csv.rb
CHANGED
@@ -18,8 +18,10 @@ module Eco
|
|
18
18
|
kargs = {headers: true, skip_blanks: true}.merge(kargs)
|
19
19
|
|
20
20
|
args = [file].tap do |arg|
|
21
|
-
|
22
|
-
|
21
|
+
encoding = Eco::API::Common::Session::FileManager.encoding(file)
|
22
|
+
#encoding = (encoding != "utf-8")? "#{encoding}|utf-8": encoding
|
23
|
+
#arg.push(encoding)
|
24
|
+
arg.push("rb:bom|utf-8") if encoding == "bom"
|
23
25
|
end
|
24
26
|
|
25
27
|
out = super(*args, **kargs).reject do |row|
|
data/lib/eco/csv/table.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
module Eco
|
3
2
|
class CSV
|
4
3
|
class Table < ::CSV::Table
|
@@ -9,6 +8,70 @@ module Eco
|
|
9
8
|
super(to_rows_array(input))
|
10
9
|
end
|
11
10
|
|
11
|
+
# @return [Hash] where keys are the groups and the values a `Eco::CSV::Table`
|
12
|
+
def group_by(&block)
|
13
|
+
rows.group_by(&block).transform_values do |rows|
|
14
|
+
self.class.new(rows)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Eco::CSV::Table]
|
19
|
+
def transform_values
|
20
|
+
transformed_rows = rows.map do |row|
|
21
|
+
res = yield(row)
|
22
|
+
case res
|
23
|
+
when Array
|
24
|
+
::CSV::Row.new(row.headers, res)
|
25
|
+
when ::CSV::Row
|
26
|
+
res
|
27
|
+
end
|
28
|
+
end
|
29
|
+
self.class.new(transformed_rows)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Slices the selected rows
|
33
|
+
# @return [Eco::CSV::Table]
|
34
|
+
def slice(*index)
|
35
|
+
case index.first
|
36
|
+
when Range, Numeric
|
37
|
+
self.class.new(rows.slice(index.first))
|
38
|
+
else
|
39
|
+
self
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Eco::CSV::Table]
|
44
|
+
def slice_columns(*index)
|
45
|
+
case index.first
|
46
|
+
when Range, Numeric
|
47
|
+
columns_to_table(columns.slice(index.first))
|
48
|
+
when String
|
49
|
+
csv_cols = columns
|
50
|
+
csv_cols = index.each_with_object([]) do |name, cols|
|
51
|
+
col = csv_cols.find {|col| col.first == name}
|
52
|
+
cols << col if col
|
53
|
+
end
|
54
|
+
columns_to_table(csv_cols)
|
55
|
+
else
|
56
|
+
self
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Eco::CSV::Table]
|
61
|
+
def delete_column(i)
|
62
|
+
csv_cols = columns
|
63
|
+
csv_cols.delete(i)
|
64
|
+
columns_to_table(csv_cols)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Adds a new column at the end
|
68
|
+
# @param header_name [String] header of the new column
|
69
|
+
# @return [Eco::CSV::Table] with a new empty column
|
70
|
+
def add_column(header_name)
|
71
|
+
new_col = Array.new(length).unshift(header_name)
|
72
|
+
columns_to_table(columns.push(new_col))
|
73
|
+
end
|
74
|
+
|
12
75
|
# @return [Array<::CSV::Row>]
|
13
76
|
def rows
|
14
77
|
[].tap do |out|
|
@@ -16,24 +79,40 @@ module Eco
|
|
16
79
|
end
|
17
80
|
end
|
18
81
|
|
82
|
+
# It removes all rows where all columns' values are the same
|
83
|
+
def delete_duplicates!
|
84
|
+
unique_rows = []
|
85
|
+
self.by_row!.delete_if do |row|
|
86
|
+
unique_rows.any? {|done| equal_rows?(row, done)}.tap do |found|
|
87
|
+
unique_rows << row unless found
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param row1 [CSV:Row] row to be compared
|
93
|
+
# @param row2 [CSV:Row] row to be compared
|
94
|
+
# @param [Boolean] `true` if all values of `row1` are as of `row2`
|
95
|
+
def equal_rows?(row1, row2)
|
96
|
+
row1.fields.zip(row2.fields).all? do |(v1, v2)|
|
97
|
+
v1 == v2
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
19
101
|
# @return [Integer] total number of rows not including the header
|
20
102
|
def length
|
21
103
|
to_a.length - 1
|
22
104
|
end
|
23
105
|
|
106
|
+
def empty?
|
107
|
+
length < 1
|
108
|
+
end
|
109
|
+
|
24
110
|
# @return [Array<Array>] each array is the column header followed by its values
|
25
111
|
def columns
|
26
112
|
to_a.transpose
|
27
113
|
end
|
28
114
|
|
29
|
-
#
|
30
|
-
# @param header_name [String] header of the new column
|
31
|
-
# @return [Eco::CSV::Table] with a new empty column
|
32
|
-
def add_column(header_name)
|
33
|
-
new_col = Array.new(length).unshift(header_name)
|
34
|
-
columns_to_table(columns.push(new_col))
|
35
|
-
end
|
36
|
-
|
115
|
+
# Creates a single `Hash` where each key, value is a column (header + values)
|
37
116
|
# @note it will override columns with same header name
|
38
117
|
# @return [Hash] keys are headers, values are arrays
|
39
118
|
def columns_hash
|
@@ -42,6 +121,17 @@ module Eco
|
|
42
121
|
end.to_h
|
43
122
|
end
|
44
123
|
|
124
|
+
# Returns an array of row hashes
|
125
|
+
# @note it will override columns with same header
|
126
|
+
def to_a_h
|
127
|
+
rows.map(&:to_h)
|
128
|
+
end
|
129
|
+
|
130
|
+
# @see #to_a_h
|
131
|
+
def to_array_of_hashes
|
132
|
+
to_a_h
|
133
|
+
end
|
134
|
+
|
45
135
|
private
|
46
136
|
|
47
137
|
def columns_to_table(columns_array)
|
@@ -51,24 +141,34 @@ module Eco
|
|
51
141
|
|
52
142
|
def to_rows_array(data)
|
53
143
|
case data
|
54
|
-
when Array
|
55
|
-
return data unless data.length > 0
|
56
|
-
if data.first.is_a?(::CSV::Row)
|
57
|
-
data
|
58
|
-
elsif data.first.is_a?(Array)
|
59
|
-
headers = data.shift
|
60
|
-
data.map do |arr_row|
|
61
|
-
CSV::Row.new(headers, arr_row)
|
62
|
-
end.compact
|
63
|
-
else
|
64
|
-
raise "Expected data that can be transformed into Array<Array>"
|
65
|
-
end
|
66
144
|
when ::CSV::Table
|
67
145
|
to_rows_array(data.to_a)
|
68
146
|
when Hash
|
69
147
|
# hash of columns header as key and column array as value
|
70
148
|
rows_arrays = [a.keys].concat(a.values.first.zip(*a.values[1..-1]))
|
71
149
|
to_rows_array(data.keys)
|
150
|
+
when Enumerable
|
151
|
+
data = data.dup.compact
|
152
|
+
return data unless data.count > 0
|
153
|
+
sample = data.first
|
154
|
+
|
155
|
+
case sample
|
156
|
+
when ::CSV::Row
|
157
|
+
data
|
158
|
+
when Array
|
159
|
+
headers = data.shift
|
160
|
+
data.map do |arr_row|
|
161
|
+
::CSV::Row.new(headers, arr_row)
|
162
|
+
end.compact
|
163
|
+
when Hash
|
164
|
+
headers = sample.keys
|
165
|
+
headers_str = headers.map(&:to_s)
|
166
|
+
data.map do |hash|
|
167
|
+
::CSV::Row.new(headers_str, hash.values_at(*headers))
|
168
|
+
end.compact
|
169
|
+
else
|
170
|
+
raise "Expected data that can be transformed into Array<::CSV::Row>. Given 'Enumerable' of '#{sample.class}'"
|
171
|
+
end
|
72
172
|
else
|
73
173
|
raise "Input type not supported. Given: #{data.class}"
|
74
174
|
end
|