loading
Generated 2025-03-31T23:00:25+00:00

All Files ( 100.0% covered at 9.05 hits/line )

135 files in total.
4106 relevant lines, 4106 lines covered and 0 lines missed. ( 100.0% )
220 total branches, 219 branches covered and 1 branches missed. ( 99.55% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
lib/tabulard.rb 100.00 % 31 5 5 0 1.00 100.00 % 0 0 0
lib/tabulard/adapters.rb 100.00 % 11 5 5 0 3.60 100.00 % 0 0 0
lib/tabulard/adapters/bare.rb 100.00 % 106 58 58 0 26.29 100.00 % 10 10 0
lib/tabulard/adapters/csv.rb 100.00 % 139 65 65 0 11.40 100.00 % 14 14 0
lib/tabulard/adapters/xlsx.rb 100.00 % 185 94 94 0 17.22 95.00 % 20 19 1
lib/tabulard/attribute.rb 100.00 % 66 21 21 0 15.33 100.00 % 2 2 0
lib/tabulard/attribute_types.rb 100.00 % 49 10 10 0 9.40 100.00 % 4 4 0
lib/tabulard/attribute_types/composite.rb 100.00 % 57 23 23 0 9.83 100.00 % 2 2 0
lib/tabulard/attribute_types/scalar.rb 100.00 % 58 20 20 0 7.40 100.00 % 2 2 0
lib/tabulard/attribute_types/value.rb 100.00 % 62 19 19 0 35.32 100.00 % 4 4 0
lib/tabulard/column.rb 100.00 % 31 12 12 0 35.00 100.00 % 0 0 0
lib/tabulard/errors/error.rb 100.00 % 8 3 3 0 1.00 100.00 % 0 0 0
lib/tabulard/errors/spec_error.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0
lib/tabulard/errors/type_error.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0
lib/tabulard/headers.rb 100.00 % 96 49 49 0 14.31 100.00 % 14 14 0
lib/tabulard/messaging.rb 100.00 % 23 14 14 0 1.21 100.00 % 0 0 0
lib/tabulard/messaging/config.rb 100.00 % 19 9 9 0 2.56 100.00 % 0 0 0
lib/tabulard/messaging/constants.rb 100.00 % 17 10 10 0 1.00 100.00 % 0 0 0
lib/tabulard/messaging/message.rb 100.00 % 70 28 28 0 25.46 100.00 % 5 5 0
lib/tabulard/messaging/message_variant.rb 100.00 % 47 11 11 0 27.73 100.00 % 0 0 0
lib/tabulard/messaging/messages/cleaned_string.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
lib/tabulard/messaging/messages/duplicated_header.rb 100.00 % 21 10 10 0 1.30 100.00 % 2 2 0
lib/tabulard/messaging/messages/invalid_header.rb 100.00 % 21 10 10 0 1.70 100.00 % 2 2 0
lib/tabulard/messaging/messages/missing_column.rb 100.00 % 21 10 10 0 1.50 100.00 % 2 2 0
lib/tabulard/messaging/messages/must_be_array.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
lib/tabulard/messaging/messages/must_be_boolsy.rb 100.00 % 21 10 10 0 1.10 100.00 % 2 2 0
lib/tabulard/messaging/messages/must_be_date.rb 100.00 % 21 10 10 0 1.10 100.00 % 2 2 0
lib/tabulard/messaging/messages/must_be_email.rb 100.00 % 21 10 10 0 1.60 100.00 % 2 2 0
lib/tabulard/messaging/messages/must_be_string.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
lib/tabulard/messaging/messages/must_exist.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
lib/tabulard/messaging/messenger.rb 100.00 % 133 64 64 0 48.97 100.00 % 8 8 0
lib/tabulard/messaging/validations.rb 100.00 % 35 18 18 0 20.17 100.00 % 8 8 0
lib/tabulard/messaging/validations/base_validator.rb 100.00 % 65 34 34 0 10.82 100.00 % 10 10 0
lib/tabulard/messaging/validations/dsl.rb 100.00 % 31 15 15 0 2.33 100.00 % 0 0 0
lib/tabulard/messaging/validations/invalid_message.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
lib/tabulard/messaging/validations/mixins.rb 100.00 % 57 28 28 0 4.57 100.00 % 6 6 0
lib/tabulard/row_processor.rb 100.00 % 41 19 19 0 16.05 100.00 % 0 0 0
lib/tabulard/row_processor_result.rb 100.00 % 20 9 9 0 8.56 100.00 % 0 0 0
lib/tabulard/row_value_builder.rb 100.00 % 53 30 30 0 29.07 100.00 % 4 4 0
lib/tabulard/specification.rb 100.00 % 26 13 13 0 21.08 100.00 % 2 2 0
lib/tabulard/table.rb 100.00 % 144 71 71 0 63.87 100.00 % 11 11 0
lib/tabulard/table/col_converter.rb 100.00 % 58 31 31 0 206.68 100.00 % 7 7 0
lib/tabulard/table_processor.rb 100.00 % 63 34 34 0 7.41 100.00 % 0 0 0
lib/tabulard/table_processor_result.rb 100.00 % 18 8 8 0 6.38 100.00 % 0 0 0
lib/tabulard/template.rb 100.00 % 85 33 33 0 9.36 100.00 % 2 2 0
lib/tabulard/template_config.rb 100.00 % 35 11 11 0 23.27 100.00 % 2 2 0
lib/tabulard/types/cast.rb 100.00 % 20 9 9 0 2.78 100.00 % 0 0 0
lib/tabulard/types/cast_chain.rb 100.00 % 49 26 26 0 30.35 100.00 % 2 2 0
lib/tabulard/types/composites/array.rb 100.00 % 16 8 8 0 3.63 100.00 % 2 2 0
lib/tabulard/types/composites/array_compact.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
lib/tabulard/types/composites/composite.rb 100.00 % 32 16 16 0 12.31 100.00 % 2 2 0
lib/tabulard/types/container.rb 100.00 % 81 45 45 0 11.11 100.00 % 4 4 0
lib/tabulard/types/scalars/boolsy.rb 100.00 % 12 6 6 0 1.00 100.00 % 0 0 0
lib/tabulard/types/scalars/boolsy_cast.rb 100.00 % 35 19 19 0 2.32 100.00 % 4 4 0
lib/tabulard/types/scalars/date_string.rb 100.00 % 12 6 6 0 1.00 100.00 % 0 0 0
lib/tabulard/types/scalars/date_string_cast.rb 100.00 % 43 23 23 0 2.91 100.00 % 7 7 0
lib/tabulard/types/scalars/email.rb 100.00 % 12 6 6 0 1.00 100.00 % 0 0 0
lib/tabulard/types/scalars/email_cast.rb 100.00 % 28 15 15 0 3.07 100.00 % 2 2 0
lib/tabulard/types/scalars/scalar.rb 100.00 % 29 15 15 0 11.40 100.00 % 2 2 0
lib/tabulard/types/scalars/scalar_cast.rb 100.00 % 49 26 26 0 17.85 100.00 % 8 8 0
lib/tabulard/types/scalars/string.rb 100.00 % 18 8 8 0 4.88 100.00 % 2 2 0
lib/tabulard/types/type.rb 100.00 % 103 53 53 0 13.43 100.00 % 10 10 0
lib/tabulard/utils/cell_string_cleaner.rb 100.00 % 29 16 16 0 11.94 100.00 % 0 0 0
lib/tabulard/utils/monadic_result.rb 100.00 % 174 82 82 0 11.93 100.00 % 10 10 0
spec/support/fixtures.rb 100.00 % 15 7 7 0 9.86 100.00 % 0 0 0
spec/support/monadic_result.rb 100.00 % 7 3 3 0 1.00 100.00 % 0 0 0
spec/support/shared/cast_class.rb 100.00 % 23 11 11 0 7.64 100.00 % 2 2 0
spec/support/shared/composite_type.rb 100.00 % 69 33 33 0 4.55 100.00 % 0 0 0
spec/support/shared/scalar_type.rb 100.00 % 47 22 22 0 6.64 100.00 % 0 0 0
spec/support/shared/table/empty.rb 100.00 % 82 43 43 0 3.40 100.00 % 2 2 0
spec/support/shared/table/factories.rb 100.00 % 41 20 20 0 33.65 100.00 % 0 0 0
spec/support/shared/table/filled.rb 100.00 % 128 64 64 0 5.47 100.00 % 4 4 0
spec/support/tabulard.rb 100.00 % 7 3 3 0 1.00 100.00 % 0 0 0
spec/tabulard/adapters/bare_spec.rb 100.00 % 91 47 47 0 22.70 100.00 % 4 4 0
spec/tabulard/adapters/csv/invalid_csv_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/adapters/csv_spec.rb 100.00 % 138 64 64 0 9.66 100.00 % 2 2 0
spec/tabulard/adapters/xlsx_spec.rb 100.00 % 130 60 60 0 4.02 100.00 % 2 2 0
spec/tabulard/adapters_spec.rb 100.00 % 27 13 13 0 1.46 100.00 % 0 0 0
spec/tabulard/attribute_spec.rb 100.00 % 36 19 19 0 1.05 100.00 % 0 0 0
spec/tabulard/attribute_types/composite_spec.rb 100.00 % 38 19 19 0 1.00 100.00 % 0 0 0
spec/tabulard/attribute_types/scalar_spec.rb 100.00 % 31 17 17 0 1.00 100.00 % 0 0 0
spec/tabulard/attribute_types/value_spec.rb 100.00 % 53 29 29 0 1.48 100.00 % 0 0 0
spec/tabulard/attribute_types_spec.rb 100.00 % 93 30 30 0 1.40 100.00 % 0 0 0
spec/tabulard/column_spec.rb 100.00 % 59 28 28 0 2.46 100.00 % 0 0 0
spec/tabulard/errors/error_spec.rb 100.00 % 9 4 4 0 1.00 100.00 % 0 0 0
spec/tabulard/errors/spec_error_spec.rb 100.00 % 9 4 4 0 1.00 100.00 % 0 0 0
spec/tabulard/errors/type_error_spec.rb 100.00 % 9 4 4 0 1.00 100.00 % 0 0 0
spec/tabulard/headers_spec.rb 100.00 % 133 54 54 0 4.22 100.00 % 0 0 0
spec/tabulard/messaging/config_spec.rb 100.00 % 57 33 33 0 1.42 100.00 % 0 0 0
spec/tabulard/messaging/message_spec.rb 100.00 % 147 62 62 0 1.98 100.00 % 0 0 0
spec/tabulard/messaging/message_variant_spec.rb 100.00 % 75 39 39 0 1.33 100.00 % 0 0 0
spec/tabulard/messaging/messages/cleaned_string_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/messaging/messages/duplicated_header_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/messaging/messages/invalid_header_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/messaging/messages/missing_column_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/messaging/messages/must_be_array_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/messaging/messages/must_be_boolsy_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/messaging/messages/must_be_date_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/messaging/messages/must_be_email_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/messaging/messages/must_be_string_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/messaging/messages/must_exist_spec.rb 100.00 % 48 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/messaging/messenger_spec.rb 100.00 % 443 225 225 0 2.55 100.00 % 0 0 0
spec/tabulard/messaging/validations/base_validator_spec.rb 100.00 % 154 89 89 0 1.54 100.00 % 0 0 0
spec/tabulard/messaging/validations_spec.rb 100.00 % 78 40 40 0 1.58 100.00 % 0 0 0
spec/tabulard/messaging_spec.rb 100.00 % 42 23 23 0 1.35 100.00 % 0 0 0
spec/tabulard/row_processor_result_spec.rb 100.00 % 31 19 19 0 1.58 100.00 % 0 0 0
spec/tabulard/row_processor_spec.rb 100.00 % 82 34 34 0 1.12 100.00 % 0 0 0
spec/tabulard/row_value_builder_spec.rb 100.00 % 141 75 75 0 1.69 100.00 % 0 0 0
spec/tabulard/specification_spec.rb 100.00 % 47 25 25 0 1.68 100.00 % 0 0 0
spec/tabulard/table_processor_result_spec.rb 100.00 % 24 14 14 0 1.36 100.00 % 0 0 0
spec/tabulard/table_processor_spec.rb 100.00 % 202 90 90 0 2.69 100.00 % 0 0 0
spec/tabulard/table_spec.rb 100.00 % 309 158 158 0 2.81 100.00 % 0 0 0
spec/tabulard/template_config_spec.rb 100.00 % 46 24 24 0 1.21 100.00 % 0 0 0
spec/tabulard/template_spec.rb 100.00 % 45 23 23 0 1.43 100.00 % 0 0 0
spec/tabulard/types/cast_chain_spec.rb 100.00 % 147 77 77 0 1.60 100.00 % 0 0 0
spec/tabulard/types/composites/array_compact_spec.rb 100.00 % 34 17 17 0 1.18 100.00 % 0 0 0
spec/tabulard/types/composites/array_spec.rb 100.00 % 51 25 25 0 1.28 100.00 % 0 0 0
spec/tabulard/types/composites/composite_spec.rb 100.00 % 18 9 9 0 1.00 100.00 % 0 0 0
spec/tabulard/types/container_spec.rb 100.00 % 162 83 83 0 1.39 100.00 % 0 0 0
spec/tabulard/types/scalars/boolsy_cast_spec.rb 100.00 % 86 43 43 0 1.42 100.00 % 0 0 0
spec/tabulard/types/scalars/boolsy_spec.rb 100.00 % 20 9 9 0 1.00 100.00 % 0 0 0
spec/tabulard/types/scalars/date_string_cast_spec.rb 100.00 % 120 54 54 0 1.37 100.00 % 0 0 0
spec/tabulard/types/scalars/date_string_spec.rb 100.00 % 24 11 11 0 1.27 100.00 % 0 0 0
spec/tabulard/types/scalars/email_cast_spec.rb 100.00 % 53 25 25 0 1.40 100.00 % 0 0 0
spec/tabulard/types/scalars/email_spec.rb 100.00 % 20 9 9 0 1.00 100.00 % 0 0 0
spec/tabulard/types/scalars/scalar_cast_spec.rb 100.00 % 109 56 56 0 1.34 100.00 % 0 0 0
spec/tabulard/types/scalars/scalar_spec.rb 100.00 % 20 9 9 0 1.00 100.00 % 0 0 0
spec/tabulard/types/scalars/string_spec.rb 100.00 % 58 28 28 0 1.29 100.00 % 0 0 0
spec/tabulard/types/type_spec.rb 100.00 % 250 129 129 0 1.53 100.00 % 0 0 0
spec/tabulard/utils/cell_string_cleaner_spec.rb 100.00 % 21 12 12 0 1.42 100.00 % 0 0 0
spec/tabulard/utils/monadic_result/failure_spec.rb 100.00 % 188 94 94 0 1.47 100.00 % 0 0 0
spec/tabulard/utils/monadic_result/success_spec.rb 100.00 % 186 93 93 0 1.43 100.00 % 0 0 0
spec/tabulard/utils/monadic_result/unit_spec.rb 100.00 % 17 8 8 0 1.50 100.00 % 0 0 0
spec/tabulard/utils/monadic_result_spec.rb 100.00 % 108 57 57 0 1.67 100.00 % 0 0 0
spec/tabulard_spec.rb 100.00 % 207 66 66 0 3.08 100.00 % 0 0 0

lib/tabulard.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. # {Tabulard} is a library designed to process tabular data according to a
  3. # {Tabulard::Template developer-defined structure}. It will turn each row into a
  4. # object whose keys and types are specified by the structure.
  5. #
  6. # It can work with tabular data presented in different formats by delegating
  7. # the parsing of documents to specialized adapters
  8. # ({Tabulard::Adapters::Xlsx}, {Tabulard::Adapters::Csv}, etc...).
  9. #
  10. # Given a tabular document and a specification of the document structure,
  11. # Tabulard may process the document by handling the following tasks:
  12. #
  13. # - validation of the document's actual structure
  14. # - arbitrary complex typecasting of each row into a validated object,
  15. # according to the document specification
  16. # - fine-grained error handling (at the table/row/col/cell level)
  17. # - all of the above done so that internationalization of messages is easy
  18. #
  19. # Tabulard is designed with memory efficiency in mind by processing documents
  20. # one row at a time, thus not requiring parsing and loading the whole document
  21. # in memory upfront (depending on the adapter). The memory consumption of the
  22. # library should therefore theoretically stay stable during the processing of a
  23. # document, disregarding how many rows it may have.
  24. 1 module Tabulard
  25. end
  26. 1 require "tabulard/template"
  27. 1 require "tabulard/template_config"
  28. 1 require "tabulard/table_processor"
  29. 1 require "tabulard/adapters/bare"

lib/tabulard/adapters.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 module Adapters
  4. 1 class << self
  5. 1 def open(*args, adapter:, **opts, &block)
  6. 14 adapter.open(*args, **opts, &block)
  7. end
  8. end
  9. end
  10. end

lib/tabulard/adapters/bare.rb

100.0% lines covered

100.0% branches covered

58 relevant lines. 58 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../table"
  3. 1 module Tabulard
  4. 1 module Adapters
  5. 1 class Bare
  6. 1 include Table
  7. 1 def initialize(table, headers: nil, **opts)
  8. 32 super(**opts)
  9. 32 then: 22 if (table_size = table.size).positive?
  10. 22 init_with_filled_table(table, table_size: table_size, headers: headers)
  11. else: 10 else
  12. 10 init_with_empty_table(headers: headers)
  13. end
  14. end
  15. 1 def each_header
  16. 21 raise_if_closed
  17. 21 else: 17 then: 2 return to_enum(:each_header) { @cols_count } unless block_given?
  18. 17 @cols_count.times do |col_index|
  19. 57 col, cell_value = read_cell(@headers, col_index)
  20. 57 yield Header.new(col: col, value: cell_value)
  21. end
  22. 17 self
  23. end
  24. 1 def each_row
  25. 16 raise_if_closed
  26. 16 else: 12 then: 2 return to_enum(:each_row) { @rows_count } unless block_given?
  27. 12 @rows_count.times do |row_index|
  28. 28 row, row_data = read_row(row_index)
  29. 28 row_value = Array.new(@cols_count) do |col_index|
  30. 118 col, cell_value = read_cell(row_data, col_index)
  31. 118 Cell.new(row: row, col: col, value: cell_value)
  32. end
  33. 28 yield Row.new(row: row, value: row_value)
  34. end
  35. 12 self
  36. end
  37. 1 private
  38. 1 def init_with_filled_table(table, table_size:, headers:)
  39. 22 @table = table
  40. 22 then: 4 if headers
  41. 4 ensure_compatible_size(table[0].size, headers.size)
  42. 2 headers_row = -1
  43. 2 @headers = headers
  44. else: 18 else
  45. 18 headers_row = 0
  46. 18 @headers = table[headers_row]
  47. end
  48. 20 @first_row = headers_row.succ
  49. 20 @first_row_name = @first_row.succ
  50. 20 @rows_count = table_size - @first_row
  51. 20 @first_col = 0
  52. 20 @first_col_name = @first_col.succ
  53. 20 @cols_count = @headers.size
  54. end
  55. 1 def init_with_empty_table(headers:)
  56. 10 @rows_count = 0
  57. 10 then: 1 if headers
  58. 1 @headers = headers
  59. 1 @first_col = 0
  60. 1 @first_col_name = @first_col.succ
  61. 1 @cols_count = headers.size
  62. else: 9 else
  63. 9 @cols_count = 0
  64. end
  65. end
  66. 1 def read_row(row_index)
  67. 28 row = @first_row_name + row_index
  68. 28 row_data = @table[@first_row + row_index]
  69. 28 [row, row_data]
  70. end
  71. 1 def read_cell(row_data, col_index)
  72. 175 col = Table.int2col(@first_col_name + col_index)
  73. 175 cell_value = row_data[@first_col + col_index]
  74. 175 [col, cell_value]
  75. end
  76. end
  77. end
  78. end

lib/tabulard/adapters/csv.rb

100.0% lines covered

100.0% branches covered

65 relevant lines. 65 lines covered and 0 lines missed.
14 total branches, 14 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "csv"
  3. 1 require_relative "../table"
  4. 1 module Tabulard
  5. 1 module Adapters
  6. 1 class Csv
  7. 1 include Table
  8. 1 class InvalidCSV < Message
  9. 1 CODE = "invalid_csv"
  10. 1 def_validator do
  11. 1 table
  12. 1 nil_code_data
  13. end
  14. end
  15. 1 DEFAULTS = {
  16. row_sep: :auto,
  17. col_sep: ",",
  18. quote_char: '"',
  19. }.freeze
  20. 1 private_constant :DEFAULTS
  21. 1 def self.defaults
  22. 87 DEFAULTS
  23. end
  24. 1 def initialize(
  25. io,
  26. row_sep: self.class.defaults[:row_sep],
  27. col_sep: self.class.defaults[:col_sep],
  28. quote_char: self.class.defaults[:quote_char],
  29. headers: nil,
  30. **opts
  31. )
  32. 29 super(**opts)
  33. 29 csv = CSV.new(
  34. io,
  35. row_sep: row_sep,
  36. col_sep: col_sep,
  37. quote_char: quote_char
  38. )
  39. 29 then: 5 if headers
  40. 5 init_with_headers(csv, headers)
  41. else: 24 else
  42. 24 init_without_headers(csv)
  43. end
  44. end
  45. 1 def each_header
  46. 16 raise_if_closed
  47. 16 else: 10 then: 4 return to_enum(:each_header) { @cols_count } unless block_given?
  48. 10 then: 3 else: 7 return self if @cols_count.zero?
  49. 7 @headers.each_with_index do |header, col_idx|
  50. 37 col = Table.int2col(col_idx + 1)
  51. 37 yield Header.new(col: col, value: header)
  52. end
  53. 7 self
  54. end
  55. 1 def each_row
  56. 11 raise_if_closed
  57. 9 else: 7 then: 2 return to_enum(:each_row) unless block_given?
  58. 7 else: 4 then: 3 return self unless @csv
  59. 4 handle_malformed_csv do
  60. 4 @csv.each.with_index(@first_row_name) do |raw, row|
  61. 13 value = Array.new(@cols_count) do |col_idx|
  62. 52 col = Table.int2col(col_idx + 1)
  63. 52 Cell.new(row: row, col: col, value: raw[col_idx])
  64. end
  65. 13 yield Row.new(row: row, value: value)
  66. end
  67. end
  68. 4 self
  69. end
  70. # The adapter isn't responsible for opening the IO, and therefore it is not responsible for
  71. # closing it either.
  72. 1 private
  73. 1 def handle_malformed_csv
  74. 33 yield
  75. rescue CSV::MalformedCSVError
  76. 1 messenger.error(InvalidCSV.new)
  77. 1 raise InputError
  78. end
  79. 1 def init_with_headers(csv, headers_data)
  80. 10 first_row_data = handle_malformed_csv { csv.shift }
  81. 5 then: 4 if first_row_data
  82. 4 ensure_compatible_size(first_row_data.size, headers_data.size)
  83. 2 csv.rewind
  84. 2 @csv = csv
  85. 2 @first_row_name = 1
  86. else: 1 else
  87. 1 @csv = nil
  88. end
  89. 3 @headers = headers_data
  90. 3 @cols_count = @headers.size
  91. end
  92. 1 def init_without_headers(csv)
  93. 48 first_row_data = handle_malformed_csv { csv.shift }
  94. 23 then: 11 if first_row_data
  95. 11 @csv = csv
  96. 11 @first_row_name = 2
  97. 11 @headers = first_row_data
  98. 11 @cols_count = @headers.size
  99. else: 12 else
  100. 12 @csv = nil
  101. 12 @headers = nil
  102. 12 @cols_count = 0
  103. end
  104. end
  105. end
  106. end
  107. end

lib/tabulard/adapters/xlsx.rb

100.0% lines covered

95.0% branches covered

94 relevant lines. 94 lines covered and 0 lines missed.
20 total branches, 19 branches covered and 1 branches missed.
    
  1. # frozen_string_literal: true
  2. # NOTE: As reference:
  3. # - {Roo::Excelx::Cell#cell_value} => the "raw" value before Excel's typecasts
  4. # - {Roo::Excelx::Cell#value} => the "user" value, after Excel's typecasts
  5. 1 require "roo"
  6. 1 require_relative "../table"
  7. 1 module Tabulard
  8. 1 module Adapters
  9. 1 class Xlsx
  10. 1 include Table
  11. 1 def initialize(path, headers: nil, **opts)
  12. 29 super(**opts)
  13. 29 @roo = Roo::Excelx.new(path)
  14. 29 worktable = @roo.sheet_for(@roo.default_sheet)
  15. 29 then: 19 if worktable.first_row
  16. 19 init_with_filled_table(worktable, headers: headers)
  17. else: 10 else
  18. 10 init_with_empty_table(headers: headers)
  19. end
  20. end
  21. 1 def each_header(&block)
  22. 15 raise_if_closed
  23. 15 else: 11 then: 2 return to_enum(:each_header) { @cols_count } unless block
  24. 11 then: 3 else: 8 return self if @cols_count.zero?
  25. 8 then: 2 if @headers
  26. 2 each_custom_header(&block)
  27. else: 6 else
  28. 6 each_cell_header(&block)
  29. end
  30. 8 self
  31. end
  32. 1 def each_row
  33. 14 raise_if_closed
  34. 14 else: 10 then: 2 return to_enum(:each_row) { @rows_count } unless block_given?
  35. 10 @rows_count.times do |row_index|
  36. 17 row = @rows.name(row_index)
  37. 17 row_value = Array.new(@cols_count) do |col_index|
  38. 85 col = @cols.name(col_index)
  39. 85 cell_coords = [@rows.coord(row_index), @cols.coord(col_index)]
  40. 85 then: 75 else: 10 cell_value = @cells[cell_coords]&.value
  41. 85 Cell.new(row: row, col: col, value: cell_value)
  42. end
  43. 17 yield Row.new(row: row, value: row_value)
  44. end
  45. 10 self
  46. end
  47. 1 def close
  48. 33 super do
  49. 27 @roo.close
  50. end
  51. end
  52. 1 private
  53. 1 def init_with_filled_table(worktable, headers:)
  54. 19 @rows = Rows.new(
  55. first_row: worktable.first_row,
  56. last_row: worktable.last_row,
  57. include_headers: headers.nil?
  58. )
  59. 19 @cols = Cols.new(
  60. first_col: worktable.first_column,
  61. last_col: worktable.last_column
  62. )
  63. 19 @rows_count = @rows.count
  64. 19 @cols_count = @cols.count
  65. 19 then: 4 else: 15 if (@headers = headers)
  66. 4 ensure_compatible_size(@cols_count, @headers.size)
  67. end
  68. 17 @cells = worktable.cells
  69. end
  70. 1 def init_with_empty_table(headers:)
  71. 10 @rows = nil
  72. 10 @rows_count = 0
  73. 10 then: 1 if headers
  74. 1 @cols = Cols.new(first_col: 1, last_col: headers.size)
  75. 1 @cols_count = @cols.count
  76. else: 9 else
  77. 9 @cols = nil
  78. 9 @cols_count = 0
  79. end
  80. 10 @headers = headers
  81. 10 @cells = nil
  82. end
  83. 1 def each_custom_header
  84. 2 @headers.each_with_index do |value, col_index|
  85. 8 col = @cols.name(col_index)
  86. 8 yield Header.new(col: col, value: value)
  87. end
  88. end
  89. 1 def each_cell_header
  90. 6 @cols_count.times do |col_index|
  91. 30 col = @cols.name(col_index)
  92. 30 cell_coords = [@rows.headers_coord, @cols.coord(col_index)]
  93. 30 then: 30 else: 0 cell_value = @cells[cell_coords]&.value
  94. 30 yield Header.new(col: col, value: cell_value)
  95. end
  96. end
  97. 1 class Rows
  98. 1 def initialize(first_row:, last_row:, include_headers:)
  99. 19 then: 15 if include_headers
  100. 15 @headers_coord = first_row
  101. 15 init(first_row.succ, last_row)
  102. else: 4 else
  103. 4 @headers_coord = nil
  104. 4 init(first_row, last_row)
  105. end
  106. end
  107. 1 attr_reader :count, :headers_coord
  108. 1 def name(row)
  109. 17 @first_name + row
  110. end
  111. 1 def coord(row)
  112. 85 @first_row + row
  113. end
  114. 1 private
  115. 1 def init(first_row, last_row)
  116. 19 offset = first_row - 1
  117. 19 @first_row = first_row
  118. 19 @first_name = first_row
  119. 19 @count = last_row - offset
  120. end
  121. end
  122. 1 class Cols
  123. 1 def initialize(first_col:, last_col:)
  124. 20 cols_offset = first_col - 1
  125. 20 @first_col = first_col
  126. 20 @first_name = first_col
  127. 20 @count = last_col - cols_offset
  128. end
  129. 1 attr_reader :count
  130. 1 def name(col)
  131. 123 Table.int2col(@first_name + col)
  132. end
  133. 1 def coord(col)
  134. 115 @first_col + col
  135. end
  136. end
  137. end
  138. end
  139. end

lib/tabulard/attribute.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "attribute_types"
  3. 1 require_relative "column"
  4. 1 module Tabulard
  5. # The main building block of a {Template}.
  6. 1 class Attribute
  7. # A smarter version of {#initialize}.
  8. #
  9. # - It automatically freezes the instance before returning it.
  10. # - It instantiates and injects a type automatically by passing the arguments to
  11. # {AttributeTypes.build}.
  12. #
  13. # @return [Attribute] a frozen instance
  14. 1 def self.build(key:, type:)
  15. 29 type = AttributeTypes.build(type)
  16. 29 attribute = new(key: key, type: type)
  17. 29 attribute.freeze
  18. end
  19. # @param key [String, Symbol] The key in the resulting Hash after processing a row.
  20. # @param type [AttributeType] The type of the value.
  21. 1 def initialize(key:, type:)
  22. 30 @key = key
  23. 30 @type = type
  24. end
  25. # @return [Symbol, String]
  26. 1 attr_reader :key
  27. # An abstract specification of the type of a value in the resulting hash.
  28. #
  29. # It will be used to produce the {Types::Type concrete type} of a column (or a list of columns)
  30. # when a {TemplateConfig} is {Template#apply applied} to the {Template} owning the attribtue.
  31. #
  32. # @return [AttributeType]
  33. 1 attr_reader :type
  34. 1 def ==(other)
  35. 2 other.is_a?(self.class) &&
  36. key == other.key &&
  37. type == other.type
  38. end
  39. 1 def each_column(config)
  40. 19 else: 18 then: 1 return enum_for(:each_column, config) unless block_given?
  41. 18 compiled_type = type.compile(config.types)
  42. 18 type.each_column do |index, required|
  43. 54 header, header_pattern = config.header(key, index)
  44. 54 yield Column.new(
  45. key: key,
  46. type: compiled_type,
  47. index: index,
  48. header: header,
  49. header_pattern: header_pattern,
  50. required: required
  51. )
  52. end
  53. end
  54. end
  55. end

lib/tabulard/attribute_types.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "attribute_types/scalar"
  3. 1 require_relative "attribute_types/composite"
  4. 1 module Tabulard
  5. 1 module AttributeTypes
  6. # A factory for all {AttributeType}.
  7. 1 def self.build(type)
  8. 33 then: 10 if type.is_a?(Hash) && type.key?(:composite)
  9. 10 else: 23 Composite.build(**type)
  10. 23 then: 1 elsif type.is_a?(Array)
  11. 1 Composite.build(composite: :array, scalars: type)
  12. else: 22 else
  13. 22 Scalar.build(type)
  14. end
  15. end
  16. end
  17. # @!parse
  18. # # The minimal interface of an {Attribute} type.
  19. # # @abstract It only exists to document the interface implemented by the different classes of
  20. # # {AttributeTypes}.
  21. # module AttributeType
  22. # # A smarter version of `#initialize`, that will always return a frozen instance of the
  23. # # type, with optional syntactic sugar for its arguments compared to `#initialize`.
  24. # def self.build(*); end
  25. #
  26. # # Given a type container, return the actual, usable type for the attribute.
  27. # # @param container [Types::Container]
  28. # # @return [Types::Type]
  29. # def compile(container); end
  30. #
  31. # # Enumerate the columns (one or more) that may compose the attribute in the tabular
  32. # # document.
  33. # #
  34. # # @yieldparam index [Integer, nil]
  35. # # If there is only one column involved, then `index` will be `nil`. Otherwise, `index`
  36. # # will start at `0` and increase by `1` at each step.
  37. # # @yieldparam required [Boolean]
  38. # # Whether the column must be present in the tabular document.
  39. # #
  40. # # @overload def each_column(&block)
  41. # # @return [self]
  42. # # @overload def each_column
  43. # # @return [Enumerator]
  44. # def each_column; end
  45. # end
  46. end

lib/tabulard/attribute_types/composite.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "value"
  3. 1 module Tabulard
  4. 1 module AttributeTypes
  5. 1 class Composite
  6. # A smarter version of {#initialize}.
  7. #
  8. # - It automatically freezes the instance before returning it.
  9. # - It instantiates, freezes and injects a list of values automatically by mapping the given
  10. # list of scalars to a list of {Value values} using {Value.build}.
  11. #
  12. # @return [Composite] a frozen instance
  13. 1 def self.build(composite:, scalars:)
  14. 64 scalars = scalars.map { |scalar| Value.build(scalar) }
  15. 12 scalars.freeze
  16. 12 composite = new(composite: composite, scalars: scalars)
  17. 12 composite.freeze
  18. end
  19. # @param composite [Symbol] The name used to refer to a composite type from a
  20. # {Types::Container}.
  21. # @param scalars [Array<Value>] The list of values of the composite.
  22. # @see .build
  23. 1 def initialize(composite:, scalars:)
  24. 15 @composite_type = composite
  25. 15 @values = scalars
  26. end
  27. 1 def compile(container)
  28. 9 container.composite(composite_type, values.map(&:type))
  29. end
  30. 1 def each_column
  31. 11 else: 9 then: 1 return enum_for(:each_column) { values.size } unless block_given?
  32. 9 values.each_with_index do |value, index|
  33. 45 yield index, value.required
  34. end
  35. 9 self
  36. end
  37. 1 def ==(other)
  38. 2 other.is_a?(self.class) &&
  39. composite_type == other.composite_type &&
  40. values == other.values
  41. end
  42. 1 protected
  43. 1 attr_reader :composite_type, :values
  44. end
  45. end
  46. end

lib/tabulard/attribute_types/scalar.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "value"
  3. 1 module Tabulard
  4. 1 module AttributeTypes
  5. # @see AttributeType
  6. 1 class Scalar
  7. # @!parse
  8. # include AttributeType
  9. # A smarter version of {#initialize}.
  10. #
  11. # - It automatically freezes the instance before returning it.
  12. # - It instantiates and injects a value automatically by passing the arguments to
  13. # {Value.build}.
  14. #
  15. # The method signature is identical to the one of {Value.build}.
  16. #
  17. # @return [Scalar] a frozen instance
  18. 1 def self.build(...)
  19. 23 value = Value.build(...)
  20. 23 scalar = new(value)
  21. 23 scalar.freeze
  22. end
  23. # @param value [Value] The value of the scalar.
  24. # @see .build
  25. 1 def initialize(value)
  26. 26 @value = value
  27. end
  28. # @see AttributeType#compile
  29. 1 def compile(container)
  30. 9 container.scalar(value.type)
  31. end
  32. # @see AttributeType#each_column
  33. 1 def each_column
  34. 11 else: 9 then: 1 return enum_for(:each_column) { 1 } unless block_given?
  35. 9 yield nil, value.required
  36. 9 self
  37. end
  38. 1 def ==(other)
  39. 4 other.is_a?(self.class) &&
  40. value == other.value
  41. end
  42. 1 protected
  43. 1 attr_reader :value
  44. end
  45. end
  46. end

lib/tabulard/attribute_types/value.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 module AttributeTypes
  4. 1 class Value
  5. # A smarter version of {#initialize}.
  6. #
  7. # - It automatically freezes the instance before returning it.
  8. # - It accepts two kinds of parameters: either those of {#initialize}, or a shortcut. See the
  9. # overloaded method signature for details.
  10. #
  11. # @overload def build(type:, required:)
  12. # @example
  13. # Value.build(type: :foo, required: true)
  14. # @see #initialize
  15. #
  16. # @overload def build(type)
  17. # @param type [Symbol] The name of the type, optionally suffixed with `!` to indicate that
  18. # the value is required.
  19. # @example
  20. # Value.build(:foo) #=> Value.build(type: :foo, required: false)
  21. # Value.build(:foo!) #=> Value.build(type: :foo, required: true)
  22. #
  23. # @return [Value] a frozen instance
  24. 1 def self.build(arg)
  25. 81 then: 7 else: 74 value = arg.is_a?(Hash) ? new(**arg) : from_type_name(arg)
  26. 81 value.freeze
  27. end
  28. 1 def self.from_type_name(type)
  29. 74 type = type.to_s
  30. 74 optional = type.end_with?("?")
  31. 74 then: 39 else: 35 type = (optional ? type.slice(0..-2) : type).to_sym
  32. 74 new(type: type, required: !optional)
  33. end
  34. 1 private_class_method :from_type_name
  35. # @param type [Symbol] The name used to refer to a scalar type from a {Types::Container}.
  36. # @param required [Boolean] Is the value required to be given in the input ?
  37. # @see .build
  38. 1 def initialize(type:, required: true)
  39. 94 @type = type
  40. 94 @required = required
  41. end
  42. # @return [Symbol]
  43. 1 attr_reader :type
  44. # @return [Boolean]
  45. 1 attr_reader :required
  46. 1 def ==(other)
  47. 15 other.is_a?(self.class) &&
  48. type == other.type &&
  49. required == other.required
  50. end
  51. end
  52. end
  53. end

lib/tabulard/column.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 class Column
  4. 1 def initialize(
  5. key:,
  6. type:,
  7. index:,
  8. header:,
  9. header_pattern:,
  10. required:
  11. )
  12. 60 @key = key
  13. 60 @type = type
  14. 60 @index = index
  15. 60 @header = header
  16. 60 @header_pattern = header_pattern
  17. 60 @required = required
  18. end
  19. 1 attr_reader :key,
  20. :type,
  21. :index,
  22. :header,
  23. :header_pattern
  24. 1 def required?
  25. 55 @required
  26. end
  27. end
  28. end

lib/tabulard/errors/error.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 module Errors
  4. 1 class Error < StandardError
  5. end
  6. end
  7. end

lib/tabulard/errors/spec_error.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "error"
  3. 1 module Tabulard
  4. 1 module Errors
  5. 1 class SpecError < Error
  6. end
  7. end
  8. end

lib/tabulard/errors/type_error.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "error"
  3. 1 module Tabulard
  4. 1 module Errors
  5. 1 class TypeError < Error
  6. end
  7. end
  8. end

lib/tabulard/headers.rb

100.0% lines covered

100.0% branches covered

49 relevant lines. 49 lines covered and 0 lines missed.
14 total branches, 14 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "set"
  3. 1 require_relative "messaging/messages/invalid_header"
  4. 1 require_relative "messaging/messages/duplicated_header"
  5. 1 require_relative "messaging/messages/missing_column"
  6. 1 module Tabulard
  7. 1 class Headers
  8. 1 include Utils::MonadicResult
  9. 1 class Header
  10. 1 def initialize(table_header, spec_column)
  11. 47 @header = table_header
  12. 47 @column = spec_column
  13. end
  14. 1 attr_reader :header, :column
  15. 1 def ==(other)
  16. 3 other.is_a?(self.class) &&
  17. header == other.header &&
  18. column == other.column
  19. end
  20. 1 def row_value_index
  21. 60 header.row_value_index
  22. end
  23. end
  24. 1 def initialize(specification:, messenger:)
  25. 16 @specification = specification
  26. 16 @messenger = messenger
  27. 16 @headers = []
  28. 16 @columns = Set.new
  29. 16 @failure = false
  30. end
  31. 1 def add(header)
  32. 54 column = @specification.get(header.value)
  33. 54 @messenger.scope_col!(header.col) do
  34. 54 else: 46 then: 8 return unless add_ensure_column_is_specified(header, column)
  35. 46 else: 44 then: 2 return unless add_ensure_column_is_unique(header, column)
  36. end
  37. 44 @headers << Header.new(header, column)
  38. end
  39. 1 def result
  40. 13 missing_columns = @specification.required_columns - @columns.to_a
  41. 13 else: 11 then: 2 unless missing_columns.empty?
  42. 2 @failure = true
  43. 2 missing_columns.each do |column|
  44. 4 @messenger.error(
  45. Messaging::Messages::MissingColumn.new(code_data: { value: column.header })
  46. )
  47. end
  48. end
  49. 13 then: 6 if @failure
  50. 6 Failure()
  51. else: 7 else
  52. 7 Success(@headers)
  53. end
  54. end
  55. 1 private
  56. 1 def add_ensure_column_is_specified(header, column)
  57. 54 else: 8 then: 46 return true unless column.nil?
  58. 8 else: 2 then: 6 unless @specification.ignore_unspecified_columns?
  59. 6 @failure = true
  60. 6 @messenger.error(
  61. Messaging::Messages::InvalidHeader.new(code_data: { value: header.value })
  62. )
  63. end
  64. 8 false
  65. end
  66. 1 def add_ensure_column_is_unique(header, column)
  67. 46 then: 44 else: 2 return true if @columns.add?(column)
  68. 2 @failure = true
  69. 2 @messenger.error(
  70. Messaging::Messages::DuplicatedHeader.new(code_data: { value: header.value })
  71. )
  72. 2 false
  73. end
  74. end
  75. end

lib/tabulard/messaging.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "messaging/config"
  3. 1 require_relative "messaging/constants"
  4. 1 require_relative "messaging/message"
  5. 1 require_relative "messaging/message_variant"
  6. 1 require_relative "messaging/messenger"
  7. 1 module Tabulard
  8. 1 module Messaging
  9. 1 class << self
  10. 1 attr_accessor :config
  11. 1 def configure
  12. 2 config = self.config.dup
  13. 2 yield config
  14. 2 self.config = config.freeze
  15. end
  16. end
  17. 1 self.config = Config.new.freeze
  18. end
  19. end

lib/tabulard/messaging/config.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 module Messaging
  4. 1 class Config
  5. 1 def initialize(validate_messages: default_validate_messages)
  6. 10 @validate_messages = validate_messages
  7. end
  8. 1 attr_accessor :validate_messages
  9. 1 private
  10. 1 def default_validate_messages
  11. 6 ENV["TABULARD_MESSAGING_VALIDATE_MESSAGES"] != "false"
  12. end
  13. end
  14. end
  15. end

lib/tabulard/messaging/constants.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 module Messaging
  4. 1 module SCOPES
  5. 1 TABLE = "TABLE"
  6. 1 ROW = "ROW"
  7. 1 COL = "COL"
  8. 1 CELL = "CELL"
  9. end
  10. 1 module SEVERITIES
  11. 1 WARN = "WARN"
  12. 1 ERROR = "ERROR"
  13. end
  14. end
  15. end

lib/tabulard/messaging/message.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
5 total branches, 5 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "constants"
  3. 1 require_relative "validations"
  4. 1 module Tabulard
  5. 1 module Messaging
  6. 1 class Message
  7. 1 include Validations
  8. 1 def initialize(
  9. code:,
  10. code_data: nil,
  11. scope: SCOPES::TABLE,
  12. scope_data: nil,
  13. severity: SEVERITIES::WARN
  14. )
  15. 129 @code = code
  16. 129 @code_data = code_data
  17. 129 @scope = scope
  18. 129 @scope_data = scope_data
  19. 129 @severity = severity
  20. end
  21. 1 attr_accessor(
  22. :code,
  23. :code_data,
  24. :scope,
  25. :scope_data,
  26. :severity
  27. )
  28. 1 def ==(other)
  29. 25 other.is_a?(self.class) &&
  30. code == other.code &&
  31. code_data == other.code_data &&
  32. scope == other.scope &&
  33. scope_data == other.scope_data &&
  34. severity == other.severity
  35. end
  36. 1 def to_s
  37. 6 parts = [scoping_to_s, "#{severity}: #{code}", code_data]
  38. 6 parts.compact!
  39. 6 parts.join(" ")
  40. end
  41. 1 def to_h
  42. {
  43. 1 code: code,
  44. code_data: code_data,
  45. scope: scope,
  46. scope_data: scope_data,
  47. severity: severity,
  48. }
  49. end
  50. 1 private
  51. 1 def scoping_to_s
  52. 6 when: 2 else: 1 case scope
  53. 2 when: 1 when SCOPES::TABLE then "[#{scope}]"
  54. 1 when: 1 when SCOPES::ROW then "[#{scope}: #{scope_data[:row]}]"
  55. 1 when: 1 when SCOPES::COL then "[#{scope}: #{scope_data[:col]}]"
  56. 1 when SCOPES::CELL then "[#{scope}: #{scope_data[:col]}#{scope_data[:row]}]"
  57. end
  58. end
  59. end
  60. end
  61. end

lib/tabulard/messaging/message_variant.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "message"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. # While a {Message} represents any kind of message, {MessageVariant} represents any subset of
  6. # messages that share the same code.
  7. #
  8. # The code of a message variant can and should be defined at the class level, given that it
  9. # won't differ among the instances (and a validation is defined to enforce that invariant).
  10. # {MessageVariant} should be considered an abstract class, and its subclasses should define
  11. # their own `CODE` constant, which will be read by {.code}.
  12. #
  13. # As far as the other methods are concerned, {.code} should be considered the only source of
  14. # truth when it comes to reading the code assigned to a message variant. The fact that {.code}
  15. # is actually implemented using a dynamic resolution of the class' `CODE` constant is an
  16. # implementation detail stemming from the fact that documentation tools such as YARD will
  17. # highlight constants, as opposed to instance variables of a class for example. Using a constant
  18. # is therefore meant to provide better documentation, and it should not be relied upon
  19. # otherwise.
  20. #
  21. # @abstract
  22. 1 class MessageVariant < Message
  23. # Reads the code assigned to the class (and its instances)
  24. # @return [String]
  25. 1 def self.code
  26. 148 self::CODE
  27. end
  28. # Simplifies the initialization of a variant
  29. #
  30. # Contrary to the requirements of {Message#initialize}, {MessageVariant.new} doesn't require
  31. # the caller to pass the `:code` keyword argument, as it is capable of prodividing it
  32. # automatically (from reading {.code}).
  33. 1 def self.new(**opts)
  34. 107 super(code: code, **opts)
  35. end
  36. 1 def_validator do
  37. 1 def validate_code(message)
  38. 42 message.code == message.class.code
  39. end
  40. end
  41. end
  42. end
  43. end

lib/tabulard/messaging/messages/cleaned_string.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../message_variant"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Messages
  6. 1 class CleanedString < MessageVariant
  7. 1 CODE = "cleaned_string"
  8. 1 def_validator do
  9. 1 cell
  10. 1 nil_code_data
  11. end
  12. end
  13. end
  14. end
  15. end

lib/tabulard/messaging/messages/duplicated_header.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../message_variant"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Messages
  6. 1 class DuplicatedHeader < MessageVariant
  7. 1 CODE = "duplicated_header"
  8. 1 def_validator do
  9. 1 col
  10. 1 def validate_code_data(message)
  11. 4 in: 3 else: 1 message.code_data in { value: String }
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

lib/tabulard/messaging/messages/invalid_header.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../message_variant"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Messages
  6. 1 class InvalidHeader < MessageVariant
  7. 1 CODE = "invalid_header"
  8. 1 def_validator do
  9. 1 col
  10. 1 def validate_code_data(message)
  11. 8 in: 7 else: 1 message.code_data in { value: String }
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

lib/tabulard/messaging/messages/missing_column.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../message_variant"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Messages
  6. 1 class MissingColumn < MessageVariant
  7. 1 CODE = "missing_column"
  8. 1 def_validator do
  9. 1 table
  10. 1 def validate_code_data(message)
  11. 6 in: 5 else: 1 message.code_data in { value: String }
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

lib/tabulard/messaging/messages/must_be_array.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../message_variant"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Messages
  6. 1 class MustBeArray < MessageVariant
  7. 1 CODE = "must_be_array"
  8. 1 def_validator do
  9. 1 cell
  10. 1 nil_code_data
  11. end
  12. end
  13. end
  14. end
  15. end

lib/tabulard/messaging/messages/must_be_boolsy.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../message_variant"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Messages
  6. 1 class MustBeBoolsy < MessageVariant
  7. 1 CODE = "must_be_boolsy"
  8. 1 def_validator do
  9. 1 cell
  10. 1 def validate_code_data(message)
  11. 2 in: 1 else: 1 message.code_data in { value: String }
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

lib/tabulard/messaging/messages/must_be_date.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../message_variant"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Messages
  6. 1 class MustBeDate < MessageVariant
  7. 1 CODE = "must_be_date"
  8. 1 def_validator do
  9. 1 cell
  10. 1 def validate_code_data(message)
  11. 2 in: 1 else: 1 message.code_data in { format: String }
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

lib/tabulard/messaging/messages/must_be_email.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../message_variant"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Messages
  6. 1 class MustBeEmail < MessageVariant
  7. 1 CODE = "must_be_email"
  8. 1 def_validator do
  9. 1 cell
  10. 1 def validate_code_data(message)
  11. 7 in: 6 else: 1 message.code_data in { value: String }
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

lib/tabulard/messaging/messages/must_be_string.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../message_variant"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Messages
  6. 1 class MustBeString < MessageVariant
  7. 1 CODE = "must_be_string"
  8. 1 def_validator do
  9. 1 cell
  10. 1 nil_code_data
  11. end
  12. end
  13. end
  14. end
  15. end

lib/tabulard/messaging/messages/must_exist.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../message_variant"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Messages
  6. 1 class MustExist < MessageVariant
  7. 1 CODE = "must_exist"
  8. 1 def_validator do
  9. 1 cell
  10. 1 nil_code_data
  11. end
  12. end
  13. end
  14. end
  15. end

lib/tabulard/messaging/messenger.rb

100.0% lines covered

100.0% branches covered

64 relevant lines. 64 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "constants"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 class Messenger
  6. 1 def initialize(
  7. scope: SCOPES::TABLE,
  8. scope_data: nil,
  9. validate_messages: Messaging.config.validate_messages
  10. )
  11. 162 @scope = scope.freeze
  12. 162 @scope_data = scope_data.freeze
  13. 162 @messages = []
  14. 162 @validate_messages = validate_messages
  15. end
  16. 1 attr_reader :scope, :scope_data, :messages, :validate_messages
  17. 1 def ==(other)
  18. 2 other.is_a?(self.class) &&
  19. scope == other.scope &&
  20. scope_data == other.scope_data &&
  21. messages == other.messages &&
  22. validate_messages == other.validate_messages
  23. end
  24. 1 def dup
  25. 24 self.class.new(
  26. scope: @scope,
  27. scope_data: @scope_data,
  28. validate_messages: @validate_messages
  29. )
  30. end
  31. 1 def scoping!(scope, scope_data, &block)
  32. 139 scope = scope.freeze
  33. 139 scope_data = scope_data.freeze
  34. 139 then: 137 if block
  35. 137 replace_scoping_block(scope, scope_data, &block)
  36. else: 2 else
  37. 2 replace_scoping_noblock(scope, scope_data)
  38. end
  39. end
  40. 1 def scoping(...)
  41. 2 dup.scoping!(...)
  42. end
  43. 1 def scope_row!(row, &)
  44. 24 scope = case @scope
  45. when: 4 when SCOPES::COL, SCOPES::CELL
  46. 4 SCOPES::CELL
  47. else: 20 else
  48. 20 SCOPES::ROW
  49. end
  50. 24 scope_data = @scope_data.dup || {}
  51. 24 scope_data[:row] = row
  52. 24 scoping!(scope, scope_data, &)
  53. end
  54. 1 def scope_col!(col, &)
  55. 125 scope = case @scope
  56. when: 67 when SCOPES::ROW, SCOPES::CELL
  57. 67 SCOPES::CELL
  58. else: 58 else
  59. 58 SCOPES::COL
  60. end
  61. 125 scope_data = @scope_data.dup || {}
  62. 125 scope_data[:col] = col
  63. 125 scoping!(scope, scope_data, &)
  64. end
  65. 1 def scope_row(...)
  66. 2 dup.scope_row!(...)
  67. end
  68. 1 def scope_col(...)
  69. 2 dup.scope_col!(...)
  70. end
  71. 1 def warn(message)
  72. 4 add(message, severity: SEVERITIES::WARN)
  73. end
  74. 1 def error(message)
  75. 20 add(message, severity: SEVERITIES::ERROR)
  76. end
  77. 1 private
  78. 1 def add(message, severity:)
  79. 24 message.scope = @scope
  80. 24 message.scope_data = @scope_data
  81. 24 message.severity = severity
  82. 24 then: 23 else: 1 message.validate if @validate_messages
  83. 24 messages << message
  84. 24 self
  85. end
  86. 1 def replace_scoping_noblock(new_scope, new_scope_data)
  87. 2 @scope = new_scope
  88. 2 @scope_data = new_scope_data
  89. 2 self
  90. end
  91. 1 def replace_scoping_block(new_scope, new_scope_data)
  92. 137 prev_scope = @scope
  93. 137 prev_scope_data = @scope_data
  94. 137 @scope = new_scope
  95. 137 @scope_data = new_scope_data
  96. begin
  97. 137 yield self
  98. ensure
  99. 137 @scope = prev_scope
  100. 137 @scope_data = prev_scope_data
  101. end
  102. end
  103. end
  104. end
  105. end

lib/tabulard/messaging/validations.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "validations/base_validator"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Validations
  6. 1 module ClassMethods
  7. 1 then: 12 else: 6 def def_validator(base: validator&.class || BaseValidator, &block)
  8. 20 @validator = Class.new(base, &block).new.freeze
  9. end
  10. 1 def validator
  11. 120 then: 94 if defined?(@validator)
  12. 94 else: 26 @validator
  13. 26 then: 14 else: 12 elsif superclass.respond_to?(:validator)
  14. 14 superclass.validator
  15. end
  16. end
  17. 1 def validate(message)
  18. 35 then: 30 else: 5 validator&.validate(message)
  19. end
  20. end
  21. 1 def self.included(message_class)
  22. 10 message_class.extend(ClassMethods)
  23. end
  24. 1 def validate
  25. 34 self.class.validate(self)
  26. end
  27. end
  28. end
  29. end

lib/tabulard/messaging/validations/base_validator.rb

100.0% lines covered

100.0% branches covered

34 relevant lines. 34 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "dsl"
  3. 1 require_relative "invalid_message"
  4. 1 module Tabulard
  5. 1 module Messaging
  6. 1 module Validations
  7. 1 class BaseValidator
  8. 1 extend DSL
  9. 1 def validate(message)
  10. 33 errors = []
  11. 33 validate_and_append_code(message, errors)
  12. 33 validate_and_append_code_data(message, errors)
  13. 33 validate_and_append_scope(message, errors)
  14. 33 validate_and_append_scope_data(message, errors)
  15. 33 then: 30 else: 3 return if errors.empty?
  16. 3 raise InvalidMessage, build_exception_message(message, errors)
  17. end
  18. 1 def validate_code(_message)
  19. 4 true
  20. end
  21. 1 def validate_code_data(_message)
  22. 4 true
  23. end
  24. 1 def validate_scope(_message)
  25. 3 true
  26. end
  27. 1 def validate_scope_data(_message)
  28. 3 true
  29. end
  30. 1 private
  31. 1 def validate_and_append_code(message, errors)
  32. 33 else: 32 then: 1 errors << "code" unless validate_code(message)
  33. end
  34. 1 def validate_and_append_code_data(message, errors)
  35. 33 else: 32 then: 1 errors << "code_data" unless validate_code_data(message)
  36. end
  37. 1 def validate_and_append_scope(message, errors)
  38. 33 else: 31 then: 2 errors << "scope" unless validate_scope(message)
  39. end
  40. 1 def validate_and_append_scope_data(message, errors)
  41. 33 else: 32 then: 1 errors << "scope_data" unless validate_scope_data(message)
  42. end
  43. 1 def build_exception_message(message, errors)
  44. 3 "#{errors.join(", ")} <#{message.class}>#{message.to_h}"
  45. end
  46. end
  47. end
  48. end
  49. end

lib/tabulard/messaging/validations/dsl.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "mixins"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Validations
  6. 1 module DSL
  7. 1 def cell
  8. 9 include Mixins::CellValidations
  9. end
  10. 1 def col
  11. 4 include Mixins::ColValidations
  12. end
  13. 1 def row
  14. 2 include Mixins::RowValidations
  15. end
  16. 1 def table
  17. 5 include Mixins::TableValidations
  18. end
  19. 1 def nil_code_data
  20. 5 include Mixins::NilCodeData
  21. end
  22. end
  23. end
  24. end
  25. end

lib/tabulard/messaging/validations/invalid_message.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../../errors/error"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Validations
  6. 1 class InvalidMessage < Errors::Error
  7. end
  8. end
  9. end
  10. end

lib/tabulard/messaging/validations/mixins.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../constants"
  3. 1 module Tabulard
  4. 1 module Messaging
  5. 1 module Validations
  6. 1 module Mixins
  7. 1 module CellValidations
  8. 1 def validate_scope(message)
  9. 21 message.scope == SCOPES::CELL
  10. end
  11. 1 def validate_scope_data(message)
  12. 21 in: 13 else: 8 message.scope_data in { col: String, row: Integer }
  13. end
  14. end
  15. 1 module ColValidations
  16. 1 def validate_scope(message)
  17. 14 message.scope == SCOPES::COL
  18. end
  19. 1 def validate_scope_data(message)
  20. 14 in: 11 else: 3 message.scope_data in { col: String }
  21. end
  22. end
  23. 1 module RowValidations
  24. 1 def validate_scope(message)
  25. 2 message.scope == SCOPES::ROW
  26. end
  27. 1 def validate_scope_data(message)
  28. 2 in: 1 else: 1 message.scope_data in { row: Integer }
  29. end
  30. end
  31. 1 module TableValidations
  32. 1 def validate_scope(message)
  33. 12 message.scope == SCOPES::TABLE
  34. end
  35. 1 def validate_scope_data(message)
  36. 12 message.scope_data.nil?
  37. end
  38. end
  39. 1 module NilCodeData
  40. 1 def validate_code_data(message)
  41. 11 message.code_data.nil?
  42. end
  43. end
  44. end
  45. end
  46. end
  47. end

lib/tabulard/row_processor.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "row_processor_result"
  3. 1 require_relative "row_value_builder"
  4. 1 module Tabulard
  5. 1 class RowProcessor
  6. 1 def initialize(headers:, messenger:)
  7. 6 @headers = headers
  8. 6 @messenger = messenger
  9. end
  10. 1 def call(row)
  11. 16 messenger = @messenger.dup
  12. 16 builder = RowValueBuilder.new(messenger)
  13. 16 messenger.scope_row!(row.row) do
  14. 16 @headers.each do |header|
  15. 63 cell = row.value[header.row_value_index]
  16. 63 messenger.scope_col!(cell.col) do
  17. 63 builder.add(header.column, cell.value)
  18. end
  19. end
  20. end
  21. 16 build_result(row, builder, messenger)
  22. end
  23. 1 private
  24. 1 def build_result(row, builder, messenger)
  25. 16 RowProcessorResult.new(
  26. row: row.row,
  27. result: builder.result,
  28. messages: messenger.messages
  29. )
  30. end
  31. end
  32. end

lib/tabulard/row_processor_result.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 class RowProcessorResult
  4. 1 def initialize(row:, result:, messages: [])
  5. 23 @row = row
  6. 23 @result = result
  7. 23 @messages = messages
  8. end
  9. 1 attr_reader :row, :result, :messages
  10. 1 def ==(other)
  11. 3 other.is_a?(self.class) &&
  12. row == other.row &&
  13. result == other.result &&
  14. messages == other.messages
  15. end
  16. end
  17. end

lib/tabulard/row_value_builder.rb

100.0% lines covered

100.0% branches covered

30 relevant lines. 30 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "set"
  3. 1 require_relative "utils/monadic_result"
  4. 1 module Tabulard
  5. 1 class RowValueBuilder
  6. 1 include Utils::MonadicResult
  7. 1 def initialize(messenger)
  8. 21 @messenger = messenger
  9. 21 @data = {}
  10. 21 @composites = Set.new
  11. 21 @failure = false
  12. end
  13. 1 def add(column, value)
  14. 68 key = column.key
  15. 68 type = column.type
  16. 68 index = column.index
  17. 68 result = type.scalar(index, value, @messenger)
  18. 68 result.bind do |scalar|
  19. 61 then: 44 if type.composite?
  20. 44 @composites << [key, type]
  21. 44 @data[key] ||= []
  22. 44 @data[key][index] = scalar
  23. else: 17 else
  24. 17 @data[key] = scalar
  25. end
  26. end
  27. 75 result.or { @failure = true }
  28. 68 result
  29. end
  30. 1 def result
  31. 21 then: 7 else: 14 return Failure() if @failure
  32. 14 Do() do
  33. 14 @composites.each do |key, type|
  34. 13 value = type.composite(@data[key], @messenger).unwrap
  35. 12 @data[key] = value
  36. end
  37. 13 Success(@data)
  38. end
  39. end
  40. end
  41. end

lib/tabulard/specification.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 class Specification
  4. 1 def initialize(columns:, ignore_unspecified_columns: false)
  5. 13 @columns = columns
  6. 13 @ignore_unspecified_columns = ignore_unspecified_columns
  7. end
  8. 1 def get(header)
  9. 42 then: 1 else: 41 return if header.nil?
  10. 41 @columns.find do |column|
  11. 144 column.header_pattern.match?(header)
  12. end
  13. end
  14. 1 def required_columns
  15. 9 @columns.select(&:required?)
  16. end
  17. 1 def ignore_unspecified_columns?
  18. 6 @ignore_unspecified_columns
  19. end
  20. end
  21. end

lib/tabulard/table.rb

100.0% lines covered

100.0% branches covered

71 relevant lines. 71 lines covered and 0 lines missed.
11 total branches, 11 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "table/col_converter"
  3. 1 require_relative "errors/error"
  4. 1 require_relative "messaging"
  5. 1 require_relative "utils/monadic_result"
  6. 1 module Tabulard
  7. 1 module Table
  8. 1 def self.included(mod)
  9. 33 mod.extend(ClassMethods)
  10. end
  11. 1 def self.col2int(...)
  12. 126 COL_CONVERTER.col2int(...)
  13. end
  14. 1 def self.int2col(...)
  15. 621 COL_CONVERTER.int2col(...)
  16. end
  17. 1 module ClassMethods
  18. 1 def open(*args, **opts)
  19. 20 table = new(*args, **opts)
  20. 17 else: 16 then: 1 return Utils::MonadicResult::Success.new(table) unless block_given?
  21. begin
  22. 16 yield table
  23. ensure
  24. 16 table.close
  25. end
  26. rescue InputError
  27. 3 Utils::MonadicResult::Failure.new
  28. end
  29. end
  30. 1 class Error < Errors::Error
  31. end
  32. 1 class ClosureError < Error
  33. end
  34. 1 class TooFewHeaders < Error
  35. end
  36. 1 class TooManyHeaders < Error
  37. end
  38. 1 class InputError < Error
  39. end
  40. 1 Message = Messaging::MessageVariant
  41. 1 class Header
  42. 1 def initialize(col:, value:)
  43. 199 @col = col
  44. 199 @value = value
  45. end
  46. 1 attr_reader :col, :value
  47. 1 def ==(other)
  48. 65 other.is_a?(self.class) && col == other.col && value == other.value
  49. end
  50. 1 def row_value_index
  51. 60 Table.col2int(col) - 1
  52. end
  53. end
  54. 1 class Row
  55. 1 def initialize(row:, value:)
  56. 97 @row = row
  57. 97 @value = value
  58. end
  59. 1 attr_reader :row, :value
  60. 1 def ==(other)
  61. 37 other.is_a?(self.class) && row == other.row && value == other.value
  62. end
  63. end
  64. 1 class Cell
  65. 1 def initialize(row:, col:, value:)
  66. 414 @row = row
  67. 414 @col = col
  68. 414 @value = value
  69. end
  70. 1 attr_reader :row, :col, :value
  71. 1 def ==(other)
  72. 157 other.is_a?(self.class) && row == other.row && col == other.col && value == other.value
  73. end
  74. end
  75. 1 def initialize(messenger: Messaging::Messenger.new)
  76. 90 @messenger = messenger
  77. 90 @closed = false
  78. end
  79. 1 attr_reader :messenger
  80. 1 def each_header
  81. 1 raise NoMethodError, "You must implement #{self.class}#each_header => self"
  82. end
  83. 1 def each_row
  84. 1 raise NoMethodError, "You must implement #{self.class}#each_row => self"
  85. end
  86. 1 def close
  87. 105 then: 19 else: 86 return if closed?
  88. 86 then: 27 else: 59 yield if block_given?
  89. 721 instance_variables.each { |ivar| remove_instance_variable(ivar) }
  90. 86 @closed = true
  91. nil
  92. end
  93. 1 def closed?
  94. 200 @closed == true
  95. end
  96. 1 private
  97. 1 def raise_if_closed
  98. 93 then: 12 else: 81 raise ClosureError if closed?
  99. end
  100. 1 def ensure_compatible_size(row_size, headers_size)
  101. 12 else: 6 case row_size <=> headers_size
  102. when: 3 when -1
  103. 3 raise TooManyHeaders, "Expected #{row_size} headers, got: #{headers_size}"
  104. when: 3 when 1
  105. 3 raise TooFewHeaders, "Expected #{row_size} headers, got: #{headers_size}"
  106. end
  107. end
  108. end
  109. end

lib/tabulard/table/col_converter.rb

100.0% lines covered

100.0% branches covered

31 relevant lines. 31 lines covered and 0 lines missed.
7 total branches, 7 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 module Table
  4. 1 class ColConverter
  5. 1 CHARSET = ("A".."Z").to_a.freeze
  6. 1 CHARSET_SIZE = CHARSET.size
  7. 1 CHAR_TO_INT = CHARSET.map.with_index(1).to_h.freeze
  8. 1 INT_TO_CHAR = CHAR_TO_INT.invert.freeze
  9. 1 def col2int(col)
  10. 126 else: 124 then: 2 raise ArgumentError unless col.is_a?(String) && !col.empty?
  11. 124 int = 0
  12. 124 col.each_char.reverse_each.with_index do |char, pow|
  13. 137 int += char2int(char) * (CHARSET_SIZE**pow)
  14. end
  15. 122 int
  16. end
  17. 1 def int2col(int)
  18. 621 else: 617 then: 4 raise ArgumentError unless int.is_a?(Integer) && int.positive?
  19. 617 col = +""
  20. 617 body: 630 until int.zero?
  21. 630 int, char_int = int.divmod(CHARSET_SIZE)
  22. 630 then: 7 else: 623 if char_int.zero?
  23. 7 int -= 1
  24. 7 char_int = CHARSET_SIZE
  25. end
  26. 630 col << int2char(char_int)
  27. end
  28. 617 col.reverse!
  29. 617 col.freeze
  30. end
  31. 1 private
  32. 1 def char2int(char)
  33. 137 CHAR_TO_INT[char] || raise(ArgumentError, char.inspect)
  34. end
  35. 1 def int2char(int)
  36. 630 INT_TO_CHAR[int] || raise(ArgumentError, int.inspect)
  37. end
  38. end
  39. 1 private_constant :ColConverter
  40. 1 COL_CONVERTER = ColConverter.new.freeze
  41. end
  42. end

lib/tabulard/table_processor.rb

100.0% lines covered

100.0% branches covered

34 relevant lines. 34 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "adapters"
  3. 1 require_relative "headers"
  4. 1 require_relative "messaging"
  5. 1 require_relative "row_processor"
  6. 1 require_relative "table"
  7. 1 require_relative "table_processor_result"
  8. 1 require_relative "utils/monadic_result"
  9. 1 module Tabulard
  10. 1 class TableProcessor
  11. 1 include Utils::MonadicResult
  12. 1 def initialize(specification)
  13. 14 @specification = specification
  14. end
  15. 1 def call(*args, **opts, &block)
  16. 14 messenger = Messaging::Messenger.new
  17. 14 result = Adapters.open(*args, **opts, messenger: messenger) do |table|
  18. 12 process(table, messenger, &block)
  19. end
  20. 14 handle_result(result, messenger)
  21. end
  22. 1 private
  23. 1 def parse_headers(table, messenger)
  24. 12 headers = Headers.new(specification: @specification, messenger: messenger)
  25. 12 table.each_header do |header|
  26. 44 headers.add(header)
  27. end
  28. 12 headers.result
  29. end
  30. 1 def build_row_processor(table, messenger)
  31. 12 parse_headers(table, messenger).bind do |headers|
  32. 7 row_processor = RowProcessor.new(headers: headers, messenger: messenger)
  33. 7 Success(row_processor)
  34. end
  35. end
  36. 1 def process(table, messenger)
  37. 12 build_row_processor(table, messenger).bind do |row_processor|
  38. 7 table.each_row do |row|
  39. 21 yield row_processor.call(row)
  40. end
  41. 7 Success()
  42. end
  43. end
  44. 1 def handle_result(result, messenger)
  45. 14 TableProcessorResult.new(result: result.discard, messages: messenger.messages)
  46. end
  47. end
  48. end

lib/tabulard/table_processor_result.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 class TableProcessorResult
  4. 1 def initialize(result:, messages: [])
  5. 21 @result = result
  6. 21 @messages = messages
  7. end
  8. 1 attr_reader :result, :messages
  9. 1 def ==(other)
  10. 4 other.is_a?(self.class) &&
  11. result == other.result &&
  12. messages == other.messages
  13. end
  14. end
  15. end

lib/tabulard/template.rb

100.0% lines covered

100.0% branches covered

33 relevant lines. 33 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "set"
  3. 1 require_relative "attribute"
  4. 1 require_relative "specification"
  5. 1 require_relative "errors/spec_error"
  6. 1 module Tabulard
  7. # A {Template} represents the abstract structure of a tabular document.
  8. #
  9. # The main component of the structure is the object obtained by processing a
  10. # row. A template therefore specifies all possible attributes of that object
  11. # as a list of (key, abstract type) pairs.
  12. #
  13. # Each attribute will eventually be compiled into as many concrete columns as
  14. # necessary with the help of a {TemplateConfig config} to produce a
  15. # {Specification specification}.
  16. #
  17. # In other words, a {Template} specifies the structure of the processing
  18. # result (its attributes), whereas a {Specification} specifies the columns
  19. # that may be involved into building the processing result.
  20. #
  21. # {Attribute Attributes} may either be _composite_ (their value is a
  22. # composition of multiple values) or _scalar_ (their value is a single
  23. # value). Scalar attributes will thus produce a single column in the
  24. # specification, and composite attributes will produce as many columns as
  25. # required by the number of scalar values they hold.
  26. 1 class Template
  27. 1 def self.build(attributes:, **kwargs)
  28. 33 attributes = attributes.map { |attribute| Attribute.build(**attribute) }
  29. 11 attributes.freeze
  30. 11 template = new(attributes: attributes, **kwargs)
  31. 11 template.freeze
  32. end
  33. 1 def initialize(attributes:, ignore_unspecified_columns: false)
  34. 15 ensure_attributes_unicity(attributes)
  35. 14 @attributes = attributes
  36. 14 @ignore_unspecified_columns = ignore_unspecified_columns
  37. end
  38. 1 def apply(config)
  39. 9 columns = []
  40. 9 attributes.each do |attribute|
  41. 18 attribute.each_column(config) do |column|
  42. 54 columns << column.freeze
  43. end
  44. end
  45. 9 specification = Specification.new(
  46. columns: columns.freeze,
  47. ignore_unspecified_columns: ignore_unspecified_columns
  48. )
  49. 9 specification.freeze
  50. end
  51. 1 def ==(other)
  52. 2 other.is_a?(self.class) &&
  53. attributes == other.attributes &&
  54. ignore_unspecified_columns == other.ignore_unspecified_columns
  55. end
  56. 1 protected
  57. 1 attr_reader :attributes, :ignore_unspecified_columns
  58. 1 private
  59. 1 def ensure_attributes_unicity(attributes)
  60. 15 keys = Set.new
  61. 15 duplicate = attributes.find do |attribute|
  62. 30 !keys.add?(attribute.key)
  63. end
  64. 15 else: 1 then: 14 return unless duplicate
  65. 1 raise Errors::SpecError, "Duplicated key: #{duplicate.key.inspect}"
  66. end
  67. end
  68. end

lib/tabulard/template_config.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "types/container"
  3. 1 module Tabulard
  4. 1 class TemplateConfig
  5. 1 def initialize(types: Types::Container.new)
  6. 14 @types = types
  7. end
  8. 1 attr_reader :types
  9. # Given an attribute key and a possibily-nil column index, return the header and header pattern
  10. # for that column.
  11. #
  12. # The return value should be an array with two items:
  13. #
  14. # 1. The first item is the header, as a String.
  15. # 2. The second item is the header pattern, and should respond to `#match?` with a boolean
  16. # value. Instances of Regexp will obviously do, but the requirement is really about the
  17. # `#match?` method.
  18. #
  19. # @param key [Symbol, String]
  20. # @param index [Integer, nil]
  21. # @return [Array(String, #match?)]
  22. 1 def header(key, index)
  23. 59 header = key.to_s.capitalize
  24. 59 then: 46 else: 13 header = "#{header} #{index + 1}" if index
  25. 59 pattern = /^#{Regexp.escape(header)}$/i
  26. 59 [header, pattern]
  27. end
  28. end
  29. end

lib/tabulard/types/cast.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 module Types
  4. # @private
  5. 1 module Cast
  6. 1 def ==(other)
  7. 3 other.is_a?(self.class) && other.config == config
  8. end
  9. 1 protected
  10. 1 def config
  11. 6 instance_variables.each_with_object({}) do |ivar, acc|
  12. 10 acc[ivar] = instance_variable_get(ivar)
  13. end
  14. end
  15. end
  16. end
  17. end

lib/tabulard/types/cast_chain.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../utils/monadic_result"
  3. 1 module Tabulard
  4. 1 module Types
  5. 1 class CastChain
  6. 1 include Utils::MonadicResult
  7. 1 def initialize(casts = [])
  8. 73 @casts = casts
  9. end
  10. 1 attr_reader :casts
  11. 1 def prepend(cast)
  12. 1 @casts.unshift(cast)
  13. 1 self
  14. end
  15. 1 def append(cast)
  16. 102 @casts.push(cast)
  17. 102 self
  18. end
  19. 1 def freeze
  20. 17 @casts.each(&:freeze)
  21. 17 @casts.freeze
  22. 17 super
  23. end
  24. 1 def call(value, messenger)
  25. 75 failure = catch(:failure) do
  26. 75 success = catch(:success) do
  27. 75 @casts.reduce(value) do |prev_value, cast|
  28. 141 cast.call(prev_value, messenger)
  29. end
  30. end
  31. 68 return Success(success)
  32. end
  33. 7 then: 6 else: 1 messenger.error(failure) if failure
  34. 7 Failure()
  35. end
  36. end
  37. end
  38. end

lib/tabulard/types/composites/array.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "composite"
  3. 1 require_relative "../../messaging/messages/must_be_array"
  4. 1 module Tabulard
  5. 1 module Types
  6. 1 module Composites
  7. 1 Array = Composite.cast do |value, _messenger|
  8. 12 else: 11 then: 1 throw :failure, Messaging::Messages::MustBeArray.new unless value.is_a?(::Array)
  9. 11 value
  10. end
  11. end
  12. end
  13. end

lib/tabulard/types/composites/array_compact.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "array"
  3. 1 module Tabulard
  4. 1 module Types
  5. 1 module Composites
  6. 1 ArrayCompact = Array.cast do |value, _messenger|
  7. 1 value.compact
  8. end
  9. end
  10. end
  11. end

lib/tabulard/types/composites/composite.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../../errors/type_error"
  3. 1 require_relative "../type"
  4. 1 module Tabulard
  5. 1 module Types
  6. 1 module Composites
  7. 1 class Composite < Type
  8. 1 def initialize(types, **opts)
  9. 21 super(**opts)
  10. 21 @types = types
  11. end
  12. 1 def composite?
  13. 43 true
  14. end
  15. 1 def scalar(index, value, messenger)
  16. 51 then: 48 if (type = @types[index])
  17. 48 type.scalar(nil, value, messenger)
  18. else: 3 else
  19. 3 raise Errors::TypeError, "Invalid index: #{index.inspect}"
  20. end
  21. end
  22. 1 alias composite cast
  23. end
  24. end
  25. end
  26. end

lib/tabulard/types/container.rb

100.0% lines covered

100.0% branches covered

45 relevant lines. 45 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../errors/type_error"
  3. 1 require_relative "scalars/scalar"
  4. 1 require_relative "scalars/string"
  5. 1 require_relative "scalars/email"
  6. 1 require_relative "scalars/boolsy"
  7. 1 require_relative "scalars/date_string"
  8. 1 require_relative "composites/array"
  9. 1 require_relative "composites/array_compact"
  10. 1 module Tabulard
  11. 1 module Types
  12. 1 class Container
  13. 1 scalar = Scalars::Scalar.new!
  14. 1 string = Scalars::String.new!
  15. 1 email = Scalars::Email.new!
  16. 1 boolsy = Scalars::Boolsy.new!
  17. 1 date_string = Scalars::DateString.new!
  18. DEFAULTS = {
  19. 1 scalars: {
  20. 29 scalar: -> { scalar },
  21. 13 string: -> { string },
  22. 13 email: -> { email },
  23. 2 boolsy: -> { boolsy },
  24. 2 date_string: -> { date_string },
  25. }.freeze,
  26. composites: {
  27. 10 array: ->(types) { Composites::Array.new!(types) },
  28. 1 array_compact: ->(types) { Composites::ArrayCompact.new!(types) },
  29. }.freeze,
  30. }.freeze
  31. 1 def initialize(scalars: nil, composites: nil, defaults: DEFAULTS)
  32. @scalars =
  33. 28 then: 11 else: 17 (scalars ? defaults[:scalars].merge(scalars) : defaults[:scalars]).freeze
  34. @composites =
  35. 28 then: 2 else: 26 (composites ? defaults[:composites].merge(composites) : defaults[:composites]).freeze
  36. end
  37. 1 def scalars
  38. 3 @scalars.keys
  39. end
  40. 1 def composites
  41. 3 @composites.keys
  42. end
  43. 1 def scalar(scalar_name)
  44. 76 builder = fetch_scalar_builder(scalar_name)
  45. 74 builder.call
  46. end
  47. 1 def composite(composite_name, scalar_names)
  48. 16 builder = fetch_composite_builder(composite_name)
  49. 68 scalars = scalar_names.map { |scalar_name| scalar(scalar_name) }
  50. 14 builder.call(scalars)
  51. end
  52. 1 private
  53. 1 def fetch_scalar_builder(type)
  54. 76 @scalars.fetch(type) do
  55. 2 raise Errors::TypeError, "Invalid scalar type: #{type.inspect}"
  56. end
  57. end
  58. 1 def fetch_composite_builder(type)
  59. 16 @composites.fetch(type) do
  60. 1 raise Errors::TypeError, "Invalid composite type: #{type.inspect}"
  61. end
  62. end
  63. end
  64. end
  65. end

lib/tabulard/types/scalars/boolsy.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "scalar"
  3. 1 require_relative "boolsy_cast"
  4. 1 module Tabulard
  5. 1 module Types
  6. 1 module Scalars
  7. 1 Boolsy = Scalar.cast(BoolsyCast)
  8. end
  9. end
  10. end

lib/tabulard/types/scalars/boolsy_cast.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../../messaging/messages/must_be_boolsy"
  3. 1 require_relative "../cast"
  4. 1 module Tabulard
  5. 1 module Types
  6. 1 module Scalars
  7. 1 class BoolsyCast
  8. 1 include Cast
  9. 1 TRUTHY = [].freeze
  10. 1 FALSY = [].freeze
  11. 1 private_constant :TRUTHY, :FALSY
  12. 1 def initialize(truthy: TRUTHY, falsy: FALSY, **)
  13. 12 @truthy = truthy
  14. 12 @falsy = falsy
  15. end
  16. 1 def call(value, _messenger)
  17. 3 then: 1 if @truthy.include?(value)
  18. 1 else: 2 true
  19. 2 then: 1 elsif @falsy.include?(value)
  20. 1 false
  21. else: 1 else
  22. 1 throw :failure, Messaging::Messages::MustBeBoolsy.new(
  23. code_data: { value: value.inspect }
  24. )
  25. end
  26. end
  27. end
  28. end
  29. end
  30. end

lib/tabulard/types/scalars/date_string.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "scalar"
  3. 1 require_relative "date_string_cast"
  4. 1 module Tabulard
  5. 1 module Types
  6. 1 module Scalars
  7. 1 DateString = Scalar.cast(DateStringCast)
  8. end
  9. end
  10. end

lib/tabulard/types/scalars/date_string_cast.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
7 total branches, 7 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "date"
  3. 1 require_relative "../../messaging/messages/must_be_date"
  4. 1 require_relative "../cast"
  5. 1 module Tabulard
  6. 1 module Types
  7. 1 module Scalars
  8. 1 class DateStringCast
  9. 1 include Cast
  10. 1 DATE_FMT = "%Y-%m-%d"
  11. 1 private_constant :DATE_FMT
  12. 1 def initialize(date_fmt: DATE_FMT, accept_date: true, **)
  13. 15 @date_fmt = date_fmt
  14. 15 @accept_date = accept_date
  15. end
  16. 1 def call(value, _messenger)
  17. 6 else: 1 case value
  18. when: 2 when ::Date
  19. 2 then: 1 else: 1 return value if @accept_date
  20. when: 3 when ::String
  21. 3 date = parse_date_string(value)
  22. 3 then: 1 else: 2 return date if date
  23. end
  24. 4 throw :failure, Messaging::Messages::MustBeDate.new(code_data: { format: @date_fmt })
  25. end
  26. 1 private
  27. 1 def parse_date_string(value)
  28. 3 ::Date.strptime(value, @date_fmt)
  29. rescue ::TypeError, ::Date::Error
  30. 2 nil
  31. end
  32. end
  33. end
  34. end
  35. end

lib/tabulard/types/scalars/email.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "string"
  3. 1 require_relative "email_cast"
  4. 1 module Tabulard
  5. 1 module Types
  6. 1 module Scalars
  7. 1 Email = String.cast(EmailCast)
  8. end
  9. end
  10. end

lib/tabulard/types/scalars/email_cast.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "uri"
  3. 1 require_relative "../../messaging/messages/must_be_email"
  4. 1 require_relative "../cast"
  5. 1 module Tabulard
  6. 1 module Types
  7. 1 module Scalars
  8. 1 class EmailCast
  9. 1 include Cast
  10. 1 EMAIL_REGEXP = ::URI::MailTo::EMAIL_REGEXP
  11. 1 private_constant :EMAIL_REGEXP
  12. 1 def initialize(email_matcher: EMAIL_REGEXP, **)
  13. 11 @email_matcher = email_matcher
  14. end
  15. 1 def call(value, _messenger)
  16. 17 then: 11 else: 6 return value if @email_matcher.match?(value)
  17. 6 throw :failure, Messaging::Messages::MustBeEmail.new(code_data: { value: value.inspect })
  18. end
  19. end
  20. end
  21. end
  22. end

lib/tabulard/types/scalars/scalar.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../../errors/type_error"
  3. 1 require_relative "../type"
  4. 1 require_relative "scalar_cast"
  5. 1 module Tabulard
  6. 1 module Types
  7. 1 module Scalars
  8. 1 class Scalar < Type
  9. 1 self.cast_classes += [ScalarCast]
  10. 1 def composite?
  11. 20 false
  12. end
  13. 1 def composite(_value, _messenger)
  14. 5 raise Errors::TypeError, "A scalar type cannot act as a composite"
  15. end
  16. 1 def scalar(index, value, messenger)
  17. 70 else: 65 then: 5 raise Errors::TypeError, "A scalar type cannot be indexed" unless index.nil?
  18. 65 cast_chain.call(value, messenger)
  19. end
  20. end
  21. end
  22. end
  23. end

lib/tabulard/types/scalars/scalar_cast.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "../../utils/cell_string_cleaner"
  3. 1 require_relative "../../messaging/messages/must_exist"
  4. 1 require_relative "../../messaging/messages/cleaned_string"
  5. 1 require_relative "../cast"
  6. 1 module Tabulard
  7. 1 module Types
  8. 1 module Scalars
  9. 1 class ScalarCast
  10. 1 include Cast
  11. 1 def initialize(nullable: true, clean_string: true, **)
  12. 43 @nullable = nullable
  13. 43 @clean_string = clean_string
  14. end
  15. 1 def call(value, messenger)
  16. 67 handle_nil(value)
  17. 50 handle_garbage(value, messenger)
  18. end
  19. 1 private
  20. 1 def handle_nil(value)
  21. 67 else: 17 then: 50 return unless value.nil?
  22. 17 then: 16 if @nullable
  23. 16 throw :success, nil
  24. else: 1 else
  25. 1 throw :failure, Messaging::Messages::MustExist.new
  26. end
  27. end
  28. 1 def handle_garbage(value, messenger)
  29. 50 else: 32 then: 18 return value unless @clean_string && value.is_a?(::String)
  30. 32 clean_string = Utils::CellStringCleaner.call(value)
  31. 32 then: 1 else: 31 messenger.warn(Messaging::Messages::CleanedString.new) if clean_string != value
  32. 32 clean_string
  33. end
  34. end
  35. end
  36. end
  37. end

lib/tabulard/types/scalars/string.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "scalar"
  3. 1 require_relative "../../messaging/messages/must_be_string"
  4. 1 module Tabulard
  5. 1 module Types
  6. 1 module Scalars
  7. 1 String = Scalar.cast do |value, _messenger|
  8. # value.to_s, because we want the native, underlying string when value
  9. # is an instance of a String subclass
  10. 32 then: 31 else: 1 next value.to_s if value.is_a?(::String)
  11. 1 throw :failure, Messaging::Messages::MustBeString.new
  12. end
  13. end
  14. end
  15. end

lib/tabulard/types/type.rb

100.0% lines covered

100.0% branches covered

53 relevant lines. 53 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "cast_chain"
  3. 1 module Tabulard
  4. 1 module Types
  5. 1 class Type
  6. 1 class << self
  7. 1 def all(&block)
  8. 6 else: 3 then: 3 return enum_for(:all) unless block
  9. 3 ObjectSpace.each_object(singleton_class, &block)
  10. nil
  11. end
  12. 1 def cast_classes
  13. 186 then: 140 else: 46 defined?(@cast_classes) ? @cast_classes : superclass.cast_classes
  14. end
  15. 1 attr_writer :cast_classes
  16. 1 def cast(cast_class = nil, &cast_block)
  17. 21 then: 1 if cast_class && cast_block
  18. 1 else: 20 raise ArgumentError, "Expected either a Class or a block, got both"
  19. 20 then: 1 else: 19 elsif !(cast_class || cast_block)
  20. 1 raise ArgumentError, "Expected either a Class or a block, got none"
  21. end
  22. 19 type = Class.new(self)
  23. 19 type.cast_classes += [cast_class || SimpleCast.new(cast_block)]
  24. 19 type
  25. end
  26. 1 def freeze
  27. 8 else: 5 then: 3 @cast_classes = cast_classes.dup unless defined?(@cast_classes)
  28. 8 @cast_classes.freeze
  29. 8 super
  30. end
  31. 1 def new!(...)
  32. 15 new(...).freeze
  33. end
  34. end
  35. 1 self.cast_classes = []
  36. 1 def initialize(**opts)
  37. 63 @cast_chain = CastChain.new
  38. 63 self.class.cast_classes.each do |cast_class|
  39. 101 @cast_chain.append(cast_class.new(**opts))
  40. end
  41. end
  42. # @private
  43. 1 attr_reader :cast_chain
  44. 1 def cast(...)
  45. 11 @cast_chain.call(...)
  46. end
  47. 1 def scalar?
  48. 1 raise NoMethodError, "You must implement this method in a subclass"
  49. end
  50. 1 def composite?
  51. 1 raise NoMethodError, "You must implement this method in a subclass"
  52. end
  53. 1 def scalar(_index, _value, _messenger)
  54. 1 raise NoMethodError, "You must implement this method in a subclass"
  55. end
  56. 1 def composite(_value, _messenger)
  57. 1 raise NoMethodError, "You must implement this method in a subclass"
  58. end
  59. 1 def freeze
  60. 16 @cast_chain.freeze
  61. 16 super
  62. end
  63. # @private
  64. 1 class SimpleCast
  65. 1 def initialize(cast)
  66. 16 @cast = cast
  67. end
  68. 1 def new(**)
  69. 61 @cast
  70. end
  71. 1 def ==(other)
  72. 1 other.is_a?(self.class) && other.cast == cast
  73. end
  74. 1 protected
  75. 1 attr_reader :cast
  76. end
  77. end
  78. end
  79. end

lib/tabulard/utils/cell_string_cleaner.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 module Utils
  4. 1 class CellStringCleaner
  5. 1 garbage = "(?:[^[:print:]]|[[:space:]])+"
  6. 1 GARBAGE_PREFIX = /\A#{garbage}/
  7. 1 GARBAGE_SUFFIX = /#{garbage}\Z/
  8. 1 private_constant :GARBAGE_PREFIX, :GARBAGE_SUFFIX
  9. 1 def self.call(...)
  10. 36 DEFAULT.call(...)
  11. end
  12. 1 def call(value)
  13. 36 value = value.dup
  14. # TODO: benchmarks
  15. 36 value.sub!(GARBAGE_PREFIX, "")
  16. 36 value.sub!(GARBAGE_SUFFIX, "")
  17. 36 value
  18. end
  19. 1 DEFAULT = new.freeze
  20. 1 private_constant :DEFAULT
  21. end
  22. end
  23. end

lib/tabulard/utils/monadic_result.rb

100.0% lines covered

100.0% branches covered

82 relevant lines. 82 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tabulard
  3. 1 module Utils
  4. 1 module MonadicResult
  5. # {Unit} is a singleton, and is used when there is no other meaningful
  6. # value that could be returned.
  7. #
  8. # It allows the {Result} implementation to distinguish between *a null
  9. # value* (i.e. `nil`) and *the lack of a value*, to provide adequate
  10. # behavior in each case.
  11. #
  12. # The {Result} API should not expose {Unit} directly to its consumers.
  13. #
  14. # @see https://en.wikipedia.org/wiki/Unit_type
  15. 1 Unit = Object.new
  16. 1 def Unit.to_s
  17. 2 "Unit"
  18. end
  19. 1 def Unit.inspect
  20. 1 "Unit"
  21. end
  22. 1 Unit.freeze
  23. 1 DO_TOKEN = :MonadicResultDo
  24. 1 private_constant :DO_TOKEN
  25. 1 module Result
  26. 1 UnwrapError = Class.new(StandardError)
  27. 1 VariantError = Class.new(UnwrapError)
  28. 1 ValueError = Class.new(UnwrapError)
  29. 1 def initialize(value = Unit)
  30. 253 @wrapped = value
  31. end
  32. 1 def empty?
  33. 140 wrapped == Unit
  34. end
  35. 1 def ==(other)
  36. 53 other.is_a?(self.class) && other.wrapped == wrapped
  37. end
  38. 1 def inspect
  39. 4 then: 2 if empty?
  40. 2 "#{variant}()"
  41. else: 2 else
  42. 2 "#{variant}(#{wrapped.inspect})"
  43. end
  44. end
  45. 1 alias to_s inspect
  46. 1 def discard
  47. 18 then: 16 else: 2 empty? ? self : self.class.new
  48. end
  49. 1 protected
  50. 1 attr_reader :wrapped
  51. 1 private
  52. 1 def value
  53. 4 then: 2 else: 2 raise ValueError, "There is no value within the result" if empty?
  54. 2 wrapped
  55. end
  56. 1 def value?
  57. 18 else: 1 then: 17 wrapped unless empty?
  58. end
  59. 1 def open
  60. 90 then: 11 if empty?
  61. 11 yield
  62. else: 79 else
  63. 79 yield wrapped
  64. end
  65. end
  66. end
  67. 1 class Success
  68. 1 include Result
  69. 1 def success?
  70. 3 true
  71. end
  72. 1 def failure?
  73. 1 false
  74. end
  75. 1 def success
  76. 2 value
  77. end
  78. 1 def failure
  79. 1 raise VariantError, "Not a Failure"
  80. end
  81. 1 def unwrap
  82. 18 value?
  83. end
  84. 1 alias bind open
  85. 1 public :bind
  86. 1 alias or itself
  87. 1 private
  88. 1 def variant
  89. 2 "Success"
  90. end
  91. end
  92. 1 class Failure
  93. 1 include Result
  94. 1 def success?
  95. 1 false
  96. end
  97. 1 def failure?
  98. 2 true
  99. end
  100. 1 def success
  101. 1 raise VariantError, "Not a Success"
  102. end
  103. 1 def failure
  104. 2 value
  105. end
  106. 1 def unwrap
  107. 5 throw DO_TOKEN, self
  108. end
  109. 1 alias bind itself
  110. 1 alias or open
  111. 1 public :or
  112. 1 private
  113. 1 def variant
  114. 2 "Failure"
  115. end
  116. end
  117. # rubocop:disable Naming/MethodName
  118. 1 def Success(...)
  119. 140 Success.new(...)
  120. end
  121. 1 def Failure(...)
  122. 49 Failure.new(...)
  123. end
  124. 1 def Do(&)
  125. 18 catch(DO_TOKEN, &)
  126. end
  127. # rubocop:enable Naming/MethodName
  128. end
  129. end
  130. end

spec/support/fixtures.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 mod = Module.new do
  3. 1 def fixtures_path
  4. 32 @fixtures_path ||= File.expand_path("./fixtures", __dir__)
  5. end
  6. 1 def fixture_path(path)
  7. 32 File.join(fixtures_path, path)
  8. end
  9. end
  10. 1 RSpec.configure do |config|
  11. 1 config.include(mod)
  12. end

spec/support/monadic_result.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/utils/monadic_result"
  3. 1 RSpec.configure do |config|
  4. 1 config.include(Tabulard::Utils::MonadicResult, monadic_result: true)
  5. end

spec/support/shared/cast_class.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 RSpec.shared_examples "cast_class" do
  3. 15 else: 3 then: 4 subject(:cast_class) { described_class } unless method_defined?(:cast_class) # rubocop:disable RSpec/LeadingSubject
  4. 7 let(:cast) do
  5. 12 cast_class.new
  6. end
  7. 7 describe "#initialize" do
  8. 7 it "tolerates any kwargs" do
  9. 7 expect do
  10. 7 cast_class.new(foo: double, qoifzj: double)
  11. end.not_to raise_error
  12. end
  13. end
  14. 7 describe "#call" do
  15. 7 it "has the right cast signature" do
  16. 7 expect(cast).to respond_to(:call).with(2).arguments
  17. end
  18. end
  19. end

spec/support/shared/composite_type.rb

100.0% lines covered

100.0% branches covered

33 relevant lines. 33 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/type"
  3. 1 require "tabulard/types/scalars/scalar"
  4. 1 RSpec.shared_examples "composite_type" do
  5. 3 subject(:type) do
  6. 12 described_class.new(scalars)
  7. end
  8. 15 let(:scalars) { instance_double(Array) }
  9. 12 let(:value) { double }
  10. 12 let(:messenger) { double }
  11. 3 it "is a type" do
  12. 3 expect(described_class.ancestors).to include(Tabulard::Types::Type)
  13. end
  14. 3 describe "#composite?" do
  15. 3 it "is true" do
  16. 3 expect(subject).to be_composite
  17. end
  18. end
  19. 3 describe "#composite" do
  20. 3 it "is an alias to #cast" do
  21. 3 expect(subject.method(:composite)).to eq(subject.method(:cast))
  22. end
  23. end
  24. 3 describe "#scalar" do
  25. 9 let(:type_index) { double }
  26. 3 def stub_scalar_index(type = double)
  27. 6 allow(scalars).to receive(:[]).with(type_index).and_return(type)
  28. 6 type
  29. end
  30. 3 context "when the index refers to a scalar type" do
  31. 6 let(:scalar_type) { instance_double(Tabulard::Types::Scalars::Scalar) }
  32. 3 before do
  33. 3 stub_scalar_index(scalar_type)
  34. end
  35. 3 it "casts the value to the scalar type" do
  36. 3 expect(scalar_type).to(
  37. receive(:scalar).with(nil, value, messenger).and_return(casted_value = double)
  38. )
  39. 3 expect(subject.scalar(type_index, value, messenger)).to be(casted_value)
  40. end
  41. end
  42. 3 context "when the index doesn't refer to a scalar type" do
  43. 3 before do
  44. 3 stub_scalar_index(nil)
  45. end
  46. 3 it "raises an error" do
  47. 6 expect { subject.scalar(type_index, value, messenger) }.to raise_error(
  48. Tabulard::Errors::TypeError,
  49. "Invalid index: #{type_index.inspect}"
  50. )
  51. end
  52. end
  53. end
  54. end

spec/support/shared/scalar_type.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/type"
  3. 1 RSpec.shared_examples "scalar_type" do
  4. 22 let(:value) { double }
  5. 22 let(:messenger) { double }
  6. 5 it "is a type" do
  7. 5 expect(described_class.ancestors).to include(Tabulard::Types::Type)
  8. end
  9. 5 describe "#composite?" do
  10. 5 it "is false" do
  11. 5 expect(subject).not_to be_composite
  12. end
  13. end
  14. 5 describe "#composite" do
  15. 5 it "fails" do
  16. 10 expect { subject.composite(value, messenger) }.to raise_error(
  17. Tabulard::Errors::TypeError, "A scalar type cannot act as a composite"
  18. )
  19. end
  20. end
  21. 5 describe "#scalar" do
  22. 5 context "when the value is not indexed" do
  23. 5 it "delegates the task to the cast chain" do
  24. 5 result = double
  25. 5 expect(subject.cast_chain).to receive(:call).with(value, messenger).and_return(result)
  26. 5 expect(subject.scalar(nil, value, messenger)).to be(result)
  27. end
  28. end
  29. 5 context "when the value is indexed" do
  30. 5 it "fails" do
  31. 5 index = double
  32. 10 expect { subject.scalar(index, value, messenger) }.to raise_error(
  33. Tabulard::Errors::TypeError, "A scalar type cannot be indexed"
  34. )
  35. end
  36. end
  37. end
  38. end

spec/support/shared/table/empty.rb

100.0% lines covered

100.0% branches covered

43 relevant lines. 43 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/table"
  3. 1 RSpec.shared_examples "table/empty" do |sized_rows_enum: false|
  4. 3 describe "#each_header" do
  5. 3 context "with a block" do
  6. 3 it "doesn't yield" do
  7. 6 expect { |b| table.each_header(&b) }.not_to yield_control
  8. end
  9. 3 it "returns self" do
  10. 3 expect(table.each_header { double }).to be(table)
  11. end
  12. end
  13. 3 context "without a block" do
  14. 3 it "returns an enumerator" do
  15. 3 enum = table.each_header
  16. 3 expect(enum).to be_a(Enumerator)
  17. 3 expect(enum.size).to eq(0)
  18. 3 expect(enum.to_a).to eq([])
  19. end
  20. end
  21. end
  22. 3 describe "#each_row" do
  23. 3 context "with a block" do
  24. 3 it "doesn't yield" do
  25. 6 expect { |b| table.each_row(&b) }.not_to yield_control
  26. end
  27. 3 it "returns self" do
  28. 3 expect(table.each_row { double }).to be(table)
  29. end
  30. end
  31. 3 context "without a block" do
  32. 3 it "returns an enumerator" do
  33. 3 enum = table.each_row
  34. 3 expect(enum).to be_a(Enumerator)
  35. 3 then: 2 else: 1 expect(enum.size).to eq(sized_rows_enum ? 0 : nil)
  36. 3 expect(enum.to_a).to eq([])
  37. end
  38. end
  39. end
  40. 3 describe "#close" do
  41. 3 it "returns nil" do
  42. 3 expect(table.close).to be_nil
  43. end
  44. end
  45. 3 context "when it is closed" do
  46. 9 before { table.close }
  47. 3 it "can't enumerate headers" do
  48. 6 expect { table.each_header }.to raise_error(Tabulard::Table::ClosureError)
  49. end
  50. 3 it "can't enumerate rows" do
  51. 6 expect { table.each_row }.to raise_error(Tabulard::Table::ClosureError)
  52. end
  53. end
  54. 3 context "when headers are customized" do
  55. 3 let(:headers_data) do
  56. 3 %w[foo bar baz]
  57. end
  58. 3 let(:table_opts) do
  59. 3 super().merge(headers: headers_data)
  60. end
  61. 3 it "relies on the custom headers" do
  62. 3 headers = build_headers(headers_data)
  63. 6 expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
  64. end
  65. end
  66. end

spec/support/shared/table/factories.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/table"
  3. 1 RSpec.shared_context "table/factories" do
  4. 3 def build_header(...)
  5. 63 Tabulard::Table::Header.new(...)
  6. end
  7. 3 def build_row(...)
  8. 35 Tabulard::Table::Row.new(...)
  9. end
  10. 3 def build_cell(...)
  11. 155 Tabulard::Table::Cell.new(...)
  12. end
  13. 3 def build_headers(values, col: "A")
  14. 15 first_col_index = Tabulard::Table.col2int(col)
  15. 15 values.map.with_index(first_col_index) do |value, col_index|
  16. 63 build_header(col: Tabulard::Table.int2col(col_index), value: value)
  17. end
  18. end
  19. 3 def build_cells(values, row:, col: "A")
  20. 35 first_col_index = Tabulard::Table.col2int(col)
  21. 35 values.map.with_index(first_col_index) do |value, col_index|
  22. 155 build_cell(row: row, col: Tabulard::Table.int2col(col_index), value: value)
  23. end
  24. end
  25. 3 def build_rows(list_of_values, row: 2, col: "A")
  26. 12 list_of_values.map.with_index(row) do |values, row_index|
  27. 35 value = build_cells(values, row: row_index, col: col)
  28. 35 build_row(row: row_index, value: value)
  29. end
  30. end
  31. end

spec/support/shared/table/filled.rb

100.0% lines covered

100.0% branches covered

64 relevant lines. 64 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/table"
  3. 1 RSpec.shared_examples "table/filled" do |sized_rows_enum: false|
  4. 3 else: 1 then: 2 unless instance_methods.include?(:source_data)
  5. 2 alias_method :source_data, :source
  6. end
  7. 3 describe "#each_header" do
  8. 3 let(:headers) do
  9. 6 build_headers(source_data[0])
  10. end
  11. 3 context "with a block" do
  12. 3 it "yields each header, with its letter-based index" do
  13. 6 expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
  14. end
  15. 3 it "returns self" do
  16. 16 expect(table.each_header { double }).to be(table)
  17. end
  18. end
  19. 3 context "without a block" do
  20. 3 it "returns an enumerator" do
  21. 3 enum = table.each_header
  22. 3 expect(enum).to be_a(Enumerator)
  23. 3 expect(enum.size).to eq(headers.size)
  24. 3 expect(enum.to_a).to eq(headers)
  25. end
  26. end
  27. end
  28. 3 describe "#each_row" do
  29. 3 let(:rows) do
  30. 6 build_rows(source_data[1..])
  31. end
  32. 3 context "with a block" do
  33. 3 it "yields each row, with its integer-based index" do
  34. 6 expect { |b| table.each_row(&b) }.to yield_successive_args(*rows)
  35. end
  36. 3 it "returns self" do
  37. 11 expect(table.each_row { double }).to be(table)
  38. end
  39. end
  40. 3 context "without a block" do
  41. 3 it "returns an enumerator" do
  42. 3 enum = table.each_row
  43. 3 expect(enum).to be_a(Enumerator)
  44. 3 then: 2 else: 1 expect(enum.size).to eq(sized_rows_enum ? rows.size : nil)
  45. 3 expect(enum.to_a).to eq(rows)
  46. end
  47. end
  48. end
  49. 3 describe "#close" do
  50. 3 it "returns nil" do
  51. 3 expect(table.close).to be_nil
  52. end
  53. end
  54. 3 context "when it is closed" do
  55. 9 before { table.close }
  56. 3 it "can't enumerate headers" do
  57. 6 expect { table.each_header }.to raise_error(Tabulard::Table::ClosureError)
  58. end
  59. 3 it "can't enumerate rows" do
  60. 6 expect { table.each_row }.to raise_error(Tabulard::Table::ClosureError)
  61. end
  62. end
  63. 3 context "when headers are customized" do
  64. 15 let(:data_size) { source_data[0].size }
  65. 15 let(:headers_size) { data_size + diff }
  66. 3 let(:headers_data) do
  67. 64 Array.new(headers_size) { |i| "header#{i}" }
  68. end
  69. 3 let(:table_opts) do
  70. 12 super().merge(headers: headers_data)
  71. end
  72. 3 context "when their size is equal to the size of the data" do
  73. 9 let(:diff) { 0 }
  74. 3 it "relies on the custom headers" do
  75. 3 headers = build_headers(headers_data)
  76. 6 expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
  77. end
  78. 3 it "treats all rows as data" do
  79. 3 rows = build_rows(source_data, row: 1)
  80. 6 expect { |b| table.each_row(&b) }.to yield_successive_args(*rows)
  81. end
  82. end
  83. 3 context "when their size is smaller than the size of the data" do
  84. 6 let(:diff) { -1 }
  85. 3 it "fails to initialize", autoclose_table: false do
  86. 6 expect { table }.to raise_error(
  87. Tabulard::Table::TooFewHeaders,
  88. "Expected #{data_size} headers, got: #{headers_size}"
  89. )
  90. end
  91. end
  92. 3 context "when their size is larger than the size of the data" do
  93. 6 let(:diff) { 1 }
  94. 3 it "fails to initialize", autoclose_table: false do
  95. 6 expect { table }.to raise_error(
  96. Tabulard::Table::TooManyHeaders,
  97. "Expected #{data_size} headers, got: #{headers_size}"
  98. )
  99. end
  100. end
  101. end
  102. end

spec/support/tabulard.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging"
  3. 1 Tabulard::Messaging.configure do |config|
  4. 1 config.validate_messages = true
  5. end

spec/tabulard/adapters/bare_spec.rb

100.0% lines covered

100.0% branches covered

47 relevant lines. 47 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/adapters/bare"
  3. 1 require "support/shared/table/factories"
  4. 1 require "support/shared/table/empty"
  5. 1 require "support/shared/table/filled"
  6. 1 RSpec.describe Tabulard::Adapters::Bare do
  7. 1 include_context "table/factories"
  8. 1 let(:table_interface) do
  9. 23 Module.new do
  10. 23 def [](_); end
  11. 23 def size; end
  12. end
  13. end
  14. 1 let(:headers_interface) do
  15. 13 Module.new do
  16. 13 def [](_); end
  17. 13 def size; end
  18. end
  19. end
  20. 1 let(:values_interfaces) do
  21. 13 Module.new do
  22. 13 def [](_); end
  23. end
  24. end
  25. 1 let(:input) do
  26. 23 stub_input(source)
  27. end
  28. 1 let(:table_opts) do
  29. 23 {}
  30. end
  31. 1 let(:table) do
  32. 23 described_class.new(input, **table_opts)
  33. end
  34. 1 def stub_input(source)
  35. 23 input = instance_double(table_interface, size: source.size)
  36. 23 source.each_with_index do |row, row_idx|
  37. 52 input_row = stub_input_row(row, row_idx)
  38. 52 allow(input).to receive(:[]).with(row_idx).and_return(input_row)
  39. end
  40. 23 input
  41. end
  42. 1 def stub_input_row(row, row_idx)
  43. input_row =
  44. 52 then: 13 if row_idx.zero?
  45. 13 instance_double(headers_interface, size: row.size)
  46. else: 39 else
  47. 39 instance_double(values_interfaces)
  48. end
  49. 52 row.each_with_index do |cell, col_idx|
  50. 208 allow(input_row).to receive(:[]).with(col_idx).and_return(cell)
  51. end
  52. end
  53. 1 after do |example|
  54. 23 else: 2 then: 21 table.close unless example.metadata[:autoclose_table] == false
  55. end
  56. 1 context "when the input table is empty" do
  57. 1 let(:source) do
  58. 10 []
  59. end
  60. 1 include_examples "table/empty", sized_rows_enum: true
  61. end
  62. 1 context "when the input table is filled" do
  63. 1 let(:source) do
  64. 13 Array.new(4) do |row|
  65. 52 Array.new(4) do |col|
  66. 208 instance_double(Object, "(#{row},#{col})")
  67. end.freeze
  68. end.freeze
  69. end
  70. 1 include_examples "table/filled", sized_rows_enum: true
  71. end
  72. end

spec/tabulard/adapters/csv/invalid_csv_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/adapters/csv"
  3. 1 RSpec.describe Tabulard::Adapters::Csv::InvalidCSV do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "invalid_csv",
  11. code_data: nil,
  12. scope: "TABLE",
  13. scope_data: nil
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have some code data" do
  27. 1 message.code_data = { foo: :baz }
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have some scope_data" do
  35. 1 message.scope_data = { foo: :baz }
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/adapters/csv_spec.rb

100.0% lines covered

100.0% branches covered

64 relevant lines. 64 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/adapters/csv"
  3. 1 require "support/shared/table/factories"
  4. 1 require "support/shared/table/empty"
  5. 1 require "support/shared/table/filled"
  6. 1 require "csv"
  7. 1 require "stringio"
  8. 1 RSpec.describe Tabulard::Adapters::Csv do
  9. 1 include_context "table/factories"
  10. 1 let(:input) do
  11. 25 stub_input(source)
  12. end
  13. 1 let(:table_opts) do
  14. 28 {}
  15. end
  16. 1 let(:table) do
  17. 29 described_class.new(input, **table_opts)
  18. end
  19. 1 def stub_input(source)
  20. 25 csv = CSV.generate do |csv_io|
  21. 25 source.each do |row|
  22. 52 csv_io << row
  23. end
  24. end
  25. 25 StringIO.new(csv, "r:UTF-8")
  26. end
  27. 1 after do |example|
  28. 28 else: 2 then: 26 table.close unless example.metadata[:autoclose_table] == false
  29. 28 input.close
  30. end
  31. 1 context "when the input table is empty" do
  32. 1 let(:source) do
  33. 10 []
  34. end
  35. 1 include_examples "table/empty"
  36. end
  37. 1 context "when the input table is filled" do
  38. 1 let(:source) do
  39. 13 Array.new(4) do |row|
  40. 52 Array.new(4) do |col|
  41. 208 "(#{row},#{col})"
  42. end.freeze
  43. end.freeze
  44. end
  45. 1 include_examples "table/filled"
  46. end
  47. 1 describe "encodings" do
  48. 1 let(:utf8_path) { fixture_path("csv/utf8.csv") }
  49. 4 let(:latin9_path) { fixture_path("csv/latin9.csv") }
  50. 1 let(:headers_data_utf8) do
  51. 2 [
  52. "Matricule",
  53. "Nom",
  54. "Prénom",
  55. "Email",
  56. "Date de naissance",
  57. "Entrée en entreprise",
  58. "Administrateur",
  59. "Bio",
  60. "Service",
  61. ]
  62. end
  63. 1 let(:headers_data_latin9) do
  64. 10 headers_data_utf8.map { |str| str.encode(Encoding::ISO_8859_15) }
  65. end
  66. 3 let(:table_headers_data) { table.each_header.map(&:value) }
  67. 1 context "when the IO is opened with the correct external encoding" do
  68. 1 let(:input) do
  69. 1 File.new(latin9_path, external_encoding: Encoding::ISO_8859_15)
  70. end
  71. 1 it "does not interfere" do
  72. 1 expect(table_headers_data).to eq(headers_data_latin9)
  73. end
  74. end
  75. 1 context "when the IO is opened with an incorrect external encoding" do
  76. 1 let(:input) do
  77. 1 File.new(latin9_path, external_encoding: Encoding::UTF_8)
  78. end
  79. 1 it "fails" do
  80. 2 expect { table }.to raise_error(described_class::InputError)
  81. end
  82. end
  83. 1 context "when the (correct) external encoding differs from the internal one" do
  84. 1 let(:input) do
  85. 1 File.new(
  86. latin9_path,
  87. external_encoding: Encoding::ISO_8859_15,
  88. internal_encoding: Encoding::UTF_8
  89. )
  90. end
  91. 1 it "does not interfere" do
  92. 1 expect(table_headers_data).to eq(headers_data_utf8)
  93. end
  94. end
  95. end
  96. 1 describe "CSV options" do
  97. 2 let(:source) { [] }
  98. 1 it "requires a specific col_sep and quote_char, and an automatic row_sep" do
  99. 1 expect(CSV).to receive(:new)
  100. .with(input, row_sep: :auto, col_sep: ",", quote_char: '"')
  101. .and_call_original
  102. 1 table
  103. end
  104. end
  105. 1 describe "#close" do
  106. 2 let(:source) { [] }
  107. 1 it "doesn't close the underlying table" do
  108. 2 expect { table.close }.not_to change(input, :closed?).from(false)
  109. end
  110. end
  111. end

spec/tabulard/adapters/xlsx_spec.rb

100.0% lines covered

100.0% branches covered

60 relevant lines. 60 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/adapters/xlsx"
  3. 1 require "support/shared/table/factories"
  4. 1 require "support/shared/table/empty"
  5. 1 require "support/shared/table/filled"
  6. 1 RSpec.describe Tabulard::Adapters::Xlsx do
  7. 1 include_context "table/factories"
  8. 1 let(:input) do
  9. 29 stub_input(source)
  10. end
  11. 1 let(:table_opts) do
  12. 29 {}
  13. end
  14. 1 let(:table) do
  15. 29 described_class.new(input, **table_opts)
  16. end
  17. 1 def stub_input(source)
  18. 29 fixture_path(source)
  19. end
  20. 1 after do |example|
  21. 29 else: 2 then: 27 table.close unless example.metadata[:autoclose_table] == false
  22. end
  23. 1 context "when the input table is empty" do
  24. 1 let(:source) do
  25. 10 "xlsx/empty.xlsx"
  26. end
  27. 1 include_examples "table/empty", sized_rows_enum: true
  28. end
  29. 1 context "when the input table is filled" do
  30. 1 let(:source) do
  31. 13 "xlsx/regular.xlsx"
  32. end
  33. 1 let(:source_data) do
  34. [
  35. 8 ["matricule", "nom", "prénom", "date de naissance", "email"],
  36. ["004774", "Ytärd", "Glœuiçe", "28/04/1998", "foo@bar.com"],
  37. [664_623, "Goulijambon", "Carasmine", Date.new(1976, 1, 20), "foo@bar.com"],
  38. ]
  39. end
  40. 1 include_examples "table/filled", sized_rows_enum: true
  41. end
  42. 1 context "when the input table includes empty rows around the content" do
  43. 1 let(:source) do
  44. 2 "xlsx/empty_rows_around.xlsx"
  45. end
  46. 1 let(:source_data) do
  47. [
  48. 2 ["matricule", "nom", "prénom", "date de naissance", "email"],
  49. ["004774", "Ytärd", "Glœuiçe", "28/04/1998", "foo@bar.com"],
  50. [664_623, "Goulijambon", "Carasmine", Date.new(1976, 1, 20), "foo@bar.com"],
  51. ]
  52. end
  53. 1 it "ignores the empty initial rows when detecting the headers" do
  54. 1 headers = build_headers(source_data[0])
  55. 2 expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
  56. end
  57. 1 it "ignores the empty final rows when detecting the rows" do
  58. 1 rows = build_rows(source_data[1..], row: 3)
  59. 2 expect { |b| table.each_row(&b) }.to yield_successive_args(*rows)
  60. end
  61. end
  62. 1 context "when the input table includes empty rows within the content" do
  63. 1 let(:source) do
  64. 2 "xlsx/empty_rows_within.xlsx"
  65. end
  66. 1 let(:source_data) do
  67. 2 empty_row = Array.new(5)
  68. [
  69. 2 ["matricule", "nom", "prénom", "date de naissance", "email"],
  70. empty_row,
  71. ["004774", "Ytärd", "Glœuiçe", "28/04/1998", "foo@bar.com"],
  72. empty_row,
  73. [664_623, "Goulijambon", "Carasmine", Date.new(1976, 1, 20), "foo@bar.com"],
  74. ]
  75. end
  76. 1 it "ignores them when detecting the headers" do
  77. 1 headers = build_headers(source_data[0])
  78. 2 expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
  79. end
  80. 1 it "doesn't ignore them when detecting the rows" do
  81. 1 rows = build_rows(source_data[1..])
  82. 2 expect { |b| table.each_row(&b) }.to yield_successive_args(*rows)
  83. end
  84. end
  85. 1 context "when the input table includes empty columns before the content" do
  86. 1 let(:source) do
  87. 2 "xlsx/empty_cols_before.xlsx"
  88. end
  89. 1 let(:source_data) do
  90. [
  91. 2 ["matricule", "nom", "prénom", "date de naissance", "email"],
  92. ["004774", "Ytärd", "Glœuiçe", "28/04/1998", "foo@bar.com"],
  93. [664_623, "Goulijambon", "Carasmine", Date.new(1976, 1, 20), "foo@bar.com"],
  94. ]
  95. end
  96. 1 it "ignores the initial empty columns when detecting the headers" do
  97. 1 headers = build_headers(source_data[0], col: "B")
  98. 2 expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
  99. end
  100. 1 it "ignores the initial empty columns when detecting the rows" do
  101. 1 rows = build_rows(source_data[1..], col: "B")
  102. 2 expect { |b| table.each_row(&b) }.to yield_successive_args(*rows)
  103. end
  104. end
  105. end

spec/tabulard/adapters_spec.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/adapters"
  3. 1 RSpec.describe Tabulard::Adapters do
  4. 1 describe "::open" do
  5. 1 let(:adapter) do
  6. 1 double
  7. end
  8. 3 let(:foo) { double }
  9. 3 let(:bar) { double }
  10. 2 let(:res) { double }
  11. 1 it "opens with a adapter" do
  12. 1 allow(adapter).to receive(:open).with(foo, bar: bar).and_return(res)
  13. 1 expect(described_class.open(foo, adapter: adapter, bar: bar)).to be(res)
  14. end
  15. 1 it "may not open without a adapter" do
  16. 2 expect { described_class.open(foo, bar: bar) }.to raise_error(
  17. ArgumentError, /missing keyword: :adapter/
  18. )
  19. end
  20. end
  21. end

spec/tabulard/attribute_spec.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/attribute"
  3. 1 RSpec.describe Tabulard::Attribute do
  4. 1 describe "::build" do
  5. 1 it "freezes the resulting instance" do
  6. 1 attribute = described_class.build(key: :foo, type: :bar)
  7. 1 expect(attribute).to be_frozen
  8. end
  9. end
  10. 1 describe "#each_column" do
  11. 1 let(:key) do
  12. 1 :foo
  13. end
  14. 1 let(:type) do
  15. 1 :bar
  16. end
  17. 1 let(:attribute) do
  18. 1 described_class.new(key: key, type: type)
  19. end
  20. 1 context "without a block" do
  21. 2 let(:config) { double }
  22. 1 it "returns an unsized enumerator" do
  23. 1 enum = attribute.each_column(config)
  24. 1 expect(enum).to be_a(Enumerator)
  25. 1 expect(enum.size).to be_nil
  26. end
  27. end
  28. end
  29. end

spec/tabulard/attribute_types/composite_spec.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/attribute_types/composite"
  3. 1 require "tabulard/attribute_types/value"
  4. 1 RSpec.describe Tabulard::AttributeTypes::Composite do
  5. 1 describe "::build" do
  6. 1 it "freezes the resulting instance" do
  7. 1 type = described_class.build(composite: :foo, scalars: [:bar])
  8. 1 expect(type).to be_frozen
  9. end
  10. end
  11. 1 describe "#each_column" do
  12. 1 let(:composite_type) do
  13. 1 :foo
  14. end
  15. 1 let(:scalars) do
  16. [
  17. 1 instance_double(Tabulard::AttributeTypes::Value),
  18. instance_double(Tabulard::AttributeTypes::Value),
  19. ]
  20. end
  21. 1 let(:composite) do
  22. 1 described_class.new(composite: composite_type, scalars: scalars)
  23. end
  24. 1 context "without a block" do
  25. 1 it "returns a sized enumerator" do
  26. 1 enum = composite.each_column
  27. 1 expect(enum).to be_a(Enumerator)
  28. 1 expect(enum.size).to eq(scalars.size)
  29. end
  30. end
  31. end
  32. end

spec/tabulard/attribute_types/scalar_spec.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/attribute_types/scalar"
  3. 1 require "tabulard/attribute_types/value"
  4. 1 RSpec.describe Tabulard::AttributeTypes::Scalar do
  5. 1 describe "::build" do
  6. 1 it "freezes the resulting instance" do
  7. 1 type = described_class.build(:foo)
  8. 1 expect(type).to be_frozen
  9. end
  10. end
  11. 1 describe "#each_column" do
  12. 1 let(:value) do
  13. 1 instance_double(Tabulard::AttributeTypes::Value)
  14. end
  15. 1 let(:scalar) do
  16. 1 described_class.new(value)
  17. end
  18. 1 context "without a block" do
  19. 1 it "returns a sized enumerator" do
  20. 1 enum = scalar.each_column
  21. 1 expect(enum).to be_a(Enumerator)
  22. 1 expect(enum.size).to eq(1)
  23. end
  24. end
  25. end
  26. end

spec/tabulard/attribute_types/value_spec.rb

100.0% lines covered

100.0% branches covered

29 relevant lines. 29 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/attribute_types/value"
  3. 1 RSpec.describe Tabulard::AttributeTypes::Value do
  4. 1 let(:type) do
  5. 6 :foo
  6. end
  7. 1 def newval(...)
  8. 5 described_class.new(...)
  9. end
  10. 1 def buildval(...)
  11. 6 described_class.build(...)
  12. end
  13. 1 describe "::build" do
  14. 1 it "returns a frozen value" do
  15. 1 value = buildval(type: type)
  16. 1 expect(value).to be_frozen
  17. end
  18. 1 context "when the type requirement is implicit" do
  19. 1 it "has a required type" do
  20. 1 value = buildval(type: type)
  21. 1 expect(value).to eq(newval(type: type, required: true))
  22. end
  23. 1 it "can be expressed with syntactic sugar" do
  24. 1 value = buildval(type)
  25. 1 expect(value).to eq(newval(type: type, required: true))
  26. end
  27. end
  28. 1 context "when the type requirement is explicit" do
  29. 1 it "may have an optional type" do
  30. 1 value = buildval(type: type, required: false)
  31. 1 expect(value).to eq(newval(type: type, required: false))
  32. end
  33. 1 it "may have a required type" do
  34. 1 value = buildval(type: type, required: true)
  35. 1 expect(value).to eq(newval(type: type, required: true))
  36. end
  37. 1 it "can be optional using syntactic sugar" do
  38. 1 value = buildval(:"#{type}?")
  39. 1 expect(value).to eq(newval(type: type, required: false))
  40. end
  41. end
  42. end
  43. end

spec/tabulard/attribute_types_spec.rb

100.0% lines covered

100.0% branches covered

30 relevant lines. 30 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/attribute_types"
  3. 1 RSpec.describe Tabulard::AttributeTypes do
  4. 1 def build(...)
  5. 4 described_class.build(...)
  6. end
  7. 1 def value(...)
  8. 8 Tabulard::AttributeTypes::Value.new(...)
  9. end
  10. 1 def scalar(...)
  11. 2 Tabulard::AttributeTypes::Scalar.new(...)
  12. end
  13. 1 def composite(...)
  14. 2 Tabulard::AttributeTypes::Composite.new(...)
  15. end
  16. 1 context "when given a scalar as a Hash" do
  17. 1 let(:type) do
  18. 1 {
  19. type: :foo,
  20. required: false,
  21. }
  22. end
  23. 1 it "has the expected type" do
  24. 1 expect(build(type)).to eq(scalar(value(type: :foo, required: false)))
  25. end
  26. end
  27. 1 context "when given a scalar as a Symbol" do
  28. 1 let(:type) do
  29. 1 :foo
  30. end
  31. 1 it "has the expected type" do
  32. 1 expect(build(type)).to eq(scalar(value(type: :foo, required: true)))
  33. end
  34. end
  35. 1 context "when given a composite as a Hash" do
  36. 1 let(:type) do
  37. {
  38. 1 composite: :oof,
  39. scalars: [
  40. :foo,
  41. :bar?,
  42. { type: :baz, required: false },
  43. ],
  44. }
  45. end
  46. 1 it "has the expected type and scalars" do
  47. 1 expect(build(type)).to eq(
  48. composite(
  49. composite: :oof,
  50. scalars: [
  51. value(type: :foo, required: true),
  52. value(type: :bar, required: false),
  53. value(type: :baz, required: false),
  54. ]
  55. )
  56. )
  57. end
  58. end
  59. 1 context "when given a composite as an Array" do
  60. 1 let(:type) do
  61. [
  62. 1 :foo,
  63. :bar?,
  64. { type: :baz, required: false },
  65. ]
  66. end
  67. 1 it "has the expected type and scalars" do
  68. 1 expect(build(type)).to eq(
  69. composite(
  70. composite: :array,
  71. scalars: [
  72. value(type: :foo, required: true),
  73. value(type: :bar, required: false),
  74. value(type: :baz, required: false),
  75. ]
  76. )
  77. )
  78. end
  79. end
  80. end

spec/tabulard/column_spec.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/column"
  3. 1 RSpec.describe Tabulard::Column do
  4. 7 let(:key) { double }
  5. 7 let(:type) { double }
  6. 7 let(:index) { double }
  7. 7 let(:header) { double }
  8. 7 let(:header_pattern) { Object.new }
  9. 7 let(:required) { false }
  10. 1 let(:col) do
  11. 6 described_class.new(
  12. key: key,
  13. type: type,
  14. index: index,
  15. header: header,
  16. header_pattern: header_pattern,
  17. required: required
  18. )
  19. end
  20. 1 describe "#key" do
  21. 1 it "reads the attribute" do
  22. 1 expect(col.key).to be(key)
  23. end
  24. end
  25. 1 describe "#type" do
  26. 1 it "reads the attribute" do
  27. 1 expect(col.type).to be(type)
  28. end
  29. end
  30. 1 describe "#index" do
  31. 1 it "reads the attribute" do
  32. 1 expect(col.index).to be(index)
  33. end
  34. end
  35. 1 describe "#header" do
  36. 1 it "reads the attribute" do
  37. 1 expect(col.header).to be(header)
  38. end
  39. end
  40. 1 describe "#header_pattern" do
  41. 1 it "reads the attribute" do
  42. 1 expect(col.header_pattern).to be(header_pattern)
  43. end
  44. end
  45. 1 describe "#required?" do
  46. 1 it "reads the attribute" do
  47. 1 expect(col.required?).to be(required)
  48. end
  49. end
  50. end

spec/tabulard/errors/error_spec.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/errors/error"
  3. 1 RSpec.describe Tabulard::Errors::Error do
  4. 1 it "is some kind of StandardError" do
  5. 1 expect(described_class).to have_attributes(superclass: StandardError)
  6. end
  7. end

spec/tabulard/errors/spec_error_spec.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/errors/spec_error"
  3. 1 RSpec.describe Tabulard::Errors::SpecError do
  4. 1 it "is some kind of Error" do
  5. 1 expect(described_class).to have_attributes(superclass: Tabulard::Errors::Error)
  6. end
  7. end

spec/tabulard/errors/type_error_spec.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/errors/type_error"
  3. 1 RSpec.describe Tabulard::Errors::TypeError do
  4. 1 it "is some kind of Error" do
  5. 1 expect(described_class).to have_attributes(superclass: Tabulard::Errors::Error)
  6. end
  7. end

spec/tabulard/headers_spec.rb

100.0% lines covered

100.0% branches covered

54 relevant lines. 54 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/headers"
  3. 1 require "tabulard/column"
  4. 1 require "tabulard/messaging"
  5. 1 require "tabulard/table"
  6. 1 require "tabulard/specification"
  7. 1 RSpec.describe Tabulard::Headers, monadic_result: true do
  8. 1 let(:specification) do
  9. 7 instance_double(
  10. Tabulard::Specification,
  11. required_columns: [],
  12. ignore_unspecified_columns?: false
  13. )
  14. end
  15. 1 let(:columns) do
  16. 7 Array.new(10) do
  17. 70 instance_double(Tabulard::Column)
  18. end
  19. end
  20. 1 let(:messenger) do
  21. 7 Tabulard::Messaging::Messenger.new
  22. end
  23. 1 let(:table_headers) do
  24. 7 Array.new(5) do |i|
  25. 35 instance_double(Tabulard::Table::Header, col: "FOO", value: "header#{i}")
  26. end
  27. end
  28. 1 let(:headers) do
  29. 7 described_class.new(specification: specification, messenger: messenger)
  30. end
  31. 1 def stub_specification(column_by_header)
  32. 7 column_by_header = column_by_header.transform_keys(&:value)
  33. 7 allow(specification).to receive(:get) do |header_value|
  34. 16 column_by_header[header_value]
  35. end
  36. end
  37. 1 before do
  38. 7 stub_specification(
  39. table_headers[0] => columns[4],
  40. table_headers[1] => columns[1],
  41. table_headers[2] => columns[7],
  42. table_headers[3] => columns[1]
  43. )
  44. end
  45. 1 describe "#result" do
  46. 1 context "without any #add" do
  47. 1 it "is a success with no items" do
  48. 1 expect(headers.result).to eq(Success([]))
  49. end
  50. end
  51. 1 context "with some successful #add" do
  52. 1 before do
  53. 2 headers.add(table_headers[1])
  54. 2 headers.add(table_headers[2])
  55. 2 headers.add(table_headers[0])
  56. end
  57. 1 it "is a success and preserve #add order" do
  58. 1 expect(headers.result).to eq(
  59. Success(
  60. [
  61. Tabulard::Headers::Header.new(table_headers[1], columns[1]),
  62. Tabulard::Headers::Header.new(table_headers[2], columns[7]),
  63. Tabulard::Headers::Header.new(table_headers[0], columns[4]),
  64. ]
  65. )
  66. )
  67. end
  68. 1 it "doesn't message" do
  69. 1 expect(messenger.messages).to be_empty
  70. end
  71. end
  72. 1 context "when a header doesn't match a column" do
  73. 1 before do
  74. 2 headers.add(table_headers[0])
  75. 2 headers.add(table_headers[4])
  76. end
  77. 1 it "is a failure" do
  78. 1 expect(headers.result).to eq(Failure())
  79. end
  80. 1 it "messages the error" do
  81. 1 expect(messenger.messages).to contain_exactly(
  82. be_a(Tabulard::Messaging::Message) & have_attributes(
  83. severity: "ERROR",
  84. code: "invalid_header",
  85. code_data: { value: table_headers[4].value },
  86. scope: "COL",
  87. scope_data: { col: table_headers[4].col }
  88. )
  89. )
  90. end
  91. end
  92. 1 context "when there is a duplicate" do
  93. 1 before do
  94. 2 headers.add(table_headers[0])
  95. 2 headers.add(table_headers[3])
  96. 2 headers.add(table_headers[1])
  97. end
  98. 1 it "is a failure" do
  99. 1 expect(headers.result).to eq(Failure())
  100. end
  101. 1 it "considers the underlying column, not the header" do
  102. 1 expect(messenger.messages).to contain_exactly(
  103. be_a(Tabulard::Messaging::Message) & have_attributes(
  104. severity: "ERROR",
  105. code: "duplicated_header",
  106. code_data: { value: table_headers[1].value },
  107. scope: "COL",
  108. scope_data: { col: table_headers[1].col }
  109. )
  110. )
  111. end
  112. end
  113. end
  114. end

spec/tabulard/messaging/config_spec.rb

100.0% lines covered

100.0% branches covered

33 relevant lines. 33 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/config"
  3. 1 require "climate_control"
  4. 1 RSpec.describe Tabulard::Messaging::Config do
  5. 1 def envmod(envval, &block)
  6. 5 ClimateControl.modify(envvar => envval, &block)
  7. end
  8. 1 describe "#validate_messages" do
  9. 6 let(:envvar) { "TABULARD_MESSAGING_VALIDATE_MESSAGES" }
  10. 1 it "is true by default" do
  11. 2 config = envmod(nil) { described_class.new }
  12. 1 expect(config.validate_messages).to be(true)
  13. end
  14. 1 context "when the ENV says true" do
  15. 1 it "is implicitly true" do
  16. 2 config = envmod("true") { described_class.new }
  17. 1 expect(config.validate_messages).to be(true)
  18. end
  19. 1 it "may explicitly be false" do
  20. 2 config = envmod("true") { described_class.new(validate_messages: false) }
  21. 1 expect(config.validate_messages).to be(false)
  22. end
  23. end
  24. 1 context "when the ENV says false" do
  25. 1 it "is implicitly false" do
  26. 2 config = envmod("false") { described_class.new }
  27. 1 expect(config.validate_messages).to be(false)
  28. end
  29. 1 it "may explicitly be true" do
  30. 2 config = envmod("false") { described_class.new(validate_messages: true) }
  31. 1 expect(config.validate_messages).to be(true)
  32. end
  33. end
  34. end
  35. 1 describe "#validate_messages=" do
  36. 1 it "can become true" do
  37. 1 config = described_class.new(validate_messages: false)
  38. 1 config.validate_messages = true
  39. 1 expect(config.validate_messages).to be(true)
  40. end
  41. 1 it "can become false" do
  42. 1 config = described_class.new(validate_messages: true)
  43. 1 config.validate_messages = false
  44. 1 expect(config.validate_messages).to be(false)
  45. end
  46. end
  47. end

spec/tabulard/messaging/message_spec.rb

100.0% lines covered

100.0% branches covered

62 relevant lines. 62 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging"
  3. 1 RSpec.describe Tabulard::Messaging::Message do
  4. 6 let(:code) { double }
  5. 5 let(:code_data) { double }
  6. 5 let(:scope) { double }
  7. 6 let(:scope_data) { double }
  8. 5 let(:severity) { double }
  9. 1 let(:message) do
  10. 10 described_class.new(
  11. code: code,
  12. code_data: code_data,
  13. scope: scope,
  14. scope_data: scope_data,
  15. severity: severity
  16. )
  17. end
  18. 1 it "needs at least a code" do
  19. 2 expect { described_class.new }.to raise_error(ArgumentError, /missing keyword: :code/)
  20. end
  21. 1 it "may have only a custom code and some defaults attributes" do
  22. 1 expect(described_class.new(code: code)).to have_attributes(
  23. code: code,
  24. code_data: nil,
  25. scope: Tabulard::Messaging::SCOPES::TABLE,
  26. scope_data: nil,
  27. severity: Tabulard::Messaging::SEVERITIES::WARN
  28. )
  29. end
  30. 1 it "may have completely custom attributes" do
  31. 1 expect(message).to have_attributes(
  32. code: code,
  33. code_data: code_data,
  34. scope: scope,
  35. scope_data: scope_data,
  36. severity: severity
  37. )
  38. end
  39. 1 it "is equivalent to a message having the same attributes" do
  40. 1 other_message = described_class.new(
  41. code: code,
  42. code_data: code_data,
  43. scope: scope,
  44. scope_data: scope_data,
  45. severity: severity
  46. )
  47. 1 expect(message).to eq(other_message)
  48. end
  49. 1 it "is not equivalent to a message having different attributes" do
  50. 1 other_message = described_class.new(
  51. code: code,
  52. code_data: code_data,
  53. scope: scope,
  54. scope_data: double,
  55. severity: severity
  56. )
  57. 1 expect(message).not_to eq(other_message)
  58. end
  59. 1 it "may be validated" do
  60. 1 expect(message).to be_a(Tabulard::Messaging::Validations)
  61. end
  62. 1 describe "#to_h" do
  63. 1 it "returns the attributes as a hash" do
  64. attrs = {
  65. 1 code: double,
  66. code_data: double,
  67. scope: double,
  68. scope_data: double,
  69. severity: double,
  70. }
  71. 1 message = described_class.new(**attrs)
  72. 1 expect(message.to_h).to eq(attrs)
  73. end
  74. end
  75. 1 describe "#to_s" do
  76. 7 let(:code) { "foo_is_bar" }
  77. 6 let(:code_data) { nil }
  78. 7 let(:severity) { "ERROR" }
  79. 1 context "when scoped to the table" do
  80. 2 let(:scope) { Tabulard::Messaging::SCOPES::TABLE }
  81. 2 let(:scope_data) { nil }
  82. 1 it "can be reduced to a string" do
  83. 1 expect(message.to_s).to eq("[TABLE] ERROR: foo_is_bar")
  84. end
  85. end
  86. 1 context "when scoped to a row" do
  87. 2 let(:scope) { Tabulard::Messaging::SCOPES::ROW }
  88. 2 let(:scope_data) { { row: 42 } }
  89. 1 it "can be reduced to a string" do
  90. 1 expect(message.to_s).to eq("[ROW: 42] ERROR: foo_is_bar")
  91. end
  92. end
  93. 1 context "when scoped to a col" do
  94. 2 let(:scope) { Tabulard::Messaging::SCOPES::COL }
  95. 2 let(:scope_data) { { col: "AA" } }
  96. 1 it "can be reduced to a string" do
  97. 1 expect(message.to_s).to eq("[COL: AA] ERROR: foo_is_bar")
  98. end
  99. end
  100. 1 context "when scoped to a cell" do
  101. 2 let(:scope) { Tabulard::Messaging::SCOPES::CELL }
  102. 2 let(:scope_data) { { row: 42, col: "AA" } }
  103. 1 it "can be reduced to a string" do
  104. 1 expect(message.to_s).to eq("[CELL: AA42] ERROR: foo_is_bar")
  105. end
  106. end
  107. 1 context "when the scope doesn't make sense" do
  108. 2 let(:scope) { "oiqjzfoi" }
  109. 1 it "can be reduced to a string" do
  110. 1 expect(message.to_s).to eq("ERROR: foo_is_bar")
  111. end
  112. end
  113. 1 context "when there is some data associated with the code" do
  114. 2 let(:scope) { Tabulard::Messaging::SCOPES::TABLE }
  115. 2 let(:scope_data) { nil }
  116. 2 let(:code_data) { { "foo" => "bar" } }
  117. 1 it "can be reduced to a string" do
  118. 1 expect(message.to_s).to match(/^\[TABLE\] ERROR: foo_is_bar {"foo" ?=> ?"bar"}$/)
  119. end
  120. end
  121. end
  122. end

spec/tabulard/messaging/message_variant_spec.rb

100.0% lines covered

100.0% branches covered

39 relevant lines. 39 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/message_variant"
  3. 1 RSpec.describe Tabulard::Messaging::MessageVariant do
  4. 6 let(:code) { double }
  5. 1 describe "::code" do
  6. 1 context "when a CODE constant is not defined" do
  7. 1 it "fails with an exception" do
  8. 2 expect { described_class.code }.to raise_error(NameError, /CODE/)
  9. end
  10. end
  11. 1 context "when a CODE constant is defined" do
  12. 1 before do
  13. 3 stub_const("#{described_class}::CODE", code)
  14. end
  15. 1 it "exposes the constant" do
  16. 1 expect(described_class.code).to eq(code)
  17. end
  18. 1 context "when called from a subclass" do
  19. 3 let(:subclass) { Class.new(described_class) }
  20. 1 before do
  21. 2 stub_const("#{described_class}Foo", subclass)
  22. end
  23. 1 it "exposes the constant from the parent class" do
  24. 1 expect(subclass.code).to eq(code)
  25. end
  26. 1 context "when the subclass also defines the constant" do
  27. 2 let(:subclass_code) { double }
  28. 1 before do
  29. 1 stub_const("#{described_class}Foo::CODE", subclass_code)
  30. end
  31. 1 it "exposes the constant from the subclass" do
  32. 1 expect(subclass.code).to eq(subclass_code)
  33. end
  34. end
  35. end
  36. end
  37. end
  38. 1 describe "::new" do
  39. 1 before do
  40. 1 allow(described_class).to receive(:code).and_return(code)
  41. end
  42. 1 it "assigns that code to instances automatically" do
  43. 1 message = described_class.new
  44. 1 expect(message.code).to eq(code)
  45. end
  46. end
  47. 1 describe "validations" do
  48. 2 let(:validator) { described_class.validator }
  49. 1 before do
  50. 1 allow(described_class).to receive(:code).and_return(code)
  51. end
  52. 1 it "validates that the message code is that same as the class code" do
  53. 1 message1 = described_class.new
  54. 1 message2 = described_class.new(code: double)
  55. 1 expect(validator.validate_code(message1)).to be true
  56. 1 expect(validator.validate_code(message2)).to be false
  57. end
  58. end
  59. end

spec/tabulard/messaging/messages/cleaned_string_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/messages/cleaned_string"
  3. 1 RSpec.describe Tabulard::Messaging::Messages::CleanedString do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "cleaned_string",
  11. code_data: nil,
  12. scope: "CELL",
  13. scope_data: { col: "FOO", row: 42 }
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have some code data" do
  27. 1 message.code_data = { foo: :bar }
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have nil scope_data" do
  35. 1 message.scope_data = nil
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/messaging/messages/duplicated_header_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/messages/duplicated_header"
  3. 1 RSpec.describe Tabulard::Messaging::Messages::DuplicatedHeader do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "duplicated_header",
  11. code_data: { value: "header_foo" },
  12. scope: "COL",
  13. scope_data: { col: "FOO" }
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have nil code data" do
  27. 1 message.code_data = nil
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have nil scope_data" do
  35. 1 message.scope_data = nil
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/messaging/messages/invalid_header_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/messages/invalid_header"
  3. 1 RSpec.describe Tabulard::Messaging::Messages::InvalidHeader do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "invalid_header",
  11. code_data: { value: "header_foo" },
  12. scope: "COL",
  13. scope_data: { col: "FOO" }
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have nil code data" do
  27. 1 message.code_data = nil
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have nil scope_data" do
  35. 1 message.scope_data = nil
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/messaging/messages/missing_column_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/messages/missing_column"
  3. 1 RSpec.describe Tabulard::Messaging::Messages::MissingColumn do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "missing_column",
  11. code_data: { value: "header_foo" },
  12. scope: "TABLE",
  13. scope_data: nil
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have nil code data" do
  27. 1 message.code_data = nil
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have some scope_data" do
  35. 1 message.scope_data = { foo: :bar }
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/messaging/messages/must_be_array_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/messages/must_be_array"
  3. 1 RSpec.describe Tabulard::Messaging::Messages::MustBeArray do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "must_be_array",
  11. code_data: nil,
  12. scope: "CELL",
  13. scope_data: { col: "FOO", row: 42 }
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have some code data" do
  27. 1 message.code_data = { foo: :baz }
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have nil scope_data" do
  35. 1 message.scope_data = nil
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/messaging/messages/must_be_boolsy_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/messages/must_be_boolsy"
  3. 1 RSpec.describe Tabulard::Messaging::Messages::MustBeBoolsy do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "must_be_boolsy",
  11. code_data: { value: "foo" },
  12. scope: "CELL",
  13. scope_data: { col: "FOO", row: 42 }
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have nil code data" do
  27. 1 message.code_data = nil
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have nil scope_data" do
  35. 1 message.scope_data = nil
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/messaging/messages/must_be_date_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/messages/must_be_date"
  3. 1 RSpec.describe Tabulard::Messaging::Messages::MustBeDate do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "must_be_date",
  11. code_data: { format: "foo" },
  12. scope: "CELL",
  13. scope_data: { col: "FOO", row: 42 }
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have nil code data" do
  27. 1 message.code_data = nil
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have nil scope_data" do
  35. 1 message.scope_data = nil
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/messaging/messages/must_be_email_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/messages/must_be_email"
  3. 1 RSpec.describe Tabulard::Messaging::Messages::MustBeEmail do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "must_be_email",
  11. code_data: { value: "foo" },
  12. scope: "CELL",
  13. scope_data: { col: "FOO", row: 42 }
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have nil code data" do
  27. 1 message.code_data = nil
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have nil scope_data" do
  35. 1 message.scope_data = nil
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/messaging/messages/must_be_string_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/messages/must_be_string"
  3. 1 RSpec.describe Tabulard::Messaging::Messages::MustBeString do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "must_be_string",
  11. code_data: nil,
  12. scope: "CELL",
  13. scope_data: { col: "FOO", row: 42 }
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have some code data" do
  27. 1 message.code_data = { foo: :baz }
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have nil scope_data" do
  35. 1 message.scope_data = nil
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/messaging/messages/must_exist_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/messages/must_exist"
  3. 1 RSpec.describe Tabulard::Messaging::Messages::MustExist do
  4. 1 it "has a default code" do
  5. 1 expect(described_class.new).to have_attributes(code: described_class::CODE)
  6. end
  7. 1 describe "validations" do
  8. 1 let(:message) do
  9. 5 described_class.new(
  10. code: "must_exist",
  11. code_data: nil,
  12. scope: "CELL",
  13. scope_data: { col: "FOO", row: 42 }
  14. )
  15. end
  16. 1 let(:validator) do
  17. 4 described_class.validator
  18. end
  19. 1 it "may be valid" do
  20. 2 expect { message.validate }.not_to raise_error
  21. end
  22. 1 it "may not have a different code" do
  23. 1 message.code = "foo"
  24. 1 expect(validator.validate_code(message)).to be false
  25. end
  26. 1 it "may not have some code data" do
  27. 1 message.code_data = { foo: :baz }
  28. 1 expect(validator.validate_code_data(message)).to be false
  29. end
  30. 1 it "may not have a different scope" do
  31. 1 message.scope = "ROW"
  32. 1 expect(validator.validate_scope(message)).to be false
  33. end
  34. 1 it "may not have nil scope_data" do
  35. 1 message.scope_data = nil
  36. 1 expect(validator.validate_scope_data(message)).to be false
  37. end
  38. end
  39. end

spec/tabulard/messaging/messenger_spec.rb

100.0% lines covered

100.0% branches covered

225 relevant lines. 225 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging"
  3. 1 RSpec.describe Tabulard::Messaging::Messenger do
  4. 18 let(:scopes) { Tabulard::Messaging::SCOPES }
  5. 3 let(:severities) { Tabulard::Messaging::SEVERITIES }
  6. 15 let(:row) { double }
  7. 15 let(:col) { double }
  8. 1 def be_the_frozen(obj)
  9. 10 be(obj) & be_frozen
  10. end
  11. 1 describe "building methods" do
  12. 4 let(:scope) { Object.new }
  13. 4 let(:scope_data) { Object.new }
  14. 1 describe "#initialize" do
  15. 1 it "has some default attributes" do
  16. 1 messenger = described_class.new
  17. 1 expect(messenger).to have_attributes(
  18. scope: scopes::TABLE,
  19. scope_data: nil,
  20. messages: []
  21. )
  22. end
  23. 1 it "may have some custom, frozen attributes" do
  24. 1 messenger = described_class.new(scope: scope, scope_data: scope_data)
  25. 1 expect(messenger).to have_attributes(
  26. scope: be_the_frozen(scope),
  27. scope_data: be_the_frozen(scope_data),
  28. messages: []
  29. )
  30. end
  31. end
  32. 1 describe "#dup" do
  33. 1 let(:messenger1) do
  34. 2 described_class.new(scope: scope, scope_data: scope_data)
  35. end
  36. 1 it "returns a new instance" do
  37. 1 messenger2 = messenger1.dup
  38. 1 expect(messenger2).to eq(messenger1)
  39. 1 expect(messenger2).not_to be(messenger1)
  40. end
  41. 1 it "preserves some attributes" do
  42. 1 messenger1.messages << "foobar"
  43. 1 messenger2 = messenger1.dup
  44. 1 expect(messenger2.messages).to be_empty
  45. 1 expect(messenger2.messages).not_to be(messenger1.messages)
  46. end
  47. end
  48. end
  49. 1 describe "#scoping!" do
  50. 1 subject do
  51. 12 ->(&block) { messenger.scoping!(new_scope, new_scope_data, &block) }
  52. end
  53. 7 let(:old_scope) { Object.new }
  54. 7 let(:old_scope_data) { Object.new }
  55. 7 let(:new_scope) { Object.new }
  56. 7 let(:new_scope_data) { Object.new }
  57. 1 let(:messenger) do
  58. 6 described_class.new(scope: old_scope, scope_data: old_scope_data)
  59. end
  60. 1 context "without a block" do
  61. 1 it "returns the receiver" do
  62. 1 expect(subject.call).to be(messenger)
  63. end
  64. 1 it "scopes the receiver" do
  65. 1 subject.call
  66. 1 expect(messenger).to have_attributes(
  67. scope: be_the_frozen(new_scope),
  68. scope_data: be_the_frozen(new_scope_data)
  69. )
  70. end
  71. end
  72. 1 context "with a block" do
  73. 1 it "returns the block value" do
  74. 1 foo = double
  75. 2 expect(subject.call { foo }).to eq(foo)
  76. end
  77. 1 it "scopes and yields the receiver" do
  78. 2 expect { |b| subject.call(&b) }.to yield_with_args(
  79. be(messenger) & have_attributes(
  80. scope: be_the_frozen(new_scope),
  81. scope_data: be_the_frozen(new_scope_data)
  82. )
  83. )
  84. end
  85. 1 it "unscopes the receiver after the block" do
  86. 1 subject.call {}
  87. 1 expect(messenger).to have_attributes(
  88. scope: be_the_frozen(old_scope),
  89. scope_data: be_the_frozen(old_scope_data)
  90. )
  91. end
  92. 1 it "unscopes the receiver after the block, even if it raises" do
  93. 1 e = StandardError.new
  94. 3 expect { subject.call { raise(e) } }.to raise_error(e)
  95. 1 expect(messenger).to have_attributes(
  96. scope: be_the_frozen(old_scope),
  97. scope_data: be_the_frozen(old_scope_data)
  98. )
  99. end
  100. end
  101. end
  102. 1 describe "#scoping! variants" do
  103. 12 let(:scoping_block) { proc {} }
  104. 1 let(:scoping_result) { double }
  105. 1 def allow_method_call_checking_block(receiver, method_name, *args, **opts, &block)
  106. 22 result = double
  107. 22 allow(receiver).to receive(method_name) do |*a, **o, &b|
  108. 22 expect(a).to eq(args)
  109. 22 expect(o).to eq(opts)
  110. 22 expect(b).to eq(block)
  111. 22 result
  112. end
  113. 22 result
  114. end
  115. 1 def stub_scoping!(receiver, ...)
  116. 18 allow_method_call_checking_block(receiver, :scoping!, ...)
  117. end
  118. 1 describe "#scoping" do
  119. 3 let(:messenger) { described_class.new }
  120. 3 let(:messenger_dup) { messenger.dup }
  121. 3 let(:scope) { double }
  122. 3 let(:scope_data) { double }
  123. 1 before do
  124. 2 allow(messenger).to receive(:dup).and_return(messenger_dup)
  125. end
  126. 1 it "applies the correct scoping to a receiver duplicate (with a block)" do
  127. 1 scoping_result = stub_scoping!(messenger_dup, scope, scope_data, &scoping_block)
  128. 1 expect(messenger.scoping(scope, scope_data, &scoping_block)).to eq(scoping_result)
  129. end
  130. 1 it "applies the correct scoping to a receiver duplicate (without a block)" do
  131. 1 scoping_result = stub_scoping!(messenger_dup, scope, scope_data)
  132. 1 expect(messenger.scoping(scope, scope_data)).to eq(scoping_result)
  133. end
  134. end
  135. 1 describe "#scope_row!" do
  136. 1 context "when the messenger is unscoped" do
  137. 3 let(:messenger) { described_class.new }
  138. 1 it "scopes the messenger to the given row (with a block)" do
  139. 1 scoping_result = stub_scoping!(messenger, scopes::ROW, { row: row }, &scoping_block)
  140. 1 expect(messenger.scope_row!(row, &scoping_block)).to eq(scoping_result)
  141. end
  142. 1 it "scopes the messenger to the given row (without a block)" do
  143. 1 scoping_result = stub_scoping!(messenger, scopes::ROW, { row: row })
  144. 1 expect(messenger.scope_row!(row)).to eq(scoping_result)
  145. end
  146. end
  147. 1 context "when the messenger is scoped to another row" do
  148. 3 let(:other_row) { double }
  149. 3 let(:messenger) { described_class.new(scope: scopes::ROW, scope_data: { row: other_row }) }
  150. 1 it "scopes the messenger to the given row (with a block)" do
  151. 1 scoping_result = stub_scoping!(messenger, scopes::ROW, { row: row }, &scoping_block)
  152. 1 expect(messenger.scope_row!(row, &scoping_block)).to eq(scoping_result)
  153. end
  154. 1 it "scopes the messenger to the given row (without a block)" do
  155. 1 scoping_result = stub_scoping!(messenger, scopes::ROW, { row: row })
  156. 1 expect(messenger.scope_row!(row)).to eq(scoping_result)
  157. end
  158. end
  159. 1 context "when the messenger is scoped to a col" do
  160. 3 let(:messenger) { described_class.new(scope: scopes::COL, scope_data: { col: col }) }
  161. 1 it "scopes the messenger to the appropriate cell (with a block)" do
  162. scoping_result =
  163. 1 stub_scoping!(messenger, scopes::CELL, { col: col, row: row }, &scoping_block)
  164. 1 expect(messenger.scope_row!(row, &scoping_block)).to eq(scoping_result)
  165. end
  166. 1 it "scopes the messenger to the appropriate cell (without a block)" do
  167. 1 scoping_result = stub_scoping!(messenger, scopes::CELL, { col: col, row: row })
  168. 1 expect(messenger.scope_row!(row)).to eq(scoping_result)
  169. end
  170. end
  171. 1 context "when the messenger is scoped to a cell" do
  172. 3 let(:other_row) { double }
  173. 1 let(:messenger) do
  174. 2 described_class.new(scope: scopes::CELL, scope_data: { col: col, row: other_row })
  175. end
  176. 1 it "scopes the messenger to the new appropriate cell (with a block)" do
  177. scoping_result =
  178. 1 stub_scoping!(messenger, scopes::CELL, { col: col, row: row }, &scoping_block)
  179. 1 expect(messenger.scope_row!(row, &scoping_block)).to eq(scoping_result)
  180. end
  181. 1 it "scopes the messenger to the new appropriate cell (without a block)" do
  182. 1 scoping_result = stub_scoping!(messenger, scopes::CELL, { col: col, row: row })
  183. 1 expect(messenger.scope_row!(row)).to eq(scoping_result)
  184. end
  185. end
  186. end
  187. 1 describe "#scope_row" do
  188. 1 def stub_scope_row!(receiver, ...)
  189. 2 allow_method_call_checking_block(receiver, :scope_row!, ...)
  190. end
  191. 3 let(:messenger) { described_class.new }
  192. 3 let(:messenger_dup) { messenger.dup }
  193. 1 before do
  194. 2 allow(messenger).to receive(:dup).and_return(messenger_dup)
  195. end
  196. 1 it "applies the correct scoping to a receiver duplicate (with a block)" do
  197. 1 scoping_result = stub_scope_row!(messenger_dup, row, &scoping_block)
  198. 1 expect(messenger.scope_row(row, &scoping_block)).to eq(scoping_result)
  199. end
  200. 1 it "applies the correct scoping to a receiver duplicate (without a block)" do
  201. 1 scoping_result = stub_scope_row!(messenger_dup, row)
  202. 1 expect(messenger.scope_row(row)).to eq(scoping_result)
  203. end
  204. end
  205. 1 describe "#scope_col!" do
  206. 1 context "when the messenger is unscoped" do
  207. 3 let(:messenger) { described_class.new }
  208. 1 it "scopes the messenger to the given col (with a block)" do
  209. 1 scoping_result = stub_scoping!(messenger, scopes::COL, { col: col }, &scoping_block)
  210. 1 expect(messenger.scope_col!(col, &scoping_block)).to eq(scoping_result)
  211. end
  212. 1 it "scopes the messenger to the given col (without a block)" do
  213. 1 scoping_result = stub_scoping!(messenger, scopes::COL, { col: col })
  214. 1 expect(messenger.scope_col!(col)).to eq(scoping_result)
  215. end
  216. end
  217. 1 context "when the messenger is scoped to another col" do
  218. 3 let(:other_col) { double }
  219. 3 let(:messenger) { described_class.new(scope: scopes::COL, scope_data: { col: other_col }) }
  220. 1 it "scopes the messenger to the given col (with a block)" do
  221. 1 scoping_result = stub_scoping!(messenger, scopes::COL, { col: col }, &scoping_block)
  222. 1 expect(messenger.scope_col!(col, &scoping_block)).to eq(scoping_result)
  223. end
  224. 1 it "scopes the messenger to the given col (without a block)" do
  225. 1 scoping_result = stub_scoping!(messenger, scopes::COL, { col: col })
  226. 1 expect(messenger.scope_col!(col)).to eq(scoping_result)
  227. end
  228. end
  229. 1 context "when the messenger is scoped to a row" do
  230. 3 let(:messenger) { described_class.new(scope: scopes::ROW, scope_data: { row: row }) }
  231. 1 it "scopes the messenger to the appropriate cell (with a block)" do
  232. scoping_result =
  233. 1 stub_scoping!(messenger, scopes::CELL, { row: row, col: col }, &scoping_block)
  234. 1 expect(messenger.scope_col!(col, &scoping_block)).to eq(scoping_result)
  235. end
  236. 1 it "scopes the messenger to the appropriate cell (without a block)" do
  237. 1 scoping_result = stub_scoping!(messenger, scopes::CELL, { row: row, col: col })
  238. 1 expect(messenger.scope_col!(col)).to eq(scoping_result)
  239. end
  240. end
  241. 1 context "when the messenger is scoped to a cell" do
  242. 3 let(:other_col) { double }
  243. 1 let(:messenger) do
  244. 2 described_class.new(scope: scopes::CELL, scope_data: { row: row, col: other_col })
  245. end
  246. 1 it "scopes the messenger to the new appropriate cell (with a block)" do
  247. scoping_result =
  248. 1 stub_scoping!(messenger, scopes::CELL, { row: row, col: col }, &scoping_block)
  249. 1 expect(messenger.scope_col!(col, &scoping_block)).to eq(scoping_result)
  250. end
  251. 1 it "scopes the messenger to the new appropriate cell (without a block)" do
  252. 1 scoping_result = stub_scoping!(messenger, scopes::CELL, { row: row, col: col })
  253. 1 expect(messenger.scope_col!(col)).to eq(scoping_result)
  254. end
  255. end
  256. end
  257. 1 describe "#scope_col" do
  258. 1 def stub_scope_col!(receiver, ...)
  259. 2 allow_method_call_checking_block(receiver, :scope_col!, ...)
  260. end
  261. 3 let(:messenger) { described_class.new }
  262. 3 let(:messenger_dup) { messenger.dup }
  263. 1 before do
  264. 2 allow(messenger).to receive(:dup).and_return(messenger_dup)
  265. end
  266. 1 it "applies the correct scoping to a receiver duplicate (with a block)" do
  267. 1 scoping_result = stub_scope_col!(messenger_dup, col, &scoping_block)
  268. 1 expect(messenger.scope_col(col, &scoping_block)).to eq(scoping_result)
  269. end
  270. 1 it "applies the correct scoping to a receiver duplicate (without a block)" do
  271. 1 scoping_result = stub_scope_col!(messenger_dup, col)
  272. 1 expect(messenger.scope_col(col)).to eq(scoping_result)
  273. end
  274. end
  275. end
  276. 1 describe "adding messages" do
  277. 5 let(:scope) { Object.new }
  278. 5 let(:scope_data) { Object.new }
  279. 5 let(:code) { double }
  280. 5 let(:code_data) { double }
  281. 1 let(:message) do
  282. 4 Tabulard::Messaging::Message.new(code: code, code_data: code_data)
  283. end
  284. 5 let(:messenger) { described_class.new(scope: scope, scope_data: scope_data) }
  285. 1 describe "#warn" do
  286. 1 it "returns the receiver" do
  287. 1 expect(messenger.warn(message)).to be(messenger)
  288. end
  289. 1 it "adds the code & code_data as a warning" do
  290. 1 messenger.warn(message)
  291. 1 expect(messenger.messages).to contain_exactly(
  292. Tabulard::Messaging::Message.new(
  293. code: code,
  294. code_data: code_data,
  295. scope: scope,
  296. scope_data: scope_data,
  297. severity: severities::WARN
  298. )
  299. )
  300. end
  301. end
  302. 1 describe "#error" do
  303. 1 it "returns the receiver" do
  304. 1 expect(messenger.error(message)).to be(messenger)
  305. end
  306. 1 it "adds the code & code_data as an error" do
  307. 1 messenger.error(message)
  308. 1 expect(messenger.messages).to contain_exactly(
  309. Tabulard::Messaging::Message.new(
  310. code: code,
  311. code_data: code_data,
  312. scope: scope,
  313. scope_data: scope_data,
  314. severity: severities::ERROR
  315. )
  316. )
  317. end
  318. end
  319. end
  320. 1 describe "validating messages" do
  321. 1 let(:message) do
  322. 2 Tabulard::Messaging::Message.new(code: double)
  323. end
  324. 1 it "implicitly depends on a global config" do
  325. 1 config = instance_double(Tabulard::Messaging::Config, validate_messages: double)
  326. 1 allow(Tabulard::Messaging).to receive(:config).and_return(config)
  327. 1 messenger = described_class.new
  328. 1 expect(messenger.validate_messages).to eq(config.validate_messages)
  329. end
  330. 1 it "is passed to a duplicate" do
  331. 1 messenger = described_class.new(validate_messages: val = double)
  332. 1 expect(messenger.dup.validate_messages).to eq(val)
  333. end
  334. 1 context "when enabled" do
  335. 1 let(:messenger) do
  336. 1 described_class.new(validate_messages: true)
  337. end
  338. 1 it "validates a message while adding it" do
  339. 1 expect(message).to receive(:validate).with(no_args)
  340. 1 messenger.warn(message)
  341. end
  342. end
  343. 1 context "when disabled" do
  344. 1 let(:messenger) do
  345. 1 described_class.new(validate_messages: false)
  346. end
  347. 1 it "validates a message while adding it" do
  348. 1 expect(message).not_to receive(:validate)
  349. 1 messenger.warn(message)
  350. end
  351. end
  352. end
  353. end

spec/tabulard/messaging/validations/base_validator_spec.rb

100.0% lines covered

100.0% branches covered

89 relevant lines. 89 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/validations"
  3. 1 RSpec.describe Tabulard::Messaging::Validations::BaseValidator do
  4. 1 let(:validator_class) do
  5. 4 described_class
  6. end
  7. 1 let(:validator) do
  8. 12 validator_class.new
  9. end
  10. 1 let(:validation_error) do
  11. 2 Tabulard::Messaging::Validations::InvalidMessage
  12. end
  13. 1 let(:message_class) do
  14. 12 Tabulard::Messaging::Message
  15. end
  16. 1 let(:message) do
  17. 4 instance_double(message_class)
  18. end
  19. 1 let(:scopes) do
  20. 4 Tabulard::Messaging::SCOPES
  21. end
  22. 1 it "allows anything by default" do
  23. 1 expect(validator.validate_code(message)).to be true
  24. 1 expect(validator.validate_code_data(message)).to be true
  25. 1 expect(validator.validate_scope(message)).to be true
  26. 1 expect(validator.validate_scope_data(message)).to be true
  27. end
  28. 1 describe "#validate" do
  29. 1 it "doesn't raise when all validations pass" do
  30. 2 expect { validator.validate(message) }.not_to raise_error
  31. end
  32. 1 context "when some validations fail" do
  33. 1 before do
  34. 2 allow(message).to receive(:to_h).and_return({})
  35. end
  36. 1 context "code_data and scope" do
  37. 1 before do
  38. 1 allow(validator).to receive(:validate_scope).with(message).and_return(false)
  39. 1 allow(validator).to receive(:validate_code_data).with(message).and_return(false)
  40. end
  41. 1 it "raises an error with details" do
  42. 2 expect { validator.validate(message) }.to raise_error(
  43. validation_error, /code_data, scope/
  44. )
  45. end
  46. end
  47. 1 context "code and scope_data" do
  48. 1 before do
  49. 1 allow(validator).to receive(:validate_code).with(message).and_return(false)
  50. 1 allow(validator).to receive(:validate_scope_data).with(message).and_return(false)
  51. end
  52. 1 it "raises an error with details" do
  53. 2 expect { validator.validate(message) }.to raise_error(
  54. validation_error, /code, scope_data/
  55. )
  56. end
  57. end
  58. end
  59. end
  60. 1 context "when validating a cell message" do
  61. 1 let(:validator_class) do
  62. 4 Class.new(described_class) { cell }
  63. end
  64. 1 it "validates an exact scope" do
  65. 1 msg1 = instance_double(message_class, scope: scopes::CELL)
  66. 1 msg2 = instance_double(message_class, scope: double)
  67. 1 expect(validator.validate_scope(msg1)).to be true
  68. 1 expect(validator.validate_scope(msg2)).to be false
  69. end
  70. 1 it "validates a subset of scope_data" do
  71. 1 msg1 = instance_double(message_class, scope_data: { col: "AD", row: 7 })
  72. 1 msg2 = instance_double(message_class, scope_data: { col: 7, row: "AD" })
  73. 1 expect(validator.validate_scope_data(msg1)).to be true
  74. 1 expect(validator.validate_scope_data(msg2)).to be false
  75. end
  76. end
  77. 1 context "when validating a row message" do
  78. 1 let(:validator_class) do
  79. 4 Class.new(described_class) { row }
  80. end
  81. 1 it "validates an exact scope" do
  82. 1 msg1 = instance_double(message_class, scope: scopes::ROW)
  83. 1 msg2 = instance_double(message_class, scope: double)
  84. 1 expect(validator.validate_scope(msg1)).to be true
  85. 1 expect(validator.validate_scope(msg2)).to be false
  86. end
  87. 1 it "validates a subset of scope_data" do
  88. 1 msg1 = instance_double(message_class, scope_data: { row: 7 })
  89. 1 msg2 = instance_double(message_class, scope_data: { row: "AD" })
  90. 1 expect(validator.validate_scope_data(msg1)).to be true
  91. 1 expect(validator.validate_scope_data(msg2)).to be false
  92. end
  93. end
  94. 1 context "when validating a col message" do
  95. 1 let(:validator_class) do
  96. 4 Class.new(described_class) { col }
  97. end
  98. 1 it "validates an exact scope" do
  99. 1 msg1 = instance_double(message_class, scope: scopes::COL)
  100. 1 msg2 = instance_double(message_class, scope: double)
  101. 1 expect(validator.validate_scope(msg1)).to be true
  102. 1 expect(validator.validate_scope(msg2)).to be false
  103. end
  104. 1 it "validates a subset of scope_data" do
  105. 1 msg1 = instance_double(message_class, scope_data: { col: "AD" })
  106. 1 msg2 = instance_double(message_class, scope_data: { col: 7 })
  107. 1 expect(validator.validate_scope_data(msg1)).to be true
  108. 1 expect(validator.validate_scope_data(msg2)).to be false
  109. end
  110. end
  111. 1 context "when validating a table message" do
  112. 1 let(:validator_class) do
  113. 4 Class.new(described_class) { table }
  114. end
  115. 1 it "validates an exact scope" do
  116. 1 msg1 = instance_double(message_class, scope: scopes::TABLE)
  117. 1 msg2 = instance_double(message_class, scope: double)
  118. 1 expect(validator.validate_scope(msg1)).to be true
  119. 1 expect(validator.validate_scope(msg2)).to be false
  120. end
  121. 1 it "validates the absence of scope_data" do
  122. 1 msg1 = instance_double(message_class, scope_data: nil)
  123. 1 msg2 = instance_double(message_class, scope_data: double)
  124. 1 expect(validator.validate_scope_data(msg1)).to be true
  125. 1 expect(validator.validate_scope_data(msg2)).to be false
  126. end
  127. end
  128. end

spec/tabulard/messaging/validations_spec.rb

100.0% lines covered

100.0% branches covered

40 relevant lines. 40 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging/validations"
  3. 1 RSpec.describe Tabulard::Messaging::Validations do
  4. 1 let(:message_class) do
  5. 9 Struct.new(
  6. :code,
  7. :code_data,
  8. :scope,
  9. :scope_data,
  10. keyword_init: true
  11. )
  12. end
  13. 1 before do
  14. 9 message_class.include(described_class)
  15. end
  16. 1 it "doesn't define a validator by default" do
  17. 1 expect(message_class.validator).to be_nil
  18. end
  19. 1 it "doesn't validate by default" do
  20. 1 message = double
  21. 2 expect { message_class.validate(message) }.not_to raise_error
  22. end
  23. 1 it "delegates instance validations to the class" do
  24. 1 message = message_class.new
  25. 1 allow(message_class).to receive(:validate).with(message).and_return(result = double)
  26. 1 expect(message.validate).to eq(result)
  27. end
  28. 1 context "when a validator is defined" do
  29. 1 it "provides a kind of BaseValidator" do
  30. 1 message_class.def_validator
  31. 1 expect(message_class.validator).to be_a(described_class::BaseValidator)
  32. end
  33. 1 it "may provide a different kind of validator" do
  34. 1 message_class.def_validator(base: validator_class = Class.new)
  35. 1 expect(message_class.validator).to be_a(validator_class)
  36. end
  37. 1 it "validates" do
  38. 2 message_class.def_validator { table }
  39. 1 message = message_class.new(scope: "bar")
  40. 2 expect { message_class.validate(message) }.to raise_error(described_class::InvalidMessage)
  41. end
  42. 1 context "when a message class is inherited" do
  43. 1 let(:message_subclass) do
  44. 3 Class.new(message_class)
  45. end
  46. 1 before do
  47. 3 message_class.def_validator
  48. end
  49. 1 it "provides the validator from the superclass" do
  50. 1 expect(message_subclass.validator).to be(message_class.validator)
  51. end
  52. 1 context "when the subclass defines its own validator" do
  53. 1 it "implicitly inherits from the superclass validator" do
  54. 1 message_subclass.def_validator
  55. 1 expect(message_subclass.validator).to be_a(message_class.validator.class)
  56. end
  57. 1 it "may inherit from another validator class" do
  58. 1 message_subclass.def_validator(base: validator_class = Class.new)
  59. 1 expect(message_subclass.validator).to be_a(validator_class)
  60. end
  61. end
  62. end
  63. end
  64. end

spec/tabulard/messaging_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging"
  3. 1 RSpec.describe Tabulard::Messaging do
  4. 1 around do |example|
  5. 3 config = described_class.config
  6. 3 example.run
  7. 3 described_class.config = config
  8. end
  9. 1 describe "::config" do
  10. 1 it "reads a global, frozen instance" do
  11. 1 expect(described_class.config).to be_a(described_class::Config) & be_frozen
  12. end
  13. end
  14. 1 describe "::config=" do
  15. 1 it "writes a global instance" do
  16. 1 described_class.config = (config = double)
  17. 1 expect(described_class.config).to eq(config)
  18. end
  19. end
  20. 1 describe "::configure" do
  21. 2 let(:old) { described_class::Config.new }
  22. 2 let(:new) { described_class::Config.new }
  23. 1 before do
  24. 1 described_class.config = old
  25. 1 allow(old).to receive(:dup).and_return(new)
  26. end
  27. 1 it "modifies a copy of the global instance" do
  28. 1 expect do |b|
  29. 1 described_class.configure(&b)
  30. end.to yield_with_args(new)
  31. 1 expect(described_class.config).to be(new)
  32. end
  33. end
  34. end

spec/tabulard/row_processor_result_spec.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/row_processor_result"
  3. 1 RSpec.describe Tabulard::RowProcessorResult do
  4. 5 let(:row) { double }
  5. 5 let(:result) { double }
  6. 4 let(:messages) { double }
  7. 1 it "wraps a result with messages" do
  8. 1 processor_result = described_class.new(row: row, result: result, messages: messages)
  9. 1 expect(processor_result).to have_attributes(row: row, result: result, messages: messages)
  10. end
  11. 1 it "is equivalent to a similar result with similar messages" do
  12. 1 processor_result0 = described_class.new(row: row, result: result, messages: messages)
  13. 1 processor_result1 = described_class.new(row: row, result: result, messages: messages)
  14. 1 expect(processor_result0).to eq(processor_result1)
  15. end
  16. 1 it "is different from a similar result with a different row" do
  17. 1 processor_result0 = described_class.new(row: row, result: result, messages: messages)
  18. 1 processor_result1 = described_class.new(row: double, result: result, messages: messages)
  19. 1 expect(processor_result0).not_to eq(processor_result1)
  20. end
  21. 1 it "may wrap a result with implicit messages" do
  22. 1 processor_result = described_class.new(row: row, result: result)
  23. 1 expect(processor_result).to have_attributes(row: row, result: result, messages: [])
  24. end
  25. end

spec/tabulard/row_processor_spec.rb

100.0% lines covered

100.0% branches covered

34 relevant lines. 34 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/messaging"
  3. 1 require "tabulard/headers"
  4. 1 require "tabulard/row_processor"
  5. 1 require "tabulard/table"
  6. 1 RSpec.describe Tabulard::RowProcessor, monadic_result: true do
  7. 1 let(:messenger) do
  8. 1 instance_double(Tabulard::Messaging::Messenger, dup: row_messenger)
  9. end
  10. 1 let(:row_messenger) do
  11. 1 Tabulard::Messaging::Messenger.new
  12. end
  13. 1 let(:headers) do
  14. [
  15. 1 instance_double(Tabulard::Headers::Header, column: double, row_value_index: 0),
  16. instance_double(Tabulard::Headers::Header, column: double, row_value_index: 1),
  17. instance_double(Tabulard::Headers::Header, column: double, row_value_index: 2),
  18. ]
  19. end
  20. 1 let(:cells) do
  21. [
  22. 1 instance_double(Tabulard::Table::Cell, value: double, col: double),
  23. instance_double(Tabulard::Table::Cell, value: double, col: double),
  24. instance_double(Tabulard::Table::Cell, value: double, col: double),
  25. ]
  26. end
  27. 1 let(:row) do
  28. 1 instance_double(Tabulard::Table::Row, row: double, value: cells)
  29. end
  30. 1 let(:row_value_builder) do
  31. 1 instance_double(Tabulard::RowValueBuilder)
  32. end
  33. 1 let(:row_value_builder_result) do
  34. 1 double
  35. end
  36. 1 let(:processor) do
  37. 1 described_class.new(headers: headers, messenger: messenger)
  38. end
  39. 1 def expect_headers_add(header, cell)
  40. 3 expect(row_value_builder).to receive(:add).with(header.column, cell.value).ordered do
  41. 3 expect(row_messenger).to have_attributes(
  42. scope: Tabulard::Messaging::SCOPES::CELL,
  43. scope_data: { row: row.row, col: cell.col }
  44. )
  45. end
  46. end
  47. 1 def expect_headers_result
  48. 1 expect(row_value_builder).to receive(:result).ordered.and_return(row_value_builder_result)
  49. end
  50. 1 before do
  51. 1 allow(Tabulard::RowValueBuilder).to(
  52. receive(:new).with(row_messenger).and_return(row_value_builder)
  53. )
  54. end
  55. 1 it "processes the row and wraps the result with a dedicated set of messages" do
  56. 1 expect_headers_add(headers[0], cells[0])
  57. 1 expect_headers_add(headers[1], cells[1])
  58. 1 expect_headers_add(headers[2], cells[2])
  59. 1 expect_headers_result
  60. 1 expect(processor.call(row)).to eq(
  61. Tabulard::RowProcessorResult.new(
  62. row: row.row,
  63. result: row_value_builder_result,
  64. messages: row_messenger.messages
  65. )
  66. )
  67. end
  68. end

spec/tabulard/row_value_builder_spec.rb

100.0% lines covered

100.0% branches covered

75 relevant lines. 75 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/row_value_builder"
  3. 1 require "tabulard/column"
  4. 1 require "tabulard/types/scalars/scalar"
  5. 1 RSpec.describe Tabulard::RowValueBuilder, monadic_result: true do
  6. 1 let(:builder) do
  7. 6 described_class.new(messenger)
  8. end
  9. 7 let(:messenger) { double }
  10. 4 let(:scalar_type) { instance_double(Tabulard::Types::Type, composite?: false) }
  11. 4 let(:scalar_key) { double }
  12. 5 let(:composite_type) { instance_double(Tabulard::Types::Type, composite?: true) }
  13. 5 let(:composite_key) { double }
  14. 1 def stub_scalar(column, value, result)
  15. 8 allow(column.type).to receive(:scalar).with(column.index, value, messenger).and_return(result)
  16. end
  17. 1 def stub_composite(type, value, result)
  18. 3 allow(type).to receive(:composite).with(value, messenger).and_return(result)
  19. end
  20. 1 context "when the column type is scalar" do
  21. 1 let(:column) do
  22. 2 instance_double(Tabulard::Column, type: scalar_type, key: scalar_key, index: nil)
  23. end
  24. 3 let(:input) { double }
  25. 2 let(:output) { double }
  26. 1 context "when the scalar type casting succeeds" do
  27. 2 before { stub_scalar(column, input, Success(output)) }
  28. 1 it "returns Success results wrapping type casted values" do
  29. 1 result = builder.add(column, input)
  30. 1 expect(result).to eq(Success(output))
  31. 1 expect(builder.result).to eq(Success(column.key => output))
  32. end
  33. end
  34. 1 context "when the scalar type casting fails" do
  35. 2 before { stub_scalar(column, input, Failure()) }
  36. 1 it "returns Failure results" do
  37. 1 result = builder.add(column, input)
  38. 1 expect(result).to eq(Failure())
  39. 1 expect(builder.result).to eq(Failure())
  40. end
  41. end
  42. end
  43. 1 context "when the column type is composite" do
  44. 1 let(:column) do
  45. 3 instance_double(Tabulard::Column, type: composite_type, key: composite_key, index: 0)
  46. end
  47. 4 let(:scalar_input) { double }
  48. 3 let(:scalar_output) { double }
  49. 1 context "when the scalar type casting succeeds" do
  50. 3 before { stub_scalar(column, scalar_input, Success(scalar_output)) }
  51. 1 context "when the composite type casting succeeds" do
  52. 2 let(:composite_output) { double }
  53. 1 before do
  54. 1 stub_composite(composite_type, [scalar_output], Success(composite_output))
  55. end
  56. 1 it "returns Success results wrapping type casted values" do
  57. 1 result = builder.add(column, scalar_input)
  58. 1 expect(result).to eq(Success(scalar_output))
  59. 1 expect(builder.result).to eq(Success(column.key => composite_output))
  60. end
  61. end
  62. 1 context "when the composite type casting fails" do
  63. 2 before { stub_composite(composite_type, [scalar_output], Failure()) }
  64. 1 it "returns Success and Failure appropriately" do
  65. 1 result = builder.add(column, scalar_input)
  66. 1 expect(result).to eq(Success(scalar_output))
  67. 1 expect(builder.result).to eq(Failure())
  68. end
  69. end
  70. end
  71. 1 context "when the scalar type casting fails" do
  72. 2 before { stub_scalar(column, scalar_input, Failure()) }
  73. 1 it "returns Failure results" do
  74. 1 result = builder.add(column, scalar_input)
  75. 1 expect(result).to eq(Failure())
  76. 1 expect(builder.result).to eq(Failure())
  77. end
  78. end
  79. end
  80. 1 context "when handling multiple columns in any order" do
  81. 1 let(:column0) do
  82. 1 instance_double(Tabulard::Column, type: composite_type, key: composite_key, index: 2)
  83. end
  84. 1 let(:column1) do
  85. 1 instance_double(Tabulard::Column, type: composite_type, key: composite_key, index: 1)
  86. end
  87. 1 let(:column2) do
  88. 1 instance_double(Tabulard::Column, type: scalar_type, key: scalar_key, index: nil)
  89. end
  90. 1 it "reduces them to a correctly typed aggregate" do
  91. 1 stub_scalar(column0, in0 = double, Success(out0 = double))
  92. 1 stub_scalar(column1, in1 = double, Success(out1 = double))
  93. 1 stub_scalar(column2, in2 = double, Success(out2 = double))
  94. 1 builder.add(column0, in0)
  95. 1 builder.add(column2, in2)
  96. 1 builder.add(column1, in1)
  97. 1 stub_composite(composite_type, [nil, out1, out0], Success(composite_out = double))
  98. 1 expect(builder.result).to eq(
  99. Success(
  100. composite_key => composite_out,
  101. scalar_key => out2
  102. )
  103. )
  104. end
  105. end
  106. end

spec/tabulard/specification_spec.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/specification"
  3. 1 require "tabulard/column"
  4. 1 RSpec.describe Tabulard::Specification do
  5. 1 describe "#get" do
  6. 1 let(:header) do
  7. 3 "foo"
  8. end
  9. 1 let(:column0) do
  10. 4 instance_double(Tabulard::Column, :column0, header_pattern: double(:pattern0))
  11. end
  12. 1 let(:column1) do
  13. 4 instance_double(Tabulard::Column, :column1, header_pattern: double(:pattern1))
  14. end
  15. 1 let(:spec) do
  16. 4 described_class.new(columns: [column0, column1])
  17. end
  18. 1 before do
  19. 4 allow(column0.header_pattern).to receive(:match?).with(anything).and_return(false)
  20. 4 allow(column1.header_pattern).to receive(:match?).with(anything).and_return(false)
  21. end
  22. 1 it "returns nil when header is nil" do
  23. 1 expect(spec.get(nil)).to be_nil
  24. end
  25. 1 it "may match with a pattern" do
  26. 1 allow(column0.header_pattern).to receive(:match?).with(header).and_return(true)
  27. 1 expect(spec.get(header)).to eq(column0)
  28. end
  29. 1 it "may match with another pattern" do
  30. 1 allow(column1.header_pattern).to receive(:match?).with(header).and_return(true)
  31. 1 expect(spec.get(header)).to eq(column1)
  32. end
  33. 1 it "may not match at all" do
  34. 1 expect(spec.get(header)).to be_nil
  35. end
  36. end
  37. end

spec/tabulard/table_processor_result_spec.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/table_processor_result"
  3. 1 RSpec.describe Tabulard::TableProcessorResult do
  4. 4 let(:result) { double }
  5. 3 let(:messages) { double }
  6. 1 it "wraps a result with messages" do
  7. 1 processor_result = described_class.new(result: result, messages: messages)
  8. 1 expect(processor_result).to have_attributes(result: result, messages: messages)
  9. end
  10. 1 it "is equivalent to a similar result with similar messages" do
  11. 1 processor_result0 = described_class.new(result: result, messages: messages)
  12. 1 processor_result1 = described_class.new(result: result, messages: messages)
  13. 1 expect(processor_result0).to eq(processor_result1)
  14. end
  15. 1 it "may wrap a result with implicit messages" do
  16. 1 processor_result = described_class.new(result: result)
  17. 1 expect(processor_result).to have_attributes(result: result, messages: [])
  18. end
  19. end

spec/tabulard/table_processor_spec.rb

100.0% lines covered

100.0% branches covered

90 relevant lines. 90 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/table_processor"
  3. 1 require "tabulard/specification"
  4. 1 RSpec.describe Tabulard::TableProcessor, monadic_result: true do
  5. 1 let(:specification) do
  6. 5 instance_double(Tabulard::Specification)
  7. end
  8. 1 let(:processor) do
  9. 5 described_class.new(specification)
  10. end
  11. 1 let(:messenger) do
  12. 2 instance_double(Tabulard::Messaging::Messenger, messages: double)
  13. end
  14. 1 let(:table_class) do
  15. 10 Class.new { include Tabulard::Table }
  16. end
  17. 1 let(:adapter_args) do
  18. 5 [double, double]
  19. end
  20. 1 let(:adapter_opts) do
  21. 5 { foo: double, bar: double }
  22. end
  23. 1 let(:table) do
  24. 3 instance_double(table_class)
  25. end
  26. 1 def call(&block)
  27. 4 block ||= proc {} # stub a dummy proc
  28. 4 processor.call(*adapter_args, adapter: table_class, **adapter_opts, &block)
  29. end
  30. 1 def stub_table_open
  31. 4 stub = yield receive(:open).with(*adapter_args, **adapter_opts, messenger: messenger)
  32. 4 allow(table_class).to(stub)
  33. end
  34. 1 def stub_table_open_ok
  35. 6 stub_table_open { _1.and_yield(table) }
  36. end
  37. 1 def stub_table_open_ko
  38. 2 stub_table_open { _1.and_return(Failure()) }
  39. end
  40. 1 before do
  41. 5 allow(Tabulard::Messaging::Messenger).to receive(:new).with(no_args).and_return(messenger)
  42. end
  43. 1 it "passes the args and opts to Adapters.open" do
  44. 1 actual_args = adapter_args
  45. 1 actual_opts = adapter_opts.merge(adapter: table_class, messenger: messenger)
  46. 1 expect(Tabulard::Adapters).to(
  47. receive(:open)
  48. .with(*actual_args, **actual_opts)
  49. .and_return(Success())
  50. )
  51. 1 processor.call(*actual_args, **actual_opts)
  52. end
  53. 1 context "when opening the table fails" do
  54. 1 before do
  55. 1 stub_table_open_ko
  56. end
  57. 1 it "is an empty failure, with messages" do
  58. 1 expect(call).to eq(
  59. Tabulard::TableProcessorResult.new(
  60. result: Failure(),
  61. messages: messenger.messages
  62. )
  63. )
  64. end
  65. end
  66. 1 shared_context "when there is no table error" do
  67. 2 let(:table_headers) do
  68. 9 Array.new(2) { instance_double(Tabulard::Table::Header) }
  69. end
  70. 2 let(:table_rows) do
  71. 12 Array.new(3) { instance_double(Tabulard::Table::Row) }
  72. end
  73. 2 let(:messenger) do
  74. 3 instance_double(Tabulard::Messaging::Messenger, messages: double)
  75. end
  76. 2 let(:headers) do
  77. 3 instance_double(Tabulard::Headers)
  78. end
  79. 2 def stub_enumeration(obj, method_name, enumerable)
  80. 6 enum = Enumerator.new do |yielder|
  81. 17 enumerable.each { |item| yielder << item }
  82. 5 obj
  83. end
  84. 6 allow(obj).to receive(method_name).with(no_args) do |&block|
  85. 5 enum.each(&block)
  86. end
  87. end
  88. 2 def stub_headers
  89. 3 allow(Tabulard::Headers).to(
  90. receive(:new)
  91. .with(specification: specification, messenger: messenger)
  92. .and_return(headers)
  93. )
  94. end
  95. 2 def stub_headers_ops(result)
  96. 3 table_headers.each do |table_header|
  97. 6 expect(headers).to receive(:add).with(table_header).ordered
  98. end
  99. 3 expect(headers).to receive(:result).and_return(result).ordered
  100. end
  101. 2 before do
  102. 3 stub_headers
  103. 3 stub_table_open_ok
  104. 3 stub_enumeration(table, :each_header, table_headers)
  105. 3 stub_enumeration(table, :each_row, table_rows)
  106. end
  107. end
  108. 1 context "when there is a header error" do
  109. 1 include_context "when there is no table error"
  110. 1 before do
  111. 1 stub_headers_ops(Failure())
  112. end
  113. 1 it "is an empty failure, with messages" do
  114. 1 result = call
  115. 1 expect(result).to eq(
  116. Tabulard::TableProcessorResult.new(
  117. result: Failure(),
  118. messages: messenger.messages
  119. )
  120. )
  121. end
  122. end
  123. 1 context "when there is no error" do
  124. 1 include_context "when there is no table error"
  125. 1 let(:headers_spec) do
  126. 2 double
  127. end
  128. 1 let(:processed_rows) do
  129. 8 Array.new(table_rows.size) { double }
  130. end
  131. 1 def stub_row_processing
  132. 2 allow(Tabulard::RowProcessor).to(
  133. receive(:new)
  134. .with(headers: headers_spec, messenger: messenger)
  135. .and_return(row_processor = instance_double(Tabulard::RowProcessor))
  136. )
  137. 2 table_rows.zip(processed_rows) do |row, processed_row|
  138. 6 allow(row_processor).to receive(:call).with(row).and_return(processed_row)
  139. end
  140. end
  141. 1 before do
  142. 2 stub_headers_ops(Success(headers_spec))
  143. 2 stub_row_processing
  144. end
  145. 1 it "is an empty success, with messages" do
  146. 1 result = call
  147. 1 expect(result).to eq(
  148. Tabulard::TableProcessorResult.new(
  149. result: Success(),
  150. messages: messenger.messages
  151. )
  152. )
  153. end
  154. 1 it "yields each processed row" do
  155. 2 expect { |b| call(&b) }.to yield_successive_args(*processed_rows)
  156. end
  157. end
  158. end

spec/tabulard/table_spec.rb

100.0% lines covered

100.0% branches covered

158 relevant lines. 158 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/table"
  3. 1 RSpec.describe Tabulard::Table, monadic_result: true do
  4. 1 let(:table_class) do
  5. 25 c = Class.new do
  6. 25 def initialize(foo, bar:)
  7. 5 @foo = foo
  8. 5 @bar = bar
  9. end
  10. end
  11. 25 c.include(described_class)
  12. 25 stub_const("TableClass", c)
  13. 25 c
  14. end
  15. 1 let(:table) do
  16. 5 table_class.new(foo, bar: bar)
  17. end
  18. 17 let(:foo) { double }
  19. 17 let(:bar) { double }
  20. 1 describe "::Error" do
  21. 2 subject { table_class::Error }
  22. 1 it "exposes some kind of Tabulard::Errors::Error" do
  23. 1 expect(subject.superclass).to be(Tabulard::Errors::Error)
  24. end
  25. end
  26. 1 describe "::ClosureError" do
  27. 2 subject { table_class::ClosureError }
  28. 1 it "exposes some kind of Tabulard::Table::Error" do
  29. 1 expect(subject.superclass).to be(Tabulard::Table::Error)
  30. end
  31. end
  32. 1 describe "::InputError" do
  33. 2 subject { table_class::InputError }
  34. 1 it "exposes some kind of Tabulard::Table::Error" do
  35. 1 expect(subject.superclass).to be(Tabulard::Table::Error)
  36. end
  37. end
  38. 1 describe "::Header" do
  39. 3 let(:col) { double }
  40. 3 let(:val) { double }
  41. 3 let(:wrapper) { table_class::Header.new(col: col, value: val) }
  42. 1 it "exposes a header wrapper" do
  43. 1 expect(wrapper).to have_attributes(col: col, value: val)
  44. end
  45. 1 it "is comparable" do
  46. 1 expect(wrapper).to eq(table_class::Header.new(col: col, value: val))
  47. 1 expect(wrapper).not_to eq(table_class::Header.new(col: double, value: val))
  48. end
  49. end
  50. 1 describe "::Row" do
  51. 3 let(:row) { double }
  52. 3 let(:val) { double }
  53. 3 let(:wrapper) { table_class::Row.new(row: row, value: val) }
  54. 1 it "exposes a row wrapper" do
  55. 1 expect(wrapper).to have_attributes(row: row, value: val)
  56. end
  57. 1 it "is comparable" do
  58. 1 expect(wrapper).to eq(table_class::Row.new(row: row, value: val))
  59. 1 expect(wrapper).not_to eq(table_class::Row.new(row: double, value: val))
  60. end
  61. end
  62. 1 describe "::Cell" do
  63. 3 let(:row) { double }
  64. 3 let(:col) { double }
  65. 3 let(:val) { double }
  66. 3 let(:wrapper) { table_class::Cell.new(row: row, col: col, value: val) }
  67. 1 it "exposes a row wrapper" do
  68. 1 expect(wrapper).to have_attributes(row: row, col: col, value: val)
  69. end
  70. 1 it "is comparable" do
  71. 1 expect(wrapper).to eq(table_class::Cell.new(row: row, col: col, value: val))
  72. 1 expect(wrapper).not_to eq(table_class::Cell.new(row: double, col: col, value: val))
  73. end
  74. end
  75. 1 describe "singleton class methods" do
  76. 1 let(:samples) do
  77. [
  78. 2 ["A", 1],
  79. ["B", 2],
  80. ["Z", 26],
  81. ["AA", 27],
  82. ["AZ", 52],
  83. ["BA", 53],
  84. ["ZA", 677],
  85. ["ZZ", 702],
  86. ["AAA", 703],
  87. ["AAZ", 728],
  88. ["ABA", 729],
  89. ["BZA", 2029],
  90. ]
  91. end
  92. 1 describe "::col2int" do
  93. 1 it "turns letter-based indexes into integer-based indexes" do
  94. 1 samples.each do |(col, int)|
  95. 12 res = described_class.col2int(col)
  96. 12 expect(res).to eq(int), "Expected #{col} => #{int}, got: #{res}"
  97. end
  98. end
  99. 1 it "fails on invalid inputs" do
  100. 2 expect { described_class.col2int(nil) }.to raise_error(ArgumentError)
  101. 2 expect { described_class.col2int("") }.to raise_error(ArgumentError)
  102. 2 expect { described_class.col2int("a") }.to raise_error(ArgumentError)
  103. 2 expect { described_class.col2int("€") }.to raise_error(ArgumentError)
  104. end
  105. end
  106. 1 describe "::int2col" do
  107. 1 it "turns integer-based indexes into letter-based indexes" do
  108. 1 samples.each do |(col, int)|
  109. 12 res = described_class.int2col(int)
  110. 12 expect(res).to eq(col), "Expected #{int} => #{col}, got: #{res}"
  111. end
  112. end
  113. 1 it "fails on invalid inputs" do
  114. 2 expect { described_class.int2col(nil) }.to raise_error(ArgumentError)
  115. 2 expect { described_class.int2col(0) }.to raise_error(ArgumentError)
  116. 2 expect { described_class.int2col(-12) }.to raise_error(ArgumentError)
  117. 2 expect { described_class.int2col(27.0) }.to raise_error(ArgumentError)
  118. end
  119. end
  120. end
  121. 1 describe "::open" do
  122. 1 let(:table) do
  123. 11 instance_double(table_class)
  124. end
  125. 1 before do
  126. 11 allow(table_class).to receive(:new).with(foo, bar: bar).and_return(table)
  127. end
  128. 1 context "without a block" do
  129. 1 it "returns a new table wrapped as a Success" do
  130. 1 expect(table_class.open(foo, bar: bar)).to eq(Success(table))
  131. end
  132. end
  133. 1 context "with a block" do
  134. 1 before do
  135. 10 allow(table).to receive(:close)
  136. end
  137. 1 it "yields a new table" do
  138. 1 yielded = false
  139. 1 table_class.open(foo, bar: bar) do |opened_table|
  140. 1 yielded = true
  141. 1 expect(opened_table).to be(table)
  142. end
  143. 1 expect(yielded).to be(true)
  144. end
  145. 1 it "returns the value of the block" do
  146. 1 block_result = double
  147. 2 actual_block_result = table_class.open(foo, bar: bar) { block_result }
  148. 1 expect(actual_block_result).to eq(block_result)
  149. end
  150. 1 it "closes after yielding" do
  151. 1 table_class.open(foo, bar: bar) do
  152. 1 expect(table).not_to have_received(:close)
  153. end
  154. 1 expect(table).to have_received(:close)
  155. end
  156. 1 context "when an exception is raised" do
  157. 3 let(:exception) { Class.new(StandardError) }
  158. 3 let(:error) { Class.new(Tabulard::Table::Error) }
  159. 4 let(:input_error) { Class.new(Tabulard::Table::InputError) }
  160. 1 context "without yielding control" do
  161. 1 it "doesn't rescue an exception" do
  162. 1 allow(table_class).to receive(:new).and_raise(exception)
  163. 1 expect do
  164. 1 table_class.open(foo, bar: bar)
  165. end.to raise_error(exception)
  166. end
  167. 1 it "doesn't rescue an error" do
  168. 1 allow(table_class).to receive(:new).and_raise(error)
  169. 1 expect do
  170. 1 table_class.open(foo, bar: bar)
  171. end.to raise_error(error)
  172. end
  173. 1 it "rescues an input error and returns a failure" do
  174. 1 allow(table_class).to receive(:new).and_raise(input_error.exception)
  175. 1 result = table_class.open(foo, bar: bar)
  176. 1 expect(result).to eq(Failure())
  177. end
  178. end
  179. 1 context "while yielding control" do
  180. 1 it "doesn't rescue but closes after an exception is raised" do
  181. 1 expect do
  182. 1 table_class.open(foo, bar: bar) do
  183. 1 expect(table).not_to have_received(:close)
  184. 1 raise exception
  185. end
  186. end.to raise_error(exception)
  187. 1 expect(table).to have_received(:close)
  188. end
  189. 1 it "doesn't rescue but closes after an error is raised" do
  190. 1 expect do
  191. 1 table_class.open(foo, bar: bar) do
  192. 1 expect(table).not_to have_received(:close)
  193. 1 raise error
  194. end
  195. end.to raise_error(error)
  196. 1 expect(table).to have_received(:close)
  197. end
  198. 1 it "rescues and closes after an input error is raised" do
  199. 1 table_class.open(foo, bar: bar) do
  200. 1 expect(table).not_to have_received(:close)
  201. 1 raise input_error
  202. end
  203. 1 expect(table).to have_received(:close)
  204. end
  205. 1 it "rescues and returns an empty failure after an input error is raised" do
  206. 1 e = input_error.exception # raise the instance directly to simplify result matching
  207. 1 result = table_class.open(foo, bar: bar) do
  208. 1 raise e
  209. end
  210. 1 expect(result).to eq(Failure())
  211. end
  212. end
  213. end
  214. end
  215. end
  216. 1 describe "#each_header" do
  217. 1 it "is abstract" do
  218. 2 expect { table.each_header }.to raise_error(
  219. NoMethodError, "You must implement TableClass#each_header => self"
  220. )
  221. end
  222. end
  223. 1 describe "#each_row" do
  224. 1 it "is abstract" do
  225. 2 expect { table.each_row }.to raise_error(
  226. NoMethodError, "You must implement TableClass#each_row => self"
  227. )
  228. end
  229. end
  230. 1 describe "#close" do
  231. 4 before { table }
  232. 1 it "removes the instance variables" do
  233. 2 expect { table.close }.to [
  234. 2 change { table.instance_variable_defined?(:@foo) }.from(true).to(false),
  235. 2 change { table.instance_variable_defined?(:@bar) }.from(true).to(false),
  236. ].reduce(:&)
  237. end
  238. 1 it "marks the instance as closed" do
  239. 2 expect { table.close }.to change(table, :closed?).from(false).to(true)
  240. end
  241. 1 it "returns nil" do
  242. 1 expect(table.close).to be_nil
  243. end
  244. end
  245. end

spec/tabulard/template_config_spec.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/template_config"
  3. 1 RSpec.describe Tabulard::TemplateConfig do
  4. 6 let(:config) { described_class.new }
  5. 1 describe "#header" do
  6. 1 it "produces a safe pattern from a header with special characters" do
  7. 1 header, pattern = config.header(".*", nil)
  8. 1 expect(pattern).to match(header)
  9. 1 expect(pattern).not_to match("foo")
  10. end
  11. 1 it "produces a case-insensitive pattern" do
  12. 1 header, pattern = config.header(:foo, nil)
  13. 1 expect(pattern).to match(header.downcase)
  14. 1 expect(pattern).to match(header.upcase)
  15. end
  16. 1 it "produces a strict pattern" do
  17. 1 header, pattern = config.header(:foo, nil)
  18. 1 expect(pattern).not_to match("#{header}foo")
  19. 1 expect(pattern).not_to match("foo#{header}")
  20. end
  21. 1 context "when the index is nil" do
  22. 1 it "capitalizes the key" do
  23. 1 header, = config.header(:foo, nil)
  24. 1 expect(header).to eq("Foo")
  25. end
  26. end
  27. 1 context "when the index is not nil" do
  28. 1 it "capitalizes the key and appends a 1-based index" do
  29. 1 header, = config.header(:foo, 3)
  30. 1 expect(header).to eq("Foo 4")
  31. end
  32. end
  33. end
  34. end

spec/tabulard/template_spec.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/template"
  3. 1 require "tabulard/attribute"
  4. 1 require "tabulard/errors/spec_error"
  5. 1 RSpec.describe Tabulard::Template do
  6. 1 let(:attributes_args) do
  7. [
  8. 4 { key: :key0, type: :type0 },
  9. { key: :key1, type: :type1 },
  10. ]
  11. end
  12. 1 let(:attributes) do
  13. 3 attributes_args.map do |args|
  14. 6 Tabulard::Attribute.build(**args)
  15. end
  16. end
  17. 1 it "doesn't ignore unspecified columns by default" do
  18. 1 template = described_class.new(attributes: attributes, ignore_unspecified_columns: false)
  19. 1 expect(template).to eq(described_class.new(attributes: attributes))
  20. end
  21. 1 context "when an attribute is duplicated" do
  22. 1 it "can't be initialized" do
  23. 1 expect do
  24. 1 described_class.new(attributes: [attributes[0], attributes[0]])
  25. end.to raise_error(Tabulard::Errors::SpecError, "Duplicated key: :key0")
  26. end
  27. end
  28. 1 describe "::build" do
  29. 1 it "simplifies initialization" do
  30. 1 template = described_class.build(attributes: attributes_args)
  31. 1 expect(template).to eq(described_class.new(attributes: attributes))
  32. end
  33. 1 it "freezes the resulting instance" do
  34. 1 template = described_class.build(attributes: attributes_args)
  35. 1 expect(template).to be_frozen
  36. end
  37. end
  38. end

spec/tabulard/types/cast_chain_spec.rb

100.0% lines covered

100.0% branches covered

77 relevant lines. 77 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/cast_chain"
  3. 1 RSpec.describe Tabulard::Types::CastChain do
  4. 1 let(:cast_interface) do
  5. 9 Class.new do
  6. 9 def call(_value, _messenger); end
  7. end
  8. end
  9. 1 let(:cast) do
  10. 1 cast_interface.new
  11. end
  12. 5 let(:cast0) { cast_double }
  13. 5 let(:cast1) { cast_double }
  14. 4 let(:cast2) { cast_double }
  15. 1 let(:chain) do
  16. 2 described_class.new([cast0, cast1])
  17. end
  18. 1 def cast_double
  19. 15 instance_double(cast_interface)
  20. end
  21. 1 describe "#initialize" do
  22. 1 it "builds an empty chain by default" do
  23. 1 chain = described_class.new
  24. 1 expect(chain.casts).to be_empty
  25. end
  26. 1 it "builds a non-empty chain using the optional parameter" do
  27. 1 chain = described_class.new([cast0, cast1])
  28. 1 expect(chain.casts).to eq([cast0, cast1])
  29. end
  30. end
  31. 1 describe "#prepend" do
  32. 1 it "prepends a cast to the chain" do
  33. 1 chain.prepend(cast2)
  34. 1 expect(chain.casts).to eq([cast2, cast0, cast1])
  35. end
  36. end
  37. 1 describe "#appends" do
  38. 1 it "appends a cast to the chain" do
  39. 1 chain.append(cast2)
  40. 1 expect(chain.casts).to eq([cast0, cast1, cast2])
  41. end
  42. end
  43. 1 describe "#freeze" do
  44. 1 it "freezes the whole chain" do
  45. 1 chain = described_class.new([cast.dup, cast.dup])
  46. 1 chain.freeze
  47. 1 expect(chain.casts).to all(be_frozen)
  48. 1 expect(chain.casts).to be_frozen
  49. 1 expect(chain).to be_frozen
  50. end
  51. end
  52. 1 describe "#call", monadic_result: true do
  53. 1 let(:messenger) do
  54. 5 double
  55. end
  56. 1 it "maps the value and passes the messenger to all casts" do
  57. 1 value0 = double
  58. 1 expect(cast0).to(receive(:call).with(value0, messenger).and_return(value1 = double))
  59. 1 expect(cast1).to(receive(:call).with(value1, messenger).and_return(value2 = double))
  60. 1 expect(cast2).to(receive(:call).with(value2, messenger).and_return(value3 = double))
  61. 1 chain = described_class.new([cast0, cast1, cast2])
  62. 1 result = chain.call(value0, messenger)
  63. 1 expect(result).to eq(Success(value3))
  64. end
  65. 1 context "when a cast throws :success without value" do
  66. 1 it "halts the chain and returns Success(nil)" do
  67. 1 chain = described_class.new [
  68. 1 ->(value, _messenger) { value.capitalize },
  69. 1 ->(_value, _messenger) { throw :success },
  70. cast_double,
  71. ]
  72. 1 result = chain.call("foo", messenger)
  73. 1 expect(result).to eq(Success(nil))
  74. end
  75. end
  76. 1 context "when a cast throws :success with a value" do
  77. 1 it "halts the chain and returns Success(<value>)" do
  78. 1 chain = described_class.new [
  79. 1 ->(value, _messenger) { value.capitalize },
  80. 1 ->(_value, _messenger) { throw :success, "bar" },
  81. cast_double,
  82. ]
  83. 1 result = chain.call("foo", messenger)
  84. 1 expect(result).to eq(Success("bar"))
  85. end
  86. end
  87. 1 context "when a cast throws :failure without a value" do
  88. 1 it "halts the chain and returns Failure()" do
  89. 1 chain = described_class.new [
  90. 1 ->(value, _messenger) { value.capitalize },
  91. 1 ->(_value, _messenger) { throw :failure },
  92. cast_double,
  93. ]
  94. 1 result = chain.call("foo", messenger)
  95. 1 expect(result).to eq(Failure())
  96. end
  97. end
  98. 1 context "when a cast throws :failure with a value" do
  99. 1 it "halts the chain, adds a <value> message as an error and returns Failure()" do
  100. 1 chain = described_class.new [
  101. 1 ->(value, _messenger) { value.capitalize },
  102. 1 ->(_value, _messenger) { throw :failure, "some_code" },
  103. cast_double,
  104. ]
  105. 1 allow(messenger).to receive(:error)
  106. 1 result = chain.call("foo", messenger)
  107. 1 expect(result).to eq(Failure())
  108. 1 expect(messenger).to have_received(:error).with("some_code")
  109. end
  110. end
  111. end
  112. end

spec/tabulard/types/composites/array_compact_spec.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/composites/array_compact"
  3. 1 require "support/shared/composite_type"
  4. 1 require "support/shared/cast_class"
  5. 1 RSpec.describe Tabulard::Types::Composites::ArrayCompact do
  6. 1 include_examples "composite_type"
  7. 1 it "inherits from the composite array type" do
  8. 1 expect(described_class.superclass).to be(Tabulard::Types::Composites::Array)
  9. end
  10. 1 describe "custom cast class" do
  11. 1 subject(:cast_class) do
  12. 4 described_class.cast_classes.last
  13. end
  14. 1 it "is appended to the superclass' cast classes" do
  15. 1 expect(described_class.cast_classes).to eq(
  16. described_class.superclass.cast_classes + [cast_class]
  17. )
  18. end
  19. 1 include_examples "cast_class"
  20. 1 describe "#call" do
  21. 1 it "compacts the given value" do
  22. 1 allow(value).to receive(:compact).and_return(compact_value = double)
  23. 1 expect(cast.call(value, messenger)).to eq(compact_value)
  24. end
  25. end
  26. end
  27. end

spec/tabulard/types/composites/array_spec.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/composites/array"
  3. 1 require "support/shared/composite_type"
  4. 1 require "support/shared/cast_class"
  5. 1 RSpec.describe Tabulard::Types::Composites::Array do
  6. 1 include_examples "composite_type"
  7. 1 it "inherits from the basic composite type" do
  8. 1 expect(described_class.superclass).to be(Tabulard::Types::Composites::Composite)
  9. end
  10. 1 describe "custom cast class" do
  11. 1 subject(:cast_class) do
  12. 5 described_class.cast_classes.last
  13. end
  14. 1 it "is appended to the superclass' cast classes" do
  15. 1 expect(described_class.cast_classes).to eq(
  16. described_class.superclass.cast_classes + [cast_class]
  17. )
  18. end
  19. 1 include_examples "cast_class"
  20. 1 describe "#call" do
  21. 1 before do
  22. 2 allow(value).to receive(:is_a?).with(Array).and_return(value_is_array)
  23. end
  24. 1 context "when the value is an array" do
  25. 2 let(:value_is_array) { true }
  26. 1 it "is a success" do
  27. 1 expect(cast.call(value, messenger)).to eq(value)
  28. end
  29. end
  30. 1 context "when the value is not an array" do
  31. 2 let(:value_is_array) { false }
  32. 1 it "is a failure" do
  33. 1 expect do
  34. 1 cast.call(value, messenger)
  35. end.to throw_symbol(:failure, Tabulard::Messaging::Messages::MustBeArray.new)
  36. end
  37. end
  38. end
  39. end
  40. end

spec/tabulard/types/composites/composite_spec.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/composites/composite"
  3. 1 require "support/shared/composite_type"
  4. 1 RSpec.describe Tabulard::Types::Composites::Composite do
  5. 1 include_examples "composite_type"
  6. 1 it "inherits from the basic type" do
  7. 1 expect(described_class.superclass).to be(Tabulard::Types::Type)
  8. end
  9. 1 describe "::cast_classes" do
  10. 1 it "is empty" do
  11. 1 expect(described_class.cast_classes).to be_empty
  12. end
  13. end
  14. end

spec/tabulard/types/container_spec.rb

100.0% lines covered

100.0% branches covered

83 relevant lines. 83 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/container"
  3. 1 RSpec.describe Tabulard::Types::Container do
  4. 1 let(:default_scalars) do
  5. 3 %i[scalar string email boolsy date_string]
  6. end
  7. 1 let(:default_composites) do
  8. 3 %i[array array_compact]
  9. end
  10. 1 context "when used by default" do
  11. 9 subject(:container) { described_class.new }
  12. 1 it "knows about some types" do
  13. 1 expect(container.scalars).to match_array(default_scalars)
  14. 1 expect(container.composites).to match_array(default_composites)
  15. end
  16. 1 describe "typemap" do
  17. 1 let(:scalar_type) do
  18. 1 Tabulard::Types::Scalars::Scalar
  19. end
  20. 1 let(:string_type) do
  21. 3 Tabulard::Types::Scalars::String
  22. end
  23. 1 let(:email_type) do
  24. 3 Tabulard::Types::Scalars::Email
  25. end
  26. 1 let(:boolsy_type) do
  27. 1 Tabulard::Types::Scalars::Boolsy
  28. end
  29. 1 let(:date_string_type) do
  30. 1 Tabulard::Types::Scalars::DateString
  31. end
  32. 1 def stub_new_type(klass, *args)
  33. 2 allow(klass).to receive(:new!).with(*args).and_return(instance = double)
  34. 2 instance
  35. end
  36. 1 it "is readable" do
  37. 1 expect(described_class::DEFAULTS).to match(
  38. scalars: include(*default_scalars) & be_frozen,
  39. composites: include(*default_composites) & be_frozen
  40. ) & be_frozen
  41. end
  42. 1 example "scalars: scalar" do
  43. 1 expect(scalar = container.scalar(:scalar)).to be_a(scalar_type) & be_frozen
  44. 1 expect(container.scalar(:scalar)).to be(scalar)
  45. end
  46. 1 example "scalars: string" do
  47. 1 expect(string = container.scalar(:string)).to be_a(string_type) & be_frozen
  48. 1 expect(container.scalar(:string)).to be(string)
  49. end
  50. 1 example "scalars: email" do
  51. 1 expect(email = container.scalar(:email)).to be_a(email_type) & be_frozen
  52. 1 expect(container.scalar(:email)).to be(email)
  53. end
  54. 1 example "scalars: boolsy" do
  55. 1 expect(boolsy = container.scalar(:boolsy)).to be_a(boolsy_type) & be_frozen
  56. 1 expect(container.scalar(:boolsy)).to be(boolsy)
  57. end
  58. 1 example "scalars: date_string" do
  59. 1 expect(date_string = container.scalar(:date_string)).to be_a(date_string_type) & be_frozen
  60. 1 expect(container.scalar(:date_string)).to be(date_string)
  61. end
  62. 1 example "composites: array" do
  63. 1 type = stub_new_type(Tabulard::Types::Composites::Array, [string_type, email_type])
  64. 1 expect(container.composite(:array, %i[string email])).to be(type)
  65. end
  66. 1 example "composites: array_composite" do
  67. 1 type = stub_new_type(Tabulard::Types::Composites::ArrayCompact, [string_type, email_type])
  68. 1 expect(container.composite(:array_compact, %i[string email])).to be(type)
  69. end
  70. end
  71. end
  72. 1 context "when extended" do
  73. 4 let(:foo_type) { double }
  74. 3 let(:bar_type) { double }
  75. 2 let(:baz_type) { double }
  76. 2 let(:oof_type) { double }
  77. 1 let(:container) do
  78. 2 described_class.new(
  79. scalars: {
  80. 3 foo: -> { foo_type },
  81. 1 string: -> { bar_type },
  82. },
  83. composites: {
  84. 1 baz: ->(_types) { baz_type },
  85. 1 array: ->(_types) { oof_type },
  86. }
  87. )
  88. end
  89. 1 it "can use custom scalars and composites" do
  90. 1 expect(container.scalars).to contain_exactly(*default_scalars, :foo)
  91. 1 expect(container.scalar(:foo)).to be(foo_type)
  92. 1 expect(container.composites).to contain_exactly(*default_composites, :baz)
  93. 1 expect(container.composite(:baz, %i[foo])).to be(baz_type)
  94. end
  95. 1 it "can override default type definitions" do
  96. 1 expect(container.scalar(:string)).to be(bar_type)
  97. 1 expect(container.composite(:array, %i[foo])).to be(oof_type)
  98. end
  99. 1 it "can override the default type map" do
  100. 1 container = described_class.new(
  101. defaults: {
  102. 2 scalars: { foo: -> { foo_type } },
  103. 1 composites: { bar: ->(_types) { bar_type } },
  104. }
  105. )
  106. 1 expect(container.scalars).to contain_exactly(:foo)
  107. 1 expect(container.scalar(:foo)).to be(foo_type)
  108. 1 expect(container.composites).to contain_exactly(:bar)
  109. 1 expect(container.composite(:bar, %i[foo])).to be(bar_type)
  110. end
  111. end
  112. 1 context "when a scalar definition doesn't exist" do
  113. 1 it "raises an error when used as a scalar" do
  114. 2 expect { subject.scalar(:foo) }.to raise_error(
  115. Tabulard::Errors::TypeError,
  116. "Invalid scalar type: :foo"
  117. )
  118. end
  119. 1 it "raises an error when used in a composite" do
  120. 2 expect { subject.composite(:array, [:foo]) }.to raise_error(
  121. Tabulard::Errors::TypeError,
  122. "Invalid scalar type: :foo"
  123. )
  124. end
  125. end
  126. 1 context "when a composite definition doesn't exist" do
  127. 1 it "raises an error" do
  128. 2 expect { subject.composite(:foo, []) }.to raise_error(
  129. Tabulard::Errors::TypeError,
  130. "Invalid composite type: :foo"
  131. )
  132. end
  133. end
  134. end

spec/tabulard/types/scalars/boolsy_cast_spec.rb

100.0% lines covered

100.0% branches covered

43 relevant lines. 43 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/scalars/boolsy_cast"
  3. 1 require "tabulard/messaging"
  4. 1 require "support/shared/cast_class"
  5. 1 RSpec.describe Tabulard::Types::Scalars::BoolsyCast do
  6. 1 it_behaves_like "cast_class"
  7. 1 describe "#initialize" do
  8. 1 it "setups blank boolsy values by default" do
  9. 1 expect(described_class.new).to eq(
  10. described_class.new(truthy: [], falsy: [])
  11. )
  12. end
  13. end
  14. 1 describe "#call" do
  15. 4 subject(:cast) { described_class.new(truthy: truthy, falsy: falsy) }
  16. 4 let(:value) { instance_double(Object, inspect: double) }
  17. 4 let(:messenger) { instance_double(Tabulard::Messaging::Messenger) }
  18. 1 let(:truthy) do
  19. 3 instance_double(Array)
  20. end
  21. 1 let(:falsy) do
  22. 3 instance_double(Array)
  23. end
  24. 1 def stub_inclusion(set, value, bool)
  25. 6 allow(set).to receive(:include?).with(value).and_return(bool)
  26. end
  27. 1 def expect_truthy(value = self.value)
  28. 1 expect(cast.call(value, messenger)).to be(true)
  29. end
  30. 1 def expect_falsy(value = self.value)
  31. 1 expect(cast.call(value, messenger)).to be(false)
  32. end
  33. 1 def expect_failure(value = self.value)
  34. 1 expect do
  35. 1 cast.call(value, messenger)
  36. end.to throw_symbol(
  37. :failure,
  38. Tabulard::Messaging::Messages::MustBeBoolsy.new(code_data: { value: value.inspect })
  39. )
  40. end
  41. 1 context "when the value is truthy" do
  42. 1 before do
  43. 1 stub_inclusion(truthy, value, true)
  44. 1 stub_inclusion(falsy, value, false)
  45. end
  46. 1 it "returns true" do
  47. 1 expect_truthy
  48. end
  49. end
  50. 1 context "when the value is falsy" do
  51. 1 before do
  52. 1 stub_inclusion(truthy, value, false)
  53. 1 stub_inclusion(falsy, value, true)
  54. end
  55. 1 it "returns false" do
  56. 1 expect_falsy
  57. end
  58. end
  59. 1 context "when the value isn't truthy nor falsy" do
  60. 1 before do
  61. 1 stub_inclusion(truthy, value, false)
  62. 1 stub_inclusion(falsy, value, false)
  63. end
  64. 1 it "fails with a message" do
  65. 1 expect_failure
  66. end
  67. end
  68. end
  69. end

spec/tabulard/types/scalars/boolsy_spec.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/scalars/boolsy"
  3. 1 require "support/shared/scalar_type"
  4. 1 RSpec.describe Tabulard::Types::Scalars::Boolsy do
  5. 1 include_examples "scalar_type"
  6. 1 it "inherits from the basic scalar type" do
  7. 1 expect(described_class.superclass).to be(Tabulard::Types::Scalars::Scalar)
  8. end
  9. 1 describe "::cast_classes" do
  10. 1 it "extends the superclass' ones" do
  11. 1 expect(described_class.cast_classes).to eq(
  12. described_class.superclass.cast_classes + [Tabulard::Types::Scalars::BoolsyCast]
  13. )
  14. end
  15. end
  16. end

spec/tabulard/types/scalars/date_string_cast_spec.rb

100.0% lines covered

100.0% branches covered

54 relevant lines. 54 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/scalars/date_string_cast"
  3. 1 require "tabulard/messaging"
  4. 1 require "support/shared/cast_class"
  5. 1 RSpec.describe Tabulard::Types::Scalars::DateStringCast do
  6. 1 let(:default_fmt) do
  7. 3 "%Y-%m-%d"
  8. end
  9. 1 it_behaves_like "cast_class"
  10. 1 describe "#initialize" do
  11. 1 it "setups a default, conventional date format and accepts native dates" do
  12. 1 expect(described_class.new).to eq(
  13. described_class.new(date_fmt: default_fmt, accept_date: true)
  14. )
  15. end
  16. end
  17. 1 describe "#call" do
  18. 4 let(:value) { double }
  19. 7 let(:messenger) { instance_double(Tabulard::Messaging::Messenger) }
  20. 1 context "when value is a Date" do
  21. 1 subject(:cast) do
  22. 2 described_class.new(accept_date: accept_date)
  23. end
  24. 1 before do
  25. 2 allow(Date).to receive(:===).with(value).and_return(true)
  26. end
  27. 1 context "when accepting Date" do
  28. 2 let(:accept_date) { true }
  29. 1 it "returns the value" do
  30. 1 expect(cast.call(value, messenger)).to eq(value)
  31. end
  32. end
  33. 1 context "when not accepting Date" do
  34. 2 let(:accept_date) { false }
  35. 1 it "fails with an error" do
  36. 1 expect do
  37. 1 cast.call(value, messenger)
  38. end.to throw_symbol(
  39. :failure,
  40. Tabulard::Messaging::Messages::MustBeDate.new(code_data: { format: default_fmt })
  41. )
  42. end
  43. end
  44. end
  45. 1 context "when value is a string" do
  46. 1 subject(:cast) do
  47. 3 described_class.new(date_fmt: date_fmt)
  48. end
  49. 4 let(:date_fmt) { "%d/%m/%Y" }
  50. 1 context "when it fits the format" do
  51. 1 let(:value) do
  52. 1 "07/03/2020"
  53. end
  54. 1 it "returns a Date" do
  55. 1 expect(cast.call(value, messenger)).to eq(Date.new(2020, 3, 7))
  56. end
  57. end
  58. 1 context "when it doesn't make sense" do
  59. 1 let(:value) do
  60. 1 "47/03/2020"
  61. end
  62. 1 it "fails with an error" do
  63. 1 expect do
  64. 1 cast.call(value, messenger)
  65. end.to throw_symbol(
  66. :failure,
  67. Tabulard::Messaging::Messages::MustBeDate.new(code_data: { format: date_fmt })
  68. )
  69. end
  70. end
  71. 1 context "when it doesn't fit the format" do
  72. 1 let(:value) do
  73. 1 "2020-01-12"
  74. end
  75. 1 it "fails with an error" do
  76. 1 expect do
  77. 1 cast.call(value, messenger)
  78. end.to throw_symbol(
  79. :failure,
  80. Tabulard::Messaging::Messages::MustBeDate.new(code_data: { format: date_fmt })
  81. )
  82. end
  83. end
  84. end
  85. 1 context "when value is anything else" do
  86. 1 subject(:cast) do
  87. 1 described_class.new
  88. end
  89. 1 it "fails with an error" do
  90. 1 expect do
  91. 1 cast.call(value, messenger)
  92. end.to throw_symbol(
  93. :failure,
  94. Tabulard::Messaging::Messages::MustBeDate.new(code_data: { format: default_fmt })
  95. )
  96. end
  97. end
  98. end
  99. end

spec/tabulard/types/scalars/date_string_spec.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/scalars/date_string"
  3. 1 require "support/shared/scalar_type"
  4. 1 RSpec.describe Tabulard::Types::Scalars::DateString do
  5. 1 subject do
  6. 4 described_class.new(date_fmt: "foobar")
  7. end
  8. 1 include_examples "scalar_type"
  9. 1 it "inherits from the basic scalar type" do
  10. 1 expect(described_class.superclass).to be(Tabulard::Types::Scalars::Scalar)
  11. end
  12. 1 describe "::cast_classes" do
  13. 1 it "extends the superclass' ones" do
  14. 1 expect(described_class.cast_classes).to eq(
  15. described_class.superclass.cast_classes + [Tabulard::Types::Scalars::DateStringCast]
  16. )
  17. end
  18. end
  19. end

spec/tabulard/types/scalars/email_cast_spec.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/scalars/email_cast"
  3. 1 require "tabulard/messaging"
  4. 1 require "support/shared/cast_class"
  5. 1 RSpec.describe Tabulard::Types::Scalars::EmailCast do
  6. 1 it_behaves_like "cast_class"
  7. 1 describe "#initialize" do
  8. 1 it "setups a default, conventional e-mail matcher" do
  9. 1 expect(described_class.new).to eq(
  10. described_class.new(email_matcher: URI::MailTo::EMAIL_REGEXP)
  11. )
  12. end
  13. end
  14. 1 describe "#call" do
  15. 3 subject(:cast) { described_class.new(email_matcher: email_matcher) }
  16. 1 let(:email_matcher) do
  17. 2 instance_double(Regexp)
  18. end
  19. 3 let(:value) { instance_double(Object, inspect: double) }
  20. 3 let(:messenger) { instance_double(Tabulard::Messaging::Messenger) }
  21. 1 before do
  22. 2 allow(email_matcher).to receive(:match?).with(value).and_return(value_is_email)
  23. end
  24. 1 context "when the value is an email address" do
  25. 2 let(:value_is_email) { true }
  26. 1 it "returns the value" do
  27. 1 expect(cast.call(value, messenger)).to eq(value)
  28. end
  29. end
  30. 1 context "when the value isn't an email address" do
  31. 2 let(:value_is_email) { false }
  32. 1 it "adds an error message and throws :failure" do
  33. 1 expect do
  34. 1 cast.call(value, messenger)
  35. end.to throw_symbol(
  36. :failure,
  37. Tabulard::Messaging::Messages::MustBeEmail.new(code_data: { value: value.inspect })
  38. )
  39. end
  40. end
  41. end
  42. end

spec/tabulard/types/scalars/email_spec.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/scalars/email"
  3. 1 require "support/shared/scalar_type"
  4. 1 RSpec.describe Tabulard::Types::Scalars::Email do
  5. 1 include_examples "scalar_type"
  6. 1 it "inherits from the scalar string type" do
  7. 1 expect(described_class.superclass).to be(Tabulard::Types::Scalars::String)
  8. end
  9. 1 describe "::cast_classes" do
  10. 1 it "extends the superclass' ones" do
  11. 1 expect(described_class.cast_classes).to eq(
  12. described_class.superclass.cast_classes + [Tabulard::Types::Scalars::EmailCast]
  13. )
  14. end
  15. end
  16. end

spec/tabulard/types/scalars/scalar_cast_spec.rb

100.0% lines covered

100.0% branches covered

56 relevant lines. 56 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/scalars/scalar_cast"
  3. 1 require "tabulard/messaging"
  4. 1 require "support/shared/cast_class"
  5. 1 RSpec.describe Tabulard::Types::Scalars::ScalarCast do
  6. 1 it_behaves_like "cast_class"
  7. 1 describe "#call" do
  8. 2 subject(:cast) { described_class.new }
  9. 1 let(:messenger) do
  10. 7 instance_double(Tabulard::Messaging::Messenger)
  11. end
  12. 1 context "when given nil" do
  13. 1 context "when nullable" do
  14. 2 subject(:cast) { described_class.new(nullable: true) }
  15. 1 it "halts with a success and nil as value" do
  16. 1 expect do
  17. 1 cast.call(nil, messenger)
  18. end.to throw_symbol(:success, nil)
  19. end
  20. end
  21. 1 context "when non-nullable" do
  22. 2 subject(:cast) { described_class.new(nullable: false) }
  23. 1 it "halts with a failure and an appropriate error code" do
  24. 1 expect do
  25. 1 cast.call(nil, messenger)
  26. end.to throw_symbol(
  27. :failure, Tabulard::Messaging::Messages::MustExist.new
  28. )
  29. end
  30. end
  31. end
  32. 1 context "when given a String" do
  33. 3 let(:string_with_garbage) { " string_foo " }
  34. 4 let(:string_without_garbage) { "string_foo" }
  35. 1 before do
  36. 4 allow(messenger).to receive(:warn)
  37. end
  38. 1 context "when cleaning strings" do
  39. 1 subject(:cast) do
  40. 2 described_class.new(clean_string: true)
  41. end
  42. 1 context "when string contains garbage" do
  43. 1 it "removes garbage around the value and warns about it" do
  44. 1 value = cast.call(string_with_garbage, messenger)
  45. 1 expect(value).to eq(string_without_garbage)
  46. 1 expect(messenger).to have_received(:warn)
  47. .with(Tabulard::Messaging::Messages::CleanedString.new)
  48. end
  49. end
  50. 1 context "when string doesn't contain garbage" do
  51. 1 it "returns the string as is" do
  52. 1 value = cast.call(string_without_garbage, messenger)
  53. 1 expect(value).to eq(string_without_garbage)
  54. 1 expect(messenger).not_to have_received(:warn)
  55. end
  56. end
  57. end
  58. 1 context "when not cleaning strings" do
  59. 1 subject(:cast) do
  60. 2 described_class.new(clean_string: false)
  61. end
  62. 1 context "when string contains garbage" do
  63. 1 it "returns the string as is" do
  64. 1 value = cast.call(string_with_garbage, messenger)
  65. 1 expect(value).to eq(string_with_garbage)
  66. 1 expect(messenger).not_to have_received(:warn)
  67. end
  68. end
  69. 1 context "when string doesn't contain garbage" do
  70. 1 it "returns the string as is" do
  71. 1 value = cast.call(string_without_garbage, messenger)
  72. 1 expect(value).to eq(string_without_garbage)
  73. 1 expect(messenger).not_to have_received(:warn)
  74. end
  75. end
  76. end
  77. end
  78. 1 context "when given something else" do
  79. 1 it "returns the string as is" do
  80. 1 something_else = double
  81. 1 value = cast.call(something_else, messenger)
  82. 1 expect(value).to eq(something_else)
  83. end
  84. end
  85. end
  86. end

spec/tabulard/types/scalars/scalar_spec.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/scalars/scalar"
  3. 1 require "support/shared/scalar_type"
  4. 1 RSpec.describe Tabulard::Types::Scalars::Scalar do
  5. 1 include_examples "scalar_type"
  6. 1 it "inherits from the basic type" do
  7. 1 expect(described_class.superclass).to be(Tabulard::Types::Type)
  8. end
  9. 1 describe "::cast_classes" do
  10. 1 it "includes a basic cast class" do
  11. 1 expect(described_class.cast_classes).to eq(
  12. [Tabulard::Types::Scalars::ScalarCast]
  13. )
  14. end
  15. end
  16. end

spec/tabulard/types/scalars/string_spec.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/scalars/string"
  3. 1 require "support/shared/scalar_type"
  4. 1 require "support/shared/cast_class"
  5. 1 RSpec.describe Tabulard::Types::Scalars::String do
  6. 1 include_examples "scalar_type"
  7. 1 it "inherits from the basic scalar type" do
  8. 1 expect(described_class.superclass).to be(Tabulard::Types::Scalars::Scalar)
  9. end
  10. 1 describe "custom cast class" do
  11. 1 subject(:cast_class) do
  12. 5 described_class.cast_classes.last
  13. end
  14. 1 it "is appended to the superclass' cast classes" do
  15. 1 expect(
  16. described_class.superclass.cast_classes + [cast_class]
  17. ).to eq(described_class.cast_classes)
  18. end
  19. 1 include_examples "cast_class"
  20. 1 describe "#call" do
  21. 1 before do
  22. 2 allow(value).to receive(:is_a?).with(String).and_return(value_is_string)
  23. end
  24. 1 context "when the value is a string" do
  25. 2 let(:value_is_string) { true }
  26. 2 let(:native_string) { double }
  27. 1 before do
  28. 1 allow(value).to receive(:to_s).and_return(native_string)
  29. end
  30. 1 it "is a success, returning a native string" do
  31. 1 expect(cast.call(value, messenger)).to eq(native_string)
  32. end
  33. end
  34. 1 context "when the value is not a string" do
  35. 2 let(:value_is_string) { false }
  36. 1 it "is a failure" do
  37. 1 expect do
  38. 1 cast.call(value, messenger)
  39. end.to throw_symbol(
  40. :failure, Tabulard::Messaging::Messages::MustBeString.new
  41. )
  42. end
  43. end
  44. end
  45. end
  46. end

spec/tabulard/types/type_spec.rb

100.0% lines covered

100.0% branches covered

129 relevant lines. 129 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/types/type"
  3. 1 RSpec.describe Tabulard::Types::Type do
  4. 1 describe "class API" do
  5. 15 let(:klass0) { Class.new(described_class) }
  6. 9 let(:klass1) { Class.new(klass0) }
  7. 6 let(:klass2) { Class.new(klass1) }
  8. 1 describe "::all" do
  9. 1 it "returns an enumerator for self and known descendant types" do
  10. 1 enum = klass0.all
  11. 1 expect(enum).to be_a(Enumerator) & contain_exactly(klass0)
  12. 1 klass1
  13. 1 klass2
  14. 1 expect(klass0.all.to_a).to contain_exactly(klass0, klass1, klass2)
  15. 1 expect(klass1.all.to_a).to contain_exactly(klass1, klass2)
  16. end
  17. end
  18. 1 describe "::cast_classes" do
  19. 1 it "is an empty array" do
  20. 1 expect(described_class.cast_classes).to eq([])
  21. end
  22. 1 it "is inheritable" do
  23. 1 expect([klass0, klass1, klass2]).to all(
  24. have_attributes(cast_classes: described_class.cast_classes)
  25. )
  26. end
  27. end
  28. 1 describe "::cast_classes=" do
  29. 5 let(:cast_classes0) { [double, double] }
  30. 3 let(:cast_classes1) { [double, double] }
  31. 1 it "mutates the class instance" do
  32. 1 klass0.cast_classes = cast_classes0
  33. 1 expect(klass0).to have_attributes(cast_classes: cast_classes0)
  34. end
  35. 1 it "applies to the inherited children" do
  36. 1 klass0.cast_classes = cast_classes0
  37. 1 expect([klass0, klass1, klass2].map(&:cast_classes)).to eq(
  38. [cast_classes0, cast_classes0, cast_classes0]
  39. )
  40. end
  41. 1 it "doesn't apply to the children mutated afterwards" do
  42. 1 klass0.cast_classes = cast_classes0
  43. 1 klass1.cast_classes = cast_classes1
  44. 1 expect([klass0, klass1, klass2].map(&:cast_classes)).to eq(
  45. [cast_classes0, cast_classes1, cast_classes1]
  46. )
  47. end
  48. 1 it "doesn't apply to the children mutated beforehand" do
  49. 1 klass1.cast_classes = cast_classes1
  50. 1 klass0.cast_classes = cast_classes0
  51. 1 expect([klass0, klass1, klass2].map(&:cast_classes)).to eq(
  52. [cast_classes0, cast_classes1, cast_classes1]
  53. )
  54. end
  55. end
  56. 1 describe "::freeze" do
  57. 1 it "freezes the instance and its cast classes" do
  58. 1 klass1.freeze
  59. 1 expect([klass1, klass1.cast_classes]).to all(be_frozen)
  60. end
  61. 1 it "doesn't freeze a warm superclass nor its cast classes" do
  62. 1 klass1.freeze
  63. 1 expect([klass0, klass0.cast_classes]).not_to include(be_frozen)
  64. end
  65. 1 it "doesn't freeze a warm subclass nor its *own* cast classes" do
  66. 1 klass0.freeze
  67. 1 expect(klass1).not_to be_frozen
  68. 1 expect(klass1.cast_classes).to be_frozen
  69. 1 klass1.cast_classes += [double]
  70. 1 expect(klass1.cast_classes).not_to be_frozen
  71. end
  72. end
  73. 1 describe "::cast" do
  74. 6 let(:cast_class0) { double }
  75. 2 let(:cast_class1) { double }
  76. 1 before do
  77. 5 klass0.cast_classes = [cast_class0]
  78. 5 klass0.freeze
  79. end
  80. 1 context "when given a class" do
  81. 1 it "creates a new type appending the given cast" do
  82. 1 type_class = klass0.cast(cast_class1)
  83. 1 expect(type_class).to have_attributes(
  84. class: Class,
  85. superclass: klass0,
  86. cast_classes: [cast_class0, cast_class1]
  87. )
  88. end
  89. end
  90. 1 context "when given a block" do
  91. 3 let(:cast) { -> {} }
  92. 3 let(:type_class) { klass0.cast(&cast) }
  93. 1 it "creates a new type appending the given cast" do
  94. 1 expect(type_class).to have_attributes(
  95. class: Class,
  96. superclass: klass0,
  97. cast_classes: [cast_class0, described_class::SimpleCast.new(cast)]
  98. )
  99. end
  100. 1 it "still behaves as a cast class" do
  101. 1 block_cast_class = type_class.cast_classes.last
  102. 1 expect(block_cast_class.new(foo: :bar)).to be(cast)
  103. end
  104. end
  105. 1 context "when given both a class and a block" do
  106. 1 it "raises an error" do
  107. 2 expect { described_class.cast(cast_class0) { double } }.to raise_error(
  108. ArgumentError, "Expected either a Class or a block, got both"
  109. )
  110. end
  111. end
  112. 1 context "when given neither a class nor a block" do
  113. 1 it "raises an error" do
  114. 2 expect { described_class.cast }.to raise_error(
  115. ArgumentError, "Expected either a Class or a block, got none"
  116. )
  117. end
  118. end
  119. end
  120. 1 describe "::new!" do
  121. 1 it "initializes a new, frozen type" do
  122. 1 type = described_class.new!
  123. 1 expect(type).to be_a(described_class) & be_frozen
  124. end
  125. end
  126. end
  127. 1 describe "instance API" do
  128. 1 describe "#initialize" do
  129. 1 let(:cast_class0) do
  130. 1 instance_double(Class)
  131. end
  132. 1 let(:cast_class1) do
  133. 1 instance_double(Class)
  134. end
  135. 1 let(:cast_block) do
  136. 1 -> {}
  137. end
  138. 1 let(:type_class) do
  139. 1 klass = described_class.cast(&cast_block)
  140. 1 klass.cast_classes << cast_class0
  141. 1 klass.cast_classes << cast_class1
  142. 1 klass
  143. end
  144. 1 def stub_new_casts(opts)
  145. 1 type_class.cast_classes.map do |cast_class|
  146. 3 allow(cast_class).to receive(:new).with(**opts).and_return(cast = double)
  147. 3 cast
  148. end
  149. end
  150. 1 it "builds a cast chain of all casts initialized with the opts" do
  151. 1 opts = { foo: :bar, hello: "world" }
  152. 1 casts = stub_new_casts(opts)
  153. 1 type = type_class.new(**opts)
  154. 1 expect(type.cast_chain).to(
  155. be_a(Tabulard::Types::CastChain) &
  156. have_attributes(casts: casts)
  157. )
  158. end
  159. end
  160. 1 describe "#cast" do
  161. 1 it "delegates the task to the cast chain" do
  162. 1 type = described_class.new
  163. 1 value = double
  164. 1 messenger = double
  165. 1 result = double
  166. 1 expect(type.cast_chain).to receive(:call).with(value, messenger).and_return(result)
  167. 1 expect(type.cast(value, messenger)).to be(result)
  168. end
  169. end
  170. 1 describe "#freeze" do
  171. 1 it "freezes self and the cast chain" do
  172. 1 type = described_class.new
  173. 1 type.freeze
  174. 1 expect(type).to be_frozen
  175. 1 expect(type.cast_chain).to be_frozen
  176. end
  177. end
  178. 1 describe "abstract API" do
  179. 5 let(:type) { described_class.new }
  180. 1 def raise_abstract_method_error
  181. 4 raise_error(NoMethodError, /you must implement this method/i)
  182. end
  183. 1 describe "#scalar?" do
  184. 1 it "is abstract" do
  185. 2 expect { type.scalar? }.to raise_abstract_method_error
  186. end
  187. end
  188. 1 describe "#composite?" do
  189. 1 it "is abstract" do
  190. 2 expect { type.composite? }.to raise_abstract_method_error
  191. end
  192. end
  193. 1 describe "#scalar" do
  194. 1 it "is abstract" do
  195. 2 expect { type.scalar(double, double, double) }.to raise_abstract_method_error
  196. end
  197. end
  198. 1 describe "#composite" do
  199. 1 it "is abstract" do
  200. 2 expect { type.composite(double, double) }.to raise_abstract_method_error
  201. end
  202. end
  203. end
  204. end
  205. end

spec/tabulard/utils/cell_string_cleaner_spec.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/utils/cell_string_cleaner"
  3. 1 RSpec.describe Tabulard::Utils::CellStringCleaner do
  4. 2 subject(:cleaner) { described_class }
  5. 2 let(:spaces) { " \t\r\n" }
  6. 2 let(:nonprints) { "\x00\x1B" }
  7. 2 let(:garbage) { spaces + nonprints }
  8. # NOTE: the line return and newline characters act as traps for single-line regexes
  9. 2 let(:string) { "foo#{spaces}\r\n#{nonprints}bar" }
  10. 1 it "removes spaces & non-printable characters around a string" do
  11. 1 expect(cleaner.call(garbage)).to eq("")
  12. 1 expect(cleaner.call(garbage + string)).to eq(string)
  13. 1 expect(cleaner.call(string + garbage)).to eq(string)
  14. 1 expect(cleaner.call(garbage + string + garbage)).to eq(string)
  15. end
  16. end

spec/tabulard/utils/monadic_result/failure_spec.rb

100.0% lines covered

100.0% branches covered

94 relevant lines. 94 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/utils/monadic_result"
  3. 1 RSpec.describe Tabulard::Utils::MonadicResult::Failure, monadic_result: true do
  4. 12 subject(:result) { described_class.new(value0) }
  5. 1 let(:value0) do
  6. 11 instance_double(Object, to_s: "value0_to_s", inspect: "value0_inspect")
  7. end
  8. 1 let(:value1) do
  9. 1 instance_double(Object, to_s: "value1_to_s", inspect: "value1_inspect")
  10. end
  11. 1 it "is a result" do
  12. 1 expect(result).to be_a(Tabulard::Utils::MonadicResult::Result)
  13. end
  14. 1 describe "#initialize" do
  15. 1 it "is empty by default" do
  16. 1 expect(described_class.new).to be_empty
  17. end
  18. end
  19. 1 describe "#empty?" do
  20. 1 it "is true by default of an explicitly wrapped value" do
  21. 1 expect(described_class.new).to be_empty
  22. end
  23. 1 it "is false otherwise" do
  24. 1 expect(result).not_to be_empty
  25. end
  26. end
  27. 1 describe "#failure?" do
  28. 1 it "is true" do
  29. 1 expect(result).to be_failure
  30. end
  31. end
  32. 1 describe "#success?" do
  33. 1 it "is false" do
  34. 1 expect(result).not_to be_success
  35. end
  36. end
  37. 1 describe "#failure" do
  38. 1 it "can unwrap the value" do
  39. 1 expect(result).to have_attributes(failure: value0)
  40. end
  41. 1 it "cannot unwrap a value when empty" do
  42. 1 empty_result = described_class.new
  43. 2 expect { empty_result.failure }.to raise_error(
  44. described_class::ValueError, "There is no value within the result"
  45. )
  46. end
  47. end
  48. 1 describe "#success" do
  49. 1 it "can't unwrap the value" do
  50. 2 expect { result.success }.to raise_error(
  51. described_class::VariantError, "Not a Success"
  52. )
  53. end
  54. end
  55. 1 describe "#==" do
  56. 1 it "is equivalent to a similar Failure" do
  57. 1 expect(result).to eq(Failure(value0))
  58. end
  59. 1 it "is not equivalent to a different Failure" do
  60. 1 expect(result).not_to eq(Failure(value1))
  61. end
  62. 1 it "is not equivalent to a similar Success" do
  63. 1 expect(result).not_to eq(Success(value0))
  64. end
  65. end
  66. 1 describe "#inspect" do
  67. 1 it "inspects the result" do
  68. 1 expect(result.inspect).to eq("Failure(value0_inspect)")
  69. end
  70. 1 context "when empty" do
  71. 2 subject(:result) { described_class.new }
  72. 1 it "inspects nothing" do
  73. 1 expect(result.inspect).to eq("Failure()")
  74. end
  75. end
  76. end
  77. 1 describe "#to_s" do
  78. 1 it "inspects the result" do
  79. 1 expect(result.method(:to_s)).to eq(result.method(:inspect))
  80. end
  81. end
  82. 1 describe "#unwrap" do
  83. 3 let(:do_token) { :MonadicResultDo }
  84. 1 context "when empty" do
  85. 1 it "returns nil" do
  86. 1 result = described_class.new
  87. 2 expect { result.unwrap }.to throw_symbol(do_token, result)
  88. end
  89. end
  90. 1 context "when non-empty" do
  91. 1 it "returns the wrapped value" do
  92. 1 result = described_class.new(double)
  93. 2 expect { result.unwrap }.to throw_symbol(do_token, result)
  94. end
  95. end
  96. end
  97. 1 describe "#discard" do
  98. 1 it "returns the same variant, without a value" do
  99. 1 empty_result = described_class.new
  100. 1 filled_result = described_class.new(double)
  101. 1 expect(empty_result.discard).to eq(empty_result)
  102. 1 expect(filled_result.discard).to eq(empty_result)
  103. end
  104. end
  105. 1 describe "#bind" do
  106. 1 context "when empty" do
  107. 3 let(:result) { described_class.new }
  108. 1 it "doesn't yield" do
  109. 2 expect { |b| result.bind(&b) }.not_to yield_control
  110. end
  111. 1 it "returns self" do
  112. 1 expect(result.bind { double }).to be(result)
  113. end
  114. end
  115. 1 context "when filled" do
  116. 3 let(:result) { described_class.new(double) }
  117. 1 it "doesn't yield" do
  118. 2 expect { |b| result.bind(&b) }.not_to yield_control
  119. end
  120. 1 it "returns self" do
  121. 1 expect(result.bind { double }).to be(result)
  122. end
  123. end
  124. end
  125. 1 describe "#or" do
  126. 1 context "when empty" do
  127. 3 let(:result) { described_class.new }
  128. 1 it "yields nil" do
  129. 2 expect { |b| result.or(&b) }.to yield_with_no_args
  130. end
  131. 1 it "returns the block result" do
  132. 1 block_value = double
  133. 2 expect(result.or { block_value }).to eq(block_value)
  134. end
  135. end
  136. 1 context "when filled" do
  137. 3 let(:value) { double }
  138. 3 let(:result) { described_class.new(value) }
  139. 1 it "yields the value" do
  140. 2 expect { |b| result.or(&b) }.to yield_with_args(value)
  141. end
  142. 1 it "returns the block result" do
  143. 1 block_value = double
  144. 2 expect(result.or { block_value }).to eq(block_value)
  145. end
  146. end
  147. end
  148. end

spec/tabulard/utils/monadic_result/success_spec.rb

100.0% lines covered

100.0% branches covered

93 relevant lines. 93 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/utils/monadic_result"
  3. 1 RSpec.describe Tabulard::Utils::MonadicResult::Success, monadic_result: true do
  4. 12 subject(:result) { described_class.new(value0) }
  5. 1 let(:value0) do
  6. 11 instance_double(Object, to_s: "value0_to_s", inspect: "value0_inspect")
  7. end
  8. 1 let(:value1) do
  9. 1 instance_double(Object, to_s: "value1_to_s", inspect: "value1_inspect")
  10. end
  11. 1 it "is a result" do
  12. 1 expect(result).to be_a(Tabulard::Utils::MonadicResult::Result)
  13. end
  14. 1 describe "#initialize" do
  15. 1 it "is empty by default" do
  16. 1 expect(described_class.new).to be_empty
  17. end
  18. end
  19. 1 describe "#empty?" do
  20. 1 it "is true by default of an explicitly wrapped value" do
  21. 1 expect(described_class.new).to be_empty
  22. end
  23. 1 it "is false otherwise" do
  24. 1 expect(result).not_to be_empty
  25. end
  26. end
  27. 1 describe "#success?" do
  28. 1 it "is true" do
  29. 1 expect(result).to be_success
  30. end
  31. end
  32. 1 describe "#failure?" do
  33. 1 it "is false" do
  34. 1 expect(result).not_to be_failure
  35. end
  36. end
  37. 1 describe "#success" do
  38. 1 it "can unwrap the value" do
  39. 1 expect(result).to have_attributes(success: value0)
  40. end
  41. 1 it "cannot unwrap a value when empty" do
  42. 1 empty_result = described_class.new
  43. 2 expect { empty_result.success }.to raise_error(
  44. described_class::ValueError, "There is no value within the result"
  45. )
  46. end
  47. end
  48. 1 describe "#failure" do
  49. 1 it "can't unwrap the value" do
  50. 2 expect { result.failure }.to raise_error(
  51. described_class::VariantError, "Not a Failure"
  52. )
  53. end
  54. end
  55. 1 describe "#==" do
  56. 1 it "is equivalent to a similar Success" do
  57. 1 expect(result).to eq(Success(value0))
  58. end
  59. 1 it "is not equivalent to a different Success" do
  60. 1 expect(result).not_to eq(Success(value1))
  61. end
  62. 1 it "is not equivalent to a similar Failure" do
  63. 1 expect(result).not_to eq(Failure(value0))
  64. end
  65. end
  66. 1 describe "#inspect" do
  67. 1 it "inspects the result" do
  68. 1 expect(result.inspect).to eq("Success(value0_inspect)")
  69. end
  70. 1 context "when empty" do
  71. 2 subject(:result) { described_class.new }
  72. 1 it "inspects nothing" do
  73. 1 expect(result.inspect).to eq("Success()")
  74. end
  75. end
  76. end
  77. 1 describe "#to_s" do
  78. 1 it "inspects the result" do
  79. 1 expect(result.method(:to_s)).to eq(result.method(:inspect))
  80. end
  81. end
  82. 1 describe "#unwrap" do
  83. 1 context "when empty" do
  84. 1 it "returns nil" do
  85. 1 result = described_class.new
  86. 1 expect(result.unwrap).to be_nil
  87. end
  88. end
  89. 1 context "when non-empty" do
  90. 1 it "returns the wrapped value" do
  91. 1 result = described_class.new(wrapped = double)
  92. 1 expect(result.unwrap).to be(wrapped)
  93. end
  94. end
  95. end
  96. 1 describe "#discard" do
  97. 1 it "returns the same variant, without a value" do
  98. 1 empty_result = described_class.new
  99. 1 filled_result = described_class.new(double)
  100. 1 expect(empty_result.discard).to eq(empty_result)
  101. 1 expect(filled_result.discard).to eq(empty_result)
  102. end
  103. end
  104. 1 describe "#bind" do
  105. 1 context "when empty" do
  106. 3 let(:result) { described_class.new }
  107. 1 it "yields nil" do
  108. 2 expect { |b| result.bind(&b) }.to yield_with_no_args
  109. end
  110. 1 it "returns the block result" do
  111. 1 block_value = double
  112. 2 expect(result.bind { block_value }).to eq(block_value)
  113. end
  114. end
  115. 1 context "when filled" do
  116. 3 let(:value) { double }
  117. 3 let(:result) { described_class.new(value) }
  118. 1 it "yields the value" do
  119. 2 expect { |b| result.bind(&b) }.to yield_with_args(value)
  120. end
  121. 1 it "returns the block result" do
  122. 1 block_value = double
  123. 2 expect(result.bind { block_value }).to eq(block_value)
  124. end
  125. end
  126. end
  127. 1 describe "#or" do
  128. 1 context "when empty" do
  129. 3 let(:result) { described_class.new }
  130. 1 it "doesn't yield" do
  131. 2 expect { |b| result.or(&b) }.not_to yield_control
  132. end
  133. 1 it "returns self" do
  134. 1 expect(result.or { double }).to be(result)
  135. end
  136. end
  137. 1 context "when filled" do
  138. 3 let(:result) { described_class.new(double) }
  139. 1 it "doesn't yield" do
  140. 2 expect { |b| result.or(&b) }.not_to yield_control
  141. end
  142. 1 it "returns self" do
  143. 1 expect(result.or { double }).to be(result)
  144. end
  145. end
  146. end
  147. end

spec/tabulard/utils/monadic_result/unit_spec.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/utils/monadic_result"
  3. 1 RSpec.describe Tabulard::Utils::MonadicResult::Unit do
  4. 4 subject(:unit) { described_class }
  5. 2 it { is_expected.to be_frozen }
  6. 1 it "can be stringified" do
  7. 1 expect(unit.to_s).to eq("Unit")
  8. end
  9. 1 it "can be inspected" do
  10. 1 expect(unit.inspect).to eq("Unit")
  11. end
  12. end

spec/tabulard/utils/monadic_result_spec.rb

100.0% lines covered

100.0% branches covered

57 relevant lines. 57 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard/utils/monadic_result"
  3. 1 RSpec.describe Tabulard::Utils::MonadicResult do
  4. 1 let(:klass) do
  5. 20 Class.new.tap { |c| c.include(described_class) }
  6. end
  7. 10 let(:builder) { klass.new }
  8. 3 let(:value) { double }
  9. 1 it "includes some constants" do
  10. 1 expect(klass.constants - Object.constants).to contain_exactly(
  11. :Unit, :Result, :Success, :Failure
  12. )
  13. 1 expect(klass::Unit).to be(described_class::Unit)
  14. 1 expect(klass::Result).to be(described_class::Result)
  15. 1 expect(klass::Success).to be(described_class::Success)
  16. 1 expect(klass::Failure).to be(described_class::Failure)
  17. end
  18. 1 it "includes three builder methods" do
  19. 1 expect(builder.methods - Object.methods).to contain_exactly(
  20. :Success, :Failure, :Do
  21. )
  22. end
  23. 1 describe "#Success" do
  24. 1 it "may wrap no value in a Success instance" do
  25. 1 expect(builder.Success()).to eq(described_class::Success.new)
  26. end
  27. 1 it "may wrap a value in a Success instance" do
  28. 1 expect(builder.Success(value)).to eq(described_class::Success.new(value))
  29. end
  30. end
  31. 1 describe "#Failure" do
  32. 1 it "may wrap no value in a Failure instance" do
  33. 1 expect(builder.Failure()).to eq(described_class::Failure.new)
  34. end
  35. 1 it "may wrap a value in a Failure instance" do
  36. 1 expect(builder.Failure(value)).to eq(described_class::Failure.new(value))
  37. end
  38. end
  39. 1 describe "#Do" do
  40. 4 let(:v1) { double }
  41. 4 let(:v2) { double }
  42. 3 let(:v3) { double }
  43. 1 let(:v4) { double }
  44. 1 let(:v5) { double }
  45. 1 it "returns the last expression of the block" do
  46. 1 result = builder.Do do
  47. 1 v1
  48. 1 v2
  49. 1 v3
  50. end
  51. 1 expect(result).to be(v3)
  52. end
  53. 1 it "continues the sequence when unwrapping a Success" do
  54. 1 v = nil
  55. 1 result = builder.Do do
  56. 1 v = builder.Success(v1).unwrap
  57. 1 v = builder.Success(v2).unwrap
  58. 1 v = builder.Success(v3).unwrap
  59. end
  60. 1 expect(result).to be(v3)
  61. 1 expect(v).to be(v3)
  62. end
  63. 1 it "aborts the sequence when unwrapping a Failure" do
  64. 1 v = nil
  65. 1 result = builder.Do do
  66. 1 v = builder.Success(v1).unwrap
  67. 1 v = builder.Failure(v2).unwrap
  68. skipped # :nocov:
  69. skipped v = builder.Success(v3).unwrap
  70. skipped # :nocov:
  71. end
  72. 1 expect(result).to eq(builder.Failure(v2))
  73. 1 expect(v).to be(v1)
  74. end
  75. 1 it "is compatible with ensure" do
  76. 1 ensured = false
  77. 1 builder.Do do
  78. 1 builder.Failure().unwrap
  79. ensure
  80. 1 ensured = true
  81. end
  82. 1 expect(ensured).to be(true)
  83. end
  84. end
  85. end

spec/tabulard_spec.rb

100.0% lines covered

100.0% branches covered

66 relevant lines. 66 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "tabulard"
  3. 1 RSpec.describe Tabulard, monadic_result: true do
  4. 1 let(:types) do
  5. 24 reverse_string = Tabulard::Types::Scalars::String.cast { |v, _m| v.reverse }
  6. 9 Tabulard::Types::Container.new(
  7. scalars: {
  8. reverse_string: reverse_string.method(:new),
  9. }
  10. )
  11. end
  12. 1 let(:template_opts) do
  13. {
  14. 9 attributes: [
  15. {
  16. key: :foo,
  17. type: :reverse_string,
  18. },
  19. {
  20. key: "bar",
  21. type: {
  22. composite: :array,
  23. scalars: %i[
  24. string?
  25. scalar?
  26. email?
  27. scalar?
  28. scalar
  29. ],
  30. },
  31. },
  32. ],
  33. }
  34. end
  35. 1 let(:template) do
  36. 9 Tabulard::Template.build(**template_opts)
  37. end
  38. 1 let(:template_config) do
  39. 9 Tabulard::TemplateConfig.new(types: types)
  40. end
  41. 1 let(:specification) do
  42. 9 template.apply(template_config)
  43. end
  44. 1 let(:processor) do
  45. 9 Tabulard::TableProcessor.new(specification)
  46. end
  47. 1 let(:input) do
  48. [
  49. 9 ["foo", "bar 3", "bar 5", "bar 1"],
  50. ["hello", "foo@bar.baz", Float, nil],
  51. ["world", "foo@bar.baz", Float, nil],
  52. ["world", "boudiou !", Float, nil],
  53. ]
  54. end
  55. 1 def process(*args, **opts, &block)
  56. 5 processor.call(*args, adapter: Tabulard::Adapters::Bare, **opts, &block)
  57. end
  58. 1 def process_to_a(*args, **opts)
  59. 4 a = []
  60. 16 processor.call(*args, adapter: Tabulard::Adapters::Bare, **opts) { |result| a << result }
  61. 4 a
  62. end
  63. 1 context "when there is no table error" do
  64. 1 it "is a success without errors" do
  65. 1 result = process(input) {}
  66. 1 expect(result).to have_attributes(result: Success(), messages: [])
  67. end
  68. 1 it "yields a commented result for each valid and invalid row" do
  69. 1 results = process_to_a(input)
  70. 1 expect(results).to have_attributes(size: 3)
  71. 1 expect(results[0]).to have_attributes(result: be_success, messages: be_empty)
  72. 1 expect(results[1]).to have_attributes(result: be_success, messages: be_empty)
  73. 1 expect(results[2]).to have_attributes(result: be_failure, messages: have_attributes(size: 1))
  74. end
  75. 1 it "yields the successful value for each valid row" do
  76. 1 results = process_to_a(input)
  77. 1 expect(results[0].result).to eq(
  78. Success(foo: "olleh", "bar" => [nil, nil, "foo@bar.baz", nil, Float])
  79. )
  80. 1 expect(results[1].result).to eq(
  81. Success(foo: "dlrow", "bar" => [nil, nil, "foo@bar.baz", nil, Float])
  82. )
  83. end
  84. 1 it "yields the failure data for each invalid row" do
  85. 1 results = process_to_a(input)
  86. 1 expect(results[2].result).to eq(Failure())
  87. 1 expect(results[2].messages).to contain_exactly(
  88. have_attributes(
  89. code: "must_be_email",
  90. code_data: { value: "boudiou !".inspect },
  91. scope: Tabulard::Messaging::SCOPES::CELL,
  92. scope_data: { row: 4, col: "B" },
  93. severity: Tabulard::Messaging::SEVERITIES::ERROR
  94. )
  95. )
  96. end
  97. end
  98. 1 context "when there are unspecified columns in the table" do
  99. 1 before do
  100. 3 input.each_index do |idx|
  101. 12 input[idx] = input[idx][0..1] + ["oof"] + input[idx][2..] + ["rab"]
  102. end
  103. end
  104. 1 context "when the template allows it" do
  105. 2 before { template_opts[:ignore_unspecified_columns] = true }
  106. 1 it "ignores the unspecified columns" do
  107. 1 results = process_to_a(input)
  108. 1 expect(results[0].result).to eq(
  109. Success(foo: "olleh", "bar" => [nil, nil, "foo@bar.baz", nil, Float])
  110. )
  111. 1 expect(results[1].result).to eq(
  112. Success(foo: "dlrow", "bar" => [nil, nil, "foo@bar.baz", nil, Float])
  113. )
  114. end
  115. end
  116. 1 context "when the template doesn't allow it" do
  117. 3 before { template_opts[:ignore_unspecified_columns] = false }
  118. 1 it "doesn't yield any row" do
  119. 2 expect { |b| process(input, &b) }.not_to yield_control
  120. end
  121. 1 it "returns a failure with data" do # rubocop:disable RSpec/ExampleLength
  122. 1 expect(process(input) {}).to have_attributes(
  123. result: Failure(),
  124. messages: contain_exactly(
  125. have_attributes(
  126. code: "invalid_header",
  127. code_data: { value: "oof" },
  128. scope: Tabulard::Messaging::SCOPES::COL,
  129. scope_data: { col: "C" },
  130. severity: Tabulard::Messaging::SEVERITIES::ERROR
  131. ),
  132. have_attributes(
  133. code: "invalid_header",
  134. code_data: { value: "rab" },
  135. scope: Tabulard::Messaging::SCOPES::COL,
  136. scope_data: { col: "F" },
  137. severity: Tabulard::Messaging::SEVERITIES::ERROR
  138. )
  139. )
  140. )
  141. end
  142. end
  143. end
  144. 1 context "when there are missing columns" do
  145. 1 before do
  146. 2 input.each do |input|
  147. 8 input.delete_at(2)
  148. 8 input.delete_at(0)
  149. end
  150. end
  151. 1 it "doesn't yield any row" do
  152. 2 expect { |b| process(input, &b) }.not_to yield_control
  153. end
  154. 1 it "returns a failure with data" do # rubocop:disable RSpec/ExampleLength
  155. 1 expect(process(input) {}).to have_attributes(
  156. result: Failure(),
  157. messages: contain_exactly(
  158. have_attributes(
  159. code: "missing_column",
  160. code_data: { value: "Foo" },
  161. scope: Tabulard::Messaging::SCOPES::TABLE,
  162. scope_data: nil,
  163. severity: Tabulard::Messaging::SEVERITIES::ERROR
  164. ),
  165. have_attributes(
  166. code: "missing_column",
  167. code_data: { value: "Bar 5" },
  168. scope: Tabulard::Messaging::SCOPES::TABLE,
  169. scope_data: nil,
  170. severity: Tabulard::Messaging::SEVERITIES::ERROR
  171. )
  172. )
  173. )
  174. end
  175. end
  176. end