eco-helpers 2.0.15 → 2.0.21

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +109 -3
  3. data/eco-helpers.gemspec +11 -5
  4. data/lib/eco-helpers.rb +2 -0
  5. data/lib/eco/api/common/base_loader.rb +14 -0
  6. data/lib/eco/api/common/loaders/parser.rb +1 -0
  7. data/lib/eco/api/common/people/default_parsers/date_parser.rb +11 -1
  8. data/lib/eco/api/common/people/default_parsers/login_providers_parser.rb +1 -1
  9. data/lib/eco/api/common/people/default_parsers/policy_groups_parser.rb +11 -11
  10. data/lib/eco/api/common/people/entries.rb +1 -0
  11. data/lib/eco/api/common/people/entry_factory.rb +74 -23
  12. data/lib/eco/api/common/people/person_entry.rb +5 -2
  13. data/lib/eco/api/common/people/supervisor_helpers.rb +27 -0
  14. data/lib/eco/api/common/session.rb +1 -0
  15. data/lib/eco/api/common/session/base_session.rb +2 -0
  16. data/lib/eco/api/common/session/file_manager.rb +2 -2
  17. data/lib/eco/api/common/session/helpers.rb +30 -0
  18. data/lib/eco/api/common/session/helpers/prompt_user.rb +34 -0
  19. data/lib/eco/api/common/session/mailer.rb +0 -1
  20. data/lib/eco/api/common/session/s3_uploader.rb +0 -1
  21. data/lib/eco/api/common/session/sftp.rb +0 -1
  22. data/lib/eco/api/common/version_patches/ecoportal_api/external_person.rb +1 -1
  23. data/lib/eco/api/common/version_patches/ecoportal_api/internal_person.rb +7 -4
  24. data/lib/eco/api/common/version_patches/exception.rb +11 -4
  25. data/lib/eco/api/microcases.rb +3 -1
  26. data/lib/eco/api/microcases/append_usergroups.rb +0 -1
  27. data/lib/eco/api/microcases/people_cache.rb +2 -2
  28. data/lib/eco/api/microcases/people_load.rb +2 -2
  29. data/lib/eco/api/microcases/people_refresh.rb +2 -2
  30. data/lib/eco/api/microcases/people_search.rb +6 -6
  31. data/lib/eco/api/microcases/preserve_default_tag.rb +23 -0
  32. data/lib/eco/api/microcases/preserve_filter_tags.rb +28 -0
  33. data/lib/eco/api/microcases/preserve_policy_groups.rb +30 -0
  34. data/lib/eco/api/microcases/set_account.rb +0 -1
  35. data/lib/eco/api/microcases/with_each.rb +67 -6
  36. data/lib/eco/api/microcases/with_each_present.rb +4 -2
  37. data/lib/eco/api/microcases/with_each_starter.rb +4 -2
  38. data/lib/eco/api/organization.rb +1 -0
  39. data/lib/eco/api/organization/people.rb +98 -22
  40. data/lib/eco/api/organization/people_similarity.rb +272 -0
  41. data/lib/eco/api/organization/person_schemas.rb +5 -1
  42. data/lib/eco/api/organization/policy_groups.rb +5 -1
  43. data/lib/eco/api/organization/presets_factory.rb +40 -80
  44. data/lib/eco/api/organization/presets_integrity.json +6 -0
  45. data/lib/eco/api/organization/presets_values.json +5 -4
  46. data/lib/eco/api/organization/tag_tree.rb +33 -0
  47. data/lib/eco/api/policies/default_policies/99_user_access_policy.rb +0 -30
  48. data/lib/eco/api/session.rb +10 -24
  49. data/lib/eco/api/session/batch.rb +25 -7
  50. data/lib/eco/api/session/config.rb +16 -15
  51. data/lib/eco/api/session/config/api.rb +4 -0
  52. data/lib/eco/api/session/config/apis.rb +80 -0
  53. data/lib/eco/api/session/config/files.rb +7 -0
  54. data/lib/eco/api/session/config/people.rb +3 -19
  55. data/lib/eco/api/usecases/default_cases.rb +4 -1
  56. data/lib/eco/api/usecases/default_cases/abstract_policygroup_abilities_case.rb +161 -0
  57. data/lib/eco/api/usecases/default_cases/analyse_people_case.rb +223 -0
  58. data/lib/eco/api/usecases/default_cases/clean_unknown_tags_case.rb +37 -0
  59. data/lib/eco/api/usecases/default_cases/codes_to_tags_case.rb +2 -3
  60. data/lib/eco/api/usecases/default_cases/reset_landing_page_case.rb +11 -1
  61. data/lib/eco/api/usecases/default_cases/restore_db_case.rb +1 -2
  62. data/lib/eco/api/usecases/default_cases/supers_cyclic_identify_case.rb +72 -0
  63. data/lib/eco/api/usecases/default_cases/supers_hierarchy_case.rb +1 -1
  64. data/lib/eco/api/usecases/default_cases/to_csv_case.rb +132 -29
  65. data/lib/eco/api/usecases/default_cases/to_csv_detailed_case.rb +61 -36
  66. data/lib/eco/api/usecases/ooze_samples/ooze_update_case.rb +3 -2
  67. data/lib/eco/cli.rb +0 -10
  68. data/lib/eco/cli/config/default/options.rb +48 -17
  69. data/lib/eco/cli/config/default/people.rb +18 -24
  70. data/lib/eco/cli/config/default/people_filters.rb +3 -3
  71. data/lib/eco/cli/config/default/usecases.rb +105 -28
  72. data/lib/eco/cli/config/default/workflow.rb +21 -12
  73. data/lib/eco/cli/config/help.rb +1 -0
  74. data/lib/eco/cli/config/options_set.rb +106 -13
  75. data/lib/eco/cli/config/use_cases.rb +33 -33
  76. data/lib/eco/cli/scripting/args_helpers.rb +30 -3
  77. data/lib/eco/csv.rb +4 -2
  78. data/lib/eco/csv/table.rb +121 -21
  79. data/lib/eco/data.rb +1 -0
  80. data/lib/eco/data/crypto/encryption.rb +3 -3
  81. data/lib/eco/data/files/directory.rb +28 -20
  82. data/lib/eco/data/files/helpers.rb +6 -4
  83. data/lib/eco/data/fuzzy_match.rb +201 -0
  84. data/lib/eco/data/fuzzy_match/array_helpers.rb +75 -0
  85. data/lib/eco/data/fuzzy_match/chars_position_score.rb +38 -0
  86. data/lib/eco/data/fuzzy_match/ngrams_score.rb +82 -0
  87. data/lib/eco/data/fuzzy_match/pairing.rb +95 -0
  88. data/lib/eco/data/fuzzy_match/result.rb +87 -0
  89. data/lib/eco/data/fuzzy_match/results.rb +77 -0
  90. data/lib/eco/data/fuzzy_match/score.rb +49 -0
  91. data/lib/eco/data/fuzzy_match/stop_words.rb +35 -0
  92. data/lib/eco/data/fuzzy_match/string_helpers.rb +82 -0
  93. data/lib/eco/version.rb +1 -1
  94. metadata +168 -11
  95. data/lib/eco/api/microcases/refresh_abilities.rb +0 -19
  96. data/lib/eco/api/organization/presets_reference.json +0 -59
  97. data/lib/eco/api/usecases/default_cases/refresh_abilities_case.rb +0 -30
@@ -6,11 +6,15 @@ ASSETS.cli.config do |config|
6
6
 
7
7
  # default rescue
8
8
  wf.rescue do |exception, io|
9
- next io if rescued
10
- rescued = true
11
-
12
- io.session.logger.debug(exception.patch_full_message)
13
- wf.run(:close, io: io)
9
+ begin
10
+ next io if rescued
11
+ rescued = true
12
+
13
+ io.session.logger.debug(exception.patch_full_message)
14
+ wf.run(:close, io: io)
15
+ rescue Exception => e
16
+ puts "Some problem in workflow.rescue: #{e}"
17
+ end
14
18
  io
15
19
  end
16
20
 
@@ -24,12 +28,15 @@ ASSETS.cli.config do |config|
24
28
  cases_with_input = config.usecases.active(io: io).select do |usecase, data|
25
29
  io.class.input_required?(usecase.type)
26
30
  end
27
- next io unless (!io.input || io.input.empty?) && !cases_with_input.empty?
31
+
32
+ input_is_required = !cases_with_input.empty? || io.options.dig(:input, :entries_from)
33
+ missing_input = !io.input || io.input.empty?
34
+ next io unless missing_input && input_is_required
28
35
 
29
36
  if io.options.dig(:input, :entries_from)
30
37
  io = io.new(input: config.input.get(io: io))
31
38
  else
32
- opt_case = cases_with_input.values.first[:option]
39
+ opt_case = cases_with_input.values.first.option
33
40
  io = io.new(input: config.input.get(io: io, option: opt_case))
34
41
  end
35
42
  io
@@ -46,8 +53,7 @@ ASSETS.cli.config do |config|
46
53
  cases_with_people = config.usecases.active(io: io).select do |usecase, data|
47
54
  io.class.people_required?(usecase.type)
48
55
  end
49
- get_people = io.options.dig(:people, :get, :from) == :remote
50
- next io unless !cases_with_people.empty? || get_people
56
+ next io if cases_with_people.empty? && !io.options.dig(:people, :get)
51
57
  io = io.new(people: config.people(io: io))
52
58
  end
53
59
 
@@ -60,7 +66,8 @@ ASSETS.cli.config do |config|
60
66
 
61
67
  wf.before(:usecases) do |wf_cases, io|
62
68
  # save partial entries -> should be native to session.workflow
63
- partial_update = io.options.dig(:people, :get, :type) == :partial
69
+ get_people = io.options.dig(:people, :get)
70
+ partial_update = get_people && get_people.dig(:type) == :partial
64
71
  if !io.options[:dry_run] && partial_update
65
72
  partial_file = io.session.config.people.partial_cache
66
73
  io.session.file_manager.save_json(io.people, partial_file, :timestamp)
@@ -91,7 +98,8 @@ ASSETS.cli.config do |config|
91
98
  if io.session.post_launch.empty?
92
99
  wf_post.skip!
93
100
  else
94
- partial_update = io.options.dig(:people, :get, :type) == :partial
101
+ get_people = io.options.dig(:people, :get)
102
+ partial_update = get_people && get_people.dig(:type) == :partial
95
103
  if !io.options[:dry_run] && partial_update
96
104
  # get target people afresh
97
105
  people = io.session.micro.people_refresh(people: io.people, include_created: true)
@@ -132,7 +140,8 @@ ASSETS.cli.config do |config|
132
140
  end
133
141
 
134
142
  wf.on(:end) do |wf_end, io|
135
- partial_update = io.options.dig(:people, :get, :type) == :partial
143
+ get_people = io.options.dig(:people, :get)
144
+ partial_update = get_people && get_people.dig(:type) == :partial
136
145
  unless !io.options[:end_get] || io.options[:dry_run] || partial_update
137
146
  people = io.session.micro.people_cache
138
147
  io = io.new(people: people)
@@ -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
- @options_set = {}
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(@options_set.keys)
18
- @options_set.keys.each do |key|
19
- lines << help_line(key, @description[key], max_len)
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
- # @param option [String] the command line option.
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
- callback = Proc.new
30
- [option].flatten.compact.each do |opt|
31
- @options_set[opt] = callback
32
- @description[opt] = desc
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
- @options_set.each do |arg, callback|
43
- callback.call(io.options, io.session) if SCR.get_arg(arg)
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 |key|
19
- lines << help_line(key, @description[key], max_len)
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
- types.each do |type, data|
62
- if usecase = get_usecase(io: io, data: data)
63
- active_cases[usecase] = {
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[:callback]&.call(*io.params)
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[:casename]
104
- io.session.usecases.case(case_name, type: data[:type])
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[:callback]
106
+ usecase ||= if callback = data.callback
107
107
  # identify/retrieve usecase via callback
108
- params = io.params(keyed: true).merge(type: data[:type])
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[:option]}'"
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
- raise "There are unknown options in your command line arguments: #{unknown}"
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 !argv.include?(key)
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
- arguments.add(key, with_param: with_param)
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,6 +71,7 @@ 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
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
- coding = Eco::API::Common::Session::FileManager.encoding(file)
22
- arg.push("rb:bom|utf-8") if coding == "bom"
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
- # Adds a new column at the end
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