eco-helpers 2.0.15 → 2.0.21

Sign up to get free protection for your applications and to get access to all the features.
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