-
# frozen_string_literal: true
-
-
# {Tabulard} is a library designed to process tabular data according to a
-
# {Tabulard::Template developer-defined structure}. It will turn each row into a
-
# object whose keys and types are specified by the structure.
-
#
-
# It can work with tabular data presented in different formats by delegating
-
# the parsing of documents to specialized adapters
-
# ({Tabulard::Adapters::Xlsx}, {Tabulard::Adapters::Csv}, etc...).
-
#
-
# Given a tabular document and a specification of the document structure,
-
# Tabulard may process the document by handling the following tasks:
-
#
-
# - validation of the document's actual structure
-
# - arbitrary complex typecasting of each row into a validated object,
-
# according to the document specification
-
# - fine-grained error handling (at the table/row/col/cell level)
-
# - all of the above done so that internationalization of messages is easy
-
#
-
# Tabulard is designed with memory efficiency in mind by processing documents
-
# one row at a time, thus not requiring parsing and loading the whole document
-
# in memory upfront (depending on the adapter). The memory consumption of the
-
# library should therefore theoretically stay stable during the processing of a
-
# document, disregarding how many rows it may have.
-
1
module Tabulard
-
end
-
-
1
require "tabulard/template"
-
1
require "tabulard/template_config"
-
1
require "tabulard/table_processor"
-
1
require "tabulard/adapters/bare"
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
module Adapters
-
1
class << self
-
1
def open(*args, adapter:, **opts, &block)
-
14
adapter.open(*args, **opts, &block)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../table"
-
-
1
module Tabulard
-
1
module Adapters
-
1
class Bare
-
1
include Table
-
-
1
def initialize(table, headers: nil, **opts)
-
32
super(**opts)
-
-
32
then: 22
if (table_size = table.size).positive?
-
22
init_with_filled_table(table, table_size: table_size, headers: headers)
-
else: 10
else
-
10
init_with_empty_table(headers: headers)
-
end
-
end
-
-
1
def each_header
-
21
raise_if_closed
-
-
21
else: 17
then: 2
return to_enum(:each_header) { @cols_count } unless block_given?
-
-
17
@cols_count.times do |col_index|
-
57
col, cell_value = read_cell(@headers, col_index)
-
-
57
yield Header.new(col: col, value: cell_value)
-
end
-
-
17
self
-
end
-
-
1
def each_row
-
16
raise_if_closed
-
-
16
else: 12
then: 2
return to_enum(:each_row) { @rows_count } unless block_given?
-
-
12
@rows_count.times do |row_index|
-
28
row, row_data = read_row(row_index)
-
-
28
row_value = Array.new(@cols_count) do |col_index|
-
118
col, cell_value = read_cell(row_data, col_index)
-
-
118
Cell.new(row: row, col: col, value: cell_value)
-
end
-
-
28
yield Row.new(row: row, value: row_value)
-
end
-
-
12
self
-
end
-
-
1
private
-
-
1
def init_with_filled_table(table, table_size:, headers:)
-
22
@table = table
-
-
22
then: 4
if headers
-
4
ensure_compatible_size(table[0].size, headers.size)
-
-
2
headers_row = -1
-
2
@headers = headers
-
else: 18
else
-
18
headers_row = 0
-
18
@headers = table[headers_row]
-
end
-
-
20
@first_row = headers_row.succ
-
20
@first_row_name = @first_row.succ
-
20
@rows_count = table_size - @first_row
-
-
20
@first_col = 0
-
20
@first_col_name = @first_col.succ
-
20
@cols_count = @headers.size
-
end
-
-
1
def init_with_empty_table(headers:)
-
10
@rows_count = 0
-
-
10
then: 1
if headers
-
1
@headers = headers
-
1
@first_col = 0
-
1
@first_col_name = @first_col.succ
-
1
@cols_count = headers.size
-
else: 9
else
-
9
@cols_count = 0
-
end
-
end
-
-
1
def read_row(row_index)
-
28
row = @first_row_name + row_index
-
28
row_data = @table[@first_row + row_index]
-
-
28
[row, row_data]
-
end
-
-
1
def read_cell(row_data, col_index)
-
175
col = Table.int2col(@first_col_name + col_index)
-
175
cell_value = row_data[@first_col + col_index]
-
-
175
[col, cell_value]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "csv"
-
-
1
require_relative "../table"
-
-
1
module Tabulard
-
1
module Adapters
-
1
class Csv
-
1
include Table
-
-
1
class InvalidCSV < Message
-
1
CODE = "invalid_csv"
-
-
1
def_validator do
-
1
table
-
1
nil_code_data
-
end
-
end
-
-
1
DEFAULTS = {
-
row_sep: :auto,
-
col_sep: ",",
-
quote_char: '"',
-
}.freeze
-
-
1
private_constant :DEFAULTS
-
-
1
def self.defaults
-
87
DEFAULTS
-
end
-
-
1
def initialize(
-
io,
-
row_sep: self.class.defaults[:row_sep],
-
col_sep: self.class.defaults[:col_sep],
-
quote_char: self.class.defaults[:quote_char],
-
headers: nil,
-
**opts
-
)
-
29
super(**opts)
-
-
29
csv = CSV.new(
-
io,
-
row_sep: row_sep,
-
col_sep: col_sep,
-
quote_char: quote_char
-
)
-
-
29
then: 5
if headers
-
5
init_with_headers(csv, headers)
-
else: 24
else
-
24
init_without_headers(csv)
-
end
-
end
-
-
1
def each_header
-
16
raise_if_closed
-
-
16
else: 10
then: 4
return to_enum(:each_header) { @cols_count } unless block_given?
-
10
then: 3
else: 7
return self if @cols_count.zero?
-
-
7
@headers.each_with_index do |header, col_idx|
-
37
col = Table.int2col(col_idx + 1)
-
-
37
yield Header.new(col: col, value: header)
-
end
-
-
7
self
-
end
-
-
1
def each_row
-
11
raise_if_closed
-
-
9
else: 7
then: 2
return to_enum(:each_row) unless block_given?
-
7
else: 4
then: 3
return self unless @csv
-
-
4
handle_malformed_csv do
-
4
@csv.each.with_index(@first_row_name) do |raw, row|
-
13
value = Array.new(@cols_count) do |col_idx|
-
52
col = Table.int2col(col_idx + 1)
-
-
52
Cell.new(row: row, col: col, value: raw[col_idx])
-
end
-
-
13
yield Row.new(row: row, value: value)
-
end
-
end
-
-
4
self
-
end
-
-
# The adapter isn't responsible for opening the IO, and therefore it is not responsible for
-
# closing it either.
-
-
1
private
-
-
1
def handle_malformed_csv
-
33
yield
-
rescue CSV::MalformedCSVError
-
1
messenger.error(InvalidCSV.new)
-
-
1
raise InputError
-
end
-
-
1
def init_with_headers(csv, headers_data)
-
10
first_row_data = handle_malformed_csv { csv.shift }
-
-
5
then: 4
if first_row_data
-
4
ensure_compatible_size(first_row_data.size, headers_data.size)
-
-
2
csv.rewind
-
2
@csv = csv
-
2
@first_row_name = 1
-
else: 1
else
-
1
@csv = nil
-
end
-
-
3
@headers = headers_data
-
3
@cols_count = @headers.size
-
end
-
-
1
def init_without_headers(csv)
-
48
first_row_data = handle_malformed_csv { csv.shift }
-
-
23
then: 11
if first_row_data
-
11
@csv = csv
-
11
@first_row_name = 2
-
11
@headers = first_row_data
-
11
@cols_count = @headers.size
-
else: 12
else
-
12
@csv = nil
-
12
@headers = nil
-
12
@cols_count = 0
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# NOTE: As reference:
-
# - {Roo::Excelx::Cell#cell_value} => the "raw" value before Excel's typecasts
-
# - {Roo::Excelx::Cell#value} => the "user" value, after Excel's typecasts
-
1
require "roo"
-
-
1
require_relative "../table"
-
-
1
module Tabulard
-
1
module Adapters
-
1
class Xlsx
-
1
include Table
-
-
1
def initialize(path, headers: nil, **opts)
-
29
super(**opts)
-
-
29
@roo = Roo::Excelx.new(path)
-
-
29
worktable = @roo.sheet_for(@roo.default_sheet)
-
-
29
then: 19
if worktable.first_row
-
19
init_with_filled_table(worktable, headers: headers)
-
else: 10
else
-
10
init_with_empty_table(headers: headers)
-
end
-
end
-
-
1
def each_header(&block)
-
15
raise_if_closed
-
-
15
else: 11
then: 2
return to_enum(:each_header) { @cols_count } unless block
-
11
then: 3
else: 8
return self if @cols_count.zero?
-
-
8
then: 2
if @headers
-
2
each_custom_header(&block)
-
else: 6
else
-
6
each_cell_header(&block)
-
end
-
-
8
self
-
end
-
-
1
def each_row
-
14
raise_if_closed
-
-
14
else: 10
then: 2
return to_enum(:each_row) { @rows_count } unless block_given?
-
-
10
@rows_count.times do |row_index|
-
17
row = @rows.name(row_index)
-
-
17
row_value = Array.new(@cols_count) do |col_index|
-
85
col = @cols.name(col_index)
-
-
85
cell_coords = [@rows.coord(row_index), @cols.coord(col_index)]
-
85
then: 75
else: 10
cell_value = @cells[cell_coords]&.value
-
-
85
Cell.new(row: row, col: col, value: cell_value)
-
end
-
-
17
yield Row.new(row: row, value: row_value)
-
end
-
-
10
self
-
end
-
-
1
def close
-
33
super do
-
27
@roo.close
-
end
-
end
-
-
1
private
-
-
1
def init_with_filled_table(worktable, headers:)
-
19
@rows = Rows.new(
-
first_row: worktable.first_row,
-
last_row: worktable.last_row,
-
include_headers: headers.nil?
-
)
-
-
19
@cols = Cols.new(
-
first_col: worktable.first_column,
-
last_col: worktable.last_column
-
)
-
-
19
@rows_count = @rows.count
-
19
@cols_count = @cols.count
-
-
19
then: 4
else: 15
if (@headers = headers)
-
4
ensure_compatible_size(@cols_count, @headers.size)
-
end
-
-
17
@cells = worktable.cells
-
end
-
-
1
def init_with_empty_table(headers:)
-
10
@rows = nil
-
10
@rows_count = 0
-
-
10
then: 1
if headers
-
1
@cols = Cols.new(first_col: 1, last_col: headers.size)
-
1
@cols_count = @cols.count
-
else: 9
else
-
9
@cols = nil
-
9
@cols_count = 0
-
end
-
-
10
@headers = headers
-
10
@cells = nil
-
end
-
-
1
def each_custom_header
-
2
@headers.each_with_index do |value, col_index|
-
8
col = @cols.name(col_index)
-
-
8
yield Header.new(col: col, value: value)
-
end
-
end
-
-
1
def each_cell_header
-
6
@cols_count.times do |col_index|
-
30
col = @cols.name(col_index)
-
-
30
cell_coords = [@rows.headers_coord, @cols.coord(col_index)]
-
30
then: 30
else: 0
cell_value = @cells[cell_coords]&.value
-
-
30
yield Header.new(col: col, value: cell_value)
-
end
-
end
-
-
1
class Rows
-
1
def initialize(first_row:, last_row:, include_headers:)
-
19
then: 15
if include_headers
-
15
@headers_coord = first_row
-
15
init(first_row.succ, last_row)
-
else: 4
else
-
4
@headers_coord = nil
-
4
init(first_row, last_row)
-
end
-
end
-
-
1
attr_reader :count, :headers_coord
-
-
1
def name(row)
-
17
@first_name + row
-
end
-
-
1
def coord(row)
-
85
@first_row + row
-
end
-
-
1
private
-
-
1
def init(first_row, last_row)
-
19
offset = first_row - 1
-
-
19
@first_row = first_row
-
19
@first_name = first_row
-
19
@count = last_row - offset
-
end
-
end
-
-
1
class Cols
-
1
def initialize(first_col:, last_col:)
-
20
cols_offset = first_col - 1
-
-
20
@first_col = first_col
-
20
@first_name = first_col
-
20
@count = last_col - cols_offset
-
end
-
-
1
attr_reader :count
-
-
1
def name(col)
-
123
Table.int2col(@first_name + col)
-
end
-
-
1
def coord(col)
-
115
@first_col + col
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "attribute_types"
-
1
require_relative "column"
-
-
1
module Tabulard
-
# The main building block of a {Template}.
-
1
class Attribute
-
# A smarter version of {#initialize}.
-
#
-
# - It automatically freezes the instance before returning it.
-
# - It instantiates and injects a type automatically by passing the arguments to
-
# {AttributeTypes.build}.
-
#
-
# @return [Attribute] a frozen instance
-
1
def self.build(key:, type:)
-
29
type = AttributeTypes.build(type)
-
-
29
attribute = new(key: key, type: type)
-
29
attribute.freeze
-
end
-
-
# @param key [String, Symbol] The key in the resulting Hash after processing a row.
-
# @param type [AttributeType] The type of the value.
-
1
def initialize(key:, type:)
-
30
@key = key
-
30
@type = type
-
end
-
-
# @return [Symbol, String]
-
1
attr_reader :key
-
-
# An abstract specification of the type of a value in the resulting hash.
-
#
-
# It will be used to produce the {Types::Type concrete type} of a column (or a list of columns)
-
# when a {TemplateConfig} is {Template#apply applied} to the {Template} owning the attribtue.
-
#
-
# @return [AttributeType]
-
1
attr_reader :type
-
-
1
def ==(other)
-
2
other.is_a?(self.class) &&
-
key == other.key &&
-
type == other.type
-
end
-
-
1
def each_column(config)
-
19
else: 18
then: 1
return enum_for(:each_column, config) unless block_given?
-
-
18
compiled_type = type.compile(config.types)
-
-
18
type.each_column do |index, required|
-
54
header, header_pattern = config.header(key, index)
-
-
54
yield Column.new(
-
key: key,
-
type: compiled_type,
-
index: index,
-
header: header,
-
header_pattern: header_pattern,
-
required: required
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "attribute_types/scalar"
-
1
require_relative "attribute_types/composite"
-
-
1
module Tabulard
-
1
module AttributeTypes
-
# A factory for all {AttributeType}.
-
1
def self.build(type)
-
33
then: 10
if type.is_a?(Hash) && type.key?(:composite)
-
10
else: 23
Composite.build(**type)
-
23
then: 1
elsif type.is_a?(Array)
-
1
Composite.build(composite: :array, scalars: type)
-
else: 22
else
-
22
Scalar.build(type)
-
end
-
end
-
end
-
-
# @!parse
-
# # The minimal interface of an {Attribute} type.
-
# # @abstract It only exists to document the interface implemented by the different classes of
-
# # {AttributeTypes}.
-
# module AttributeType
-
# # A smarter version of `#initialize`, that will always return a frozen instance of the
-
# # type, with optional syntactic sugar for its arguments compared to `#initialize`.
-
# def self.build(*); end
-
#
-
# # Given a type container, return the actual, usable type for the attribute.
-
# # @param container [Types::Container]
-
# # @return [Types::Type]
-
# def compile(container); end
-
#
-
# # Enumerate the columns (one or more) that may compose the attribute in the tabular
-
# # document.
-
# #
-
# # @yieldparam index [Integer, nil]
-
# # If there is only one column involved, then `index` will be `nil`. Otherwise, `index`
-
# # will start at `0` and increase by `1` at each step.
-
# # @yieldparam required [Boolean]
-
# # Whether the column must be present in the tabular document.
-
# #
-
# # @overload def each_column(&block)
-
# # @return [self]
-
# # @overload def each_column
-
# # @return [Enumerator]
-
# def each_column; end
-
# end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "value"
-
-
1
module Tabulard
-
1
module AttributeTypes
-
1
class Composite
-
# A smarter version of {#initialize}.
-
#
-
# - It automatically freezes the instance before returning it.
-
# - It instantiates, freezes and injects a list of values automatically by mapping the given
-
# list of scalars to a list of {Value values} using {Value.build}.
-
#
-
# @return [Composite] a frozen instance
-
1
def self.build(composite:, scalars:)
-
64
scalars = scalars.map { |scalar| Value.build(scalar) }
-
12
scalars.freeze
-
-
12
composite = new(composite: composite, scalars: scalars)
-
12
composite.freeze
-
end
-
-
# @param composite [Symbol] The name used to refer to a composite type from a
-
# {Types::Container}.
-
# @param scalars [Array<Value>] The list of values of the composite.
-
# @see .build
-
1
def initialize(composite:, scalars:)
-
15
@composite_type = composite
-
15
@values = scalars
-
end
-
-
1
def compile(container)
-
9
container.composite(composite_type, values.map(&:type))
-
end
-
-
1
def each_column
-
11
else: 9
then: 1
return enum_for(:each_column) { values.size } unless block_given?
-
-
9
values.each_with_index do |value, index|
-
45
yield index, value.required
-
end
-
-
9
self
-
end
-
-
1
def ==(other)
-
2
other.is_a?(self.class) &&
-
composite_type == other.composite_type &&
-
values == other.values
-
end
-
-
1
protected
-
-
1
attr_reader :composite_type, :values
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "value"
-
-
1
module Tabulard
-
1
module AttributeTypes
-
# @see AttributeType
-
1
class Scalar
-
# @!parse
-
# include AttributeType
-
-
# A smarter version of {#initialize}.
-
#
-
# - It automatically freezes the instance before returning it.
-
# - It instantiates and injects a value automatically by passing the arguments to
-
# {Value.build}.
-
#
-
# The method signature is identical to the one of {Value.build}.
-
#
-
# @return [Scalar] a frozen instance
-
1
def self.build(...)
-
23
value = Value.build(...)
-
-
23
scalar = new(value)
-
23
scalar.freeze
-
end
-
-
# @param value [Value] The value of the scalar.
-
# @see .build
-
1
def initialize(value)
-
26
@value = value
-
end
-
-
# @see AttributeType#compile
-
1
def compile(container)
-
9
container.scalar(value.type)
-
end
-
-
# @see AttributeType#each_column
-
1
def each_column
-
11
else: 9
then: 1
return enum_for(:each_column) { 1 } unless block_given?
-
-
9
yield nil, value.required
-
-
9
self
-
end
-
-
1
def ==(other)
-
4
other.is_a?(self.class) &&
-
value == other.value
-
end
-
-
1
protected
-
-
1
attr_reader :value
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
module AttributeTypes
-
1
class Value
-
# A smarter version of {#initialize}.
-
#
-
# - It automatically freezes the instance before returning it.
-
# - It accepts two kinds of parameters: either those of {#initialize}, or a shortcut. See the
-
# overloaded method signature for details.
-
#
-
# @overload def build(type:, required:)
-
# @example
-
# Value.build(type: :foo, required: true)
-
# @see #initialize
-
#
-
# @overload def build(type)
-
# @param type [Symbol] The name of the type, optionally suffixed with `!` to indicate that
-
# the value is required.
-
# @example
-
# Value.build(:foo) #=> Value.build(type: :foo, required: false)
-
# Value.build(:foo!) #=> Value.build(type: :foo, required: true)
-
#
-
# @return [Value] a frozen instance
-
1
def self.build(arg)
-
81
then: 7
else: 74
value = arg.is_a?(Hash) ? new(**arg) : from_type_name(arg)
-
81
value.freeze
-
end
-
-
1
def self.from_type_name(type)
-
74
type = type.to_s
-
-
74
optional = type.end_with?("?")
-
74
then: 39
else: 35
type = (optional ? type.slice(0..-2) : type).to_sym
-
-
74
new(type: type, required: !optional)
-
end
-
-
1
private_class_method :from_type_name
-
-
# @param type [Symbol] The name used to refer to a scalar type from a {Types::Container}.
-
# @param required [Boolean] Is the value required to be given in the input ?
-
# @see .build
-
1
def initialize(type:, required: true)
-
94
@type = type
-
94
@required = required
-
end
-
-
# @return [Symbol]
-
1
attr_reader :type
-
-
# @return [Boolean]
-
1
attr_reader :required
-
-
1
def ==(other)
-
15
other.is_a?(self.class) &&
-
type == other.type &&
-
required == other.required
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
class Column
-
1
def initialize(
-
key:,
-
type:,
-
index:,
-
header:,
-
header_pattern:,
-
required:
-
)
-
60
@key = key
-
60
@type = type
-
60
@index = index
-
60
@header = header
-
60
@header_pattern = header_pattern
-
60
@required = required
-
end
-
-
1
attr_reader :key,
-
:type,
-
:index,
-
:header,
-
:header_pattern
-
-
1
def required?
-
55
@required
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
module Errors
-
1
class Error < StandardError
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "error"
-
-
1
module Tabulard
-
1
module Errors
-
1
class SpecError < Error
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "error"
-
-
1
module Tabulard
-
1
module Errors
-
1
class TypeError < Error
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "set"
-
1
require_relative "messaging/messages/invalid_header"
-
1
require_relative "messaging/messages/duplicated_header"
-
1
require_relative "messaging/messages/missing_column"
-
-
1
module Tabulard
-
1
class Headers
-
1
include Utils::MonadicResult
-
-
1
class Header
-
1
def initialize(table_header, spec_column)
-
47
@header = table_header
-
47
@column = spec_column
-
end
-
-
1
attr_reader :header, :column
-
-
1
def ==(other)
-
3
other.is_a?(self.class) &&
-
header == other.header &&
-
column == other.column
-
end
-
-
1
def row_value_index
-
60
header.row_value_index
-
end
-
end
-
-
1
def initialize(specification:, messenger:)
-
16
@specification = specification
-
16
@messenger = messenger
-
16
@headers = []
-
16
@columns = Set.new
-
16
@failure = false
-
end
-
-
1
def add(header)
-
54
column = @specification.get(header.value)
-
-
54
@messenger.scope_col!(header.col) do
-
54
else: 46
then: 8
return unless add_ensure_column_is_specified(header, column)
-
46
else: 44
then: 2
return unless add_ensure_column_is_unique(header, column)
-
end
-
-
44
@headers << Header.new(header, column)
-
end
-
-
1
def result
-
13
missing_columns = @specification.required_columns - @columns.to_a
-
-
13
else: 11
then: 2
unless missing_columns.empty?
-
2
@failure = true
-
-
2
missing_columns.each do |column|
-
4
@messenger.error(
-
Messaging::Messages::MissingColumn.new(code_data: { value: column.header })
-
)
-
end
-
end
-
-
13
then: 6
if @failure
-
6
Failure()
-
else: 7
else
-
7
Success(@headers)
-
end
-
end
-
-
1
private
-
-
1
def add_ensure_column_is_specified(header, column)
-
54
else: 8
then: 46
return true unless column.nil?
-
-
8
else: 2
then: 6
unless @specification.ignore_unspecified_columns?
-
6
@failure = true
-
6
@messenger.error(
-
Messaging::Messages::InvalidHeader.new(code_data: { value: header.value })
-
)
-
end
-
-
8
false
-
end
-
-
1
def add_ensure_column_is_unique(header, column)
-
46
then: 44
else: 2
return true if @columns.add?(column)
-
-
2
@failure = true
-
2
@messenger.error(
-
Messaging::Messages::DuplicatedHeader.new(code_data: { value: header.value })
-
)
-
-
2
false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "messaging/config"
-
1
require_relative "messaging/constants"
-
1
require_relative "messaging/message"
-
1
require_relative "messaging/message_variant"
-
1
require_relative "messaging/messenger"
-
-
1
module Tabulard
-
1
module Messaging
-
1
class << self
-
1
attr_accessor :config
-
-
1
def configure
-
2
config = self.config.dup
-
2
yield config
-
2
self.config = config.freeze
-
end
-
end
-
-
1
self.config = Config.new.freeze
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
module Messaging
-
1
class Config
-
1
def initialize(validate_messages: default_validate_messages)
-
10
@validate_messages = validate_messages
-
end
-
-
1
attr_accessor :validate_messages
-
-
1
private
-
-
1
def default_validate_messages
-
6
ENV["TABULARD_MESSAGING_VALIDATE_MESSAGES"] != "false"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
module Messaging
-
1
module SCOPES
-
1
TABLE = "TABLE"
-
1
ROW = "ROW"
-
1
COL = "COL"
-
1
CELL = "CELL"
-
end
-
-
1
module SEVERITIES
-
1
WARN = "WARN"
-
1
ERROR = "ERROR"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "constants"
-
1
require_relative "validations"
-
-
1
module Tabulard
-
1
module Messaging
-
1
class Message
-
1
include Validations
-
-
1
def initialize(
-
code:,
-
code_data: nil,
-
scope: SCOPES::TABLE,
-
scope_data: nil,
-
severity: SEVERITIES::WARN
-
)
-
129
@code = code
-
129
@code_data = code_data
-
129
@scope = scope
-
129
@scope_data = scope_data
-
129
@severity = severity
-
end
-
-
1
attr_accessor(
-
:code,
-
:code_data,
-
:scope,
-
:scope_data,
-
:severity
-
)
-
-
1
def ==(other)
-
25
other.is_a?(self.class) &&
-
code == other.code &&
-
code_data == other.code_data &&
-
scope == other.scope &&
-
scope_data == other.scope_data &&
-
severity == other.severity
-
end
-
-
1
def to_s
-
6
parts = [scoping_to_s, "#{severity}: #{code}", code_data]
-
6
parts.compact!
-
6
parts.join(" ")
-
end
-
-
1
def to_h
-
{
-
1
code: code,
-
code_data: code_data,
-
scope: scope,
-
scope_data: scope_data,
-
severity: severity,
-
}
-
end
-
-
1
private
-
-
1
def scoping_to_s
-
6
when: 2
else: 1
case scope
-
2
when: 1
when SCOPES::TABLE then "[#{scope}]"
-
1
when: 1
when SCOPES::ROW then "[#{scope}: #{scope_data[:row]}]"
-
1
when: 1
when SCOPES::COL then "[#{scope}: #{scope_data[:col]}]"
-
1
when SCOPES::CELL then "[#{scope}: #{scope_data[:col]}#{scope_data[:row]}]"
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "message"
-
-
1
module Tabulard
-
1
module Messaging
-
# While a {Message} represents any kind of message, {MessageVariant} represents any subset of
-
# messages that share the same code.
-
#
-
# The code of a message variant can and should be defined at the class level, given that it
-
# won't differ among the instances (and a validation is defined to enforce that invariant).
-
# {MessageVariant} should be considered an abstract class, and its subclasses should define
-
# their own `CODE` constant, which will be read by {.code}.
-
#
-
# As far as the other methods are concerned, {.code} should be considered the only source of
-
# truth when it comes to reading the code assigned to a message variant. The fact that {.code}
-
# is actually implemented using a dynamic resolution of the class' `CODE` constant is an
-
# implementation detail stemming from the fact that documentation tools such as YARD will
-
# highlight constants, as opposed to instance variables of a class for example. Using a constant
-
# is therefore meant to provide better documentation, and it should not be relied upon
-
# otherwise.
-
#
-
# @abstract
-
1
class MessageVariant < Message
-
# Reads the code assigned to the class (and its instances)
-
# @return [String]
-
1
def self.code
-
148
self::CODE
-
end
-
-
# Simplifies the initialization of a variant
-
#
-
# Contrary to the requirements of {Message#initialize}, {MessageVariant.new} doesn't require
-
# the caller to pass the `:code` keyword argument, as it is capable of prodividing it
-
# automatically (from reading {.code}).
-
1
def self.new(**opts)
-
107
super(code: code, **opts)
-
end
-
-
1
def_validator do
-
1
def validate_code(message)
-
42
message.code == message.class.code
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../message_variant"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Messages
-
1
class CleanedString < MessageVariant
-
1
CODE = "cleaned_string"
-
-
1
def_validator do
-
1
cell
-
1
nil_code_data
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../message_variant"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Messages
-
1
class DuplicatedHeader < MessageVariant
-
1
CODE = "duplicated_header"
-
-
1
def_validator do
-
1
col
-
-
1
def validate_code_data(message)
-
4
in: 3
else: 1
message.code_data in { value: String }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../message_variant"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Messages
-
1
class InvalidHeader < MessageVariant
-
1
CODE = "invalid_header"
-
-
1
def_validator do
-
1
col
-
-
1
def validate_code_data(message)
-
8
in: 7
else: 1
message.code_data in { value: String }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../message_variant"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Messages
-
1
class MissingColumn < MessageVariant
-
1
CODE = "missing_column"
-
-
1
def_validator do
-
1
table
-
-
1
def validate_code_data(message)
-
6
in: 5
else: 1
message.code_data in { value: String }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../message_variant"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Messages
-
1
class MustBeArray < MessageVariant
-
1
CODE = "must_be_array"
-
-
1
def_validator do
-
1
cell
-
1
nil_code_data
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../message_variant"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Messages
-
1
class MustBeBoolsy < MessageVariant
-
1
CODE = "must_be_boolsy"
-
-
1
def_validator do
-
1
cell
-
-
1
def validate_code_data(message)
-
2
in: 1
else: 1
message.code_data in { value: String }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../message_variant"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Messages
-
1
class MustBeDate < MessageVariant
-
1
CODE = "must_be_date"
-
-
1
def_validator do
-
1
cell
-
-
1
def validate_code_data(message)
-
2
in: 1
else: 1
message.code_data in { format: String }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../message_variant"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Messages
-
1
class MustBeEmail < MessageVariant
-
1
CODE = "must_be_email"
-
-
1
def_validator do
-
1
cell
-
-
1
def validate_code_data(message)
-
7
in: 6
else: 1
message.code_data in { value: String }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../message_variant"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Messages
-
1
class MustBeString < MessageVariant
-
1
CODE = "must_be_string"
-
-
1
def_validator do
-
1
cell
-
1
nil_code_data
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../message_variant"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Messages
-
1
class MustExist < MessageVariant
-
1
CODE = "must_exist"
-
-
1
def_validator do
-
1
cell
-
1
nil_code_data
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "constants"
-
-
1
module Tabulard
-
1
module Messaging
-
1
class Messenger
-
1
def initialize(
-
scope: SCOPES::TABLE,
-
scope_data: nil,
-
validate_messages: Messaging.config.validate_messages
-
)
-
162
@scope = scope.freeze
-
162
@scope_data = scope_data.freeze
-
162
@messages = []
-
162
@validate_messages = validate_messages
-
end
-
-
1
attr_reader :scope, :scope_data, :messages, :validate_messages
-
-
1
def ==(other)
-
2
other.is_a?(self.class) &&
-
scope == other.scope &&
-
scope_data == other.scope_data &&
-
messages == other.messages &&
-
validate_messages == other.validate_messages
-
end
-
-
1
def dup
-
24
self.class.new(
-
scope: @scope,
-
scope_data: @scope_data,
-
validate_messages: @validate_messages
-
)
-
end
-
-
1
def scoping!(scope, scope_data, &block)
-
139
scope = scope.freeze
-
139
scope_data = scope_data.freeze
-
-
139
then: 137
if block
-
137
replace_scoping_block(scope, scope_data, &block)
-
else: 2
else
-
2
replace_scoping_noblock(scope, scope_data)
-
end
-
end
-
-
1
def scoping(...)
-
2
dup.scoping!(...)
-
end
-
-
1
def scope_row!(row, &)
-
24
scope = case @scope
-
when: 4
when SCOPES::COL, SCOPES::CELL
-
4
SCOPES::CELL
-
else: 20
else
-
20
SCOPES::ROW
-
end
-
-
24
scope_data = @scope_data.dup || {}
-
24
scope_data[:row] = row
-
-
24
scoping!(scope, scope_data, &)
-
end
-
-
1
def scope_col!(col, &)
-
125
scope = case @scope
-
when: 67
when SCOPES::ROW, SCOPES::CELL
-
67
SCOPES::CELL
-
else: 58
else
-
58
SCOPES::COL
-
end
-
-
125
scope_data = @scope_data.dup || {}
-
125
scope_data[:col] = col
-
-
125
scoping!(scope, scope_data, &)
-
end
-
-
1
def scope_row(...)
-
2
dup.scope_row!(...)
-
end
-
-
1
def scope_col(...)
-
2
dup.scope_col!(...)
-
end
-
-
1
def warn(message)
-
4
add(message, severity: SEVERITIES::WARN)
-
end
-
-
1
def error(message)
-
20
add(message, severity: SEVERITIES::ERROR)
-
end
-
-
1
private
-
-
1
def add(message, severity:)
-
24
message.scope = @scope
-
24
message.scope_data = @scope_data
-
24
message.severity = severity
-
-
24
then: 23
else: 1
message.validate if @validate_messages
-
-
24
messages << message
-
-
24
self
-
end
-
-
1
def replace_scoping_noblock(new_scope, new_scope_data)
-
2
@scope = new_scope
-
2
@scope_data = new_scope_data
-
-
2
self
-
end
-
-
1
def replace_scoping_block(new_scope, new_scope_data)
-
137
prev_scope = @scope
-
137
prev_scope_data = @scope_data
-
-
137
@scope = new_scope
-
137
@scope_data = new_scope_data
-
-
begin
-
137
yield self
-
ensure
-
137
@scope = prev_scope
-
137
@scope_data = prev_scope_data
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "validations/base_validator"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Validations
-
1
module ClassMethods
-
1
then: 12
else: 6
def def_validator(base: validator&.class || BaseValidator, &block)
-
20
@validator = Class.new(base, &block).new.freeze
-
end
-
-
1
def validator
-
120
then: 94
if defined?(@validator)
-
94
else: 26
@validator
-
26
then: 14
else: 12
elsif superclass.respond_to?(:validator)
-
14
superclass.validator
-
end
-
end
-
-
1
def validate(message)
-
35
then: 30
else: 5
validator&.validate(message)
-
end
-
end
-
-
1
def self.included(message_class)
-
10
message_class.extend(ClassMethods)
-
end
-
-
1
def validate
-
34
self.class.validate(self)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "dsl"
-
1
require_relative "invalid_message"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Validations
-
1
class BaseValidator
-
1
extend DSL
-
-
1
def validate(message)
-
33
errors = []
-
-
33
validate_and_append_code(message, errors)
-
33
validate_and_append_code_data(message, errors)
-
33
validate_and_append_scope(message, errors)
-
33
validate_and_append_scope_data(message, errors)
-
-
33
then: 30
else: 3
return if errors.empty?
-
-
3
raise InvalidMessage, build_exception_message(message, errors)
-
end
-
-
1
def validate_code(_message)
-
4
true
-
end
-
-
1
def validate_code_data(_message)
-
4
true
-
end
-
-
1
def validate_scope(_message)
-
3
true
-
end
-
-
1
def validate_scope_data(_message)
-
3
true
-
end
-
-
1
private
-
-
1
def validate_and_append_code(message, errors)
-
33
else: 32
then: 1
errors << "code" unless validate_code(message)
-
end
-
-
1
def validate_and_append_code_data(message, errors)
-
33
else: 32
then: 1
errors << "code_data" unless validate_code_data(message)
-
end
-
-
1
def validate_and_append_scope(message, errors)
-
33
else: 31
then: 2
errors << "scope" unless validate_scope(message)
-
end
-
-
1
def validate_and_append_scope_data(message, errors)
-
33
else: 32
then: 1
errors << "scope_data" unless validate_scope_data(message)
-
end
-
-
1
def build_exception_message(message, errors)
-
3
"#{errors.join(", ")} <#{message.class}>#{message.to_h}"
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "mixins"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Validations
-
1
module DSL
-
1
def cell
-
9
include Mixins::CellValidations
-
end
-
-
1
def col
-
4
include Mixins::ColValidations
-
end
-
-
1
def row
-
2
include Mixins::RowValidations
-
end
-
-
1
def table
-
5
include Mixins::TableValidations
-
end
-
-
1
def nil_code_data
-
5
include Mixins::NilCodeData
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../../errors/error"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Validations
-
1
class InvalidMessage < Errors::Error
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../constants"
-
-
1
module Tabulard
-
1
module Messaging
-
1
module Validations
-
1
module Mixins
-
1
module CellValidations
-
1
def validate_scope(message)
-
21
message.scope == SCOPES::CELL
-
end
-
-
1
def validate_scope_data(message)
-
21
in: 13
else: 8
message.scope_data in { col: String, row: Integer }
-
end
-
end
-
-
1
module ColValidations
-
1
def validate_scope(message)
-
14
message.scope == SCOPES::COL
-
end
-
-
1
def validate_scope_data(message)
-
14
in: 11
else: 3
message.scope_data in { col: String }
-
end
-
end
-
-
1
module RowValidations
-
1
def validate_scope(message)
-
2
message.scope == SCOPES::ROW
-
end
-
-
1
def validate_scope_data(message)
-
2
in: 1
else: 1
message.scope_data in { row: Integer }
-
end
-
end
-
-
1
module TableValidations
-
1
def validate_scope(message)
-
12
message.scope == SCOPES::TABLE
-
end
-
-
1
def validate_scope_data(message)
-
12
message.scope_data.nil?
-
end
-
end
-
-
1
module NilCodeData
-
1
def validate_code_data(message)
-
11
message.code_data.nil?
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "row_processor_result"
-
1
require_relative "row_value_builder"
-
-
1
module Tabulard
-
1
class RowProcessor
-
1
def initialize(headers:, messenger:)
-
6
@headers = headers
-
6
@messenger = messenger
-
end
-
-
1
def call(row)
-
16
messenger = @messenger.dup
-
-
16
builder = RowValueBuilder.new(messenger)
-
-
16
messenger.scope_row!(row.row) do
-
16
@headers.each do |header|
-
63
cell = row.value[header.row_value_index]
-
-
63
messenger.scope_col!(cell.col) do
-
63
builder.add(header.column, cell.value)
-
end
-
end
-
end
-
-
16
build_result(row, builder, messenger)
-
end
-
-
1
private
-
-
1
def build_result(row, builder, messenger)
-
16
RowProcessorResult.new(
-
row: row.row,
-
result: builder.result,
-
messages: messenger.messages
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
class RowProcessorResult
-
1
def initialize(row:, result:, messages: [])
-
23
@row = row
-
23
@result = result
-
23
@messages = messages
-
end
-
-
1
attr_reader :row, :result, :messages
-
-
1
def ==(other)
-
3
other.is_a?(self.class) &&
-
row == other.row &&
-
result == other.result &&
-
messages == other.messages
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "set"
-
1
require_relative "utils/monadic_result"
-
-
1
module Tabulard
-
1
class RowValueBuilder
-
1
include Utils::MonadicResult
-
-
1
def initialize(messenger)
-
21
@messenger = messenger
-
21
@data = {}
-
21
@composites = Set.new
-
21
@failure = false
-
end
-
-
1
def add(column, value)
-
68
key = column.key
-
68
type = column.type
-
68
index = column.index
-
-
68
result = type.scalar(index, value, @messenger)
-
-
68
result.bind do |scalar|
-
61
then: 44
if type.composite?
-
44
@composites << [key, type]
-
44
@data[key] ||= []
-
44
@data[key][index] = scalar
-
else: 17
else
-
17
@data[key] = scalar
-
end
-
end
-
-
75
result.or { @failure = true }
-
-
68
result
-
end
-
-
1
def result
-
21
then: 7
else: 14
return Failure() if @failure
-
-
14
Do() do
-
14
@composites.each do |key, type|
-
13
value = type.composite(@data[key], @messenger).unwrap
-
-
12
@data[key] = value
-
end
-
-
13
Success(@data)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
class Specification
-
1
def initialize(columns:, ignore_unspecified_columns: false)
-
13
@columns = columns
-
13
@ignore_unspecified_columns = ignore_unspecified_columns
-
end
-
-
1
def get(header)
-
42
then: 1
else: 41
return if header.nil?
-
-
41
@columns.find do |column|
-
144
column.header_pattern.match?(header)
-
end
-
end
-
-
1
def required_columns
-
9
@columns.select(&:required?)
-
end
-
-
1
def ignore_unspecified_columns?
-
6
@ignore_unspecified_columns
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "table/col_converter"
-
1
require_relative "errors/error"
-
1
require_relative "messaging"
-
1
require_relative "utils/monadic_result"
-
-
1
module Tabulard
-
1
module Table
-
1
def self.included(mod)
-
33
mod.extend(ClassMethods)
-
end
-
-
1
def self.col2int(...)
-
126
COL_CONVERTER.col2int(...)
-
end
-
-
1
def self.int2col(...)
-
621
COL_CONVERTER.int2col(...)
-
end
-
-
1
module ClassMethods
-
1
def open(*args, **opts)
-
20
table = new(*args, **opts)
-
17
else: 16
then: 1
return Utils::MonadicResult::Success.new(table) unless block_given?
-
-
begin
-
16
yield table
-
ensure
-
16
table.close
-
end
-
rescue InputError
-
3
Utils::MonadicResult::Failure.new
-
end
-
end
-
-
1
class Error < Errors::Error
-
end
-
-
1
class ClosureError < Error
-
end
-
-
1
class TooFewHeaders < Error
-
end
-
-
1
class TooManyHeaders < Error
-
end
-
-
1
class InputError < Error
-
end
-
-
1
Message = Messaging::MessageVariant
-
-
1
class Header
-
1
def initialize(col:, value:)
-
199
@col = col
-
199
@value = value
-
end
-
-
1
attr_reader :col, :value
-
-
1
def ==(other)
-
65
other.is_a?(self.class) && col == other.col && value == other.value
-
end
-
-
1
def row_value_index
-
60
Table.col2int(col) - 1
-
end
-
end
-
-
1
class Row
-
1
def initialize(row:, value:)
-
97
@row = row
-
97
@value = value
-
end
-
-
1
attr_reader :row, :value
-
-
1
def ==(other)
-
37
other.is_a?(self.class) && row == other.row && value == other.value
-
end
-
end
-
-
1
class Cell
-
1
def initialize(row:, col:, value:)
-
414
@row = row
-
414
@col = col
-
414
@value = value
-
end
-
-
1
attr_reader :row, :col, :value
-
-
1
def ==(other)
-
157
other.is_a?(self.class) && row == other.row && col == other.col && value == other.value
-
end
-
end
-
-
1
def initialize(messenger: Messaging::Messenger.new)
-
90
@messenger = messenger
-
90
@closed = false
-
end
-
-
1
attr_reader :messenger
-
-
1
def each_header
-
1
raise NoMethodError, "You must implement #{self.class}#each_header => self"
-
end
-
-
1
def each_row
-
1
raise NoMethodError, "You must implement #{self.class}#each_row => self"
-
end
-
-
1
def close
-
105
then: 19
else: 86
return if closed?
-
-
86
then: 27
else: 59
yield if block_given?
-
-
721
instance_variables.each { |ivar| remove_instance_variable(ivar) }
-
-
86
@closed = true
-
-
nil
-
end
-
-
1
def closed?
-
200
@closed == true
-
end
-
-
1
private
-
-
1
def raise_if_closed
-
93
then: 12
else: 81
raise ClosureError if closed?
-
end
-
-
1
def ensure_compatible_size(row_size, headers_size)
-
12
else: 6
case row_size <=> headers_size
-
when: 3
when -1
-
3
raise TooManyHeaders, "Expected #{row_size} headers, got: #{headers_size}"
-
when: 3
when 1
-
3
raise TooFewHeaders, "Expected #{row_size} headers, got: #{headers_size}"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
module Table
-
1
class ColConverter
-
1
CHARSET = ("A".."Z").to_a.freeze
-
1
CHARSET_SIZE = CHARSET.size
-
1
CHAR_TO_INT = CHARSET.map.with_index(1).to_h.freeze
-
1
INT_TO_CHAR = CHAR_TO_INT.invert.freeze
-
-
1
def col2int(col)
-
126
else: 124
then: 2
raise ArgumentError unless col.is_a?(String) && !col.empty?
-
-
124
int = 0
-
-
124
col.each_char.reverse_each.with_index do |char, pow|
-
137
int += char2int(char) * (CHARSET_SIZE**pow)
-
end
-
-
122
int
-
end
-
-
1
def int2col(int)
-
621
else: 617
then: 4
raise ArgumentError unless int.is_a?(Integer) && int.positive?
-
-
617
col = +""
-
-
617
body: 630
until int.zero?
-
630
int, char_int = int.divmod(CHARSET_SIZE)
-
-
630
then: 7
else: 623
if char_int.zero?
-
7
int -= 1
-
7
char_int = CHARSET_SIZE
-
end
-
-
630
col << int2char(char_int)
-
end
-
-
617
col.reverse!
-
617
col.freeze
-
end
-
-
1
private
-
-
1
def char2int(char)
-
137
CHAR_TO_INT[char] || raise(ArgumentError, char.inspect)
-
end
-
-
1
def int2char(int)
-
630
INT_TO_CHAR[int] || raise(ArgumentError, int.inspect)
-
end
-
end
-
-
1
private_constant :ColConverter
-
-
1
COL_CONVERTER = ColConverter.new.freeze
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "adapters"
-
1
require_relative "headers"
-
1
require_relative "messaging"
-
1
require_relative "row_processor"
-
1
require_relative "table"
-
1
require_relative "table_processor_result"
-
1
require_relative "utils/monadic_result"
-
-
1
module Tabulard
-
1
class TableProcessor
-
1
include Utils::MonadicResult
-
-
1
def initialize(specification)
-
14
@specification = specification
-
end
-
-
1
def call(*args, **opts, &block)
-
14
messenger = Messaging::Messenger.new
-
-
14
result = Adapters.open(*args, **opts, messenger: messenger) do |table|
-
12
process(table, messenger, &block)
-
end
-
-
14
handle_result(result, messenger)
-
end
-
-
1
private
-
-
1
def parse_headers(table, messenger)
-
12
headers = Headers.new(specification: @specification, messenger: messenger)
-
-
12
table.each_header do |header|
-
44
headers.add(header)
-
end
-
-
12
headers.result
-
end
-
-
1
def build_row_processor(table, messenger)
-
12
parse_headers(table, messenger).bind do |headers|
-
7
row_processor = RowProcessor.new(headers: headers, messenger: messenger)
-
-
7
Success(row_processor)
-
end
-
end
-
-
1
def process(table, messenger)
-
12
build_row_processor(table, messenger).bind do |row_processor|
-
7
table.each_row do |row|
-
21
yield row_processor.call(row)
-
end
-
-
7
Success()
-
end
-
end
-
-
1
def handle_result(result, messenger)
-
14
TableProcessorResult.new(result: result.discard, messages: messenger.messages)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
class TableProcessorResult
-
1
def initialize(result:, messages: [])
-
21
@result = result
-
21
@messages = messages
-
end
-
-
1
attr_reader :result, :messages
-
-
1
def ==(other)
-
4
other.is_a?(self.class) &&
-
result == other.result &&
-
messages == other.messages
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "set"
-
1
require_relative "attribute"
-
1
require_relative "specification"
-
1
require_relative "errors/spec_error"
-
-
1
module Tabulard
-
# A {Template} represents the abstract structure of a tabular document.
-
#
-
# The main component of the structure is the object obtained by processing a
-
# row. A template therefore specifies all possible attributes of that object
-
# as a list of (key, abstract type) pairs.
-
#
-
# Each attribute will eventually be compiled into as many concrete columns as
-
# necessary with the help of a {TemplateConfig config} to produce a
-
# {Specification specification}.
-
#
-
# In other words, a {Template} specifies the structure of the processing
-
# result (its attributes), whereas a {Specification} specifies the columns
-
# that may be involved into building the processing result.
-
#
-
# {Attribute Attributes} may either be _composite_ (their value is a
-
# composition of multiple values) or _scalar_ (their value is a single
-
# value). Scalar attributes will thus produce a single column in the
-
# specification, and composite attributes will produce as many columns as
-
# required by the number of scalar values they hold.
-
1
class Template
-
1
def self.build(attributes:, **kwargs)
-
33
attributes = attributes.map { |attribute| Attribute.build(**attribute) }
-
11
attributes.freeze
-
-
11
template = new(attributes: attributes, **kwargs)
-
11
template.freeze
-
end
-
-
1
def initialize(attributes:, ignore_unspecified_columns: false)
-
15
ensure_attributes_unicity(attributes)
-
-
14
@attributes = attributes
-
14
@ignore_unspecified_columns = ignore_unspecified_columns
-
end
-
-
1
def apply(config)
-
9
columns = []
-
-
9
attributes.each do |attribute|
-
18
attribute.each_column(config) do |column|
-
54
columns << column.freeze
-
end
-
end
-
-
9
specification = Specification.new(
-
columns: columns.freeze,
-
ignore_unspecified_columns: ignore_unspecified_columns
-
)
-
-
9
specification.freeze
-
end
-
-
1
def ==(other)
-
2
other.is_a?(self.class) &&
-
attributes == other.attributes &&
-
ignore_unspecified_columns == other.ignore_unspecified_columns
-
end
-
-
1
protected
-
-
1
attr_reader :attributes, :ignore_unspecified_columns
-
-
1
private
-
-
1
def ensure_attributes_unicity(attributes)
-
15
keys = Set.new
-
-
15
duplicate = attributes.find do |attribute|
-
30
!keys.add?(attribute.key)
-
end
-
-
15
else: 1
then: 14
return unless duplicate
-
-
1
raise Errors::SpecError, "Duplicated key: #{duplicate.key.inspect}"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "types/container"
-
-
1
module Tabulard
-
1
class TemplateConfig
-
1
def initialize(types: Types::Container.new)
-
14
@types = types
-
end
-
-
1
attr_reader :types
-
-
# Given an attribute key and a possibily-nil column index, return the header and header pattern
-
# for that column.
-
#
-
# The return value should be an array with two items:
-
#
-
# 1. The first item is the header, as a String.
-
# 2. The second item is the header pattern, and should respond to `#match?` with a boolean
-
# value. Instances of Regexp will obviously do, but the requirement is really about the
-
# `#match?` method.
-
#
-
# @param key [Symbol, String]
-
# @param index [Integer, nil]
-
# @return [Array(String, #match?)]
-
1
def header(key, index)
-
59
header = key.to_s.capitalize
-
59
then: 46
else: 13
header = "#{header} #{index + 1}" if index
-
-
59
pattern = /^#{Regexp.escape(header)}$/i
-
-
59
[header, pattern]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
module Types
-
# @private
-
1
module Cast
-
1
def ==(other)
-
3
other.is_a?(self.class) && other.config == config
-
end
-
-
1
protected
-
-
1
def config
-
6
instance_variables.each_with_object({}) do |ivar, acc|
-
10
acc[ivar] = instance_variable_get(ivar)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../utils/monadic_result"
-
-
1
module Tabulard
-
1
module Types
-
1
class CastChain
-
1
include Utils::MonadicResult
-
-
1
def initialize(casts = [])
-
73
@casts = casts
-
end
-
-
1
attr_reader :casts
-
-
1
def prepend(cast)
-
1
@casts.unshift(cast)
-
1
self
-
end
-
-
1
def append(cast)
-
102
@casts.push(cast)
-
102
self
-
end
-
-
1
def freeze
-
17
@casts.each(&:freeze)
-
17
@casts.freeze
-
17
super
-
end
-
-
1
def call(value, messenger)
-
75
failure = catch(:failure) do
-
75
success = catch(:success) do
-
75
@casts.reduce(value) do |prev_value, cast|
-
141
cast.call(prev_value, messenger)
-
end
-
end
-
-
68
return Success(success)
-
end
-
-
7
then: 6
else: 1
messenger.error(failure) if failure
-
-
7
Failure()
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "composite"
-
1
require_relative "../../messaging/messages/must_be_array"
-
-
1
module Tabulard
-
1
module Types
-
1
module Composites
-
1
Array = Composite.cast do |value, _messenger|
-
12
else: 11
then: 1
throw :failure, Messaging::Messages::MustBeArray.new unless value.is_a?(::Array)
-
-
11
value
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "array"
-
-
1
module Tabulard
-
1
module Types
-
1
module Composites
-
1
ArrayCompact = Array.cast do |value, _messenger|
-
1
value.compact
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../../errors/type_error"
-
1
require_relative "../type"
-
-
1
module Tabulard
-
1
module Types
-
1
module Composites
-
1
class Composite < Type
-
1
def initialize(types, **opts)
-
21
super(**opts)
-
-
21
@types = types
-
end
-
-
1
def composite?
-
43
true
-
end
-
-
1
def scalar(index, value, messenger)
-
51
then: 48
if (type = @types[index])
-
48
type.scalar(nil, value, messenger)
-
else: 3
else
-
3
raise Errors::TypeError, "Invalid index: #{index.inspect}"
-
end
-
end
-
-
1
alias composite cast
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../errors/type_error"
-
-
1
require_relative "scalars/scalar"
-
1
require_relative "scalars/string"
-
1
require_relative "scalars/email"
-
1
require_relative "scalars/boolsy"
-
1
require_relative "scalars/date_string"
-
1
require_relative "composites/array"
-
1
require_relative "composites/array_compact"
-
-
1
module Tabulard
-
1
module Types
-
1
class Container
-
1
scalar = Scalars::Scalar.new!
-
1
string = Scalars::String.new!
-
1
email = Scalars::Email.new!
-
1
boolsy = Scalars::Boolsy.new!
-
1
date_string = Scalars::DateString.new!
-
-
DEFAULTS = {
-
1
scalars: {
-
29
scalar: -> { scalar },
-
13
string: -> { string },
-
13
email: -> { email },
-
2
boolsy: -> { boolsy },
-
2
date_string: -> { date_string },
-
}.freeze,
-
composites: {
-
10
array: ->(types) { Composites::Array.new!(types) },
-
1
array_compact: ->(types) { Composites::ArrayCompact.new!(types) },
-
}.freeze,
-
}.freeze
-
-
1
def initialize(scalars: nil, composites: nil, defaults: DEFAULTS)
-
@scalars =
-
28
then: 11
else: 17
(scalars ? defaults[:scalars].merge(scalars) : defaults[:scalars]).freeze
-
-
@composites =
-
28
then: 2
else: 26
(composites ? defaults[:composites].merge(composites) : defaults[:composites]).freeze
-
end
-
-
1
def scalars
-
3
@scalars.keys
-
end
-
-
1
def composites
-
3
@composites.keys
-
end
-
-
1
def scalar(scalar_name)
-
76
builder = fetch_scalar_builder(scalar_name)
-
-
74
builder.call
-
end
-
-
1
def composite(composite_name, scalar_names)
-
16
builder = fetch_composite_builder(composite_name)
-
-
68
scalars = scalar_names.map { |scalar_name| scalar(scalar_name) }
-
-
14
builder.call(scalars)
-
end
-
-
1
private
-
-
1
def fetch_scalar_builder(type)
-
76
@scalars.fetch(type) do
-
2
raise Errors::TypeError, "Invalid scalar type: #{type.inspect}"
-
end
-
end
-
-
1
def fetch_composite_builder(type)
-
16
@composites.fetch(type) do
-
1
raise Errors::TypeError, "Invalid composite type: #{type.inspect}"
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "scalar"
-
1
require_relative "boolsy_cast"
-
-
1
module Tabulard
-
1
module Types
-
1
module Scalars
-
1
Boolsy = Scalar.cast(BoolsyCast)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../../messaging/messages/must_be_boolsy"
-
1
require_relative "../cast"
-
-
1
module Tabulard
-
1
module Types
-
1
module Scalars
-
1
class BoolsyCast
-
1
include Cast
-
-
1
TRUTHY = [].freeze
-
1
FALSY = [].freeze
-
1
private_constant :TRUTHY, :FALSY
-
-
1
def initialize(truthy: TRUTHY, falsy: FALSY, **)
-
12
@truthy = truthy
-
12
@falsy = falsy
-
end
-
-
1
def call(value, _messenger)
-
3
then: 1
if @truthy.include?(value)
-
1
else: 2
true
-
2
then: 1
elsif @falsy.include?(value)
-
1
false
-
else: 1
else
-
1
throw :failure, Messaging::Messages::MustBeBoolsy.new(
-
code_data: { value: value.inspect }
-
)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "scalar"
-
1
require_relative "date_string_cast"
-
-
1
module Tabulard
-
1
module Types
-
1
module Scalars
-
1
DateString = Scalar.cast(DateStringCast)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "date"
-
1
require_relative "../../messaging/messages/must_be_date"
-
1
require_relative "../cast"
-
-
1
module Tabulard
-
1
module Types
-
1
module Scalars
-
1
class DateStringCast
-
1
include Cast
-
-
1
DATE_FMT = "%Y-%m-%d"
-
1
private_constant :DATE_FMT
-
-
1
def initialize(date_fmt: DATE_FMT, accept_date: true, **)
-
15
@date_fmt = date_fmt
-
15
@accept_date = accept_date
-
end
-
-
1
def call(value, _messenger)
-
6
else: 1
case value
-
when: 2
when ::Date
-
2
then: 1
else: 1
return value if @accept_date
-
when: 3
when ::String
-
3
date = parse_date_string(value)
-
3
then: 1
else: 2
return date if date
-
end
-
-
4
throw :failure, Messaging::Messages::MustBeDate.new(code_data: { format: @date_fmt })
-
end
-
-
1
private
-
-
1
def parse_date_string(value)
-
3
::Date.strptime(value, @date_fmt)
-
rescue ::TypeError, ::Date::Error
-
2
nil
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "string"
-
1
require_relative "email_cast"
-
-
1
module Tabulard
-
1
module Types
-
1
module Scalars
-
1
Email = String.cast(EmailCast)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "uri"
-
1
require_relative "../../messaging/messages/must_be_email"
-
1
require_relative "../cast"
-
-
1
module Tabulard
-
1
module Types
-
1
module Scalars
-
1
class EmailCast
-
1
include Cast
-
-
1
EMAIL_REGEXP = ::URI::MailTo::EMAIL_REGEXP
-
1
private_constant :EMAIL_REGEXP
-
-
1
def initialize(email_matcher: EMAIL_REGEXP, **)
-
11
@email_matcher = email_matcher
-
end
-
-
1
def call(value, _messenger)
-
17
then: 11
else: 6
return value if @email_matcher.match?(value)
-
-
6
throw :failure, Messaging::Messages::MustBeEmail.new(code_data: { value: value.inspect })
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../../errors/type_error"
-
1
require_relative "../type"
-
1
require_relative "scalar_cast"
-
-
1
module Tabulard
-
1
module Types
-
1
module Scalars
-
1
class Scalar < Type
-
1
self.cast_classes += [ScalarCast]
-
-
1
def composite?
-
20
false
-
end
-
-
1
def composite(_value, _messenger)
-
5
raise Errors::TypeError, "A scalar type cannot act as a composite"
-
end
-
-
1
def scalar(index, value, messenger)
-
70
else: 65
then: 5
raise Errors::TypeError, "A scalar type cannot be indexed" unless index.nil?
-
-
65
cast_chain.call(value, messenger)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../../utils/cell_string_cleaner"
-
1
require_relative "../../messaging/messages/must_exist"
-
1
require_relative "../../messaging/messages/cleaned_string"
-
1
require_relative "../cast"
-
-
1
module Tabulard
-
1
module Types
-
1
module Scalars
-
1
class ScalarCast
-
1
include Cast
-
-
1
def initialize(nullable: true, clean_string: true, **)
-
43
@nullable = nullable
-
43
@clean_string = clean_string
-
end
-
-
1
def call(value, messenger)
-
67
handle_nil(value)
-
-
50
handle_garbage(value, messenger)
-
end
-
-
1
private
-
-
1
def handle_nil(value)
-
67
else: 17
then: 50
return unless value.nil?
-
-
17
then: 16
if @nullable
-
16
throw :success, nil
-
else: 1
else
-
1
throw :failure, Messaging::Messages::MustExist.new
-
end
-
end
-
-
1
def handle_garbage(value, messenger)
-
50
else: 32
then: 18
return value unless @clean_string && value.is_a?(::String)
-
-
32
clean_string = Utils::CellStringCleaner.call(value)
-
-
32
then: 1
else: 31
messenger.warn(Messaging::Messages::CleanedString.new) if clean_string != value
-
-
32
clean_string
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "scalar"
-
1
require_relative "../../messaging/messages/must_be_string"
-
-
1
module Tabulard
-
1
module Types
-
1
module Scalars
-
1
String = Scalar.cast do |value, _messenger|
-
# value.to_s, because we want the native, underlying string when value
-
# is an instance of a String subclass
-
32
then: 31
else: 1
next value.to_s if value.is_a?(::String)
-
-
1
throw :failure, Messaging::Messages::MustBeString.new
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "cast_chain"
-
-
1
module Tabulard
-
1
module Types
-
1
class Type
-
1
class << self
-
1
def all(&block)
-
6
else: 3
then: 3
return enum_for(:all) unless block
-
-
3
ObjectSpace.each_object(singleton_class, &block)
-
nil
-
end
-
-
1
def cast_classes
-
186
then: 140
else: 46
defined?(@cast_classes) ? @cast_classes : superclass.cast_classes
-
end
-
-
1
attr_writer :cast_classes
-
-
1
def cast(cast_class = nil, &cast_block)
-
21
then: 1
if cast_class && cast_block
-
1
else: 20
raise ArgumentError, "Expected either a Class or a block, got both"
-
20
then: 1
else: 19
elsif !(cast_class || cast_block)
-
1
raise ArgumentError, "Expected either a Class or a block, got none"
-
end
-
-
19
type = Class.new(self)
-
19
type.cast_classes += [cast_class || SimpleCast.new(cast_block)]
-
19
type
-
end
-
-
1
def freeze
-
8
else: 5
then: 3
@cast_classes = cast_classes.dup unless defined?(@cast_classes)
-
8
@cast_classes.freeze
-
8
super
-
end
-
-
1
def new!(...)
-
15
new(...).freeze
-
end
-
end
-
-
1
self.cast_classes = []
-
-
1
def initialize(**opts)
-
63
@cast_chain = CastChain.new
-
-
63
self.class.cast_classes.each do |cast_class|
-
101
@cast_chain.append(cast_class.new(**opts))
-
end
-
end
-
-
# @private
-
1
attr_reader :cast_chain
-
-
1
def cast(...)
-
11
@cast_chain.call(...)
-
end
-
-
1
def scalar?
-
1
raise NoMethodError, "You must implement this method in a subclass"
-
end
-
-
1
def composite?
-
1
raise NoMethodError, "You must implement this method in a subclass"
-
end
-
-
1
def scalar(_index, _value, _messenger)
-
1
raise NoMethodError, "You must implement this method in a subclass"
-
end
-
-
1
def composite(_value, _messenger)
-
1
raise NoMethodError, "You must implement this method in a subclass"
-
end
-
-
1
def freeze
-
16
@cast_chain.freeze
-
16
super
-
end
-
-
# @private
-
1
class SimpleCast
-
1
def initialize(cast)
-
16
@cast = cast
-
end
-
-
1
def new(**)
-
61
@cast
-
end
-
-
1
def ==(other)
-
1
other.is_a?(self.class) && other.cast == cast
-
end
-
-
1
protected
-
-
1
attr_reader :cast
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
module Utils
-
1
class CellStringCleaner
-
1
garbage = "(?:[^[:print:]]|[[:space:]])+"
-
1
GARBAGE_PREFIX = /\A#{garbage}/
-
1
GARBAGE_SUFFIX = /#{garbage}\Z/
-
1
private_constant :GARBAGE_PREFIX, :GARBAGE_SUFFIX
-
-
1
def self.call(...)
-
36
DEFAULT.call(...)
-
end
-
-
1
def call(value)
-
36
value = value.dup
-
-
# TODO: benchmarks
-
36
value.sub!(GARBAGE_PREFIX, "")
-
36
value.sub!(GARBAGE_SUFFIX, "")
-
-
36
value
-
end
-
-
1
DEFAULT = new.freeze
-
1
private_constant :DEFAULT
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tabulard
-
1
module Utils
-
1
module MonadicResult
-
# {Unit} is a singleton, and is used when there is no other meaningful
-
# value that could be returned.
-
#
-
# It allows the {Result} implementation to distinguish between *a null
-
# value* (i.e. `nil`) and *the lack of a value*, to provide adequate
-
# behavior in each case.
-
#
-
# The {Result} API should not expose {Unit} directly to its consumers.
-
#
-
# @see https://en.wikipedia.org/wiki/Unit_type
-
1
Unit = Object.new
-
-
1
def Unit.to_s
-
2
"Unit"
-
end
-
-
1
def Unit.inspect
-
1
"Unit"
-
end
-
-
1
Unit.freeze
-
-
1
DO_TOKEN = :MonadicResultDo
-
1
private_constant :DO_TOKEN
-
-
1
module Result
-
1
UnwrapError = Class.new(StandardError)
-
1
VariantError = Class.new(UnwrapError)
-
1
ValueError = Class.new(UnwrapError)
-
-
1
def initialize(value = Unit)
-
253
@wrapped = value
-
end
-
-
1
def empty?
-
140
wrapped == Unit
-
end
-
-
1
def ==(other)
-
53
other.is_a?(self.class) && other.wrapped == wrapped
-
end
-
-
1
def inspect
-
4
then: 2
if empty?
-
2
"#{variant}()"
-
else: 2
else
-
2
"#{variant}(#{wrapped.inspect})"
-
end
-
end
-
-
1
alias to_s inspect
-
-
1
def discard
-
18
then: 16
else: 2
empty? ? self : self.class.new
-
end
-
-
1
protected
-
-
1
attr_reader :wrapped
-
-
1
private
-
-
1
def value
-
4
then: 2
else: 2
raise ValueError, "There is no value within the result" if empty?
-
-
2
wrapped
-
end
-
-
1
def value?
-
18
else: 1
then: 17
wrapped unless empty?
-
end
-
-
1
def open
-
90
then: 11
if empty?
-
11
yield
-
else: 79
else
-
79
yield wrapped
-
end
-
end
-
end
-
-
1
class Success
-
1
include Result
-
-
1
def success?
-
3
true
-
end
-
-
1
def failure?
-
1
false
-
end
-
-
1
def success
-
2
value
-
end
-
-
1
def failure
-
1
raise VariantError, "Not a Failure"
-
end
-
-
1
def unwrap
-
18
value?
-
end
-
-
1
alias bind open
-
1
public :bind
-
-
1
alias or itself
-
-
1
private
-
-
1
def variant
-
2
"Success"
-
end
-
end
-
-
1
class Failure
-
1
include Result
-
-
1
def success?
-
1
false
-
end
-
-
1
def failure?
-
2
true
-
end
-
-
1
def success
-
1
raise VariantError, "Not a Success"
-
end
-
-
1
def failure
-
2
value
-
end
-
-
1
def unwrap
-
5
throw DO_TOKEN, self
-
end
-
-
1
alias bind itself
-
-
1
alias or open
-
1
public :or
-
-
1
private
-
-
1
def variant
-
2
"Failure"
-
end
-
end
-
-
# rubocop:disable Naming/MethodName
-
-
1
def Success(...)
-
140
Success.new(...)
-
end
-
-
1
def Failure(...)
-
49
Failure.new(...)
-
end
-
-
1
def Do(&)
-
18
catch(DO_TOKEN, &)
-
end
-
-
# rubocop:enable Naming/MethodName
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
mod = Module.new do
-
1
def fixtures_path
-
32
@fixtures_path ||= File.expand_path("./fixtures", __dir__)
-
end
-
-
1
def fixture_path(path)
-
32
File.join(fixtures_path, path)
-
end
-
end
-
-
1
RSpec.configure do |config|
-
1
config.include(mod)
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/utils/monadic_result"
-
-
1
RSpec.configure do |config|
-
1
config.include(Tabulard::Utils::MonadicResult, monadic_result: true)
-
end
-
# frozen_string_literal: true
-
-
1
RSpec.shared_examples "cast_class" do
-
15
else: 3
then: 4
subject(:cast_class) { described_class } unless method_defined?(:cast_class) # rubocop:disable RSpec/LeadingSubject
-
-
7
let(:cast) do
-
12
cast_class.new
-
end
-
-
7
describe "#initialize" do
-
7
it "tolerates any kwargs" do
-
7
expect do
-
7
cast_class.new(foo: double, qoifzj: double)
-
end.not_to raise_error
-
end
-
end
-
-
7
describe "#call" do
-
7
it "has the right cast signature" do
-
7
expect(cast).to respond_to(:call).with(2).arguments
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/type"
-
1
require "tabulard/types/scalars/scalar"
-
-
1
RSpec.shared_examples "composite_type" do
-
3
subject(:type) do
-
12
described_class.new(scalars)
-
end
-
-
15
let(:scalars) { instance_double(Array) }
-
-
12
let(:value) { double }
-
12
let(:messenger) { double }
-
-
3
it "is a type" do
-
3
expect(described_class.ancestors).to include(Tabulard::Types::Type)
-
end
-
-
3
describe "#composite?" do
-
3
it "is true" do
-
3
expect(subject).to be_composite
-
end
-
end
-
-
3
describe "#composite" do
-
3
it "is an alias to #cast" do
-
3
expect(subject.method(:composite)).to eq(subject.method(:cast))
-
end
-
end
-
-
3
describe "#scalar" do
-
9
let(:type_index) { double }
-
-
3
def stub_scalar_index(type = double)
-
6
allow(scalars).to receive(:[]).with(type_index).and_return(type)
-
6
type
-
end
-
-
3
context "when the index refers to a scalar type" do
-
6
let(:scalar_type) { instance_double(Tabulard::Types::Scalars::Scalar) }
-
-
3
before do
-
3
stub_scalar_index(scalar_type)
-
end
-
-
3
it "casts the value to the scalar type" do
-
3
expect(scalar_type).to(
-
receive(:scalar).with(nil, value, messenger).and_return(casted_value = double)
-
)
-
-
3
expect(subject.scalar(type_index, value, messenger)).to be(casted_value)
-
end
-
end
-
-
3
context "when the index doesn't refer to a scalar type" do
-
3
before do
-
3
stub_scalar_index(nil)
-
end
-
-
3
it "raises an error" do
-
6
expect { subject.scalar(type_index, value, messenger) }.to raise_error(
-
Tabulard::Errors::TypeError,
-
"Invalid index: #{type_index.inspect}"
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/type"
-
-
1
RSpec.shared_examples "scalar_type" do
-
22
let(:value) { double }
-
22
let(:messenger) { double }
-
-
5
it "is a type" do
-
5
expect(described_class.ancestors).to include(Tabulard::Types::Type)
-
end
-
-
5
describe "#composite?" do
-
5
it "is false" do
-
5
expect(subject).not_to be_composite
-
end
-
end
-
-
5
describe "#composite" do
-
5
it "fails" do
-
10
expect { subject.composite(value, messenger) }.to raise_error(
-
Tabulard::Errors::TypeError, "A scalar type cannot act as a composite"
-
)
-
end
-
end
-
-
5
describe "#scalar" do
-
5
context "when the value is not indexed" do
-
5
it "delegates the task to the cast chain" do
-
5
result = double
-
-
5
expect(subject.cast_chain).to receive(:call).with(value, messenger).and_return(result)
-
5
expect(subject.scalar(nil, value, messenger)).to be(result)
-
end
-
end
-
-
5
context "when the value is indexed" do
-
5
it "fails" do
-
5
index = double
-
-
10
expect { subject.scalar(index, value, messenger) }.to raise_error(
-
Tabulard::Errors::TypeError, "A scalar type cannot be indexed"
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/table"
-
-
1
RSpec.shared_examples "table/empty" do |sized_rows_enum: false|
-
3
describe "#each_header" do
-
3
context "with a block" do
-
3
it "doesn't yield" do
-
6
expect { |b| table.each_header(&b) }.not_to yield_control
-
end
-
-
3
it "returns self" do
-
3
expect(table.each_header { double }).to be(table)
-
end
-
end
-
-
3
context "without a block" do
-
3
it "returns an enumerator" do
-
3
enum = table.each_header
-
-
3
expect(enum).to be_a(Enumerator)
-
3
expect(enum.size).to eq(0)
-
3
expect(enum.to_a).to eq([])
-
end
-
end
-
end
-
-
3
describe "#each_row" do
-
3
context "with a block" do
-
3
it "doesn't yield" do
-
6
expect { |b| table.each_row(&b) }.not_to yield_control
-
end
-
-
3
it "returns self" do
-
3
expect(table.each_row { double }).to be(table)
-
end
-
end
-
-
3
context "without a block" do
-
3
it "returns an enumerator" do
-
3
enum = table.each_row
-
-
3
expect(enum).to be_a(Enumerator)
-
3
then: 2
else: 1
expect(enum.size).to eq(sized_rows_enum ? 0 : nil)
-
3
expect(enum.to_a).to eq([])
-
end
-
end
-
end
-
-
3
describe "#close" do
-
3
it "returns nil" do
-
3
expect(table.close).to be_nil
-
end
-
end
-
-
3
context "when it is closed" do
-
9
before { table.close }
-
-
3
it "can't enumerate headers" do
-
6
expect { table.each_header }.to raise_error(Tabulard::Table::ClosureError)
-
end
-
-
3
it "can't enumerate rows" do
-
6
expect { table.each_row }.to raise_error(Tabulard::Table::ClosureError)
-
end
-
end
-
-
3
context "when headers are customized" do
-
3
let(:headers_data) do
-
3
%w[foo bar baz]
-
end
-
-
3
let(:table_opts) do
-
3
super().merge(headers: headers_data)
-
end
-
-
3
it "relies on the custom headers" do
-
3
headers = build_headers(headers_data)
-
6
expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/table"
-
-
1
RSpec.shared_context "table/factories" do
-
3
def build_header(...)
-
63
Tabulard::Table::Header.new(...)
-
end
-
-
3
def build_row(...)
-
35
Tabulard::Table::Row.new(...)
-
end
-
-
3
def build_cell(...)
-
155
Tabulard::Table::Cell.new(...)
-
end
-
-
3
def build_headers(values, col: "A")
-
15
first_col_index = Tabulard::Table.col2int(col)
-
-
15
values.map.with_index(first_col_index) do |value, col_index|
-
63
build_header(col: Tabulard::Table.int2col(col_index), value: value)
-
end
-
end
-
-
3
def build_cells(values, row:, col: "A")
-
35
first_col_index = Tabulard::Table.col2int(col)
-
-
35
values.map.with_index(first_col_index) do |value, col_index|
-
155
build_cell(row: row, col: Tabulard::Table.int2col(col_index), value: value)
-
end
-
end
-
-
3
def build_rows(list_of_values, row: 2, col: "A")
-
12
list_of_values.map.with_index(row) do |values, row_index|
-
35
value = build_cells(values, row: row_index, col: col)
-
-
35
build_row(row: row_index, value: value)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/table"
-
-
1
RSpec.shared_examples "table/filled" do |sized_rows_enum: false|
-
3
else: 1
then: 2
unless instance_methods.include?(:source_data)
-
2
alias_method :source_data, :source
-
end
-
-
3
describe "#each_header" do
-
3
let(:headers) do
-
6
build_headers(source_data[0])
-
end
-
-
3
context "with a block" do
-
3
it "yields each header, with its letter-based index" do
-
6
expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
-
end
-
-
3
it "returns self" do
-
16
expect(table.each_header { double }).to be(table)
-
end
-
end
-
-
3
context "without a block" do
-
3
it "returns an enumerator" do
-
3
enum = table.each_header
-
-
3
expect(enum).to be_a(Enumerator)
-
3
expect(enum.size).to eq(headers.size)
-
3
expect(enum.to_a).to eq(headers)
-
end
-
end
-
end
-
-
3
describe "#each_row" do
-
3
let(:rows) do
-
6
build_rows(source_data[1..])
-
end
-
-
3
context "with a block" do
-
3
it "yields each row, with its integer-based index" do
-
6
expect { |b| table.each_row(&b) }.to yield_successive_args(*rows)
-
end
-
-
3
it "returns self" do
-
11
expect(table.each_row { double }).to be(table)
-
end
-
end
-
-
3
context "without a block" do
-
3
it "returns an enumerator" do
-
3
enum = table.each_row
-
-
3
expect(enum).to be_a(Enumerator)
-
3
then: 2
else: 1
expect(enum.size).to eq(sized_rows_enum ? rows.size : nil)
-
3
expect(enum.to_a).to eq(rows)
-
end
-
end
-
end
-
-
3
describe "#close" do
-
3
it "returns nil" do
-
3
expect(table.close).to be_nil
-
end
-
end
-
-
3
context "when it is closed" do
-
9
before { table.close }
-
-
3
it "can't enumerate headers" do
-
6
expect { table.each_header }.to raise_error(Tabulard::Table::ClosureError)
-
end
-
-
3
it "can't enumerate rows" do
-
6
expect { table.each_row }.to raise_error(Tabulard::Table::ClosureError)
-
end
-
end
-
-
3
context "when headers are customized" do
-
15
let(:data_size) { source_data[0].size }
-
15
let(:headers_size) { data_size + diff }
-
-
3
let(:headers_data) do
-
64
Array.new(headers_size) { |i| "header#{i}" }
-
end
-
-
3
let(:table_opts) do
-
12
super().merge(headers: headers_data)
-
end
-
-
3
context "when their size is equal to the size of the data" do
-
9
let(:diff) { 0 }
-
-
3
it "relies on the custom headers" do
-
3
headers = build_headers(headers_data)
-
6
expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
-
end
-
-
3
it "treats all rows as data" do
-
3
rows = build_rows(source_data, row: 1)
-
6
expect { |b| table.each_row(&b) }.to yield_successive_args(*rows)
-
end
-
end
-
-
3
context "when their size is smaller than the size of the data" do
-
6
let(:diff) { -1 }
-
-
3
it "fails to initialize", autoclose_table: false do
-
6
expect { table }.to raise_error(
-
Tabulard::Table::TooFewHeaders,
-
"Expected #{data_size} headers, got: #{headers_size}"
-
)
-
end
-
end
-
-
3
context "when their size is larger than the size of the data" do
-
6
let(:diff) { 1 }
-
-
3
it "fails to initialize", autoclose_table: false do
-
6
expect { table }.to raise_error(
-
Tabulard::Table::TooManyHeaders,
-
"Expected #{data_size} headers, got: #{headers_size}"
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging"
-
-
1
Tabulard::Messaging.configure do |config|
-
1
config.validate_messages = true
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/adapters/bare"
-
1
require "support/shared/table/factories"
-
1
require "support/shared/table/empty"
-
1
require "support/shared/table/filled"
-
-
1
RSpec.describe Tabulard::Adapters::Bare do
-
1
include_context "table/factories"
-
-
1
let(:table_interface) do
-
23
Module.new do
-
23
def [](_); end
-
23
def size; end
-
end
-
end
-
-
1
let(:headers_interface) do
-
13
Module.new do
-
13
def [](_); end
-
13
def size; end
-
end
-
end
-
-
1
let(:values_interfaces) do
-
13
Module.new do
-
13
def [](_); end
-
end
-
end
-
-
1
let(:input) do
-
23
stub_input(source)
-
end
-
-
1
let(:table_opts) do
-
23
{}
-
end
-
-
1
let(:table) do
-
23
described_class.new(input, **table_opts)
-
end
-
-
1
def stub_input(source)
-
23
input = instance_double(table_interface, size: source.size)
-
-
23
source.each_with_index do |row, row_idx|
-
52
input_row = stub_input_row(row, row_idx)
-
-
52
allow(input).to receive(:[]).with(row_idx).and_return(input_row)
-
end
-
-
23
input
-
end
-
-
1
def stub_input_row(row, row_idx)
-
input_row =
-
52
then: 13
if row_idx.zero?
-
13
instance_double(headers_interface, size: row.size)
-
else: 39
else
-
39
instance_double(values_interfaces)
-
end
-
-
52
row.each_with_index do |cell, col_idx|
-
208
allow(input_row).to receive(:[]).with(col_idx).and_return(cell)
-
end
-
end
-
-
1
after do |example|
-
23
else: 2
then: 21
table.close unless example.metadata[:autoclose_table] == false
-
end
-
-
1
context "when the input table is empty" do
-
1
let(:source) do
-
10
[]
-
end
-
-
1
include_examples "table/empty", sized_rows_enum: true
-
end
-
-
1
context "when the input table is filled" do
-
1
let(:source) do
-
13
Array.new(4) do |row|
-
52
Array.new(4) do |col|
-
208
instance_double(Object, "(#{row},#{col})")
-
end.freeze
-
end.freeze
-
end
-
-
1
include_examples "table/filled", sized_rows_enum: true
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/adapters/csv"
-
-
1
RSpec.describe Tabulard::Adapters::Csv::InvalidCSV do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "invalid_csv",
-
code_data: nil,
-
scope: "TABLE",
-
scope_data: nil
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have some code data" do
-
1
message.code_data = { foo: :baz }
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have some scope_data" do
-
1
message.scope_data = { foo: :baz }
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/adapters/csv"
-
1
require "support/shared/table/factories"
-
1
require "support/shared/table/empty"
-
1
require "support/shared/table/filled"
-
1
require "csv"
-
1
require "stringio"
-
-
1
RSpec.describe Tabulard::Adapters::Csv do
-
1
include_context "table/factories"
-
-
1
let(:input) do
-
25
stub_input(source)
-
end
-
-
1
let(:table_opts) do
-
28
{}
-
end
-
-
1
let(:table) do
-
29
described_class.new(input, **table_opts)
-
end
-
-
1
def stub_input(source)
-
25
csv = CSV.generate do |csv_io|
-
25
source.each do |row|
-
52
csv_io << row
-
end
-
end
-
-
25
StringIO.new(csv, "r:UTF-8")
-
end
-
-
1
after do |example|
-
28
else: 2
then: 26
table.close unless example.metadata[:autoclose_table] == false
-
28
input.close
-
end
-
-
1
context "when the input table is empty" do
-
1
let(:source) do
-
10
[]
-
end
-
-
1
include_examples "table/empty"
-
end
-
-
1
context "when the input table is filled" do
-
1
let(:source) do
-
13
Array.new(4) do |row|
-
52
Array.new(4) do |col|
-
208
"(#{row},#{col})"
-
end.freeze
-
end.freeze
-
end
-
-
1
include_examples "table/filled"
-
end
-
-
1
describe "encodings" do
-
1
let(:utf8_path) { fixture_path("csv/utf8.csv") }
-
4
let(:latin9_path) { fixture_path("csv/latin9.csv") }
-
-
1
let(:headers_data_utf8) do
-
2
[
-
"Matricule",
-
"Nom",
-
"Prénom",
-
"Email",
-
"Date de naissance",
-
"Entrée en entreprise",
-
"Administrateur",
-
"Bio",
-
"Service",
-
]
-
end
-
-
1
let(:headers_data_latin9) do
-
10
headers_data_utf8.map { |str| str.encode(Encoding::ISO_8859_15) }
-
end
-
-
3
let(:table_headers_data) { table.each_header.map(&:value) }
-
-
1
context "when the IO is opened with the correct external encoding" do
-
1
let(:input) do
-
1
File.new(latin9_path, external_encoding: Encoding::ISO_8859_15)
-
end
-
-
1
it "does not interfere" do
-
1
expect(table_headers_data).to eq(headers_data_latin9)
-
end
-
end
-
-
1
context "when the IO is opened with an incorrect external encoding" do
-
1
let(:input) do
-
1
File.new(latin9_path, external_encoding: Encoding::UTF_8)
-
end
-
-
1
it "fails" do
-
2
expect { table }.to raise_error(described_class::InputError)
-
end
-
end
-
-
1
context "when the (correct) external encoding differs from the internal one" do
-
1
let(:input) do
-
1
File.new(
-
latin9_path,
-
external_encoding: Encoding::ISO_8859_15,
-
internal_encoding: Encoding::UTF_8
-
)
-
end
-
-
1
it "does not interfere" do
-
1
expect(table_headers_data).to eq(headers_data_utf8)
-
end
-
end
-
end
-
-
1
describe "CSV options" do
-
2
let(:source) { [] }
-
-
1
it "requires a specific col_sep and quote_char, and an automatic row_sep" do
-
1
expect(CSV).to receive(:new)
-
.with(input, row_sep: :auto, col_sep: ",", quote_char: '"')
-
.and_call_original
-
-
1
table
-
end
-
end
-
-
1
describe "#close" do
-
2
let(:source) { [] }
-
-
1
it "doesn't close the underlying table" do
-
2
expect { table.close }.not_to change(input, :closed?).from(false)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/adapters/xlsx"
-
1
require "support/shared/table/factories"
-
1
require "support/shared/table/empty"
-
1
require "support/shared/table/filled"
-
-
1
RSpec.describe Tabulard::Adapters::Xlsx do
-
1
include_context "table/factories"
-
-
1
let(:input) do
-
29
stub_input(source)
-
end
-
-
1
let(:table_opts) do
-
29
{}
-
end
-
-
1
let(:table) do
-
29
described_class.new(input, **table_opts)
-
end
-
-
1
def stub_input(source)
-
29
fixture_path(source)
-
end
-
-
1
after do |example|
-
29
else: 2
then: 27
table.close unless example.metadata[:autoclose_table] == false
-
end
-
-
1
context "when the input table is empty" do
-
1
let(:source) do
-
10
"xlsx/empty.xlsx"
-
end
-
-
1
include_examples "table/empty", sized_rows_enum: true
-
end
-
-
1
context "when the input table is filled" do
-
1
let(:source) do
-
13
"xlsx/regular.xlsx"
-
end
-
-
1
let(:source_data) do
-
[
-
8
["matricule", "nom", "prénom", "date de naissance", "email"],
-
["004774", "Ytärd", "Glœuiçe", "28/04/1998", "foo@bar.com"],
-
[664_623, "Goulijambon", "Carasmine", Date.new(1976, 1, 20), "foo@bar.com"],
-
]
-
end
-
-
1
include_examples "table/filled", sized_rows_enum: true
-
end
-
-
1
context "when the input table includes empty rows around the content" do
-
1
let(:source) do
-
2
"xlsx/empty_rows_around.xlsx"
-
end
-
-
1
let(:source_data) do
-
[
-
2
["matricule", "nom", "prénom", "date de naissance", "email"],
-
["004774", "Ytärd", "Glœuiçe", "28/04/1998", "foo@bar.com"],
-
[664_623, "Goulijambon", "Carasmine", Date.new(1976, 1, 20), "foo@bar.com"],
-
]
-
end
-
-
1
it "ignores the empty initial rows when detecting the headers" do
-
1
headers = build_headers(source_data[0])
-
2
expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
-
end
-
-
1
it "ignores the empty final rows when detecting the rows" do
-
1
rows = build_rows(source_data[1..], row: 3)
-
2
expect { |b| table.each_row(&b) }.to yield_successive_args(*rows)
-
end
-
end
-
-
1
context "when the input table includes empty rows within the content" do
-
1
let(:source) do
-
2
"xlsx/empty_rows_within.xlsx"
-
end
-
-
1
let(:source_data) do
-
2
empty_row = Array.new(5)
-
-
[
-
2
["matricule", "nom", "prénom", "date de naissance", "email"],
-
empty_row,
-
["004774", "Ytärd", "Glœuiçe", "28/04/1998", "foo@bar.com"],
-
empty_row,
-
[664_623, "Goulijambon", "Carasmine", Date.new(1976, 1, 20), "foo@bar.com"],
-
]
-
end
-
-
1
it "ignores them when detecting the headers" do
-
1
headers = build_headers(source_data[0])
-
2
expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
-
end
-
-
1
it "doesn't ignore them when detecting the rows" do
-
1
rows = build_rows(source_data[1..])
-
2
expect { |b| table.each_row(&b) }.to yield_successive_args(*rows)
-
end
-
end
-
-
1
context "when the input table includes empty columns before the content" do
-
1
let(:source) do
-
2
"xlsx/empty_cols_before.xlsx"
-
end
-
-
1
let(:source_data) do
-
[
-
2
["matricule", "nom", "prénom", "date de naissance", "email"],
-
["004774", "Ytärd", "Glœuiçe", "28/04/1998", "foo@bar.com"],
-
[664_623, "Goulijambon", "Carasmine", Date.new(1976, 1, 20), "foo@bar.com"],
-
]
-
end
-
-
1
it "ignores the initial empty columns when detecting the headers" do
-
1
headers = build_headers(source_data[0], col: "B")
-
2
expect { |b| table.each_header(&b) }.to yield_successive_args(*headers)
-
end
-
-
1
it "ignores the initial empty columns when detecting the rows" do
-
1
rows = build_rows(source_data[1..], col: "B")
-
2
expect { |b| table.each_row(&b) }.to yield_successive_args(*rows)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/adapters"
-
-
1
RSpec.describe Tabulard::Adapters do
-
1
describe "::open" do
-
1
let(:adapter) do
-
1
double
-
end
-
-
3
let(:foo) { double }
-
3
let(:bar) { double }
-
2
let(:res) { double }
-
-
1
it "opens with a adapter" do
-
1
allow(adapter).to receive(:open).with(foo, bar: bar).and_return(res)
-
-
1
expect(described_class.open(foo, adapter: adapter, bar: bar)).to be(res)
-
end
-
-
1
it "may not open without a adapter" do
-
2
expect { described_class.open(foo, bar: bar) }.to raise_error(
-
ArgumentError, /missing keyword: :adapter/
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/attribute"
-
-
1
RSpec.describe Tabulard::Attribute do
-
1
describe "::build" do
-
1
it "freezes the resulting instance" do
-
1
attribute = described_class.build(key: :foo, type: :bar)
-
1
expect(attribute).to be_frozen
-
end
-
end
-
-
1
describe "#each_column" do
-
1
let(:key) do
-
1
:foo
-
end
-
-
1
let(:type) do
-
1
:bar
-
end
-
-
1
let(:attribute) do
-
1
described_class.new(key: key, type: type)
-
end
-
-
1
context "without a block" do
-
2
let(:config) { double }
-
-
1
it "returns an unsized enumerator" do
-
1
enum = attribute.each_column(config)
-
1
expect(enum).to be_a(Enumerator)
-
1
expect(enum.size).to be_nil
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/attribute_types/composite"
-
1
require "tabulard/attribute_types/value"
-
-
1
RSpec.describe Tabulard::AttributeTypes::Composite do
-
1
describe "::build" do
-
1
it "freezes the resulting instance" do
-
1
type = described_class.build(composite: :foo, scalars: [:bar])
-
1
expect(type).to be_frozen
-
end
-
end
-
-
1
describe "#each_column" do
-
1
let(:composite_type) do
-
1
:foo
-
end
-
-
1
let(:scalars) do
-
[
-
1
instance_double(Tabulard::AttributeTypes::Value),
-
instance_double(Tabulard::AttributeTypes::Value),
-
]
-
end
-
-
1
let(:composite) do
-
1
described_class.new(composite: composite_type, scalars: scalars)
-
end
-
-
1
context "without a block" do
-
1
it "returns a sized enumerator" do
-
1
enum = composite.each_column
-
1
expect(enum).to be_a(Enumerator)
-
1
expect(enum.size).to eq(scalars.size)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/attribute_types/scalar"
-
1
require "tabulard/attribute_types/value"
-
-
1
RSpec.describe Tabulard::AttributeTypes::Scalar do
-
1
describe "::build" do
-
1
it "freezes the resulting instance" do
-
1
type = described_class.build(:foo)
-
1
expect(type).to be_frozen
-
end
-
end
-
-
1
describe "#each_column" do
-
1
let(:value) do
-
1
instance_double(Tabulard::AttributeTypes::Value)
-
end
-
-
1
let(:scalar) do
-
1
described_class.new(value)
-
end
-
-
1
context "without a block" do
-
1
it "returns a sized enumerator" do
-
1
enum = scalar.each_column
-
1
expect(enum).to be_a(Enumerator)
-
1
expect(enum.size).to eq(1)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/attribute_types/value"
-
-
1
RSpec.describe Tabulard::AttributeTypes::Value do
-
1
let(:type) do
-
6
:foo
-
end
-
-
1
def newval(...)
-
5
described_class.new(...)
-
end
-
-
1
def buildval(...)
-
6
described_class.build(...)
-
end
-
-
1
describe "::build" do
-
1
it "returns a frozen value" do
-
1
value = buildval(type: type)
-
1
expect(value).to be_frozen
-
end
-
-
1
context "when the type requirement is implicit" do
-
1
it "has a required type" do
-
1
value = buildval(type: type)
-
1
expect(value).to eq(newval(type: type, required: true))
-
end
-
-
1
it "can be expressed with syntactic sugar" do
-
1
value = buildval(type)
-
1
expect(value).to eq(newval(type: type, required: true))
-
end
-
end
-
-
1
context "when the type requirement is explicit" do
-
1
it "may have an optional type" do
-
1
value = buildval(type: type, required: false)
-
1
expect(value).to eq(newval(type: type, required: false))
-
end
-
-
1
it "may have a required type" do
-
1
value = buildval(type: type, required: true)
-
1
expect(value).to eq(newval(type: type, required: true))
-
end
-
-
1
it "can be optional using syntactic sugar" do
-
1
value = buildval(:"#{type}?")
-
1
expect(value).to eq(newval(type: type, required: false))
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/attribute_types"
-
-
1
RSpec.describe Tabulard::AttributeTypes do
-
1
def build(...)
-
4
described_class.build(...)
-
end
-
-
1
def value(...)
-
8
Tabulard::AttributeTypes::Value.new(...)
-
end
-
-
1
def scalar(...)
-
2
Tabulard::AttributeTypes::Scalar.new(...)
-
end
-
-
1
def composite(...)
-
2
Tabulard::AttributeTypes::Composite.new(...)
-
end
-
-
1
context "when given a scalar as a Hash" do
-
1
let(:type) do
-
1
{
-
type: :foo,
-
required: false,
-
}
-
end
-
-
1
it "has the expected type" do
-
1
expect(build(type)).to eq(scalar(value(type: :foo, required: false)))
-
end
-
end
-
-
1
context "when given a scalar as a Symbol" do
-
1
let(:type) do
-
1
:foo
-
end
-
-
1
it "has the expected type" do
-
1
expect(build(type)).to eq(scalar(value(type: :foo, required: true)))
-
end
-
end
-
-
1
context "when given a composite as a Hash" do
-
1
let(:type) do
-
{
-
1
composite: :oof,
-
scalars: [
-
:foo,
-
:bar?,
-
{ type: :baz, required: false },
-
],
-
}
-
end
-
-
1
it "has the expected type and scalars" do
-
1
expect(build(type)).to eq(
-
composite(
-
composite: :oof,
-
scalars: [
-
value(type: :foo, required: true),
-
value(type: :bar, required: false),
-
value(type: :baz, required: false),
-
]
-
)
-
)
-
end
-
end
-
-
1
context "when given a composite as an Array" do
-
1
let(:type) do
-
[
-
1
:foo,
-
:bar?,
-
{ type: :baz, required: false },
-
]
-
end
-
-
1
it "has the expected type and scalars" do
-
1
expect(build(type)).to eq(
-
composite(
-
composite: :array,
-
scalars: [
-
value(type: :foo, required: true),
-
value(type: :bar, required: false),
-
value(type: :baz, required: false),
-
]
-
)
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/column"
-
-
1
RSpec.describe Tabulard::Column do
-
7
let(:key) { double }
-
7
let(:type) { double }
-
7
let(:index) { double }
-
7
let(:header) { double }
-
7
let(:header_pattern) { Object.new }
-
7
let(:required) { false }
-
-
1
let(:col) do
-
6
described_class.new(
-
key: key,
-
type: type,
-
index: index,
-
header: header,
-
header_pattern: header_pattern,
-
required: required
-
)
-
end
-
-
1
describe "#key" do
-
1
it "reads the attribute" do
-
1
expect(col.key).to be(key)
-
end
-
end
-
-
1
describe "#type" do
-
1
it "reads the attribute" do
-
1
expect(col.type).to be(type)
-
end
-
end
-
-
1
describe "#index" do
-
1
it "reads the attribute" do
-
1
expect(col.index).to be(index)
-
end
-
end
-
-
1
describe "#header" do
-
1
it "reads the attribute" do
-
1
expect(col.header).to be(header)
-
end
-
end
-
-
1
describe "#header_pattern" do
-
1
it "reads the attribute" do
-
1
expect(col.header_pattern).to be(header_pattern)
-
end
-
end
-
-
1
describe "#required?" do
-
1
it "reads the attribute" do
-
1
expect(col.required?).to be(required)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/errors/error"
-
-
1
RSpec.describe Tabulard::Errors::Error do
-
1
it "is some kind of StandardError" do
-
1
expect(described_class).to have_attributes(superclass: StandardError)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/errors/spec_error"
-
-
1
RSpec.describe Tabulard::Errors::SpecError do
-
1
it "is some kind of Error" do
-
1
expect(described_class).to have_attributes(superclass: Tabulard::Errors::Error)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/errors/type_error"
-
-
1
RSpec.describe Tabulard::Errors::TypeError do
-
1
it "is some kind of Error" do
-
1
expect(described_class).to have_attributes(superclass: Tabulard::Errors::Error)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/headers"
-
1
require "tabulard/column"
-
1
require "tabulard/messaging"
-
1
require "tabulard/table"
-
1
require "tabulard/specification"
-
-
1
RSpec.describe Tabulard::Headers, monadic_result: true do
-
1
let(:specification) do
-
7
instance_double(
-
Tabulard::Specification,
-
required_columns: [],
-
ignore_unspecified_columns?: false
-
)
-
end
-
-
1
let(:columns) do
-
7
Array.new(10) do
-
70
instance_double(Tabulard::Column)
-
end
-
end
-
-
1
let(:messenger) do
-
7
Tabulard::Messaging::Messenger.new
-
end
-
-
1
let(:table_headers) do
-
7
Array.new(5) do |i|
-
35
instance_double(Tabulard::Table::Header, col: "FOO", value: "header#{i}")
-
end
-
end
-
-
1
let(:headers) do
-
7
described_class.new(specification: specification, messenger: messenger)
-
end
-
-
1
def stub_specification(column_by_header)
-
7
column_by_header = column_by_header.transform_keys(&:value)
-
-
7
allow(specification).to receive(:get) do |header_value|
-
16
column_by_header[header_value]
-
end
-
end
-
-
1
before do
-
7
stub_specification(
-
table_headers[0] => columns[4],
-
table_headers[1] => columns[1],
-
table_headers[2] => columns[7],
-
table_headers[3] => columns[1]
-
)
-
end
-
-
1
describe "#result" do
-
1
context "without any #add" do
-
1
it "is a success with no items" do
-
1
expect(headers.result).to eq(Success([]))
-
end
-
end
-
-
1
context "with some successful #add" do
-
1
before do
-
2
headers.add(table_headers[1])
-
2
headers.add(table_headers[2])
-
2
headers.add(table_headers[0])
-
end
-
-
1
it "is a success and preserve #add order" do
-
1
expect(headers.result).to eq(
-
Success(
-
[
-
Tabulard::Headers::Header.new(table_headers[1], columns[1]),
-
Tabulard::Headers::Header.new(table_headers[2], columns[7]),
-
Tabulard::Headers::Header.new(table_headers[0], columns[4]),
-
]
-
)
-
)
-
end
-
-
1
it "doesn't message" do
-
1
expect(messenger.messages).to be_empty
-
end
-
end
-
-
1
context "when a header doesn't match a column" do
-
1
before do
-
2
headers.add(table_headers[0])
-
2
headers.add(table_headers[4])
-
end
-
-
1
it "is a failure" do
-
1
expect(headers.result).to eq(Failure())
-
end
-
-
1
it "messages the error" do
-
1
expect(messenger.messages).to contain_exactly(
-
be_a(Tabulard::Messaging::Message) & have_attributes(
-
severity: "ERROR",
-
code: "invalid_header",
-
code_data: { value: table_headers[4].value },
-
scope: "COL",
-
scope_data: { col: table_headers[4].col }
-
)
-
)
-
end
-
end
-
-
1
context "when there is a duplicate" do
-
1
before do
-
2
headers.add(table_headers[0])
-
2
headers.add(table_headers[3])
-
2
headers.add(table_headers[1])
-
end
-
-
1
it "is a failure" do
-
1
expect(headers.result).to eq(Failure())
-
end
-
-
1
it "considers the underlying column, not the header" do
-
1
expect(messenger.messages).to contain_exactly(
-
be_a(Tabulard::Messaging::Message) & have_attributes(
-
severity: "ERROR",
-
code: "duplicated_header",
-
code_data: { value: table_headers[1].value },
-
scope: "COL",
-
scope_data: { col: table_headers[1].col }
-
)
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/config"
-
1
require "climate_control"
-
-
1
RSpec.describe Tabulard::Messaging::Config do
-
1
def envmod(envval, &block)
-
5
ClimateControl.modify(envvar => envval, &block)
-
end
-
-
1
describe "#validate_messages" do
-
6
let(:envvar) { "TABULARD_MESSAGING_VALIDATE_MESSAGES" }
-
-
1
it "is true by default" do
-
2
config = envmod(nil) { described_class.new }
-
1
expect(config.validate_messages).to be(true)
-
end
-
-
1
context "when the ENV says true" do
-
1
it "is implicitly true" do
-
2
config = envmod("true") { described_class.new }
-
1
expect(config.validate_messages).to be(true)
-
end
-
-
1
it "may explicitly be false" do
-
2
config = envmod("true") { described_class.new(validate_messages: false) }
-
1
expect(config.validate_messages).to be(false)
-
end
-
end
-
-
1
context "when the ENV says false" do
-
1
it "is implicitly false" do
-
2
config = envmod("false") { described_class.new }
-
1
expect(config.validate_messages).to be(false)
-
end
-
-
1
it "may explicitly be true" do
-
2
config = envmod("false") { described_class.new(validate_messages: true) }
-
1
expect(config.validate_messages).to be(true)
-
end
-
end
-
end
-
-
1
describe "#validate_messages=" do
-
1
it "can become true" do
-
1
config = described_class.new(validate_messages: false)
-
1
config.validate_messages = true
-
1
expect(config.validate_messages).to be(true)
-
end
-
-
1
it "can become false" do
-
1
config = described_class.new(validate_messages: true)
-
1
config.validate_messages = false
-
1
expect(config.validate_messages).to be(false)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging"
-
-
1
RSpec.describe Tabulard::Messaging::Message do
-
6
let(:code) { double }
-
5
let(:code_data) { double }
-
5
let(:scope) { double }
-
6
let(:scope_data) { double }
-
5
let(:severity) { double }
-
-
1
let(:message) do
-
10
described_class.new(
-
code: code,
-
code_data: code_data,
-
scope: scope,
-
scope_data: scope_data,
-
severity: severity
-
)
-
end
-
-
1
it "needs at least a code" do
-
2
expect { described_class.new }.to raise_error(ArgumentError, /missing keyword: :code/)
-
end
-
-
1
it "may have only a custom code and some defaults attributes" do
-
1
expect(described_class.new(code: code)).to have_attributes(
-
code: code,
-
code_data: nil,
-
scope: Tabulard::Messaging::SCOPES::TABLE,
-
scope_data: nil,
-
severity: Tabulard::Messaging::SEVERITIES::WARN
-
)
-
end
-
-
1
it "may have completely custom attributes" do
-
1
expect(message).to have_attributes(
-
code: code,
-
code_data: code_data,
-
scope: scope,
-
scope_data: scope_data,
-
severity: severity
-
)
-
end
-
-
1
it "is equivalent to a message having the same attributes" do
-
1
other_message = described_class.new(
-
code: code,
-
code_data: code_data,
-
scope: scope,
-
scope_data: scope_data,
-
severity: severity
-
)
-
1
expect(message).to eq(other_message)
-
end
-
-
1
it "is not equivalent to a message having different attributes" do
-
1
other_message = described_class.new(
-
code: code,
-
code_data: code_data,
-
scope: scope,
-
scope_data: double,
-
severity: severity
-
)
-
1
expect(message).not_to eq(other_message)
-
end
-
-
1
it "may be validated" do
-
1
expect(message).to be_a(Tabulard::Messaging::Validations)
-
end
-
-
1
describe "#to_h" do
-
1
it "returns the attributes as a hash" do
-
attrs = {
-
1
code: double,
-
code_data: double,
-
scope: double,
-
scope_data: double,
-
severity: double,
-
}
-
-
1
message = described_class.new(**attrs)
-
-
1
expect(message.to_h).to eq(attrs)
-
end
-
end
-
-
1
describe "#to_s" do
-
7
let(:code) { "foo_is_bar" }
-
6
let(:code_data) { nil }
-
7
let(:severity) { "ERROR" }
-
-
1
context "when scoped to the table" do
-
2
let(:scope) { Tabulard::Messaging::SCOPES::TABLE }
-
2
let(:scope_data) { nil }
-
-
1
it "can be reduced to a string" do
-
1
expect(message.to_s).to eq("[TABLE] ERROR: foo_is_bar")
-
end
-
end
-
-
1
context "when scoped to a row" do
-
2
let(:scope) { Tabulard::Messaging::SCOPES::ROW }
-
2
let(:scope_data) { { row: 42 } }
-
-
1
it "can be reduced to a string" do
-
1
expect(message.to_s).to eq("[ROW: 42] ERROR: foo_is_bar")
-
end
-
end
-
-
1
context "when scoped to a col" do
-
2
let(:scope) { Tabulard::Messaging::SCOPES::COL }
-
2
let(:scope_data) { { col: "AA" } }
-
-
1
it "can be reduced to a string" do
-
1
expect(message.to_s).to eq("[COL: AA] ERROR: foo_is_bar")
-
end
-
end
-
-
1
context "when scoped to a cell" do
-
2
let(:scope) { Tabulard::Messaging::SCOPES::CELL }
-
2
let(:scope_data) { { row: 42, col: "AA" } }
-
-
1
it "can be reduced to a string" do
-
1
expect(message.to_s).to eq("[CELL: AA42] ERROR: foo_is_bar")
-
end
-
end
-
-
1
context "when the scope doesn't make sense" do
-
2
let(:scope) { "oiqjzfoi" }
-
-
1
it "can be reduced to a string" do
-
1
expect(message.to_s).to eq("ERROR: foo_is_bar")
-
end
-
end
-
-
1
context "when there is some data associated with the code" do
-
2
let(:scope) { Tabulard::Messaging::SCOPES::TABLE }
-
2
let(:scope_data) { nil }
-
2
let(:code_data) { { "foo" => "bar" } }
-
-
1
it "can be reduced to a string" do
-
1
expect(message.to_s).to match(/^\[TABLE\] ERROR: foo_is_bar {"foo" ?=> ?"bar"}$/)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/message_variant"
-
-
1
RSpec.describe Tabulard::Messaging::MessageVariant do
-
6
let(:code) { double }
-
-
1
describe "::code" do
-
1
context "when a CODE constant is not defined" do
-
1
it "fails with an exception" do
-
2
expect { described_class.code }.to raise_error(NameError, /CODE/)
-
end
-
end
-
-
1
context "when a CODE constant is defined" do
-
1
before do
-
3
stub_const("#{described_class}::CODE", code)
-
end
-
-
1
it "exposes the constant" do
-
1
expect(described_class.code).to eq(code)
-
end
-
-
1
context "when called from a subclass" do
-
3
let(:subclass) { Class.new(described_class) }
-
-
1
before do
-
2
stub_const("#{described_class}Foo", subclass)
-
end
-
-
1
it "exposes the constant from the parent class" do
-
1
expect(subclass.code).to eq(code)
-
end
-
-
1
context "when the subclass also defines the constant" do
-
2
let(:subclass_code) { double }
-
-
1
before do
-
1
stub_const("#{described_class}Foo::CODE", subclass_code)
-
end
-
-
1
it "exposes the constant from the subclass" do
-
1
expect(subclass.code).to eq(subclass_code)
-
end
-
end
-
end
-
end
-
end
-
-
1
describe "::new" do
-
1
before do
-
1
allow(described_class).to receive(:code).and_return(code)
-
end
-
-
1
it "assigns that code to instances automatically" do
-
1
message = described_class.new
-
1
expect(message.code).to eq(code)
-
end
-
end
-
-
1
describe "validations" do
-
2
let(:validator) { described_class.validator }
-
-
1
before do
-
1
allow(described_class).to receive(:code).and_return(code)
-
end
-
-
1
it "validates that the message code is that same as the class code" do
-
1
message1 = described_class.new
-
1
message2 = described_class.new(code: double)
-
1
expect(validator.validate_code(message1)).to be true
-
1
expect(validator.validate_code(message2)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/messages/cleaned_string"
-
-
1
RSpec.describe Tabulard::Messaging::Messages::CleanedString do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "cleaned_string",
-
code_data: nil,
-
scope: "CELL",
-
scope_data: { col: "FOO", row: 42 }
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have some code data" do
-
1
message.code_data = { foo: :bar }
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have nil scope_data" do
-
1
message.scope_data = nil
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/messages/duplicated_header"
-
-
1
RSpec.describe Tabulard::Messaging::Messages::DuplicatedHeader do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "duplicated_header",
-
code_data: { value: "header_foo" },
-
scope: "COL",
-
scope_data: { col: "FOO" }
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have nil code data" do
-
1
message.code_data = nil
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have nil scope_data" do
-
1
message.scope_data = nil
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/messages/invalid_header"
-
-
1
RSpec.describe Tabulard::Messaging::Messages::InvalidHeader do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "invalid_header",
-
code_data: { value: "header_foo" },
-
scope: "COL",
-
scope_data: { col: "FOO" }
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have nil code data" do
-
1
message.code_data = nil
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have nil scope_data" do
-
1
message.scope_data = nil
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/messages/missing_column"
-
-
1
RSpec.describe Tabulard::Messaging::Messages::MissingColumn do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "missing_column",
-
code_data: { value: "header_foo" },
-
scope: "TABLE",
-
scope_data: nil
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have nil code data" do
-
1
message.code_data = nil
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have some scope_data" do
-
1
message.scope_data = { foo: :bar }
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/messages/must_be_array"
-
-
1
RSpec.describe Tabulard::Messaging::Messages::MustBeArray do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "must_be_array",
-
code_data: nil,
-
scope: "CELL",
-
scope_data: { col: "FOO", row: 42 }
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have some code data" do
-
1
message.code_data = { foo: :baz }
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have nil scope_data" do
-
1
message.scope_data = nil
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/messages/must_be_boolsy"
-
-
1
RSpec.describe Tabulard::Messaging::Messages::MustBeBoolsy do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "must_be_boolsy",
-
code_data: { value: "foo" },
-
scope: "CELL",
-
scope_data: { col: "FOO", row: 42 }
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have nil code data" do
-
1
message.code_data = nil
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have nil scope_data" do
-
1
message.scope_data = nil
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/messages/must_be_date"
-
-
1
RSpec.describe Tabulard::Messaging::Messages::MustBeDate do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "must_be_date",
-
code_data: { format: "foo" },
-
scope: "CELL",
-
scope_data: { col: "FOO", row: 42 }
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have nil code data" do
-
1
message.code_data = nil
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have nil scope_data" do
-
1
message.scope_data = nil
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/messages/must_be_email"
-
-
1
RSpec.describe Tabulard::Messaging::Messages::MustBeEmail do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "must_be_email",
-
code_data: { value: "foo" },
-
scope: "CELL",
-
scope_data: { col: "FOO", row: 42 }
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have nil code data" do
-
1
message.code_data = nil
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have nil scope_data" do
-
1
message.scope_data = nil
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/messages/must_be_string"
-
-
1
RSpec.describe Tabulard::Messaging::Messages::MustBeString do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "must_be_string",
-
code_data: nil,
-
scope: "CELL",
-
scope_data: { col: "FOO", row: 42 }
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have some code data" do
-
1
message.code_data = { foo: :baz }
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have nil scope_data" do
-
1
message.scope_data = nil
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/messages/must_exist"
-
-
1
RSpec.describe Tabulard::Messaging::Messages::MustExist do
-
1
it "has a default code" do
-
1
expect(described_class.new).to have_attributes(code: described_class::CODE)
-
end
-
-
1
describe "validations" do
-
1
let(:message) do
-
5
described_class.new(
-
code: "must_exist",
-
code_data: nil,
-
scope: "CELL",
-
scope_data: { col: "FOO", row: 42 }
-
)
-
end
-
-
1
let(:validator) do
-
4
described_class.validator
-
end
-
-
1
it "may be valid" do
-
2
expect { message.validate }.not_to raise_error
-
end
-
-
1
it "may not have a different code" do
-
1
message.code = "foo"
-
1
expect(validator.validate_code(message)).to be false
-
end
-
-
1
it "may not have some code data" do
-
1
message.code_data = { foo: :baz }
-
1
expect(validator.validate_code_data(message)).to be false
-
end
-
-
1
it "may not have a different scope" do
-
1
message.scope = "ROW"
-
1
expect(validator.validate_scope(message)).to be false
-
end
-
-
1
it "may not have nil scope_data" do
-
1
message.scope_data = nil
-
1
expect(validator.validate_scope_data(message)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging"
-
-
1
RSpec.describe Tabulard::Messaging::Messenger do
-
18
let(:scopes) { Tabulard::Messaging::SCOPES }
-
3
let(:severities) { Tabulard::Messaging::SEVERITIES }
-
-
15
let(:row) { double }
-
15
let(:col) { double }
-
-
1
def be_the_frozen(obj)
-
10
be(obj) & be_frozen
-
end
-
-
1
describe "building methods" do
-
4
let(:scope) { Object.new }
-
4
let(:scope_data) { Object.new }
-
-
1
describe "#initialize" do
-
1
it "has some default attributes" do
-
1
messenger = described_class.new
-
-
1
expect(messenger).to have_attributes(
-
scope: scopes::TABLE,
-
scope_data: nil,
-
messages: []
-
)
-
end
-
-
1
it "may have some custom, frozen attributes" do
-
1
messenger = described_class.new(scope: scope, scope_data: scope_data)
-
-
1
expect(messenger).to have_attributes(
-
scope: be_the_frozen(scope),
-
scope_data: be_the_frozen(scope_data),
-
messages: []
-
)
-
end
-
end
-
-
1
describe "#dup" do
-
1
let(:messenger1) do
-
2
described_class.new(scope: scope, scope_data: scope_data)
-
end
-
-
1
it "returns a new instance" do
-
1
messenger2 = messenger1.dup
-
1
expect(messenger2).to eq(messenger1)
-
1
expect(messenger2).not_to be(messenger1)
-
end
-
-
1
it "preserves some attributes" do
-
1
messenger1.messages << "foobar"
-
-
1
messenger2 = messenger1.dup
-
1
expect(messenger2.messages).to be_empty
-
1
expect(messenger2.messages).not_to be(messenger1.messages)
-
end
-
end
-
end
-
-
1
describe "#scoping!" do
-
1
subject do
-
12
->(&block) { messenger.scoping!(new_scope, new_scope_data, &block) }
-
end
-
-
7
let(:old_scope) { Object.new }
-
7
let(:old_scope_data) { Object.new }
-
-
7
let(:new_scope) { Object.new }
-
7
let(:new_scope_data) { Object.new }
-
-
1
let(:messenger) do
-
6
described_class.new(scope: old_scope, scope_data: old_scope_data)
-
end
-
-
1
context "without a block" do
-
1
it "returns the receiver" do
-
1
expect(subject.call).to be(messenger)
-
end
-
-
1
it "scopes the receiver" do
-
1
subject.call
-
-
1
expect(messenger).to have_attributes(
-
scope: be_the_frozen(new_scope),
-
scope_data: be_the_frozen(new_scope_data)
-
)
-
end
-
end
-
-
1
context "with a block" do
-
1
it "returns the block value" do
-
1
foo = double
-
2
expect(subject.call { foo }).to eq(foo)
-
end
-
-
1
it "scopes and yields the receiver" do
-
2
expect { |b| subject.call(&b) }.to yield_with_args(
-
be(messenger) & have_attributes(
-
scope: be_the_frozen(new_scope),
-
scope_data: be_the_frozen(new_scope_data)
-
)
-
)
-
end
-
-
1
it "unscopes the receiver after the block" do
-
1
subject.call {}
-
-
1
expect(messenger).to have_attributes(
-
scope: be_the_frozen(old_scope),
-
scope_data: be_the_frozen(old_scope_data)
-
)
-
end
-
-
1
it "unscopes the receiver after the block, even if it raises" do
-
1
e = StandardError.new
-
-
3
expect { subject.call { raise(e) } }.to raise_error(e)
-
-
1
expect(messenger).to have_attributes(
-
scope: be_the_frozen(old_scope),
-
scope_data: be_the_frozen(old_scope_data)
-
)
-
end
-
end
-
end
-
-
1
describe "#scoping! variants" do
-
12
let(:scoping_block) { proc {} }
-
1
let(:scoping_result) { double }
-
-
1
def allow_method_call_checking_block(receiver, method_name, *args, **opts, &block)
-
22
result = double
-
-
22
allow(receiver).to receive(method_name) do |*a, **o, &b|
-
22
expect(a).to eq(args)
-
22
expect(o).to eq(opts)
-
22
expect(b).to eq(block)
-
22
result
-
end
-
-
22
result
-
end
-
-
1
def stub_scoping!(receiver, ...)
-
18
allow_method_call_checking_block(receiver, :scoping!, ...)
-
end
-
-
1
describe "#scoping" do
-
3
let(:messenger) { described_class.new }
-
3
let(:messenger_dup) { messenger.dup }
-
-
3
let(:scope) { double }
-
3
let(:scope_data) { double }
-
-
1
before do
-
2
allow(messenger).to receive(:dup).and_return(messenger_dup)
-
end
-
-
1
it "applies the correct scoping to a receiver duplicate (with a block)" do
-
1
scoping_result = stub_scoping!(messenger_dup, scope, scope_data, &scoping_block)
-
1
expect(messenger.scoping(scope, scope_data, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "applies the correct scoping to a receiver duplicate (without a block)" do
-
1
scoping_result = stub_scoping!(messenger_dup, scope, scope_data)
-
1
expect(messenger.scoping(scope, scope_data)).to eq(scoping_result)
-
end
-
end
-
-
1
describe "#scope_row!" do
-
1
context "when the messenger is unscoped" do
-
3
let(:messenger) { described_class.new }
-
-
1
it "scopes the messenger to the given row (with a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::ROW, { row: row }, &scoping_block)
-
1
expect(messenger.scope_row!(row, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "scopes the messenger to the given row (without a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::ROW, { row: row })
-
1
expect(messenger.scope_row!(row)).to eq(scoping_result)
-
end
-
end
-
-
1
context "when the messenger is scoped to another row" do
-
3
let(:other_row) { double }
-
3
let(:messenger) { described_class.new(scope: scopes::ROW, scope_data: { row: other_row }) }
-
-
1
it "scopes the messenger to the given row (with a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::ROW, { row: row }, &scoping_block)
-
1
expect(messenger.scope_row!(row, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "scopes the messenger to the given row (without a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::ROW, { row: row })
-
1
expect(messenger.scope_row!(row)).to eq(scoping_result)
-
end
-
end
-
-
1
context "when the messenger is scoped to a col" do
-
3
let(:messenger) { described_class.new(scope: scopes::COL, scope_data: { col: col }) }
-
-
1
it "scopes the messenger to the appropriate cell (with a block)" do
-
scoping_result =
-
1
stub_scoping!(messenger, scopes::CELL, { col: col, row: row }, &scoping_block)
-
1
expect(messenger.scope_row!(row, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "scopes the messenger to the appropriate cell (without a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::CELL, { col: col, row: row })
-
1
expect(messenger.scope_row!(row)).to eq(scoping_result)
-
end
-
end
-
-
1
context "when the messenger is scoped to a cell" do
-
3
let(:other_row) { double }
-
-
1
let(:messenger) do
-
2
described_class.new(scope: scopes::CELL, scope_data: { col: col, row: other_row })
-
end
-
-
1
it "scopes the messenger to the new appropriate cell (with a block)" do
-
scoping_result =
-
1
stub_scoping!(messenger, scopes::CELL, { col: col, row: row }, &scoping_block)
-
1
expect(messenger.scope_row!(row, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "scopes the messenger to the new appropriate cell (without a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::CELL, { col: col, row: row })
-
1
expect(messenger.scope_row!(row)).to eq(scoping_result)
-
end
-
end
-
end
-
-
1
describe "#scope_row" do
-
1
def stub_scope_row!(receiver, ...)
-
2
allow_method_call_checking_block(receiver, :scope_row!, ...)
-
end
-
-
3
let(:messenger) { described_class.new }
-
3
let(:messenger_dup) { messenger.dup }
-
-
1
before do
-
2
allow(messenger).to receive(:dup).and_return(messenger_dup)
-
end
-
-
1
it "applies the correct scoping to a receiver duplicate (with a block)" do
-
1
scoping_result = stub_scope_row!(messenger_dup, row, &scoping_block)
-
1
expect(messenger.scope_row(row, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "applies the correct scoping to a receiver duplicate (without a block)" do
-
1
scoping_result = stub_scope_row!(messenger_dup, row)
-
1
expect(messenger.scope_row(row)).to eq(scoping_result)
-
end
-
end
-
-
1
describe "#scope_col!" do
-
1
context "when the messenger is unscoped" do
-
3
let(:messenger) { described_class.new }
-
-
1
it "scopes the messenger to the given col (with a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::COL, { col: col }, &scoping_block)
-
1
expect(messenger.scope_col!(col, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "scopes the messenger to the given col (without a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::COL, { col: col })
-
1
expect(messenger.scope_col!(col)).to eq(scoping_result)
-
end
-
end
-
-
1
context "when the messenger is scoped to another col" do
-
3
let(:other_col) { double }
-
3
let(:messenger) { described_class.new(scope: scopes::COL, scope_data: { col: other_col }) }
-
-
1
it "scopes the messenger to the given col (with a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::COL, { col: col }, &scoping_block)
-
1
expect(messenger.scope_col!(col, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "scopes the messenger to the given col (without a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::COL, { col: col })
-
1
expect(messenger.scope_col!(col)).to eq(scoping_result)
-
end
-
end
-
-
1
context "when the messenger is scoped to a row" do
-
3
let(:messenger) { described_class.new(scope: scopes::ROW, scope_data: { row: row }) }
-
-
1
it "scopes the messenger to the appropriate cell (with a block)" do
-
scoping_result =
-
1
stub_scoping!(messenger, scopes::CELL, { row: row, col: col }, &scoping_block)
-
1
expect(messenger.scope_col!(col, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "scopes the messenger to the appropriate cell (without a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::CELL, { row: row, col: col })
-
1
expect(messenger.scope_col!(col)).to eq(scoping_result)
-
end
-
end
-
-
1
context "when the messenger is scoped to a cell" do
-
3
let(:other_col) { double }
-
-
1
let(:messenger) do
-
2
described_class.new(scope: scopes::CELL, scope_data: { row: row, col: other_col })
-
end
-
-
1
it "scopes the messenger to the new appropriate cell (with a block)" do
-
scoping_result =
-
1
stub_scoping!(messenger, scopes::CELL, { row: row, col: col }, &scoping_block)
-
1
expect(messenger.scope_col!(col, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "scopes the messenger to the new appropriate cell (without a block)" do
-
1
scoping_result = stub_scoping!(messenger, scopes::CELL, { row: row, col: col })
-
1
expect(messenger.scope_col!(col)).to eq(scoping_result)
-
end
-
end
-
end
-
-
1
describe "#scope_col" do
-
1
def stub_scope_col!(receiver, ...)
-
2
allow_method_call_checking_block(receiver, :scope_col!, ...)
-
end
-
-
3
let(:messenger) { described_class.new }
-
3
let(:messenger_dup) { messenger.dup }
-
-
1
before do
-
2
allow(messenger).to receive(:dup).and_return(messenger_dup)
-
end
-
-
1
it "applies the correct scoping to a receiver duplicate (with a block)" do
-
1
scoping_result = stub_scope_col!(messenger_dup, col, &scoping_block)
-
1
expect(messenger.scope_col(col, &scoping_block)).to eq(scoping_result)
-
end
-
-
1
it "applies the correct scoping to a receiver duplicate (without a block)" do
-
1
scoping_result = stub_scope_col!(messenger_dup, col)
-
1
expect(messenger.scope_col(col)).to eq(scoping_result)
-
end
-
end
-
end
-
-
1
describe "adding messages" do
-
5
let(:scope) { Object.new }
-
5
let(:scope_data) { Object.new }
-
-
5
let(:code) { double }
-
5
let(:code_data) { double }
-
-
1
let(:message) do
-
4
Tabulard::Messaging::Message.new(code: code, code_data: code_data)
-
end
-
-
5
let(:messenger) { described_class.new(scope: scope, scope_data: scope_data) }
-
-
1
describe "#warn" do
-
1
it "returns the receiver" do
-
1
expect(messenger.warn(message)).to be(messenger)
-
end
-
-
1
it "adds the code & code_data as a warning" do
-
1
messenger.warn(message)
-
-
1
expect(messenger.messages).to contain_exactly(
-
Tabulard::Messaging::Message.new(
-
code: code,
-
code_data: code_data,
-
scope: scope,
-
scope_data: scope_data,
-
severity: severities::WARN
-
)
-
)
-
end
-
end
-
-
1
describe "#error" do
-
1
it "returns the receiver" do
-
1
expect(messenger.error(message)).to be(messenger)
-
end
-
-
1
it "adds the code & code_data as an error" do
-
1
messenger.error(message)
-
-
1
expect(messenger.messages).to contain_exactly(
-
Tabulard::Messaging::Message.new(
-
code: code,
-
code_data: code_data,
-
scope: scope,
-
scope_data: scope_data,
-
severity: severities::ERROR
-
)
-
)
-
end
-
end
-
end
-
-
1
describe "validating messages" do
-
1
let(:message) do
-
2
Tabulard::Messaging::Message.new(code: double)
-
end
-
-
1
it "implicitly depends on a global config" do
-
1
config = instance_double(Tabulard::Messaging::Config, validate_messages: double)
-
1
allow(Tabulard::Messaging).to receive(:config).and_return(config)
-
1
messenger = described_class.new
-
1
expect(messenger.validate_messages).to eq(config.validate_messages)
-
end
-
-
1
it "is passed to a duplicate" do
-
1
messenger = described_class.new(validate_messages: val = double)
-
1
expect(messenger.dup.validate_messages).to eq(val)
-
end
-
-
1
context "when enabled" do
-
1
let(:messenger) do
-
1
described_class.new(validate_messages: true)
-
end
-
-
1
it "validates a message while adding it" do
-
1
expect(message).to receive(:validate).with(no_args)
-
1
messenger.warn(message)
-
end
-
end
-
-
1
context "when disabled" do
-
1
let(:messenger) do
-
1
described_class.new(validate_messages: false)
-
end
-
-
1
it "validates a message while adding it" do
-
1
expect(message).not_to receive(:validate)
-
1
messenger.warn(message)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/validations"
-
-
1
RSpec.describe Tabulard::Messaging::Validations::BaseValidator do
-
1
let(:validator_class) do
-
4
described_class
-
end
-
-
1
let(:validator) do
-
12
validator_class.new
-
end
-
-
1
let(:validation_error) do
-
2
Tabulard::Messaging::Validations::InvalidMessage
-
end
-
-
1
let(:message_class) do
-
12
Tabulard::Messaging::Message
-
end
-
-
1
let(:message) do
-
4
instance_double(message_class)
-
end
-
-
1
let(:scopes) do
-
4
Tabulard::Messaging::SCOPES
-
end
-
-
1
it "allows anything by default" do
-
1
expect(validator.validate_code(message)).to be true
-
1
expect(validator.validate_code_data(message)).to be true
-
1
expect(validator.validate_scope(message)).to be true
-
1
expect(validator.validate_scope_data(message)).to be true
-
end
-
-
1
describe "#validate" do
-
1
it "doesn't raise when all validations pass" do
-
2
expect { validator.validate(message) }.not_to raise_error
-
end
-
-
1
context "when some validations fail" do
-
1
before do
-
2
allow(message).to receive(:to_h).and_return({})
-
end
-
-
1
context "code_data and scope" do
-
1
before do
-
1
allow(validator).to receive(:validate_scope).with(message).and_return(false)
-
1
allow(validator).to receive(:validate_code_data).with(message).and_return(false)
-
end
-
-
1
it "raises an error with details" do
-
2
expect { validator.validate(message) }.to raise_error(
-
validation_error, /code_data, scope/
-
)
-
end
-
end
-
-
1
context "code and scope_data" do
-
1
before do
-
1
allow(validator).to receive(:validate_code).with(message).and_return(false)
-
1
allow(validator).to receive(:validate_scope_data).with(message).and_return(false)
-
end
-
-
1
it "raises an error with details" do
-
2
expect { validator.validate(message) }.to raise_error(
-
validation_error, /code, scope_data/
-
)
-
end
-
end
-
end
-
end
-
-
1
context "when validating a cell message" do
-
1
let(:validator_class) do
-
4
Class.new(described_class) { cell }
-
end
-
-
1
it "validates an exact scope" do
-
1
msg1 = instance_double(message_class, scope: scopes::CELL)
-
1
msg2 = instance_double(message_class, scope: double)
-
1
expect(validator.validate_scope(msg1)).to be true
-
1
expect(validator.validate_scope(msg2)).to be false
-
end
-
-
1
it "validates a subset of scope_data" do
-
1
msg1 = instance_double(message_class, scope_data: { col: "AD", row: 7 })
-
1
msg2 = instance_double(message_class, scope_data: { col: 7, row: "AD" })
-
1
expect(validator.validate_scope_data(msg1)).to be true
-
1
expect(validator.validate_scope_data(msg2)).to be false
-
end
-
end
-
-
1
context "when validating a row message" do
-
1
let(:validator_class) do
-
4
Class.new(described_class) { row }
-
end
-
-
1
it "validates an exact scope" do
-
1
msg1 = instance_double(message_class, scope: scopes::ROW)
-
1
msg2 = instance_double(message_class, scope: double)
-
1
expect(validator.validate_scope(msg1)).to be true
-
1
expect(validator.validate_scope(msg2)).to be false
-
end
-
-
1
it "validates a subset of scope_data" do
-
1
msg1 = instance_double(message_class, scope_data: { row: 7 })
-
1
msg2 = instance_double(message_class, scope_data: { row: "AD" })
-
1
expect(validator.validate_scope_data(msg1)).to be true
-
1
expect(validator.validate_scope_data(msg2)).to be false
-
end
-
end
-
-
1
context "when validating a col message" do
-
1
let(:validator_class) do
-
4
Class.new(described_class) { col }
-
end
-
-
1
it "validates an exact scope" do
-
1
msg1 = instance_double(message_class, scope: scopes::COL)
-
1
msg2 = instance_double(message_class, scope: double)
-
1
expect(validator.validate_scope(msg1)).to be true
-
1
expect(validator.validate_scope(msg2)).to be false
-
end
-
-
1
it "validates a subset of scope_data" do
-
1
msg1 = instance_double(message_class, scope_data: { col: "AD" })
-
1
msg2 = instance_double(message_class, scope_data: { col: 7 })
-
1
expect(validator.validate_scope_data(msg1)).to be true
-
1
expect(validator.validate_scope_data(msg2)).to be false
-
end
-
end
-
-
1
context "when validating a table message" do
-
1
let(:validator_class) do
-
4
Class.new(described_class) { table }
-
end
-
-
1
it "validates an exact scope" do
-
1
msg1 = instance_double(message_class, scope: scopes::TABLE)
-
1
msg2 = instance_double(message_class, scope: double)
-
1
expect(validator.validate_scope(msg1)).to be true
-
1
expect(validator.validate_scope(msg2)).to be false
-
end
-
-
1
it "validates the absence of scope_data" do
-
1
msg1 = instance_double(message_class, scope_data: nil)
-
1
msg2 = instance_double(message_class, scope_data: double)
-
1
expect(validator.validate_scope_data(msg1)).to be true
-
1
expect(validator.validate_scope_data(msg2)).to be false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging/validations"
-
-
1
RSpec.describe Tabulard::Messaging::Validations do
-
1
let(:message_class) do
-
9
Struct.new(
-
:code,
-
:code_data,
-
:scope,
-
:scope_data,
-
keyword_init: true
-
)
-
end
-
-
1
before do
-
9
message_class.include(described_class)
-
end
-
-
1
it "doesn't define a validator by default" do
-
1
expect(message_class.validator).to be_nil
-
end
-
-
1
it "doesn't validate by default" do
-
1
message = double
-
2
expect { message_class.validate(message) }.not_to raise_error
-
end
-
-
1
it "delegates instance validations to the class" do
-
1
message = message_class.new
-
1
allow(message_class).to receive(:validate).with(message).and_return(result = double)
-
1
expect(message.validate).to eq(result)
-
end
-
-
1
context "when a validator is defined" do
-
1
it "provides a kind of BaseValidator" do
-
1
message_class.def_validator
-
1
expect(message_class.validator).to be_a(described_class::BaseValidator)
-
end
-
-
1
it "may provide a different kind of validator" do
-
1
message_class.def_validator(base: validator_class = Class.new)
-
1
expect(message_class.validator).to be_a(validator_class)
-
end
-
-
1
it "validates" do
-
2
message_class.def_validator { table }
-
1
message = message_class.new(scope: "bar")
-
2
expect { message_class.validate(message) }.to raise_error(described_class::InvalidMessage)
-
end
-
-
1
context "when a message class is inherited" do
-
1
let(:message_subclass) do
-
3
Class.new(message_class)
-
end
-
-
1
before do
-
3
message_class.def_validator
-
end
-
-
1
it "provides the validator from the superclass" do
-
1
expect(message_subclass.validator).to be(message_class.validator)
-
end
-
-
1
context "when the subclass defines its own validator" do
-
1
it "implicitly inherits from the superclass validator" do
-
1
message_subclass.def_validator
-
1
expect(message_subclass.validator).to be_a(message_class.validator.class)
-
end
-
-
1
it "may inherit from another validator class" do
-
1
message_subclass.def_validator(base: validator_class = Class.new)
-
1
expect(message_subclass.validator).to be_a(validator_class)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging"
-
-
1
RSpec.describe Tabulard::Messaging do
-
1
around do |example|
-
3
config = described_class.config
-
3
example.run
-
3
described_class.config = config
-
end
-
-
1
describe "::config" do
-
1
it "reads a global, frozen instance" do
-
1
expect(described_class.config).to be_a(described_class::Config) & be_frozen
-
end
-
end
-
-
1
describe "::config=" do
-
1
it "writes a global instance" do
-
1
described_class.config = (config = double)
-
1
expect(described_class.config).to eq(config)
-
end
-
end
-
-
1
describe "::configure" do
-
2
let(:old) { described_class::Config.new }
-
2
let(:new) { described_class::Config.new }
-
-
1
before do
-
1
described_class.config = old
-
1
allow(old).to receive(:dup).and_return(new)
-
end
-
-
1
it "modifies a copy of the global instance" do
-
1
expect do |b|
-
1
described_class.configure(&b)
-
end.to yield_with_args(new)
-
-
1
expect(described_class.config).to be(new)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/row_processor_result"
-
-
1
RSpec.describe Tabulard::RowProcessorResult do
-
5
let(:row) { double }
-
5
let(:result) { double }
-
4
let(:messages) { double }
-
-
1
it "wraps a result with messages" do
-
1
processor_result = described_class.new(row: row, result: result, messages: messages)
-
1
expect(processor_result).to have_attributes(row: row, result: result, messages: messages)
-
end
-
-
1
it "is equivalent to a similar result with similar messages" do
-
1
processor_result0 = described_class.new(row: row, result: result, messages: messages)
-
1
processor_result1 = described_class.new(row: row, result: result, messages: messages)
-
1
expect(processor_result0).to eq(processor_result1)
-
end
-
-
1
it "is different from a similar result with a different row" do
-
1
processor_result0 = described_class.new(row: row, result: result, messages: messages)
-
1
processor_result1 = described_class.new(row: double, result: result, messages: messages)
-
1
expect(processor_result0).not_to eq(processor_result1)
-
end
-
-
1
it "may wrap a result with implicit messages" do
-
1
processor_result = described_class.new(row: row, result: result)
-
1
expect(processor_result).to have_attributes(row: row, result: result, messages: [])
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/messaging"
-
1
require "tabulard/headers"
-
1
require "tabulard/row_processor"
-
1
require "tabulard/table"
-
-
1
RSpec.describe Tabulard::RowProcessor, monadic_result: true do
-
1
let(:messenger) do
-
1
instance_double(Tabulard::Messaging::Messenger, dup: row_messenger)
-
end
-
-
1
let(:row_messenger) do
-
1
Tabulard::Messaging::Messenger.new
-
end
-
-
1
let(:headers) do
-
[
-
1
instance_double(Tabulard::Headers::Header, column: double, row_value_index: 0),
-
instance_double(Tabulard::Headers::Header, column: double, row_value_index: 1),
-
instance_double(Tabulard::Headers::Header, column: double, row_value_index: 2),
-
]
-
end
-
-
1
let(:cells) do
-
[
-
1
instance_double(Tabulard::Table::Cell, value: double, col: double),
-
instance_double(Tabulard::Table::Cell, value: double, col: double),
-
instance_double(Tabulard::Table::Cell, value: double, col: double),
-
]
-
end
-
-
1
let(:row) do
-
1
instance_double(Tabulard::Table::Row, row: double, value: cells)
-
end
-
-
1
let(:row_value_builder) do
-
1
instance_double(Tabulard::RowValueBuilder)
-
end
-
-
1
let(:row_value_builder_result) do
-
1
double
-
end
-
-
1
let(:processor) do
-
1
described_class.new(headers: headers, messenger: messenger)
-
end
-
-
1
def expect_headers_add(header, cell)
-
3
expect(row_value_builder).to receive(:add).with(header.column, cell.value).ordered do
-
3
expect(row_messenger).to have_attributes(
-
scope: Tabulard::Messaging::SCOPES::CELL,
-
scope_data: { row: row.row, col: cell.col }
-
)
-
end
-
end
-
-
1
def expect_headers_result
-
1
expect(row_value_builder).to receive(:result).ordered.and_return(row_value_builder_result)
-
end
-
-
1
before do
-
1
allow(Tabulard::RowValueBuilder).to(
-
receive(:new).with(row_messenger).and_return(row_value_builder)
-
)
-
end
-
-
1
it "processes the row and wraps the result with a dedicated set of messages" do
-
1
expect_headers_add(headers[0], cells[0])
-
1
expect_headers_add(headers[1], cells[1])
-
1
expect_headers_add(headers[2], cells[2])
-
1
expect_headers_result
-
-
1
expect(processor.call(row)).to eq(
-
Tabulard::RowProcessorResult.new(
-
row: row.row,
-
result: row_value_builder_result,
-
messages: row_messenger.messages
-
)
-
)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/row_value_builder"
-
1
require "tabulard/column"
-
1
require "tabulard/types/scalars/scalar"
-
-
1
RSpec.describe Tabulard::RowValueBuilder, monadic_result: true do
-
1
let(:builder) do
-
6
described_class.new(messenger)
-
end
-
-
7
let(:messenger) { double }
-
-
4
let(:scalar_type) { instance_double(Tabulard::Types::Type, composite?: false) }
-
4
let(:scalar_key) { double }
-
-
5
let(:composite_type) { instance_double(Tabulard::Types::Type, composite?: true) }
-
5
let(:composite_key) { double }
-
-
1
def stub_scalar(column, value, result)
-
8
allow(column.type).to receive(:scalar).with(column.index, value, messenger).and_return(result)
-
end
-
-
1
def stub_composite(type, value, result)
-
3
allow(type).to receive(:composite).with(value, messenger).and_return(result)
-
end
-
-
1
context "when the column type is scalar" do
-
1
let(:column) do
-
2
instance_double(Tabulard::Column, type: scalar_type, key: scalar_key, index: nil)
-
end
-
-
3
let(:input) { double }
-
2
let(:output) { double }
-
-
1
context "when the scalar type casting succeeds" do
-
2
before { stub_scalar(column, input, Success(output)) }
-
-
1
it "returns Success results wrapping type casted values" do
-
1
result = builder.add(column, input)
-
-
1
expect(result).to eq(Success(output))
-
1
expect(builder.result).to eq(Success(column.key => output))
-
end
-
end
-
-
1
context "when the scalar type casting fails" do
-
2
before { stub_scalar(column, input, Failure()) }
-
-
1
it "returns Failure results" do
-
1
result = builder.add(column, input)
-
-
1
expect(result).to eq(Failure())
-
1
expect(builder.result).to eq(Failure())
-
end
-
end
-
end
-
-
1
context "when the column type is composite" do
-
1
let(:column) do
-
3
instance_double(Tabulard::Column, type: composite_type, key: composite_key, index: 0)
-
end
-
-
4
let(:scalar_input) { double }
-
3
let(:scalar_output) { double }
-
-
1
context "when the scalar type casting succeeds" do
-
3
before { stub_scalar(column, scalar_input, Success(scalar_output)) }
-
-
1
context "when the composite type casting succeeds" do
-
2
let(:composite_output) { double }
-
-
1
before do
-
1
stub_composite(composite_type, [scalar_output], Success(composite_output))
-
end
-
-
1
it "returns Success results wrapping type casted values" do
-
1
result = builder.add(column, scalar_input)
-
-
1
expect(result).to eq(Success(scalar_output))
-
1
expect(builder.result).to eq(Success(column.key => composite_output))
-
end
-
end
-
-
1
context "when the composite type casting fails" do
-
2
before { stub_composite(composite_type, [scalar_output], Failure()) }
-
-
1
it "returns Success and Failure appropriately" do
-
1
result = builder.add(column, scalar_input)
-
-
1
expect(result).to eq(Success(scalar_output))
-
1
expect(builder.result).to eq(Failure())
-
end
-
end
-
end
-
-
1
context "when the scalar type casting fails" do
-
2
before { stub_scalar(column, scalar_input, Failure()) }
-
-
1
it "returns Failure results" do
-
1
result = builder.add(column, scalar_input)
-
-
1
expect(result).to eq(Failure())
-
1
expect(builder.result).to eq(Failure())
-
end
-
end
-
end
-
-
1
context "when handling multiple columns in any order" do
-
1
let(:column0) do
-
1
instance_double(Tabulard::Column, type: composite_type, key: composite_key, index: 2)
-
end
-
-
1
let(:column1) do
-
1
instance_double(Tabulard::Column, type: composite_type, key: composite_key, index: 1)
-
end
-
-
1
let(:column2) do
-
1
instance_double(Tabulard::Column, type: scalar_type, key: scalar_key, index: nil)
-
end
-
-
1
it "reduces them to a correctly typed aggregate" do
-
1
stub_scalar(column0, in0 = double, Success(out0 = double))
-
1
stub_scalar(column1, in1 = double, Success(out1 = double))
-
1
stub_scalar(column2, in2 = double, Success(out2 = double))
-
-
1
builder.add(column0, in0)
-
1
builder.add(column2, in2)
-
1
builder.add(column1, in1)
-
-
1
stub_composite(composite_type, [nil, out1, out0], Success(composite_out = double))
-
-
1
expect(builder.result).to eq(
-
Success(
-
composite_key => composite_out,
-
scalar_key => out2
-
)
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/specification"
-
1
require "tabulard/column"
-
-
1
RSpec.describe Tabulard::Specification do
-
1
describe "#get" do
-
1
let(:header) do
-
3
"foo"
-
end
-
-
1
let(:column0) do
-
4
instance_double(Tabulard::Column, :column0, header_pattern: double(:pattern0))
-
end
-
-
1
let(:column1) do
-
4
instance_double(Tabulard::Column, :column1, header_pattern: double(:pattern1))
-
end
-
-
1
let(:spec) do
-
4
described_class.new(columns: [column0, column1])
-
end
-
-
1
before do
-
4
allow(column0.header_pattern).to receive(:match?).with(anything).and_return(false)
-
4
allow(column1.header_pattern).to receive(:match?).with(anything).and_return(false)
-
end
-
-
1
it "returns nil when header is nil" do
-
1
expect(spec.get(nil)).to be_nil
-
end
-
-
1
it "may match with a pattern" do
-
1
allow(column0.header_pattern).to receive(:match?).with(header).and_return(true)
-
1
expect(spec.get(header)).to eq(column0)
-
end
-
-
1
it "may match with another pattern" do
-
1
allow(column1.header_pattern).to receive(:match?).with(header).and_return(true)
-
1
expect(spec.get(header)).to eq(column1)
-
end
-
-
1
it "may not match at all" do
-
1
expect(spec.get(header)).to be_nil
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/table_processor_result"
-
-
1
RSpec.describe Tabulard::TableProcessorResult do
-
4
let(:result) { double }
-
3
let(:messages) { double }
-
-
1
it "wraps a result with messages" do
-
1
processor_result = described_class.new(result: result, messages: messages)
-
1
expect(processor_result).to have_attributes(result: result, messages: messages)
-
end
-
-
1
it "is equivalent to a similar result with similar messages" do
-
1
processor_result0 = described_class.new(result: result, messages: messages)
-
1
processor_result1 = described_class.new(result: result, messages: messages)
-
1
expect(processor_result0).to eq(processor_result1)
-
end
-
-
1
it "may wrap a result with implicit messages" do
-
1
processor_result = described_class.new(result: result)
-
1
expect(processor_result).to have_attributes(result: result, messages: [])
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/table_processor"
-
1
require "tabulard/specification"
-
-
1
RSpec.describe Tabulard::TableProcessor, monadic_result: true do
-
1
let(:specification) do
-
5
instance_double(Tabulard::Specification)
-
end
-
-
1
let(:processor) do
-
5
described_class.new(specification)
-
end
-
-
1
let(:messenger) do
-
2
instance_double(Tabulard::Messaging::Messenger, messages: double)
-
end
-
-
1
let(:table_class) do
-
10
Class.new { include Tabulard::Table }
-
end
-
-
1
let(:adapter_args) do
-
5
[double, double]
-
end
-
-
1
let(:adapter_opts) do
-
5
{ foo: double, bar: double }
-
end
-
-
1
let(:table) do
-
3
instance_double(table_class)
-
end
-
-
1
def call(&block)
-
4
block ||= proc {} # stub a dummy proc
-
4
processor.call(*adapter_args, adapter: table_class, **adapter_opts, &block)
-
end
-
-
1
def stub_table_open
-
4
stub = yield receive(:open).with(*adapter_args, **adapter_opts, messenger: messenger)
-
4
allow(table_class).to(stub)
-
end
-
-
1
def stub_table_open_ok
-
6
stub_table_open { _1.and_yield(table) }
-
end
-
-
1
def stub_table_open_ko
-
2
stub_table_open { _1.and_return(Failure()) }
-
end
-
-
1
before do
-
5
allow(Tabulard::Messaging::Messenger).to receive(:new).with(no_args).and_return(messenger)
-
end
-
-
1
it "passes the args and opts to Adapters.open" do
-
1
actual_args = adapter_args
-
1
actual_opts = adapter_opts.merge(adapter: table_class, messenger: messenger)
-
-
1
expect(Tabulard::Adapters).to(
-
receive(:open)
-
.with(*actual_args, **actual_opts)
-
.and_return(Success())
-
)
-
-
1
processor.call(*actual_args, **actual_opts)
-
end
-
-
1
context "when opening the table fails" do
-
1
before do
-
1
stub_table_open_ko
-
end
-
-
1
it "is an empty failure, with messages" do
-
1
expect(call).to eq(
-
Tabulard::TableProcessorResult.new(
-
result: Failure(),
-
messages: messenger.messages
-
)
-
)
-
end
-
end
-
-
1
shared_context "when there is no table error" do
-
2
let(:table_headers) do
-
9
Array.new(2) { instance_double(Tabulard::Table::Header) }
-
end
-
-
2
let(:table_rows) do
-
12
Array.new(3) { instance_double(Tabulard::Table::Row) }
-
end
-
-
2
let(:messenger) do
-
3
instance_double(Tabulard::Messaging::Messenger, messages: double)
-
end
-
-
2
let(:headers) do
-
3
instance_double(Tabulard::Headers)
-
end
-
-
2
def stub_enumeration(obj, method_name, enumerable)
-
6
enum = Enumerator.new do |yielder|
-
17
enumerable.each { |item| yielder << item }
-
5
obj
-
end
-
-
6
allow(obj).to receive(method_name).with(no_args) do |&block|
-
5
enum.each(&block)
-
end
-
end
-
-
2
def stub_headers
-
3
allow(Tabulard::Headers).to(
-
receive(:new)
-
.with(specification: specification, messenger: messenger)
-
.and_return(headers)
-
)
-
end
-
-
2
def stub_headers_ops(result)
-
3
table_headers.each do |table_header|
-
6
expect(headers).to receive(:add).with(table_header).ordered
-
end
-
-
3
expect(headers).to receive(:result).and_return(result).ordered
-
end
-
-
2
before do
-
3
stub_headers
-
-
3
stub_table_open_ok
-
-
3
stub_enumeration(table, :each_header, table_headers)
-
3
stub_enumeration(table, :each_row, table_rows)
-
end
-
end
-
-
1
context "when there is a header error" do
-
1
include_context "when there is no table error"
-
-
1
before do
-
1
stub_headers_ops(Failure())
-
end
-
-
1
it "is an empty failure, with messages" do
-
1
result = call
-
-
1
expect(result).to eq(
-
Tabulard::TableProcessorResult.new(
-
result: Failure(),
-
messages: messenger.messages
-
)
-
)
-
end
-
end
-
-
1
context "when there is no error" do
-
1
include_context "when there is no table error"
-
-
1
let(:headers_spec) do
-
2
double
-
end
-
-
1
let(:processed_rows) do
-
8
Array.new(table_rows.size) { double }
-
end
-
-
1
def stub_row_processing
-
2
allow(Tabulard::RowProcessor).to(
-
receive(:new)
-
.with(headers: headers_spec, messenger: messenger)
-
.and_return(row_processor = instance_double(Tabulard::RowProcessor))
-
)
-
-
2
table_rows.zip(processed_rows) do |row, processed_row|
-
6
allow(row_processor).to receive(:call).with(row).and_return(processed_row)
-
end
-
end
-
-
1
before do
-
2
stub_headers_ops(Success(headers_spec))
-
-
2
stub_row_processing
-
end
-
-
1
it "is an empty success, with messages" do
-
1
result = call
-
-
1
expect(result).to eq(
-
Tabulard::TableProcessorResult.new(
-
result: Success(),
-
messages: messenger.messages
-
)
-
)
-
end
-
-
1
it "yields each processed row" do
-
2
expect { |b| call(&b) }.to yield_successive_args(*processed_rows)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/table"
-
-
1
RSpec.describe Tabulard::Table, monadic_result: true do
-
1
let(:table_class) do
-
25
c = Class.new do
-
25
def initialize(foo, bar:)
-
5
@foo = foo
-
5
@bar = bar
-
end
-
end
-
-
25
c.include(described_class)
-
-
25
stub_const("TableClass", c)
-
-
25
c
-
end
-
-
1
let(:table) do
-
5
table_class.new(foo, bar: bar)
-
end
-
-
17
let(:foo) { double }
-
17
let(:bar) { double }
-
-
1
describe "::Error" do
-
2
subject { table_class::Error }
-
-
1
it "exposes some kind of Tabulard::Errors::Error" do
-
1
expect(subject.superclass).to be(Tabulard::Errors::Error)
-
end
-
end
-
-
1
describe "::ClosureError" do
-
2
subject { table_class::ClosureError }
-
-
1
it "exposes some kind of Tabulard::Table::Error" do
-
1
expect(subject.superclass).to be(Tabulard::Table::Error)
-
end
-
end
-
-
1
describe "::InputError" do
-
2
subject { table_class::InputError }
-
-
1
it "exposes some kind of Tabulard::Table::Error" do
-
1
expect(subject.superclass).to be(Tabulard::Table::Error)
-
end
-
end
-
-
1
describe "::Header" do
-
3
let(:col) { double }
-
3
let(:val) { double }
-
-
3
let(:wrapper) { table_class::Header.new(col: col, value: val) }
-
-
1
it "exposes a header wrapper" do
-
1
expect(wrapper).to have_attributes(col: col, value: val)
-
end
-
-
1
it "is comparable" do
-
1
expect(wrapper).to eq(table_class::Header.new(col: col, value: val))
-
1
expect(wrapper).not_to eq(table_class::Header.new(col: double, value: val))
-
end
-
end
-
-
1
describe "::Row" do
-
3
let(:row) { double }
-
3
let(:val) { double }
-
-
3
let(:wrapper) { table_class::Row.new(row: row, value: val) }
-
-
1
it "exposes a row wrapper" do
-
1
expect(wrapper).to have_attributes(row: row, value: val)
-
end
-
-
1
it "is comparable" do
-
1
expect(wrapper).to eq(table_class::Row.new(row: row, value: val))
-
1
expect(wrapper).not_to eq(table_class::Row.new(row: double, value: val))
-
end
-
end
-
-
1
describe "::Cell" do
-
3
let(:row) { double }
-
3
let(:col) { double }
-
3
let(:val) { double }
-
-
3
let(:wrapper) { table_class::Cell.new(row: row, col: col, value: val) }
-
-
1
it "exposes a row wrapper" do
-
1
expect(wrapper).to have_attributes(row: row, col: col, value: val)
-
end
-
-
1
it "is comparable" do
-
1
expect(wrapper).to eq(table_class::Cell.new(row: row, col: col, value: val))
-
1
expect(wrapper).not_to eq(table_class::Cell.new(row: double, col: col, value: val))
-
end
-
end
-
-
1
describe "singleton class methods" do
-
1
let(:samples) do
-
[
-
2
["A", 1],
-
["B", 2],
-
["Z", 26],
-
["AA", 27],
-
["AZ", 52],
-
["BA", 53],
-
["ZA", 677],
-
["ZZ", 702],
-
["AAA", 703],
-
["AAZ", 728],
-
["ABA", 729],
-
["BZA", 2029],
-
]
-
end
-
-
1
describe "::col2int" do
-
1
it "turns letter-based indexes into integer-based indexes" do
-
1
samples.each do |(col, int)|
-
12
res = described_class.col2int(col)
-
12
expect(res).to eq(int), "Expected #{col} => #{int}, got: #{res}"
-
end
-
end
-
-
1
it "fails on invalid inputs" do
-
2
expect { described_class.col2int(nil) }.to raise_error(ArgumentError)
-
2
expect { described_class.col2int("") }.to raise_error(ArgumentError)
-
2
expect { described_class.col2int("a") }.to raise_error(ArgumentError)
-
2
expect { described_class.col2int("€") }.to raise_error(ArgumentError)
-
end
-
end
-
-
1
describe "::int2col" do
-
1
it "turns integer-based indexes into letter-based indexes" do
-
1
samples.each do |(col, int)|
-
12
res = described_class.int2col(int)
-
12
expect(res).to eq(col), "Expected #{int} => #{col}, got: #{res}"
-
end
-
end
-
-
1
it "fails on invalid inputs" do
-
2
expect { described_class.int2col(nil) }.to raise_error(ArgumentError)
-
2
expect { described_class.int2col(0) }.to raise_error(ArgumentError)
-
2
expect { described_class.int2col(-12) }.to raise_error(ArgumentError)
-
2
expect { described_class.int2col(27.0) }.to raise_error(ArgumentError)
-
end
-
end
-
end
-
-
1
describe "::open" do
-
1
let(:table) do
-
11
instance_double(table_class)
-
end
-
-
1
before do
-
11
allow(table_class).to receive(:new).with(foo, bar: bar).and_return(table)
-
end
-
-
1
context "without a block" do
-
1
it "returns a new table wrapped as a Success" do
-
1
expect(table_class.open(foo, bar: bar)).to eq(Success(table))
-
end
-
end
-
-
1
context "with a block" do
-
1
before do
-
10
allow(table).to receive(:close)
-
end
-
-
1
it "yields a new table" do
-
1
yielded = false
-
-
1
table_class.open(foo, bar: bar) do |opened_table|
-
1
yielded = true
-
1
expect(opened_table).to be(table)
-
end
-
-
1
expect(yielded).to be(true)
-
end
-
-
1
it "returns the value of the block" do
-
1
block_result = double
-
2
actual_block_result = table_class.open(foo, bar: bar) { block_result }
-
-
1
expect(actual_block_result).to eq(block_result)
-
end
-
-
1
it "closes after yielding" do
-
1
table_class.open(foo, bar: bar) do
-
1
expect(table).not_to have_received(:close)
-
end
-
-
1
expect(table).to have_received(:close)
-
end
-
-
1
context "when an exception is raised" do
-
3
let(:exception) { Class.new(StandardError) }
-
3
let(:error) { Class.new(Tabulard::Table::Error) }
-
4
let(:input_error) { Class.new(Tabulard::Table::InputError) }
-
-
1
context "without yielding control" do
-
1
it "doesn't rescue an exception" do
-
1
allow(table_class).to receive(:new).and_raise(exception)
-
-
1
expect do
-
1
table_class.open(foo, bar: bar)
-
end.to raise_error(exception)
-
end
-
-
1
it "doesn't rescue an error" do
-
1
allow(table_class).to receive(:new).and_raise(error)
-
-
1
expect do
-
1
table_class.open(foo, bar: bar)
-
end.to raise_error(error)
-
end
-
-
1
it "rescues an input error and returns a failure" do
-
1
allow(table_class).to receive(:new).and_raise(input_error.exception)
-
-
1
result = table_class.open(foo, bar: bar)
-
-
1
expect(result).to eq(Failure())
-
end
-
end
-
-
1
context "while yielding control" do
-
1
it "doesn't rescue but closes after an exception is raised" do
-
1
expect do
-
1
table_class.open(foo, bar: bar) do
-
1
expect(table).not_to have_received(:close)
-
1
raise exception
-
end
-
end.to raise_error(exception)
-
-
1
expect(table).to have_received(:close)
-
end
-
-
1
it "doesn't rescue but closes after an error is raised" do
-
1
expect do
-
1
table_class.open(foo, bar: bar) do
-
1
expect(table).not_to have_received(:close)
-
1
raise error
-
end
-
end.to raise_error(error)
-
-
1
expect(table).to have_received(:close)
-
end
-
-
1
it "rescues and closes after an input error is raised" do
-
1
table_class.open(foo, bar: bar) do
-
1
expect(table).not_to have_received(:close)
-
1
raise input_error
-
end
-
-
1
expect(table).to have_received(:close)
-
end
-
-
1
it "rescues and returns an empty failure after an input error is raised" do
-
1
e = input_error.exception # raise the instance directly to simplify result matching
-
-
1
result = table_class.open(foo, bar: bar) do
-
1
raise e
-
end
-
-
1
expect(result).to eq(Failure())
-
end
-
end
-
end
-
end
-
end
-
-
1
describe "#each_header" do
-
1
it "is abstract" do
-
2
expect { table.each_header }.to raise_error(
-
NoMethodError, "You must implement TableClass#each_header => self"
-
)
-
end
-
end
-
-
1
describe "#each_row" do
-
1
it "is abstract" do
-
2
expect { table.each_row }.to raise_error(
-
NoMethodError, "You must implement TableClass#each_row => self"
-
)
-
end
-
end
-
-
1
describe "#close" do
-
4
before { table }
-
-
1
it "removes the instance variables" do
-
2
expect { table.close }.to [
-
2
change { table.instance_variable_defined?(:@foo) }.from(true).to(false),
-
2
change { table.instance_variable_defined?(:@bar) }.from(true).to(false),
-
].reduce(:&)
-
end
-
-
1
it "marks the instance as closed" do
-
2
expect { table.close }.to change(table, :closed?).from(false).to(true)
-
end
-
-
1
it "returns nil" do
-
1
expect(table.close).to be_nil
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/template_config"
-
-
1
RSpec.describe Tabulard::TemplateConfig do
-
6
let(:config) { described_class.new }
-
-
1
describe "#header" do
-
1
it "produces a safe pattern from a header with special characters" do
-
1
header, pattern = config.header(".*", nil)
-
-
1
expect(pattern).to match(header)
-
1
expect(pattern).not_to match("foo")
-
end
-
-
1
it "produces a case-insensitive pattern" do
-
1
header, pattern = config.header(:foo, nil)
-
-
1
expect(pattern).to match(header.downcase)
-
1
expect(pattern).to match(header.upcase)
-
end
-
-
1
it "produces a strict pattern" do
-
1
header, pattern = config.header(:foo, nil)
-
-
1
expect(pattern).not_to match("#{header}foo")
-
1
expect(pattern).not_to match("foo#{header}")
-
end
-
-
1
context "when the index is nil" do
-
1
it "capitalizes the key" do
-
1
header, = config.header(:foo, nil)
-
-
1
expect(header).to eq("Foo")
-
end
-
end
-
-
1
context "when the index is not nil" do
-
1
it "capitalizes the key and appends a 1-based index" do
-
1
header, = config.header(:foo, 3)
-
-
1
expect(header).to eq("Foo 4")
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/template"
-
1
require "tabulard/attribute"
-
1
require "tabulard/errors/spec_error"
-
-
1
RSpec.describe Tabulard::Template do
-
1
let(:attributes_args) do
-
[
-
4
{ key: :key0, type: :type0 },
-
{ key: :key1, type: :type1 },
-
]
-
end
-
-
1
let(:attributes) do
-
3
attributes_args.map do |args|
-
6
Tabulard::Attribute.build(**args)
-
end
-
end
-
-
1
it "doesn't ignore unspecified columns by default" do
-
1
template = described_class.new(attributes: attributes, ignore_unspecified_columns: false)
-
1
expect(template).to eq(described_class.new(attributes: attributes))
-
end
-
-
1
context "when an attribute is duplicated" do
-
1
it "can't be initialized" do
-
1
expect do
-
1
described_class.new(attributes: [attributes[0], attributes[0]])
-
end.to raise_error(Tabulard::Errors::SpecError, "Duplicated key: :key0")
-
end
-
end
-
-
1
describe "::build" do
-
1
it "simplifies initialization" do
-
1
template = described_class.build(attributes: attributes_args)
-
1
expect(template).to eq(described_class.new(attributes: attributes))
-
end
-
-
1
it "freezes the resulting instance" do
-
1
template = described_class.build(attributes: attributes_args)
-
1
expect(template).to be_frozen
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/cast_chain"
-
-
1
RSpec.describe Tabulard::Types::CastChain do
-
1
let(:cast_interface) do
-
9
Class.new do
-
9
def call(_value, _messenger); end
-
end
-
end
-
-
1
let(:cast) do
-
1
cast_interface.new
-
end
-
-
5
let(:cast0) { cast_double }
-
5
let(:cast1) { cast_double }
-
4
let(:cast2) { cast_double }
-
-
1
let(:chain) do
-
2
described_class.new([cast0, cast1])
-
end
-
-
1
def cast_double
-
15
instance_double(cast_interface)
-
end
-
-
1
describe "#initialize" do
-
1
it "builds an empty chain by default" do
-
1
chain = described_class.new
-
-
1
expect(chain.casts).to be_empty
-
end
-
-
1
it "builds a non-empty chain using the optional parameter" do
-
1
chain = described_class.new([cast0, cast1])
-
-
1
expect(chain.casts).to eq([cast0, cast1])
-
end
-
end
-
-
1
describe "#prepend" do
-
1
it "prepends a cast to the chain" do
-
1
chain.prepend(cast2)
-
-
1
expect(chain.casts).to eq([cast2, cast0, cast1])
-
end
-
end
-
-
1
describe "#appends" do
-
1
it "appends a cast to the chain" do
-
1
chain.append(cast2)
-
-
1
expect(chain.casts).to eq([cast0, cast1, cast2])
-
end
-
end
-
-
1
describe "#freeze" do
-
1
it "freezes the whole chain" do
-
1
chain = described_class.new([cast.dup, cast.dup])
-
-
1
chain.freeze
-
-
1
expect(chain.casts).to all(be_frozen)
-
1
expect(chain.casts).to be_frozen
-
1
expect(chain).to be_frozen
-
end
-
end
-
-
1
describe "#call", monadic_result: true do
-
1
let(:messenger) do
-
5
double
-
end
-
-
1
it "maps the value and passes the messenger to all casts" do
-
1
value0 = double
-
-
1
expect(cast0).to(receive(:call).with(value0, messenger).and_return(value1 = double))
-
1
expect(cast1).to(receive(:call).with(value1, messenger).and_return(value2 = double))
-
1
expect(cast2).to(receive(:call).with(value2, messenger).and_return(value3 = double))
-
-
1
chain = described_class.new([cast0, cast1, cast2])
-
-
1
result = chain.call(value0, messenger)
-
1
expect(result).to eq(Success(value3))
-
end
-
-
1
context "when a cast throws :success without value" do
-
1
it "halts the chain and returns Success(nil)" do
-
1
chain = described_class.new [
-
1
->(value, _messenger) { value.capitalize },
-
1
->(_value, _messenger) { throw :success },
-
cast_double,
-
]
-
-
1
result = chain.call("foo", messenger)
-
-
1
expect(result).to eq(Success(nil))
-
end
-
end
-
-
1
context "when a cast throws :success with a value" do
-
1
it "halts the chain and returns Success(<value>)" do
-
1
chain = described_class.new [
-
1
->(value, _messenger) { value.capitalize },
-
1
->(_value, _messenger) { throw :success, "bar" },
-
cast_double,
-
]
-
-
1
result = chain.call("foo", messenger)
-
-
1
expect(result).to eq(Success("bar"))
-
end
-
end
-
-
1
context "when a cast throws :failure without a value" do
-
1
it "halts the chain and returns Failure()" do
-
1
chain = described_class.new [
-
1
->(value, _messenger) { value.capitalize },
-
1
->(_value, _messenger) { throw :failure },
-
cast_double,
-
]
-
-
1
result = chain.call("foo", messenger)
-
-
1
expect(result).to eq(Failure())
-
end
-
end
-
-
1
context "when a cast throws :failure with a value" do
-
1
it "halts the chain, adds a <value> message as an error and returns Failure()" do
-
1
chain = described_class.new [
-
1
->(value, _messenger) { value.capitalize },
-
1
->(_value, _messenger) { throw :failure, "some_code" },
-
cast_double,
-
]
-
-
1
allow(messenger).to receive(:error)
-
-
1
result = chain.call("foo", messenger)
-
-
1
expect(result).to eq(Failure())
-
1
expect(messenger).to have_received(:error).with("some_code")
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/composites/array_compact"
-
1
require "support/shared/composite_type"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Tabulard::Types::Composites::ArrayCompact do
-
1
include_examples "composite_type"
-
-
1
it "inherits from the composite array type" do
-
1
expect(described_class.superclass).to be(Tabulard::Types::Composites::Array)
-
end
-
-
1
describe "custom cast class" do
-
1
subject(:cast_class) do
-
4
described_class.cast_classes.last
-
end
-
-
1
it "is appended to the superclass' cast classes" do
-
1
expect(described_class.cast_classes).to eq(
-
described_class.superclass.cast_classes + [cast_class]
-
)
-
end
-
-
1
include_examples "cast_class"
-
-
1
describe "#call" do
-
1
it "compacts the given value" do
-
1
allow(value).to receive(:compact).and_return(compact_value = double)
-
1
expect(cast.call(value, messenger)).to eq(compact_value)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/composites/array"
-
1
require "support/shared/composite_type"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Tabulard::Types::Composites::Array do
-
1
include_examples "composite_type"
-
-
1
it "inherits from the basic composite type" do
-
1
expect(described_class.superclass).to be(Tabulard::Types::Composites::Composite)
-
end
-
-
1
describe "custom cast class" do
-
1
subject(:cast_class) do
-
5
described_class.cast_classes.last
-
end
-
-
1
it "is appended to the superclass' cast classes" do
-
1
expect(described_class.cast_classes).to eq(
-
described_class.superclass.cast_classes + [cast_class]
-
)
-
end
-
-
1
include_examples "cast_class"
-
-
1
describe "#call" do
-
1
before do
-
2
allow(value).to receive(:is_a?).with(Array).and_return(value_is_array)
-
end
-
-
1
context "when the value is an array" do
-
2
let(:value_is_array) { true }
-
-
1
it "is a success" do
-
1
expect(cast.call(value, messenger)).to eq(value)
-
end
-
end
-
-
1
context "when the value is not an array" do
-
2
let(:value_is_array) { false }
-
-
1
it "is a failure" do
-
1
expect do
-
1
cast.call(value, messenger)
-
end.to throw_symbol(:failure, Tabulard::Messaging::Messages::MustBeArray.new)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/composites/composite"
-
1
require "support/shared/composite_type"
-
-
1
RSpec.describe Tabulard::Types::Composites::Composite do
-
1
include_examples "composite_type"
-
-
1
it "inherits from the basic type" do
-
1
expect(described_class.superclass).to be(Tabulard::Types::Type)
-
end
-
-
1
describe "::cast_classes" do
-
1
it "is empty" do
-
1
expect(described_class.cast_classes).to be_empty
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/container"
-
-
1
RSpec.describe Tabulard::Types::Container do
-
1
let(:default_scalars) do
-
3
%i[scalar string email boolsy date_string]
-
end
-
-
1
let(:default_composites) do
-
3
%i[array array_compact]
-
end
-
-
1
context "when used by default" do
-
9
subject(:container) { described_class.new }
-
-
1
it "knows about some types" do
-
1
expect(container.scalars).to match_array(default_scalars)
-
1
expect(container.composites).to match_array(default_composites)
-
end
-
-
1
describe "typemap" do
-
1
let(:scalar_type) do
-
1
Tabulard::Types::Scalars::Scalar
-
end
-
-
1
let(:string_type) do
-
3
Tabulard::Types::Scalars::String
-
end
-
-
1
let(:email_type) do
-
3
Tabulard::Types::Scalars::Email
-
end
-
-
1
let(:boolsy_type) do
-
1
Tabulard::Types::Scalars::Boolsy
-
end
-
-
1
let(:date_string_type) do
-
1
Tabulard::Types::Scalars::DateString
-
end
-
-
1
def stub_new_type(klass, *args)
-
2
allow(klass).to receive(:new!).with(*args).and_return(instance = double)
-
2
instance
-
end
-
-
1
it "is readable" do
-
1
expect(described_class::DEFAULTS).to match(
-
scalars: include(*default_scalars) & be_frozen,
-
composites: include(*default_composites) & be_frozen
-
) & be_frozen
-
end
-
-
1
example "scalars: scalar" do
-
1
expect(scalar = container.scalar(:scalar)).to be_a(scalar_type) & be_frozen
-
1
expect(container.scalar(:scalar)).to be(scalar)
-
end
-
-
1
example "scalars: string" do
-
1
expect(string = container.scalar(:string)).to be_a(string_type) & be_frozen
-
1
expect(container.scalar(:string)).to be(string)
-
end
-
-
1
example "scalars: email" do
-
1
expect(email = container.scalar(:email)).to be_a(email_type) & be_frozen
-
1
expect(container.scalar(:email)).to be(email)
-
end
-
-
1
example "scalars: boolsy" do
-
1
expect(boolsy = container.scalar(:boolsy)).to be_a(boolsy_type) & be_frozen
-
1
expect(container.scalar(:boolsy)).to be(boolsy)
-
end
-
-
1
example "scalars: date_string" do
-
1
expect(date_string = container.scalar(:date_string)).to be_a(date_string_type) & be_frozen
-
1
expect(container.scalar(:date_string)).to be(date_string)
-
end
-
-
1
example "composites: array" do
-
1
type = stub_new_type(Tabulard::Types::Composites::Array, [string_type, email_type])
-
1
expect(container.composite(:array, %i[string email])).to be(type)
-
end
-
-
1
example "composites: array_composite" do
-
1
type = stub_new_type(Tabulard::Types::Composites::ArrayCompact, [string_type, email_type])
-
1
expect(container.composite(:array_compact, %i[string email])).to be(type)
-
end
-
end
-
end
-
-
1
context "when extended" do
-
4
let(:foo_type) { double }
-
3
let(:bar_type) { double }
-
2
let(:baz_type) { double }
-
2
let(:oof_type) { double }
-
-
1
let(:container) do
-
2
described_class.new(
-
scalars: {
-
3
foo: -> { foo_type },
-
1
string: -> { bar_type },
-
},
-
composites: {
-
1
baz: ->(_types) { baz_type },
-
1
array: ->(_types) { oof_type },
-
}
-
)
-
end
-
-
1
it "can use custom scalars and composites" do
-
1
expect(container.scalars).to contain_exactly(*default_scalars, :foo)
-
1
expect(container.scalar(:foo)).to be(foo_type)
-
1
expect(container.composites).to contain_exactly(*default_composites, :baz)
-
1
expect(container.composite(:baz, %i[foo])).to be(baz_type)
-
end
-
-
1
it "can override default type definitions" do
-
1
expect(container.scalar(:string)).to be(bar_type)
-
1
expect(container.composite(:array, %i[foo])).to be(oof_type)
-
end
-
-
1
it "can override the default type map" do
-
1
container = described_class.new(
-
defaults: {
-
2
scalars: { foo: -> { foo_type } },
-
1
composites: { bar: ->(_types) { bar_type } },
-
}
-
)
-
-
1
expect(container.scalars).to contain_exactly(:foo)
-
1
expect(container.scalar(:foo)).to be(foo_type)
-
1
expect(container.composites).to contain_exactly(:bar)
-
1
expect(container.composite(:bar, %i[foo])).to be(bar_type)
-
end
-
end
-
-
1
context "when a scalar definition doesn't exist" do
-
1
it "raises an error when used as a scalar" do
-
2
expect { subject.scalar(:foo) }.to raise_error(
-
Tabulard::Errors::TypeError,
-
"Invalid scalar type: :foo"
-
)
-
end
-
-
1
it "raises an error when used in a composite" do
-
2
expect { subject.composite(:array, [:foo]) }.to raise_error(
-
Tabulard::Errors::TypeError,
-
"Invalid scalar type: :foo"
-
)
-
end
-
end
-
-
1
context "when a composite definition doesn't exist" do
-
1
it "raises an error" do
-
2
expect { subject.composite(:foo, []) }.to raise_error(
-
Tabulard::Errors::TypeError,
-
"Invalid composite type: :foo"
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/scalars/boolsy_cast"
-
1
require "tabulard/messaging"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Tabulard::Types::Scalars::BoolsyCast do
-
1
it_behaves_like "cast_class"
-
-
1
describe "#initialize" do
-
1
it "setups blank boolsy values by default" do
-
1
expect(described_class.new).to eq(
-
described_class.new(truthy: [], falsy: [])
-
)
-
end
-
end
-
-
1
describe "#call" do
-
4
subject(:cast) { described_class.new(truthy: truthy, falsy: falsy) }
-
-
4
let(:value) { instance_double(Object, inspect: double) }
-
4
let(:messenger) { instance_double(Tabulard::Messaging::Messenger) }
-
-
1
let(:truthy) do
-
3
instance_double(Array)
-
end
-
-
1
let(:falsy) do
-
3
instance_double(Array)
-
end
-
-
1
def stub_inclusion(set, value, bool)
-
6
allow(set).to receive(:include?).with(value).and_return(bool)
-
end
-
-
1
def expect_truthy(value = self.value)
-
1
expect(cast.call(value, messenger)).to be(true)
-
end
-
-
1
def expect_falsy(value = self.value)
-
1
expect(cast.call(value, messenger)).to be(false)
-
end
-
-
1
def expect_failure(value = self.value)
-
1
expect do
-
1
cast.call(value, messenger)
-
end.to throw_symbol(
-
:failure,
-
Tabulard::Messaging::Messages::MustBeBoolsy.new(code_data: { value: value.inspect })
-
)
-
end
-
-
1
context "when the value is truthy" do
-
1
before do
-
1
stub_inclusion(truthy, value, true)
-
1
stub_inclusion(falsy, value, false)
-
end
-
-
1
it "returns true" do
-
1
expect_truthy
-
end
-
end
-
-
1
context "when the value is falsy" do
-
1
before do
-
1
stub_inclusion(truthy, value, false)
-
1
stub_inclusion(falsy, value, true)
-
end
-
-
1
it "returns false" do
-
1
expect_falsy
-
end
-
end
-
-
1
context "when the value isn't truthy nor falsy" do
-
1
before do
-
1
stub_inclusion(truthy, value, false)
-
1
stub_inclusion(falsy, value, false)
-
end
-
-
1
it "fails with a message" do
-
1
expect_failure
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/scalars/boolsy"
-
1
require "support/shared/scalar_type"
-
-
1
RSpec.describe Tabulard::Types::Scalars::Boolsy do
-
1
include_examples "scalar_type"
-
-
1
it "inherits from the basic scalar type" do
-
1
expect(described_class.superclass).to be(Tabulard::Types::Scalars::Scalar)
-
end
-
-
1
describe "::cast_classes" do
-
1
it "extends the superclass' ones" do
-
1
expect(described_class.cast_classes).to eq(
-
described_class.superclass.cast_classes + [Tabulard::Types::Scalars::BoolsyCast]
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/scalars/date_string_cast"
-
1
require "tabulard/messaging"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Tabulard::Types::Scalars::DateStringCast do
-
1
let(:default_fmt) do
-
3
"%Y-%m-%d"
-
end
-
-
1
it_behaves_like "cast_class"
-
-
1
describe "#initialize" do
-
1
it "setups a default, conventional date format and accepts native dates" do
-
1
expect(described_class.new).to eq(
-
described_class.new(date_fmt: default_fmt, accept_date: true)
-
)
-
end
-
end
-
-
1
describe "#call" do
-
4
let(:value) { double }
-
7
let(:messenger) { instance_double(Tabulard::Messaging::Messenger) }
-
-
1
context "when value is a Date" do
-
1
subject(:cast) do
-
2
described_class.new(accept_date: accept_date)
-
end
-
-
1
before do
-
2
allow(Date).to receive(:===).with(value).and_return(true)
-
end
-
-
1
context "when accepting Date" do
-
2
let(:accept_date) { true }
-
-
1
it "returns the value" do
-
1
expect(cast.call(value, messenger)).to eq(value)
-
end
-
end
-
-
1
context "when not accepting Date" do
-
2
let(:accept_date) { false }
-
-
1
it "fails with an error" do
-
1
expect do
-
1
cast.call(value, messenger)
-
end.to throw_symbol(
-
:failure,
-
Tabulard::Messaging::Messages::MustBeDate.new(code_data: { format: default_fmt })
-
)
-
end
-
end
-
end
-
-
1
context "when value is a string" do
-
1
subject(:cast) do
-
3
described_class.new(date_fmt: date_fmt)
-
end
-
-
4
let(:date_fmt) { "%d/%m/%Y" }
-
-
1
context "when it fits the format" do
-
1
let(:value) do
-
1
"07/03/2020"
-
end
-
-
1
it "returns a Date" do
-
1
expect(cast.call(value, messenger)).to eq(Date.new(2020, 3, 7))
-
end
-
end
-
-
1
context "when it doesn't make sense" do
-
1
let(:value) do
-
1
"47/03/2020"
-
end
-
-
1
it "fails with an error" do
-
1
expect do
-
1
cast.call(value, messenger)
-
end.to throw_symbol(
-
:failure,
-
Tabulard::Messaging::Messages::MustBeDate.new(code_data: { format: date_fmt })
-
)
-
end
-
end
-
-
1
context "when it doesn't fit the format" do
-
1
let(:value) do
-
1
"2020-01-12"
-
end
-
-
1
it "fails with an error" do
-
1
expect do
-
1
cast.call(value, messenger)
-
end.to throw_symbol(
-
:failure,
-
Tabulard::Messaging::Messages::MustBeDate.new(code_data: { format: date_fmt })
-
)
-
end
-
end
-
end
-
-
1
context "when value is anything else" do
-
1
subject(:cast) do
-
1
described_class.new
-
end
-
-
1
it "fails with an error" do
-
1
expect do
-
1
cast.call(value, messenger)
-
end.to throw_symbol(
-
:failure,
-
Tabulard::Messaging::Messages::MustBeDate.new(code_data: { format: default_fmt })
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/scalars/date_string"
-
1
require "support/shared/scalar_type"
-
-
1
RSpec.describe Tabulard::Types::Scalars::DateString do
-
1
subject do
-
4
described_class.new(date_fmt: "foobar")
-
end
-
-
1
include_examples "scalar_type"
-
-
1
it "inherits from the basic scalar type" do
-
1
expect(described_class.superclass).to be(Tabulard::Types::Scalars::Scalar)
-
end
-
-
1
describe "::cast_classes" do
-
1
it "extends the superclass' ones" do
-
1
expect(described_class.cast_classes).to eq(
-
described_class.superclass.cast_classes + [Tabulard::Types::Scalars::DateStringCast]
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/scalars/email_cast"
-
1
require "tabulard/messaging"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Tabulard::Types::Scalars::EmailCast do
-
1
it_behaves_like "cast_class"
-
-
1
describe "#initialize" do
-
1
it "setups a default, conventional e-mail matcher" do
-
1
expect(described_class.new).to eq(
-
described_class.new(email_matcher: URI::MailTo::EMAIL_REGEXP)
-
)
-
end
-
end
-
-
1
describe "#call" do
-
3
subject(:cast) { described_class.new(email_matcher: email_matcher) }
-
-
1
let(:email_matcher) do
-
2
instance_double(Regexp)
-
end
-
-
3
let(:value) { instance_double(Object, inspect: double) }
-
3
let(:messenger) { instance_double(Tabulard::Messaging::Messenger) }
-
-
1
before do
-
2
allow(email_matcher).to receive(:match?).with(value).and_return(value_is_email)
-
end
-
-
1
context "when the value is an email address" do
-
2
let(:value_is_email) { true }
-
-
1
it "returns the value" do
-
1
expect(cast.call(value, messenger)).to eq(value)
-
end
-
end
-
-
1
context "when the value isn't an email address" do
-
2
let(:value_is_email) { false }
-
-
1
it "adds an error message and throws :failure" do
-
1
expect do
-
1
cast.call(value, messenger)
-
end.to throw_symbol(
-
:failure,
-
Tabulard::Messaging::Messages::MustBeEmail.new(code_data: { value: value.inspect })
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/scalars/email"
-
1
require "support/shared/scalar_type"
-
-
1
RSpec.describe Tabulard::Types::Scalars::Email do
-
1
include_examples "scalar_type"
-
-
1
it "inherits from the scalar string type" do
-
1
expect(described_class.superclass).to be(Tabulard::Types::Scalars::String)
-
end
-
-
1
describe "::cast_classes" do
-
1
it "extends the superclass' ones" do
-
1
expect(described_class.cast_classes).to eq(
-
described_class.superclass.cast_classes + [Tabulard::Types::Scalars::EmailCast]
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/scalars/scalar_cast"
-
1
require "tabulard/messaging"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Tabulard::Types::Scalars::ScalarCast do
-
1
it_behaves_like "cast_class"
-
-
1
describe "#call" do
-
2
subject(:cast) { described_class.new }
-
-
1
let(:messenger) do
-
7
instance_double(Tabulard::Messaging::Messenger)
-
end
-
-
1
context "when given nil" do
-
1
context "when nullable" do
-
2
subject(:cast) { described_class.new(nullable: true) }
-
-
1
it "halts with a success and nil as value" do
-
1
expect do
-
1
cast.call(nil, messenger)
-
end.to throw_symbol(:success, nil)
-
end
-
end
-
-
1
context "when non-nullable" do
-
2
subject(:cast) { described_class.new(nullable: false) }
-
-
1
it "halts with a failure and an appropriate error code" do
-
1
expect do
-
1
cast.call(nil, messenger)
-
end.to throw_symbol(
-
:failure, Tabulard::Messaging::Messages::MustExist.new
-
)
-
end
-
end
-
end
-
-
1
context "when given a String" do
-
3
let(:string_with_garbage) { " string_foo " }
-
4
let(:string_without_garbage) { "string_foo" }
-
-
1
before do
-
4
allow(messenger).to receive(:warn)
-
end
-
-
1
context "when cleaning strings" do
-
1
subject(:cast) do
-
2
described_class.new(clean_string: true)
-
end
-
-
1
context "when string contains garbage" do
-
1
it "removes garbage around the value and warns about it" do
-
1
value = cast.call(string_with_garbage, messenger)
-
-
1
expect(value).to eq(string_without_garbage)
-
1
expect(messenger).to have_received(:warn)
-
.with(Tabulard::Messaging::Messages::CleanedString.new)
-
end
-
end
-
-
1
context "when string doesn't contain garbage" do
-
1
it "returns the string as is" do
-
1
value = cast.call(string_without_garbage, messenger)
-
-
1
expect(value).to eq(string_without_garbage)
-
1
expect(messenger).not_to have_received(:warn)
-
end
-
end
-
end
-
-
1
context "when not cleaning strings" do
-
1
subject(:cast) do
-
2
described_class.new(clean_string: false)
-
end
-
-
1
context "when string contains garbage" do
-
1
it "returns the string as is" do
-
1
value = cast.call(string_with_garbage, messenger)
-
-
1
expect(value).to eq(string_with_garbage)
-
1
expect(messenger).not_to have_received(:warn)
-
end
-
end
-
-
1
context "when string doesn't contain garbage" do
-
1
it "returns the string as is" do
-
1
value = cast.call(string_without_garbage, messenger)
-
-
1
expect(value).to eq(string_without_garbage)
-
1
expect(messenger).not_to have_received(:warn)
-
end
-
end
-
end
-
end
-
-
1
context "when given something else" do
-
1
it "returns the string as is" do
-
1
something_else = double
-
-
1
value = cast.call(something_else, messenger)
-
-
1
expect(value).to eq(something_else)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/scalars/scalar"
-
1
require "support/shared/scalar_type"
-
-
1
RSpec.describe Tabulard::Types::Scalars::Scalar do
-
1
include_examples "scalar_type"
-
-
1
it "inherits from the basic type" do
-
1
expect(described_class.superclass).to be(Tabulard::Types::Type)
-
end
-
-
1
describe "::cast_classes" do
-
1
it "includes a basic cast class" do
-
1
expect(described_class.cast_classes).to eq(
-
[Tabulard::Types::Scalars::ScalarCast]
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/scalars/string"
-
1
require "support/shared/scalar_type"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Tabulard::Types::Scalars::String do
-
1
include_examples "scalar_type"
-
-
1
it "inherits from the basic scalar type" do
-
1
expect(described_class.superclass).to be(Tabulard::Types::Scalars::Scalar)
-
end
-
-
1
describe "custom cast class" do
-
1
subject(:cast_class) do
-
5
described_class.cast_classes.last
-
end
-
-
1
it "is appended to the superclass' cast classes" do
-
1
expect(
-
described_class.superclass.cast_classes + [cast_class]
-
).to eq(described_class.cast_classes)
-
end
-
-
1
include_examples "cast_class"
-
-
1
describe "#call" do
-
1
before do
-
2
allow(value).to receive(:is_a?).with(String).and_return(value_is_string)
-
end
-
-
1
context "when the value is a string" do
-
2
let(:value_is_string) { true }
-
2
let(:native_string) { double }
-
-
1
before do
-
1
allow(value).to receive(:to_s).and_return(native_string)
-
end
-
-
1
it "is a success, returning a native string" do
-
1
expect(cast.call(value, messenger)).to eq(native_string)
-
end
-
end
-
-
1
context "when the value is not a string" do
-
2
let(:value_is_string) { false }
-
-
1
it "is a failure" do
-
1
expect do
-
1
cast.call(value, messenger)
-
end.to throw_symbol(
-
:failure, Tabulard::Messaging::Messages::MustBeString.new
-
)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/types/type"
-
-
1
RSpec.describe Tabulard::Types::Type do
-
1
describe "class API" do
-
15
let(:klass0) { Class.new(described_class) }
-
9
let(:klass1) { Class.new(klass0) }
-
6
let(:klass2) { Class.new(klass1) }
-
-
1
describe "::all" do
-
1
it "returns an enumerator for self and known descendant types" do
-
1
enum = klass0.all
-
1
expect(enum).to be_a(Enumerator) & contain_exactly(klass0)
-
-
1
klass1
-
1
klass2
-
1
expect(klass0.all.to_a).to contain_exactly(klass0, klass1, klass2)
-
1
expect(klass1.all.to_a).to contain_exactly(klass1, klass2)
-
end
-
end
-
-
1
describe "::cast_classes" do
-
1
it "is an empty array" do
-
1
expect(described_class.cast_classes).to eq([])
-
end
-
-
1
it "is inheritable" do
-
1
expect([klass0, klass1, klass2]).to all(
-
have_attributes(cast_classes: described_class.cast_classes)
-
)
-
end
-
end
-
-
1
describe "::cast_classes=" do
-
5
let(:cast_classes0) { [double, double] }
-
3
let(:cast_classes1) { [double, double] }
-
-
1
it "mutates the class instance" do
-
1
klass0.cast_classes = cast_classes0
-
1
expect(klass0).to have_attributes(cast_classes: cast_classes0)
-
end
-
-
1
it "applies to the inherited children" do
-
1
klass0.cast_classes = cast_classes0
-
1
expect([klass0, klass1, klass2].map(&:cast_classes)).to eq(
-
[cast_classes0, cast_classes0, cast_classes0]
-
)
-
end
-
-
1
it "doesn't apply to the children mutated afterwards" do
-
1
klass0.cast_classes = cast_classes0
-
1
klass1.cast_classes = cast_classes1
-
1
expect([klass0, klass1, klass2].map(&:cast_classes)).to eq(
-
[cast_classes0, cast_classes1, cast_classes1]
-
)
-
end
-
-
1
it "doesn't apply to the children mutated beforehand" do
-
1
klass1.cast_classes = cast_classes1
-
1
klass0.cast_classes = cast_classes0
-
1
expect([klass0, klass1, klass2].map(&:cast_classes)).to eq(
-
[cast_classes0, cast_classes1, cast_classes1]
-
)
-
end
-
end
-
-
1
describe "::freeze" do
-
1
it "freezes the instance and its cast classes" do
-
1
klass1.freeze
-
1
expect([klass1, klass1.cast_classes]).to all(be_frozen)
-
end
-
-
1
it "doesn't freeze a warm superclass nor its cast classes" do
-
1
klass1.freeze
-
1
expect([klass0, klass0.cast_classes]).not_to include(be_frozen)
-
end
-
-
1
it "doesn't freeze a warm subclass nor its *own* cast classes" do
-
1
klass0.freeze
-
1
expect(klass1).not_to be_frozen
-
1
expect(klass1.cast_classes).to be_frozen
-
-
1
klass1.cast_classes += [double]
-
1
expect(klass1.cast_classes).not_to be_frozen
-
end
-
end
-
-
1
describe "::cast" do
-
6
let(:cast_class0) { double }
-
2
let(:cast_class1) { double }
-
-
1
before do
-
5
klass0.cast_classes = [cast_class0]
-
5
klass0.freeze
-
end
-
-
1
context "when given a class" do
-
1
it "creates a new type appending the given cast" do
-
1
type_class = klass0.cast(cast_class1)
-
-
1
expect(type_class).to have_attributes(
-
class: Class,
-
superclass: klass0,
-
cast_classes: [cast_class0, cast_class1]
-
)
-
end
-
end
-
-
1
context "when given a block" do
-
3
let(:cast) { -> {} }
-
3
let(:type_class) { klass0.cast(&cast) }
-
-
1
it "creates a new type appending the given cast" do
-
1
expect(type_class).to have_attributes(
-
class: Class,
-
superclass: klass0,
-
cast_classes: [cast_class0, described_class::SimpleCast.new(cast)]
-
)
-
end
-
-
1
it "still behaves as a cast class" do
-
1
block_cast_class = type_class.cast_classes.last
-
-
1
expect(block_cast_class.new(foo: :bar)).to be(cast)
-
end
-
end
-
-
1
context "when given both a class and a block" do
-
1
it "raises an error" do
-
2
expect { described_class.cast(cast_class0) { double } }.to raise_error(
-
ArgumentError, "Expected either a Class or a block, got both"
-
)
-
end
-
end
-
-
1
context "when given neither a class nor a block" do
-
1
it "raises an error" do
-
2
expect { described_class.cast }.to raise_error(
-
ArgumentError, "Expected either a Class or a block, got none"
-
)
-
end
-
end
-
end
-
-
1
describe "::new!" do
-
1
it "initializes a new, frozen type" do
-
1
type = described_class.new!
-
1
expect(type).to be_a(described_class) & be_frozen
-
end
-
end
-
end
-
-
1
describe "instance API" do
-
1
describe "#initialize" do
-
1
let(:cast_class0) do
-
1
instance_double(Class)
-
end
-
-
1
let(:cast_class1) do
-
1
instance_double(Class)
-
end
-
-
1
let(:cast_block) do
-
1
-> {}
-
end
-
-
1
let(:type_class) do
-
1
klass = described_class.cast(&cast_block)
-
1
klass.cast_classes << cast_class0
-
1
klass.cast_classes << cast_class1
-
1
klass
-
end
-
-
1
def stub_new_casts(opts)
-
1
type_class.cast_classes.map do |cast_class|
-
3
allow(cast_class).to receive(:new).with(**opts).and_return(cast = double)
-
3
cast
-
end
-
end
-
-
1
it "builds a cast chain of all casts initialized with the opts" do
-
1
opts = { foo: :bar, hello: "world" }
-
-
1
casts = stub_new_casts(opts)
-
-
1
type = type_class.new(**opts)
-
-
1
expect(type.cast_chain).to(
-
be_a(Tabulard::Types::CastChain) &
-
have_attributes(casts: casts)
-
)
-
end
-
end
-
-
1
describe "#cast" do
-
1
it "delegates the task to the cast chain" do
-
1
type = described_class.new
-
1
value = double
-
1
messenger = double
-
1
result = double
-
-
1
expect(type.cast_chain).to receive(:call).with(value, messenger).and_return(result)
-
1
expect(type.cast(value, messenger)).to be(result)
-
end
-
end
-
-
1
describe "#freeze" do
-
1
it "freezes self and the cast chain" do
-
1
type = described_class.new
-
1
type.freeze
-
-
1
expect(type).to be_frozen
-
1
expect(type.cast_chain).to be_frozen
-
end
-
end
-
-
1
describe "abstract API" do
-
5
let(:type) { described_class.new }
-
-
1
def raise_abstract_method_error
-
4
raise_error(NoMethodError, /you must implement this method/i)
-
end
-
-
1
describe "#scalar?" do
-
1
it "is abstract" do
-
2
expect { type.scalar? }.to raise_abstract_method_error
-
end
-
end
-
-
1
describe "#composite?" do
-
1
it "is abstract" do
-
2
expect { type.composite? }.to raise_abstract_method_error
-
end
-
end
-
-
1
describe "#scalar" do
-
1
it "is abstract" do
-
2
expect { type.scalar(double, double, double) }.to raise_abstract_method_error
-
end
-
end
-
-
1
describe "#composite" do
-
1
it "is abstract" do
-
2
expect { type.composite(double, double) }.to raise_abstract_method_error
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/utils/cell_string_cleaner"
-
-
1
RSpec.describe Tabulard::Utils::CellStringCleaner do
-
2
subject(:cleaner) { described_class }
-
-
2
let(:spaces) { " \t\r\n" }
-
2
let(:nonprints) { "\x00\x1B" }
-
2
let(:garbage) { spaces + nonprints }
-
-
# NOTE: the line return and newline characters act as traps for single-line regexes
-
2
let(:string) { "foo#{spaces}\r\n#{nonprints}bar" }
-
-
1
it "removes spaces & non-printable characters around a string" do
-
1
expect(cleaner.call(garbage)).to eq("")
-
1
expect(cleaner.call(garbage + string)).to eq(string)
-
1
expect(cleaner.call(string + garbage)).to eq(string)
-
1
expect(cleaner.call(garbage + string + garbage)).to eq(string)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/utils/monadic_result"
-
-
1
RSpec.describe Tabulard::Utils::MonadicResult::Failure, monadic_result: true do
-
12
subject(:result) { described_class.new(value0) }
-
-
1
let(:value0) do
-
11
instance_double(Object, to_s: "value0_to_s", inspect: "value0_inspect")
-
end
-
-
1
let(:value1) do
-
1
instance_double(Object, to_s: "value1_to_s", inspect: "value1_inspect")
-
end
-
-
1
it "is a result" do
-
1
expect(result).to be_a(Tabulard::Utils::MonadicResult::Result)
-
end
-
-
1
describe "#initialize" do
-
1
it "is empty by default" do
-
1
expect(described_class.new).to be_empty
-
end
-
end
-
-
1
describe "#empty?" do
-
1
it "is true by default of an explicitly wrapped value" do
-
1
expect(described_class.new).to be_empty
-
end
-
-
1
it "is false otherwise" do
-
1
expect(result).not_to be_empty
-
end
-
end
-
-
1
describe "#failure?" do
-
1
it "is true" do
-
1
expect(result).to be_failure
-
end
-
end
-
-
1
describe "#success?" do
-
1
it "is false" do
-
1
expect(result).not_to be_success
-
end
-
end
-
-
1
describe "#failure" do
-
1
it "can unwrap the value" do
-
1
expect(result).to have_attributes(failure: value0)
-
end
-
-
1
it "cannot unwrap a value when empty" do
-
1
empty_result = described_class.new
-
-
2
expect { empty_result.failure }.to raise_error(
-
described_class::ValueError, "There is no value within the result"
-
)
-
end
-
end
-
-
1
describe "#success" do
-
1
it "can't unwrap the value" do
-
2
expect { result.success }.to raise_error(
-
described_class::VariantError, "Not a Success"
-
)
-
end
-
end
-
-
1
describe "#==" do
-
1
it "is equivalent to a similar Failure" do
-
1
expect(result).to eq(Failure(value0))
-
end
-
-
1
it "is not equivalent to a different Failure" do
-
1
expect(result).not_to eq(Failure(value1))
-
end
-
-
1
it "is not equivalent to a similar Success" do
-
1
expect(result).not_to eq(Success(value0))
-
end
-
end
-
-
1
describe "#inspect" do
-
1
it "inspects the result" do
-
1
expect(result.inspect).to eq("Failure(value0_inspect)")
-
end
-
-
1
context "when empty" do
-
2
subject(:result) { described_class.new }
-
-
1
it "inspects nothing" do
-
1
expect(result.inspect).to eq("Failure()")
-
end
-
end
-
end
-
-
1
describe "#to_s" do
-
1
it "inspects the result" do
-
1
expect(result.method(:to_s)).to eq(result.method(:inspect))
-
end
-
end
-
-
1
describe "#unwrap" do
-
3
let(:do_token) { :MonadicResultDo }
-
-
1
context "when empty" do
-
1
it "returns nil" do
-
1
result = described_class.new
-
-
2
expect { result.unwrap }.to throw_symbol(do_token, result)
-
end
-
end
-
-
1
context "when non-empty" do
-
1
it "returns the wrapped value" do
-
1
result = described_class.new(double)
-
-
2
expect { result.unwrap }.to throw_symbol(do_token, result)
-
end
-
end
-
end
-
-
1
describe "#discard" do
-
1
it "returns the same variant, without a value" do
-
1
empty_result = described_class.new
-
1
filled_result = described_class.new(double)
-
-
1
expect(empty_result.discard).to eq(empty_result)
-
1
expect(filled_result.discard).to eq(empty_result)
-
end
-
end
-
-
1
describe "#bind" do
-
1
context "when empty" do
-
3
let(:result) { described_class.new }
-
-
1
it "doesn't yield" do
-
2
expect { |b| result.bind(&b) }.not_to yield_control
-
end
-
-
1
it "returns self" do
-
1
expect(result.bind { double }).to be(result)
-
end
-
end
-
-
1
context "when filled" do
-
3
let(:result) { described_class.new(double) }
-
-
1
it "doesn't yield" do
-
2
expect { |b| result.bind(&b) }.not_to yield_control
-
end
-
-
1
it "returns self" do
-
1
expect(result.bind { double }).to be(result)
-
end
-
end
-
end
-
-
1
describe "#or" do
-
1
context "when empty" do
-
3
let(:result) { described_class.new }
-
-
1
it "yields nil" do
-
2
expect { |b| result.or(&b) }.to yield_with_no_args
-
end
-
-
1
it "returns the block result" do
-
1
block_value = double
-
2
expect(result.or { block_value }).to eq(block_value)
-
end
-
end
-
-
1
context "when filled" do
-
3
let(:value) { double }
-
3
let(:result) { described_class.new(value) }
-
-
1
it "yields the value" do
-
2
expect { |b| result.or(&b) }.to yield_with_args(value)
-
end
-
-
1
it "returns the block result" do
-
1
block_value = double
-
2
expect(result.or { block_value }).to eq(block_value)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/utils/monadic_result"
-
-
1
RSpec.describe Tabulard::Utils::MonadicResult::Success, monadic_result: true do
-
12
subject(:result) { described_class.new(value0) }
-
-
1
let(:value0) do
-
11
instance_double(Object, to_s: "value0_to_s", inspect: "value0_inspect")
-
end
-
-
1
let(:value1) do
-
1
instance_double(Object, to_s: "value1_to_s", inspect: "value1_inspect")
-
end
-
-
1
it "is a result" do
-
1
expect(result).to be_a(Tabulard::Utils::MonadicResult::Result)
-
end
-
-
1
describe "#initialize" do
-
1
it "is empty by default" do
-
1
expect(described_class.new).to be_empty
-
end
-
end
-
-
1
describe "#empty?" do
-
1
it "is true by default of an explicitly wrapped value" do
-
1
expect(described_class.new).to be_empty
-
end
-
-
1
it "is false otherwise" do
-
1
expect(result).not_to be_empty
-
end
-
end
-
-
1
describe "#success?" do
-
1
it "is true" do
-
1
expect(result).to be_success
-
end
-
end
-
-
1
describe "#failure?" do
-
1
it "is false" do
-
1
expect(result).not_to be_failure
-
end
-
end
-
-
1
describe "#success" do
-
1
it "can unwrap the value" do
-
1
expect(result).to have_attributes(success: value0)
-
end
-
-
1
it "cannot unwrap a value when empty" do
-
1
empty_result = described_class.new
-
-
2
expect { empty_result.success }.to raise_error(
-
described_class::ValueError, "There is no value within the result"
-
)
-
end
-
end
-
-
1
describe "#failure" do
-
1
it "can't unwrap the value" do
-
2
expect { result.failure }.to raise_error(
-
described_class::VariantError, "Not a Failure"
-
)
-
end
-
end
-
-
1
describe "#==" do
-
1
it "is equivalent to a similar Success" do
-
1
expect(result).to eq(Success(value0))
-
end
-
-
1
it "is not equivalent to a different Success" do
-
1
expect(result).not_to eq(Success(value1))
-
end
-
-
1
it "is not equivalent to a similar Failure" do
-
1
expect(result).not_to eq(Failure(value0))
-
end
-
end
-
-
1
describe "#inspect" do
-
1
it "inspects the result" do
-
1
expect(result.inspect).to eq("Success(value0_inspect)")
-
end
-
-
1
context "when empty" do
-
2
subject(:result) { described_class.new }
-
-
1
it "inspects nothing" do
-
1
expect(result.inspect).to eq("Success()")
-
end
-
end
-
end
-
-
1
describe "#to_s" do
-
1
it "inspects the result" do
-
1
expect(result.method(:to_s)).to eq(result.method(:inspect))
-
end
-
end
-
-
1
describe "#unwrap" do
-
1
context "when empty" do
-
1
it "returns nil" do
-
1
result = described_class.new
-
-
1
expect(result.unwrap).to be_nil
-
end
-
end
-
-
1
context "when non-empty" do
-
1
it "returns the wrapped value" do
-
1
result = described_class.new(wrapped = double)
-
-
1
expect(result.unwrap).to be(wrapped)
-
end
-
end
-
end
-
-
1
describe "#discard" do
-
1
it "returns the same variant, without a value" do
-
1
empty_result = described_class.new
-
1
filled_result = described_class.new(double)
-
-
1
expect(empty_result.discard).to eq(empty_result)
-
1
expect(filled_result.discard).to eq(empty_result)
-
end
-
end
-
-
1
describe "#bind" do
-
1
context "when empty" do
-
3
let(:result) { described_class.new }
-
-
1
it "yields nil" do
-
2
expect { |b| result.bind(&b) }.to yield_with_no_args
-
end
-
-
1
it "returns the block result" do
-
1
block_value = double
-
2
expect(result.bind { block_value }).to eq(block_value)
-
end
-
end
-
-
1
context "when filled" do
-
3
let(:value) { double }
-
3
let(:result) { described_class.new(value) }
-
-
1
it "yields the value" do
-
2
expect { |b| result.bind(&b) }.to yield_with_args(value)
-
end
-
-
1
it "returns the block result" do
-
1
block_value = double
-
2
expect(result.bind { block_value }).to eq(block_value)
-
end
-
end
-
end
-
-
1
describe "#or" do
-
1
context "when empty" do
-
3
let(:result) { described_class.new }
-
-
1
it "doesn't yield" do
-
2
expect { |b| result.or(&b) }.not_to yield_control
-
end
-
-
1
it "returns self" do
-
1
expect(result.or { double }).to be(result)
-
end
-
end
-
-
1
context "when filled" do
-
3
let(:result) { described_class.new(double) }
-
-
1
it "doesn't yield" do
-
2
expect { |b| result.or(&b) }.not_to yield_control
-
end
-
-
1
it "returns self" do
-
1
expect(result.or { double }).to be(result)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/utils/monadic_result"
-
-
1
RSpec.describe Tabulard::Utils::MonadicResult::Unit do
-
4
subject(:unit) { described_class }
-
-
2
it { is_expected.to be_frozen }
-
-
1
it "can be stringified" do
-
1
expect(unit.to_s).to eq("Unit")
-
end
-
-
1
it "can be inspected" do
-
1
expect(unit.inspect).to eq("Unit")
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard/utils/monadic_result"
-
-
1
RSpec.describe Tabulard::Utils::MonadicResult do
-
1
let(:klass) do
-
20
Class.new.tap { |c| c.include(described_class) }
-
end
-
-
10
let(:builder) { klass.new }
-
-
3
let(:value) { double }
-
-
1
it "includes some constants" do
-
1
expect(klass.constants - Object.constants).to contain_exactly(
-
:Unit, :Result, :Success, :Failure
-
)
-
-
1
expect(klass::Unit).to be(described_class::Unit)
-
1
expect(klass::Result).to be(described_class::Result)
-
1
expect(klass::Success).to be(described_class::Success)
-
1
expect(klass::Failure).to be(described_class::Failure)
-
end
-
-
1
it "includes three builder methods" do
-
1
expect(builder.methods - Object.methods).to contain_exactly(
-
:Success, :Failure, :Do
-
)
-
end
-
-
1
describe "#Success" do
-
1
it "may wrap no value in a Success instance" do
-
1
expect(builder.Success()).to eq(described_class::Success.new)
-
end
-
-
1
it "may wrap a value in a Success instance" do
-
1
expect(builder.Success(value)).to eq(described_class::Success.new(value))
-
end
-
end
-
-
1
describe "#Failure" do
-
1
it "may wrap no value in a Failure instance" do
-
1
expect(builder.Failure()).to eq(described_class::Failure.new)
-
end
-
-
1
it "may wrap a value in a Failure instance" do
-
1
expect(builder.Failure(value)).to eq(described_class::Failure.new(value))
-
end
-
end
-
-
1
describe "#Do" do
-
4
let(:v1) { double }
-
4
let(:v2) { double }
-
3
let(:v3) { double }
-
1
let(:v4) { double }
-
1
let(:v5) { double }
-
-
1
it "returns the last expression of the block" do
-
1
result = builder.Do do
-
1
v1
-
1
v2
-
1
v3
-
end
-
-
1
expect(result).to be(v3)
-
end
-
-
1
it "continues the sequence when unwrapping a Success" do
-
1
v = nil
-
-
1
result = builder.Do do
-
1
v = builder.Success(v1).unwrap
-
1
v = builder.Success(v2).unwrap
-
1
v = builder.Success(v3).unwrap
-
end
-
-
1
expect(result).to be(v3)
-
1
expect(v).to be(v3)
-
end
-
-
1
it "aborts the sequence when unwrapping a Failure" do
-
1
v = nil
-
-
1
result = builder.Do do
-
1
v = builder.Success(v1).unwrap
-
1
v = builder.Failure(v2).unwrap
-
skipped
# :nocov:
-
skipped
v = builder.Success(v3).unwrap
-
skipped
# :nocov:
-
end
-
-
1
expect(result).to eq(builder.Failure(v2))
-
1
expect(v).to be(v1)
-
end
-
-
1
it "is compatible with ensure" do
-
1
ensured = false
-
-
1
builder.Do do
-
1
builder.Failure().unwrap
-
ensure
-
1
ensured = true
-
end
-
-
1
expect(ensured).to be(true)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "tabulard"
-
-
1
RSpec.describe Tabulard, monadic_result: true do
-
1
let(:types) do
-
24
reverse_string = Tabulard::Types::Scalars::String.cast { |v, _m| v.reverse }
-
-
9
Tabulard::Types::Container.new(
-
scalars: {
-
reverse_string: reverse_string.method(:new),
-
}
-
)
-
end
-
-
1
let(:template_opts) do
-
{
-
9
attributes: [
-
{
-
key: :foo,
-
type: :reverse_string,
-
},
-
{
-
key: "bar",
-
type: {
-
composite: :array,
-
scalars: %i[
-
string?
-
scalar?
-
email?
-
scalar?
-
scalar
-
],
-
},
-
},
-
],
-
}
-
end
-
-
1
let(:template) do
-
9
Tabulard::Template.build(**template_opts)
-
end
-
-
1
let(:template_config) do
-
9
Tabulard::TemplateConfig.new(types: types)
-
end
-
-
1
let(:specification) do
-
9
template.apply(template_config)
-
end
-
-
1
let(:processor) do
-
9
Tabulard::TableProcessor.new(specification)
-
end
-
-
1
let(:input) do
-
[
-
9
["foo", "bar 3", "bar 5", "bar 1"],
-
["hello", "foo@bar.baz", Float, nil],
-
["world", "foo@bar.baz", Float, nil],
-
["world", "boudiou !", Float, nil],
-
]
-
end
-
-
1
def process(*args, **opts, &block)
-
5
processor.call(*args, adapter: Tabulard::Adapters::Bare, **opts, &block)
-
end
-
-
1
def process_to_a(*args, **opts)
-
4
a = []
-
16
processor.call(*args, adapter: Tabulard::Adapters::Bare, **opts) { |result| a << result }
-
4
a
-
end
-
-
1
context "when there is no table error" do
-
1
it "is a success without errors" do
-
1
result = process(input) {}
-
-
1
expect(result).to have_attributes(result: Success(), messages: [])
-
end
-
-
1
it "yields a commented result for each valid and invalid row" do
-
1
results = process_to_a(input)
-
-
1
expect(results).to have_attributes(size: 3)
-
1
expect(results[0]).to have_attributes(result: be_success, messages: be_empty)
-
1
expect(results[1]).to have_attributes(result: be_success, messages: be_empty)
-
1
expect(results[2]).to have_attributes(result: be_failure, messages: have_attributes(size: 1))
-
end
-
-
1
it "yields the successful value for each valid row" do
-
1
results = process_to_a(input)
-
-
1
expect(results[0].result).to eq(
-
Success(foo: "olleh", "bar" => [nil, nil, "foo@bar.baz", nil, Float])
-
)
-
-
1
expect(results[1].result).to eq(
-
Success(foo: "dlrow", "bar" => [nil, nil, "foo@bar.baz", nil, Float])
-
)
-
end
-
-
1
it "yields the failure data for each invalid row" do
-
1
results = process_to_a(input)
-
-
1
expect(results[2].result).to eq(Failure())
-
1
expect(results[2].messages).to contain_exactly(
-
have_attributes(
-
code: "must_be_email",
-
code_data: { value: "boudiou !".inspect },
-
scope: Tabulard::Messaging::SCOPES::CELL,
-
scope_data: { row: 4, col: "B" },
-
severity: Tabulard::Messaging::SEVERITIES::ERROR
-
)
-
)
-
end
-
end
-
-
1
context "when there are unspecified columns in the table" do
-
1
before do
-
3
input.each_index do |idx|
-
12
input[idx] = input[idx][0..1] + ["oof"] + input[idx][2..] + ["rab"]
-
end
-
end
-
-
1
context "when the template allows it" do
-
2
before { template_opts[:ignore_unspecified_columns] = true }
-
-
1
it "ignores the unspecified columns" do
-
1
results = process_to_a(input)
-
-
1
expect(results[0].result).to eq(
-
Success(foo: "olleh", "bar" => [nil, nil, "foo@bar.baz", nil, Float])
-
)
-
-
1
expect(results[1].result).to eq(
-
Success(foo: "dlrow", "bar" => [nil, nil, "foo@bar.baz", nil, Float])
-
)
-
end
-
end
-
-
1
context "when the template doesn't allow it" do
-
3
before { template_opts[:ignore_unspecified_columns] = false }
-
-
1
it "doesn't yield any row" do
-
2
expect { |b| process(input, &b) }.not_to yield_control
-
end
-
-
1
it "returns a failure with data" do # rubocop:disable RSpec/ExampleLength
-
1
expect(process(input) {}).to have_attributes(
-
result: Failure(),
-
messages: contain_exactly(
-
have_attributes(
-
code: "invalid_header",
-
code_data: { value: "oof" },
-
scope: Tabulard::Messaging::SCOPES::COL,
-
scope_data: { col: "C" },
-
severity: Tabulard::Messaging::SEVERITIES::ERROR
-
),
-
have_attributes(
-
code: "invalid_header",
-
code_data: { value: "rab" },
-
scope: Tabulard::Messaging::SCOPES::COL,
-
scope_data: { col: "F" },
-
severity: Tabulard::Messaging::SEVERITIES::ERROR
-
)
-
)
-
)
-
end
-
end
-
end
-
-
1
context "when there are missing columns" do
-
1
before do
-
2
input.each do |input|
-
8
input.delete_at(2)
-
8
input.delete_at(0)
-
end
-
end
-
-
1
it "doesn't yield any row" do
-
2
expect { |b| process(input, &b) }.not_to yield_control
-
end
-
-
1
it "returns a failure with data" do # rubocop:disable RSpec/ExampleLength
-
1
expect(process(input) {}).to have_attributes(
-
result: Failure(),
-
messages: contain_exactly(
-
have_attributes(
-
code: "missing_column",
-
code_data: { value: "Foo" },
-
scope: Tabulard::Messaging::SCOPES::TABLE,
-
scope_data: nil,
-
severity: Tabulard::Messaging::SEVERITIES::ERROR
-
),
-
have_attributes(
-
code: "missing_column",
-
code_data: { value: "Bar 5" },
-
scope: Tabulard::Messaging::SCOPES::TABLE,
-
scope_data: nil,
-
severity: Tabulard::Messaging::SEVERITIES::ERROR
-
)
-
)
-
)
-
end
-
end
-
end