lib/cucumber/multiline_argument/data_table.rb in cucumber-7.1.0 vs lib/cucumber/multiline_argument/data_table.rb in cucumber-8.0.0.rc.1

- old
+ new

@@ -25,11 +25,11 @@ # end # # This will store <tt>[['a', 'b'], ['c', 'd']]</tt> in the <tt>data</tt> variable. # class DataTable - def self.default_arg_name #:nodoc: + def self.default_arg_name # :nodoc: 'table' end def describe_to(visitor, *args) visitor.legacy_table(self, *args) @@ -76,15 +76,16 @@ end NULL_CONVERSIONS = Hash.new(strict: false, proc: ->(cell_value) { cell_value }).freeze # @param data [Core::Test::DataTable] the data for the table - # @param conversion_procs [Hash] see map_columns! - # @param header_mappings [Hash] see map_headers! - # @param header_conversion_proc [Proc] see map_headers! + # @param conversion_procs [Hash] see map_column + # @param header_mappings [Hash] see map_headers + # @param header_conversion_proc [Proc] see map_headers def initialize(data, conversion_procs = NULL_CONVERSIONS.dup, header_mappings = {}, header_conversion_proc = nil) raise ArgumentError, 'data must be a Core::Test::DataTable' unless data.is_a? Core::Test::DataTable + ast_table = data # Verify that it's square ast_table.transpose @cell_matrix = create_cell_matrix(ast_table) @conversion_procs = conversion_procs @@ -105,17 +106,10 @@ def location @ast_table.location end - # Creates a copy of this table, inheriting any column and header mappings - # registered with #map_column! and #map_headers!. - # - def dup - self.class.new(Core::Test::DataTable.new(raw), @conversion_procs.dup, @header_mappings.dup, @header_conversion_proc) - end - # Returns a new, transposed table. Example: # # | a | 7 | 4 | # | b | 9 | 2 | # @@ -139,11 +133,11 @@ # # Gets converted into the following: # # [{'a' => '2', 'b' => '3', 'sum' => '5'}, {'a' => '7', 'b' => '9', 'sum' => '16'}] # - # Use #map_column! to specify how values in a column are converted. + # Use #map_column to specify how values in a column are converted. # def hashes @hashes ||= build_hashes end @@ -159,11 +153,11 @@ # [{:foo => '2', :bar => '3', :foo_bar => '5'}, {:foo => '7', :bar => '9', :foo_bar => '16'}] # def symbolic_hashes @symbolic_hashes ||= hashes.map do |string_hash| - Hash[string_hash.map { |a, b| [symbolize_key(a), b] }] + string_hash.transform_keys { |a| symbolize_key(a) } end end # Converts this table into a Hash where the first column is # used as keys and the second column is used as values @@ -177,10 +171,11 @@ # # The table must be exactly two columns wide # def rows_hash return @rows_hash if @rows_hash + verify_table_width(2) @rows_hash = transpose.hashes[0] end # Gets the raw data of this table. For example, a Table built from @@ -197,21 +192,21 @@ cell_matrix.map do |row| row.map(&:value) end end - def column_names #:nodoc: + def column_names # :nodoc: @column_names ||= cell_matrix[0].map(&:value) end def rows hashes.map do |hash| hash.values_at *headers end end - def each_cells_row(&proc) #:nodoc: + def each_cells_row(&proc) # :nodoc: cells_rows.each(&proc) end # Matches +pattern+ against the header row of the table. # This is used especially for argument transforms. @@ -226,11 +221,12 @@ def match(pattern) header_to_match = "table:#{headers.join(',')}" pattern.match(header_to_match) end - # Redefines the table headers. This makes it possible to use + # Returns a new Table where the headers are redefined. + # This makes it possible to use # prettier and more flexible header names in the features. The # keys of +mappings+ are Strings or regular expressions # (anything that responds to #=== will work) that may match # column headings in the table. The values of +mappings+ are # desired names for the columns. @@ -242,58 +238,45 @@ # | 345678 | abc | # # A StepDefinition receiving this table can then map the columns # with both Regexp and String: # - # table.map_headers!(/phone( number)?/i => :phone, 'Address' => :address) + # table.map_headers(/phone( number)?/i => :phone, 'Address' => :address) # table.hashes # # => [{:phone => '123456', :address => 'xyz'}, {:phone => '345678', :address => 'abc'}] # # You may also pass in a block if you wish to convert all of the headers: # - # table.map_headers! { |header| header.downcase } + # table.map_headers { |header| header.downcase } # table.hashes.keys # # => ['phone number', 'address'] # # When a block is passed in along with a hash then the mappings in the hash take precendence: # - # table.map_headers!('Address' => 'ADDRESS') { |header| header.downcase } + # table.map_headers('Address' => 'ADDRESS') { |header| header.downcase } # table.hashes.keys # # => ['phone number', 'ADDRESS'] # - def map_headers!(mappings = {}, &block) - # TODO: Remove this method for 2.0 - clear_cache! - @header_mappings = mappings - @header_conversion_proc = block - end - - # Returns a new Table where the headers are redefined. See #map_headers! def map_headers(mappings = {}, &block) self.class.new(Core::Test::DataTable.new(raw), @conversion_procs.dup, mappings, block) end + # Returns a new Table with an additional column mapping. + # # Change how #hashes converts column values. The +column_name+ argument identifies the column # and +conversion_proc+ performs the conversion for each cell in that column. If +strict+ is # true, an error will be raised if the column named +column_name+ is not found. If +strict+ # is false, no error will be raised. Example: # # Given /^an expense report for (.*) with the following posts:$/ do |table| - # posts_table.map_column!('amount') { |a| a.to_i } + # posts_table = posts_table.map_column('amount') { |a| a.to_i } # posts_table.hashes.each do |post| # # post['amount'] is a Fixnum, rather than a String # end # end # - def map_column!(column_name, strict = true, &conversion_proc) - # TODO: Remove this method for 2.0 - @conversion_procs[column_name.to_s] = { strict: strict, proc: conversion_proc } - self - end - - # Returns a new Table with an additional column mapping. See #map_column! - def map_column(column_name, strict = true, &conversion_proc) + def map_column(column_name, strict: true, &conversion_proc) conversion_procs = @conversion_procs.dup conversion_procs[column_name.to_s] = { strict: strict, proc: conversion_proc } self.class.new(Core::Test::DataTable.new(raw), conversion_procs, @header_mappings.dup, @header_conversion_proc) end @@ -310,12 +293,12 @@ # boolean true and the string "true") are converted to their Object#inspect # representation and preceded with (i) - to make it easier to identify # where the difference actually is. # # Since all tables that are passed to StepDefinitions always have String - # objects in their cells, you may want to use #map_column! before calling - # #diff!. You can use #map_column! on either of the tables. + # objects in their cells, you may want to use #map_column before calling + # #diff!. You can use #map_column on either of the tables. # # A Different error is raised if there are missing rows or columns, or # surplus rows. An error is <em>not</em> raised for surplus columns. An # error is <em>not</em> raised for misplaced (out of sequence) columns. # Whether to raise or not raise can be changed by setting values in @@ -344,75 +327,76 @@ DiffMatrices.new(cell_matrix, other_table.cell_matrix, options).call end class Different < StandardError attr_reader :table + def initialize(table) @table = table super("Tables were not identical:\n#{table}") end end def to_hash cells_rows.map { |cells| cells.map(&:value) } end - def cells_to_hash(cells) #:nodoc: + def cells_to_hash(cells) # :nodoc: hash = Hash.new do |hash_inner, key| hash_inner[key.to_s] if key.is_a?(Symbol) end column_names.each_with_index do |column_name, column_index| hash[column_name] = cells.value(column_index) end hash end - def index(cells) #:nodoc: + def index(cells) # :nodoc: cells_rows.index(cells) end - def verify_column(column_name) #:nodoc: + def verify_column(column_name) # :nodoc: raise %(The column named "#{column_name}" does not exist) unless raw[0].include?(column_name) end - def verify_table_width(width) #:nodoc: + def verify_table_width(width) # :nodoc: raise %(The table must have exactly #{width} columns) unless raw[0].size == width end # TODO: remove the below function if it's not actually being used. # Nothing else in this repo calls it. - def text?(text) #:nodoc: + def text?(text) # :nodoc: raw.flatten.compact.detect { |cell_value| cell_value.index(text) } end - def cells_rows #:nodoc: + def cells_rows # :nodoc: @rows ||= cell_matrix.map do |cell_row| # rubocop:disable Naming/MemoizedInstanceVariableName Cells.new(self, cell_row) end end - def headers #:nodoc: + def headers # :nodoc: raw.first end - def header_cell(col) #:nodoc: + def header_cell(col) # :nodoc: cells_rows[0][col] end attr_reader :cell_matrix - def col_width(col) #:nodoc: + def col_width(col) # :nodoc: columns[col].__send__(:width) end - def to_s(options = {}) #:nodoc: + def to_s(options = {}) # :nodoc: indentation = options.key?(:indent) ? options[:indent] : 2 prefixes = options.key?(:prefixes) ? options[:prefixes] : TO_S_PREFIXES DataTablePrinter.new(self, indentation, prefixes).to_s end - class DataTablePrinter #:nodoc: + class DataTablePrinter # :nodoc: include Cucumber::Gherkin::Formatter::Escaping attr_reader :data_table, :indentation, :prefixes private :data_table, :indentation, :prefixes def initialize(data_table, indentation, prefixes) @@ -422,19 +406,19 @@ end def to_s leading_row = "\n" end_indentation = indentation - 2 - trailing_row = "\n" + (' ' * end_indentation) + trailing_row = "\n#{' ' * end_indentation}" table_rows = data_table.cell_matrix.map { |row| format_row(row) } leading_row + table_rows.join("\n") + trailing_row end private def format_row(row) - row_start = (' ' * indentation) + '| ' + row_start = "#{' ' * indentation}| " row_end = '|' cells = row.map.with_index do |cell, i| format_cell(cell, data_table.col_width(i)) end row_start + cells.join('| ') + row_end @@ -447,11 +431,11 @@ prefix = prefixes[cell.status] "#{prefix}#{padded_text} " end end - def columns #:nodoc: + def columns # :nodoc: @columns ||= cell_matrix.transpose.map do |cell_row| Cells.new(self, cell_row) end end @@ -467,72 +451,74 @@ protected def build_hashes convert_headers! convert_columns! - cells_rows[1..-1].map(&:to_hash) + cells_rows[1..].map(&:to_hash) end - def create_cell_matrix(ast_table) #:nodoc: + def create_cell_matrix(ast_table) # :nodoc: ast_table.raw.map do |raw_row| line = begin - raw_row.line - rescue StandardError - -1 - end + raw_row.line + rescue StandardError + -1 + end raw_row.map do |raw_cell| Cell.new(raw_cell, self, line) end end end - def convert_columns! #:nodoc: + def convert_columns! # :nodoc: @conversion_procs.each do |column_name, conversion_proc| verify_column(column_name) if conversion_proc[:strict] end cell_matrix.transpose.each do |col| column_name = col[0].value conversion_proc = @conversion_procs[column_name][:proc] - col[1..-1].each do |cell| + col[1..].each do |cell| cell.value = conversion_proc.call(cell.value) end end end - def convert_headers! #:nodoc: + def convert_headers! # :nodoc: header_cells = cell_matrix[0] if @header_conversion_proc header_values = header_cells.map(&:value) - @header_mappings.keys @header_mappings = @header_mappings.merge(Hash[*header_values.zip(header_values.map(&@header_conversion_proc)).flatten]) end @header_mappings.each_pair do |pre, post| - mapped_cells = header_cells.reject { |cell| cell.value.match(pre).nil? } + mapped_cells = header_cells.select { |cell| pre.is_a?(Regexp) ? cell.value.match?(pre) : cell.value == pre } raise "No headers matched #{pre.inspect}" if mapped_cells.empty? raise "#{mapped_cells.length} headers matched #{pre.inspect}: #{mapped_cells.map(&:value).inspect}" if mapped_cells.length > 1 + mapped_cells[0].value = post @conversion_procs[post] = @conversion_procs.delete(pre) if @conversion_procs.key?(pre) end end - def clear_cache! #:nodoc: + def clear_cache! # :nodoc: @hashes = @rows_hash = @column_names = @rows = @columns = nil end - def ensure_table(table_or_array) #:nodoc: + def ensure_table(table_or_array) # :nodoc: return table_or_array if DataTable == table_or_array.class + DataTable.from(table_or_array) end def symbolize_key(key) key.downcase.tr(' ', '_').to_sym end # Represents a row of cells or columns of cells - class Cells #:nodoc: + class Cells # :nodoc: include Enumerable include Cucumber::Gherkin::Formatter::Escaping attr_reader :exception @@ -541,26 +527,27 @@ @cells = cells end def accept(visitor) return if Cucumber.wants_to_quit + each do |cell| visitor.visit_table_cell(cell) end nil end # For testing only - def to_sexp #:nodoc: + def to_sexp # :nodoc: [:row, line, *@cells.map(&:to_sexp)] end - def to_hash #:nodoc: + def to_hash # :nodoc: @to_hash ||= @table.cells_to_hash(self) end - def value(n) #:nodoc: + def value(n) # :nodoc: self[n].value end def [](n) @cells[n] @@ -587,11 +574,11 @@ def width map { |cell| cell.value ? escape_cell(cell.value.to_s).unpack('U*').length : 0 }.max end end - class Cell #:nodoc: + class Cell # :nodoc: attr_reader :line, :table attr_accessor :status, :value def initialize(value, table, line) @value = value @@ -614,15 +601,15 @@ def hash 0 end # For testing only - def to_sexp #:nodoc: + def to_sexp # :nodoc: [:cell, @value] end end - class SurplusCell < Cell #:nodoc: + class SurplusCell < Cell # :nodoc: def status :comment end def ==(_other)