-
# frozen_string_literal: true
-
-
# {Sheetah} is a library designed to process tabular data according to a
-
# {Sheetah::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 backends
-
# ({Sheetah::Backends::Xlsx}, {Sheetah::Backends::Csv}, etc...).
-
#
-
# Given a tabular document and a specification of the document structure,
-
# Sheetah 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 sheet/row/col/cell level)
-
# - all of the above done so that internationalization of messages is easy
-
#
-
# Sheetah 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 backend). 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 Sheetah
-
end
-
-
1
require "sheetah/template"
-
1
require "sheetah/template_config"
-
1
require "sheetah/sheet_processor"
-
1
require "sheetah/backends/wrapper"
-
# frozen_string_literal: true
-
-
1
require_relative "column"
-
-
1
module Sheetah
-
1
class Attribute
-
1
def initialize(key:, type:)
-
18
@key = key
-
-
@type =
-
18
case type
-
when: 9
when Hash
-
9
CompositeType.new(**type)
-
when: 0
when Array
-
CompositeType.new(composite: :array, scalars: type)
-
else: 9
else
-
9
ScalarType.new(type)
-
end
-
-
18
freeze
-
end
-
-
1
attr_reader :key, :type
-
-
1
def each_column(config)
-
18
else: 18
then: 0
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
-
-
1
class Scalar
-
1
def initialize(name)
-
54
@required = name.end_with?("!")
-
54
then: 18
else: 36
@name = (@required ? name.slice(0..-2) : name).to_sym
-
end
-
-
1
attr_reader :name, :required
-
end
-
-
1
class ScalarType
-
1
def initialize(scalar)
-
9
@scalar = Scalar.new(scalar)
-
9
freeze
-
end
-
-
1
def compile(container)
-
9
container.scalar(@scalar.name)
-
end
-
-
1
def each_column
-
9
else: 9
then: 0
return enum_for(:each_column) { 1 } unless block_given?
-
-
9
yield nil, @scalar.required
-
-
9
self
-
end
-
end
-
-
1
class CompositeType
-
1
def initialize(composite:, scalars:)
-
9
@composite = composite
-
54
@scalars = scalars.map { |scalar| Scalar.new(scalar) }.freeze
-
9
freeze
-
end
-
-
1
def compile(container)
-
9
container.composite(@composite, @scalars.map(&:name))
-
end
-
-
1
def each_column
-
9
else: 9
then: 0
return enum_for(:each_column) { @scalars.size } unless block_given?
-
-
9
@scalars.each_with_index do |scalar, index|
-
45
yield index, scalar.required
-
end
-
-
9
self
-
end
-
end
-
-
1
private_constant :Scalar, :ScalarType, :CompositeType
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "backends_registry"
-
1
require_relative "utils/monadic_result"
-
-
1
module Sheetah
-
1
module Backends
-
1
@registry = BackendsRegistry.new
-
-
1
SimpleError = Struct.new(:msg_code)
-
1
private_constant :SimpleError
-
-
1
class << self
-
1
attr_reader :registry
-
-
1
def open(*args, **opts, &block)
-
16
backend = opts.delete(:backend) || registry.get(*args, **opts)
-
-
16
then: 1
else: 15
if backend.nil?
-
1
return Utils::MonadicResult::Failure.new(SimpleError.new("no_applicable_backend"))
-
end
-
-
15
backend.open(*args, **opts, &block)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "csv"
-
-
1
require_relative "../sheet"
-
1
require_relative "../backends"
-
-
1
module Sheetah
-
1
module Backends
-
# Expect:
-
# - UTF-8 without BOM, or the correct encoding given explicitly
-
# - line endings as \n or \r\n
-
# - comma-separated
-
# - quoted with "
-
1
class Csv
-
1
include Sheet
-
-
1
class ArgumentError < Error
-
end
-
-
1
class EncodingError < Error
-
end
-
-
1
CSV_OPTS = {
-
col_sep: ",",
-
quote_char: '"',
-
}.freeze
-
-
1
private_constant :CSV_OPTS
-
-
1
def self.register(registry = Backends.registry)
-
4
registry.set(self) do |args, opts|
-
8
else: 7
then: 1
next false unless args.empty?
-
-
case opts
-
7
in { io: _, **nil } | \
-
{ io: _, encoding: String | Encoding, **nil } | \
-
{ path: /\.csv$/i, **nil } | \
-
{ path: /\.csv$/i, encoding: String | Encoding, **nil }
-
in: 6
then
-
6
true
-
else: 1
else
-
1
false
-
end
-
end
-
end
-
-
1
def initialize(io: nil, path: nil, encoding: nil)
-
22
io = setup_io(io, path, encoding)
-
-
19
@csv = CSV.new(io, **CSV_OPTS)
-
19
@headers = detect_headers(@csv)
-
17
@cols_count = @headers.size
-
end
-
-
1
def each_header
-
15
else: 9
then: 5
return to_enum(:each_header) { @cols_count } unless block_given?
-
-
9
@headers.each_with_index do |header, col_idx|
-
48
col = Sheet.int2col(col_idx + 1)
-
-
48
yield Header.new(col: col, value: header)
-
end
-
-
9
self
-
end
-
-
1
def each_row
-
6
else: 5
then: 1
return to_enum(:each_row) unless block_given?
-
-
5
@csv.each.with_index(1) do |raw, row|
-
9
value = Array.new(@cols_count) do |col_idx|
-
36
col = Sheet.int2col(col_idx + 1)
-
-
36
Cell.new(row: row, col: col, value: raw[col_idx])
-
end
-
-
9
yield Row.new(row: row, value: value)
-
end
-
-
5
self
-
end
-
-
1
def close
-
2
@csv.close
-
-
nil
-
end
-
-
1
private
-
-
1
def setup_io(io, path, encoding)
-
22
then: 3
if io.nil? && !path.nil?
-
3
else: 19
setup_io_from_path(path, encoding)
-
19
then: 16
elsif !io.nil? && path.nil?
-
16
setup_io_from_io(io, encoding)
-
else: 3
else
-
3
raise ArgumentError, "Expected either IO or path"
-
end
-
end
-
-
1
def setup_io_from_io(io, encoding)
-
16
then: 1
else: 15
io.set_encoding(encoding, Encoding::UTF_8) if encoding
-
16
io
-
end
-
-
1
def setup_io_from_path(path, encoding)
-
3
opts = { mode: "r" }
-
-
3
then: 1
else: 2
if encoding
-
1
opts[:external_encoding] = encoding
-
1
opts[:internal_encoding] = Encoding::UTF_8
-
end
-
-
3
File.new(path, **opts)
-
end
-
-
1
def detect_headers(csv)
-
headers =
-
begin
-
19
csv.shift
-
rescue CSV::MalformedCSVError
-
2
raise EncodingError
-
end
-
-
17
headers || []
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../sheet"
-
-
1
module Sheetah
-
1
module Backends
-
1
class Wrapper
-
1
include Sheet
-
-
1
def initialize(table)
-
21
then: 1
else: 20
raise Error if table.nil?
-
-
20
@table = table
-
-
20
then: 18
if (table_size = @table.size).positive?
-
18
@headers = @table[0]
-
18
@rows_count = table_size - 1
-
18
@cols_count = @headers.size
-
else: 2
else
-
2
@headers = []
-
2
@rows_count = 0
-
2
@cols_count = 0
-
end
-
end
-
-
1
def each_header
-
16
else: 14
then: 1
return to_enum(:each_header) { @cols_count } unless block_given?
-
-
14
1.upto(@cols_count) do |col|
-
50
yield Header.new(col: Sheet.int2col(col), value: @headers[col - 1])
-
end
-
-
14
self
-
end
-
-
1
def each_row
-
11
else: 10
then: 1
return to_enum(:each_row) unless block_given?
-
-
10
1.upto(@rows_count) do |row|
-
24
raw = @table[row]
-
-
24
value = Array.new(@cols_count) do |col_idx|
-
102
Cell.new(row: row, col: Sheet.int2col(col_idx + 1), value: raw[col_idx])
-
end
-
-
24
yield Row.new(row: row, value: value)
-
end
-
-
10
self
-
end
-
-
1
def close
-
# nothing to do here
-
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 "../sheet"
-
1
require_relative "../backends"
-
-
1
module Sheetah
-
1
module Backends
-
1
class Xlsx
-
1
include Sheet
-
-
1
def self.register(registry = Backends.registry)
-
3
registry.set(self) do |args, opts|
-
3
else: 2
then: 1
next false unless args.empty?
-
-
case opts
-
2
in: 1
in { path: /\.xlsx$/i, **nil }
-
1
true
-
else: 1
else
-
1
false
-
end
-
end
-
end
-
-
1
def initialize(path:)
-
14
then: 1
else: 13
raise Error if path.nil?
-
-
13
@roo = Roo::Excelx.new(path)
-
13
@is_empty = worksheet.first_row.nil?
-
13
@headers = detect_headers
-
13
@cols_count = @headers.size
-
end
-
-
1
def each_header
-
10
else: 7
then: 2
return to_enum(:each_header) { @cols_count } unless block_given?
-
-
7
@headers.each_with_index do |header, col_idx|
-
30
col = Sheet.int2col(col_idx + 1)
-
-
30
yield Header.new(col: col, value: header)
-
end
-
-
7
self
-
end
-
-
1
def each_row
-
7
else: 6
then: 1
return to_enum(:each_row) unless block_given?
-
-
6
then: 1
else: 5
return if @is_empty
-
-
5
first_row = 2
-
5
last_row = worksheet.last_row
-
5
row = 0
-
-
5
first_row.upto(last_row) do |cursor|
-
13
raw = worksheet.row(cursor)
-
13
row += 1
-
-
13
value = Array.new(@cols_count) do |col_idx|
-
65
col = Sheet.int2col(col_idx + 1)
-
-
65
Cell.new(row: row, col: col, value: raw[col_idx])
-
end
-
-
13
yield Row.new(row: row, value: value)
-
end
-
-
5
self
-
end
-
-
1
def close
-
1
@roo.close
-
-
nil
-
end
-
-
1
private
-
-
1
def worksheet
-
42
@worksheet ||= @roo.sheet_for(@roo.default_sheet)
-
end
-
-
1
def detect_headers
-
13
then: 2
else: 11
return [] if @is_empty
-
-
11
worksheet.row(1) || []
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sheetah
-
1
class BackendsRegistry
-
1
def initialize
-
14
@registry = {}
-
end
-
-
1
def set(backend, &matcher)
-
22
@registry[backend] = matcher
-
21
self
-
end
-
-
1
def get(*args, **opts)
-
16
@registry.each do |backend, matcher|
-
19
then: 11
else: 8
return backend if matcher.call(args, opts)
-
end
-
-
nil
-
end
-
-
1
def freeze
-
3
@registry.freeze
-
3
super
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sheetah
-
1
class Column
-
1
def initialize(
-
key:,
-
type:,
-
index:,
-
header:,
-
header_pattern: nil,
-
required: false
-
)
-
61
@key = key
-
61
@type = type
-
61
@index = index
-
61
@header = header
-
61
@header_pattern = (header_pattern || header.dup).freeze
-
61
@required = required
-
-
61
freeze
-
end
-
-
1
attr_reader :key, :type, :index, :header, :header_pattern
-
-
1
def required?
-
54
@required
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sheetah
-
1
module Errors
-
1
class Error < StandardError
-
1
class << self
-
1
def inherited(klass)
-
24
super
-
-
24
then: 11
else: 13
klass.msg_code! if klass.detect_msg_code?
-
end
-
-
1
attr_reader :msg_code
-
-
1
def detect_msg_code?
-
41
name && /^[a-z0-9:]+$/i.match?(name)
-
end
-
-
1
def msg_code!(msg_code = build_msg_code)
-
19
@msg_code = msg_code
-
end
-
-
1
private
-
-
1
def build_msg_code
-
17
else: 15
then: 2
unless detect_msg_code?
-
2
raise ::TypeError, "Cannot build msg_code from anonymous exception: #{inspect}"
-
end
-
-
15
msg_code = name.dup
-
15
msg_code.gsub!("::", ".")
-
15
msg_code.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
-
15
msg_code.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
-
15
msg_code.downcase!
-
-
15
msg_code
-
end
-
end
-
-
1
msg_code!
-
-
1
def msg_code
-
2
self.class.msg_code
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "error"
-
-
1
module Sheetah
-
1
module Errors
-
1
class SpecError < Error
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "error"
-
-
1
module Sheetah
-
1
module Errors
-
1
class TypeError < Error
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "set"
-
-
1
module Sheetah
-
1
class Headers
-
1
include Utils::MonadicResult
-
-
1
class Header
-
1
def initialize(sheet_header, spec_column)
-
47
@header = sheet_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
@messenger.scope_col!(header.col) do
-
54
column = @specification.get(header.value)
-
-
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)
-
-
44
@headers << Header.new(header, column)
-
end
-
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("missing_column", 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("invalid_header", 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("duplicated_header", header.value)
-
-
2
false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sheetah
-
1
module Messaging
-
1
module SCOPES
-
1
SHEET = "SHEET"
-
1
ROW = "ROW"
-
1
COL = "COL"
-
1
CELL = "CELL"
-
end
-
-
1
module SEVERITIES
-
1
WARN = "WARN"
-
1
ERROR = "ERROR"
-
end
-
-
# TODO: list all possible message code in a systematic way,
-
# so that i18n do not miss any by mistake.
-
1
class Message
-
1
def initialize(
-
code:,
-
code_data: nil,
-
scope: nil,
-
scope_data: nil,
-
severity: nil
-
)
-
46
@code = code
-
46
@code_data = code_data || nil
-
46
@scope = scope || SCOPES::SHEET
-
46
@scope_data = scope_data || nil
-
46
@severity = severity || SEVERITIES::WARN
-
end
-
-
1
attr_reader(
-
:code,
-
:code_data,
-
:scope,
-
:scope_data,
-
:severity
-
)
-
-
1
def ==(other)
-
12
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
private
-
-
1
def scoping_to_s
-
6
when: 2
else: 1
case scope
-
2
when: 1
when SCOPES::SHEET 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
-
-
1
class Messenger
-
1
def initialize(
-
scope: SCOPES::SHEET,
-
scope_data: nil
-
)
-
83
@scope = scope.freeze
-
83
@scope_data = scope_data.freeze
-
83
@messages = []
-
end
-
-
1
attr_reader :scope, :scope_data, :messages
-
-
1
def ==(other)
-
2
other.is_a?(self.class) &&
-
scope == other.scope &&
-
scope_data == other.scope_data &&
-
messages == other.messages
-
end
-
-
1
def dup
-
23
self.class.new(
-
scope: @scope,
-
scope_data: @scope_data
-
)
-
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, &block)
-
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, &block)
-
end
-
-
1
def scope_col!(col, &block)
-
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, &block)
-
end
-
-
1
def scope_row(...)
-
2
dup.scope_row!(...)
-
end
-
-
1
def scope_col(...)
-
2
dup.scope_col!(...)
-
end
-
-
1
def warn(code, data = nil)
-
3
add(SEVERITIES::WARN, code, data)
-
end
-
-
1
def error(code, data = nil)
-
23
add(SEVERITIES::ERROR, code, data)
-
end
-
-
1
def exception(error)
-
2
error(error.msg_code)
-
end
-
-
1
private
-
-
1
def add(severity, code, data)
-
26
messages << Message.new(
-
code: code,
-
code_data: data,
-
scope: @scope,
-
scope_data: @scope_data,
-
severity: severity
-
)
-
-
26
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 "row_processor_result"
-
1
require_relative "row_value_builder"
-
-
1
module Sheetah
-
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 Sheetah
-
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 Sheetah
-
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
require_relative "sheet/col_converter"
-
1
require_relative "errors/error"
-
1
require_relative "utils/monadic_result"
-
-
1
module Sheetah
-
1
module Sheet
-
1
def self.included(mod)
-
29
mod.extend(ClassMethods)
-
end
-
-
1
def self.col2int(...)
-
92
COL_CONVERTER.col2int(...)
-
end
-
-
1
def self.int2col(...)
-
415
COL_CONVERTER.int2col(...)
-
end
-
-
1
module ClassMethods
-
1
def open(*args, **opts)
-
20
handle_sheet_error do
-
20
sheet = new(*args, **opts)
-
17
else: 16
then: 1
next sheet unless block_given?
-
-
begin
-
16
yield sheet
-
ensure
-
16
sheet.close
-
end
-
end
-
end
-
-
1
private
-
-
1
def handle_sheet_error
-
20
Utils::MonadicResult::Success.new(yield)
-
rescue Error => e
-
3
Utils::MonadicResult::Failure.new(e)
-
end
-
end
-
-
1
class Error < Errors::Error
-
end
-
-
1
class Header
-
1
def initialize(col:, value:)
-
158
@col = col
-
158
@value = value
-
end
-
-
1
attr_reader :col, :value
-
-
1
def ==(other)
-
28
other.is_a?(self.class) && col == other.col && value == other.value
-
end
-
-
1
def row_value_index
-
60
Sheet.col2int(col) - 1
-
end
-
end
-
-
1
class Row
-
1
def initialize(row:, value:)
-
66
@row = row
-
66
@value = value
-
end
-
-
1
attr_reader :row, :value
-
-
1
def ==(other)
-
18
other.is_a?(self.class) && row == other.row && value == other.value
-
end
-
end
-
-
1
class Cell
-
1
def initialize(row:, col:, value:)
-
275
@row = row
-
275
@col = col
-
275
@value = value
-
end
-
-
1
attr_reader :row, :col, :value
-
-
1
def ==(other)
-
70
other.is_a?(self.class) && row == other.row && col == other.col && value == other.value
-
end
-
end
-
-
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
-
1
raise NoMethodError, "You must implement #{self.class}#close => nil"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sheetah
-
1
module Sheet
-
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)
-
92
else: 90
then: 2
raise ArgumentError unless col.is_a?(String) && !col.empty?
-
-
90
int = 0
-
-
90
col.each_char.reverse_each.with_index do |char, pow|
-
103
int += char2int(char) * (CHARSET_SIZE**pow)
-
end
-
-
88
int
-
end
-
-
1
def int2col(int)
-
415
else: 411
then: 4
raise ArgumentError unless int.is_a?(Integer) && int.positive?
-
-
411
x = int
-
411
y = CHARSET_SIZE
-
411
col = +""
-
-
411
body: 424
until x.zero?
-
424
q, r = x.divmod(y)
-
-
424
then: 7
else: 417
if r.zero?
-
7
q -= 1
-
7
r = y
-
end
-
-
424
x = q
-
-
424
col << int2char(r)
-
end
-
-
411
col.reverse!
-
411
col.freeze
-
end
-
-
1
private
-
-
1
def char2int(char)
-
103
CHAR_TO_INT[char] || raise(ArgumentError, char.inspect)
-
end
-
-
1
def int2char(int)
-
424
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 "backends"
-
1
require_relative "headers"
-
1
require_relative "messaging"
-
1
require_relative "row_processor"
-
1
require_relative "sheet"
-
1
require_relative "sheet_processor_result"
-
1
require_relative "utils/monadic_result"
-
-
1
module Sheetah
-
1
class SheetProcessor
-
1
include Utils::MonadicResult
-
-
1
def initialize(specification)
-
15
@specification = specification
-
end
-
-
1
def call(*args, **opts)
-
15
messenger = Messaging::Messenger.new
-
-
15
result = Do() do
-
15
Backends.open(*args, **opts) do |sheet|
-
12
row_processor = build_row_processor(sheet, messenger)
-
-
7
sheet.each_row do |row|
-
21
yield row_processor.call(row)
-
end
-
end
-
end
-
-
15
handle_result(result, messenger)
-
end
-
-
1
private
-
-
1
def parse_headers(sheet, messenger)
-
12
headers = Headers.new(specification: @specification, messenger: messenger)
-
-
12
sheet.each_header do |header|
-
44
headers.add(header)
-
end
-
-
12
headers.result
-
end
-
-
1
def build_row_processor(sheet, messenger)
-
12
headers = parse_headers(sheet, messenger).unwrap
-
-
7
RowProcessor.new(headers: headers, messenger: messenger)
-
end
-
-
1
def handle_result(result, messenger)
-
15
result.or do |failure|
-
6
then: 1
else: 5
messenger.error(failure.msg_code) if failure.respond_to?(:msg_code)
-
end
-
-
15
SheetProcessorResult.new(result: result.discard, messages: messenger.messages)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sheetah
-
1
class SheetProcessorResult
-
1
def initialize(result:, messages: [])
-
24
@result = result
-
24
@messages = messages
-
end
-
-
1
attr_reader :result, :messages
-
-
1
def ==(other)
-
6
other.is_a?(self.class) &&
-
result == other.result &&
-
messages == other.messages
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "errors/spec_error"
-
-
1
module Sheetah
-
1
class Specification
-
1
class InvalidPatternError < Errors::SpecError
-
end
-
-
1
class MutablePatternError < Errors::SpecError
-
end
-
-
1
class DuplicatedPatternError < Errors::SpecError
-
end
-
-
1
def initialize(ignore_unspecified_columns: false)
-
21
@column_by_pattern = {}
-
21
@ignore_unspecified_columns = ignore_unspecified_columns
-
end
-
-
1
def set(pattern, column)
-
83
then: 1
else: 82
if pattern.nil?
-
1
raise InvalidPatternError, pattern.inspect
-
end
-
-
82
else: 81
then: 1
unless pattern.frozen?
-
1
raise MutablePatternError, pattern.inspect
-
end
-
-
81
then: 2
else: 79
if @column_by_pattern.key?(pattern)
-
2
raise DuplicatedPatternError, pattern.inspect
-
end
-
-
79
@column_by_pattern[pattern] = column
-
end
-
-
1
def get(header)
-
46
then: 1
else: 45
return if header.nil?
-
-
45
@column_by_pattern.each do |pattern, column|
-
154
then: 37
else: 117
return column if pattern === header # rubocop:disable Style/CaseEquality
-
end
-
-
nil
-
end
-
-
1
def required_columns
-
9
@column_by_pattern.each_value.select(&:required?)
-
end
-
-
1
def optional_columns
-
@column_by_pattern.each_value.reject(&:required?)
-
end
-
-
1
def ignore_unspecified_columns?
-
6
@ignore_unspecified_columns
-
end
-
-
1
def freeze
-
11
@column_by_pattern.freeze
-
11
super
-
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 Sheetah
-
# 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 initialize(attributes:, ignore_unspecified_columns: false)
-
9
@attributes = build_attributes(attributes)
-
9
@ignore_unspecified_columns = ignore_unspecified_columns
-
end
-
-
1
def apply(config)
-
9
specification = Specification.new(ignore_unspecified_columns: @ignore_unspecified_columns)
-
-
9
@attributes.each do |attribute|
-
18
attribute.each_column(config) do |column|
-
54
specification.set(column.header_pattern, column)
-
end
-
end
-
-
9
specification.freeze
-
end
-
-
1
private
-
-
1
def build_attributes(attributes)
-
9
uniq_keys = Set.new
-
-
9
uniq_attributes = attributes.map do |kwargs|
-
18
attribute = Attribute.new(**kwargs)
-
-
18
else: 18
then: 0
unless uniq_keys.add?(attribute.key)
-
raise Errors::SpecError, "Duplicated key: #{attribute.key.inspect}"
-
end
-
-
18
attribute
-
end
-
-
9
uniq_attributes.freeze
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "types/container"
-
-
1
module Sheetah
-
1
class TemplateConfig
-
1
def initialize(types: Types::Container.new)
-
9
@types = types
-
end
-
-
1
attr_reader :types
-
-
1
def header(key, index)
-
54
header = key.to_s.capitalize
-
54
then: 45
else: 9
header = "#{header} #{index + 1}" if index
-
-
54
pattern = /^#{header}$/i
-
-
54
[header, pattern]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Sheetah
-
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 Sheetah
-
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: 1
else: 6
messenger.error(failure) if failure
-
-
7
Failure()
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "composite"
-
-
1
module Sheetah
-
1
module Types
-
1
module Composites
-
1
Array = Composite.cast do |value, _messenger|
-
12
else: 11
then: 1
throw :failure, "must_be_array" unless value.is_a?(::Array)
-
-
11
value
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "array"
-
-
1
module Sheetah
-
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 Sheetah
-
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 Sheetah
-
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 =
-
23
then: 11
else: 12
(scalars ? defaults[:scalars].merge(scalars) : defaults[:scalars]).freeze
-
-
@composites =
-
23
then: 2
else: 21
(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 Sheetah
-
1
module Types
-
1
module Scalars
-
1
Boolsy = Scalar.cast(BoolsyCast)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "../cast"
-
-
1
module Sheetah
-
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
messenger.error("must_be_boolsy", value: value.inspect)
-
1
throw :failure
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "scalar"
-
1
require_relative "date_string_cast"
-
-
1
module Sheetah
-
1
module Types
-
1
module Scalars
-
1
DateString = Scalar.cast(DateStringCast)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "date"
-
1
require_relative "../cast"
-
-
1
module Sheetah
-
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
messenger.error("must_be_date", format: @date_fmt)
-
4
throw :failure
-
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 Sheetah
-
1
module Types
-
1
module Scalars
-
1
Email = String.cast(EmailCast)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "uri"
-
1
require_relative "../cast"
-
-
1
module Sheetah
-
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
messenger.error("must_be_email", value: value.inspect)
-
-
6
throw :failure
-
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 Sheetah
-
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 "../cast"
-
-
1
module Sheetah
-
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, "must_exist"
-
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("cleaned_string") if clean_string != value
-
-
32
clean_string
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "scalar"
-
-
1
module Sheetah
-
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, "must_be_string"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "cast_chain"
-
-
1
module Sheetah
-
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 Sheetah
-
1
module Utils
-
1
class CellStringCleaner
-
1
garbage = "(?:[^[:print:]]|[[:space:]])+"
-
1
GARBAGE_PREFIX = /\A#{garbage}/.freeze
-
1
GARBAGE_SUFFIX = /#{garbage}\Z/.freeze
-
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 Sheetah
-
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)
-
263
@wrapped = value
-
end
-
-
1
def empty?
-
141
wrapped == Unit
-
end
-
-
1
def ==(other)
-
56
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
-
19
then: 9
else: 10
empty? ? self : self.class.new
-
end
-
-
1
protected
-
-
1
attr_reader :wrapped
-
-
1
private
-
-
1
def value
-
5
then: 2
else: 3
raise ValueError, "There is no value within the result" if empty?
-
-
3
wrapped
-
end
-
-
1
def value?
-
25
else: 1
then: 24
wrapped unless empty?
-
end
-
-
1
def open
-
82
then: 16
if empty?
-
16
yield
-
else: 66
else
-
66
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
-
25
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?
-
3
true
-
end
-
-
1
def success
-
1
raise VariantError, "Not a Success"
-
end
-
-
1
def failure
-
3
value
-
end
-
-
1
def unwrap
-
10
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(...)
-
133
Success.new(...)
-
end
-
-
1
def Failure(...)
-
49
Failure.new(...)
-
end
-
-
1
def Do(&block)
-
33
catch(DO_TOKEN, &block)
-
end
-
-
# rubocop:enable Naming/MethodName
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/attribute"
-
-
1
RSpec.describe Sheetah::Attribute do
-
1
pending "TODO"
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/backends/csv"
-
1
require "support/shared/sheet_factories"
-
1
require "csv"
-
1
require "stringio"
-
1
require "tempfile"
-
-
1
RSpec.describe Sheetah::Backends::Csv do
-
1
include_context "sheet_factories"
-
-
1
let(:raw_table) do
-
9
Array.new(4) do |row|
-
36
Array.new(4) do |col|
-
144
"(#{row},#{col})"
-
end.freeze
-
end.freeze
-
end
-
-
1
let(:raw_sheet) do
-
9
stub_sheet(raw_table)
-
end
-
-
1
let(:sheet) do
-
9
described_class.new(io: raw_sheet)
-
end
-
-
1
def stub_sheet(table)
-
14
then: 1
else: 13
return if table.nil?
-
-
13
csv = CSV.generate do |csv_io|
-
13
table.each do |row|
-
38
csv_io << row
-
end
-
end
-
-
13
StringIO.new(csv, "r")
-
end
-
-
1
def new_sheet(...)
-
5
described_class.new(io: stub_sheet(...))
-
end
-
-
1
describe "::register" do
-
5
let(:registry) { Sheetah::BackendsRegistry.new }
-
-
1
before do
-
4
described_class.register(registry)
-
end
-
-
1
it "matches any so-called IO and an optional encoding" do
-
1
io = double
-
-
1
expect(registry.get(io: io)).to eq(described_class)
-
1
expect(registry.get(io: io, encoding: "UTF-8")).to eq(described_class)
-
1
expect(registry.get(io: io, encoding: Encoding::UTF_8)).to eq(described_class)
-
end
-
-
1
it "matches a CSV path and an optional encoding" do
-
1
expect(registry.get(path: "foo.csv")).to eq(described_class)
-
1
expect(registry.get(path: "foo.csv", encoding: "UTF-8")).to eq(described_class)
-
1
expect(registry.get(path: "foo.csv", encoding: Encoding::UTF_8)).to eq(described_class)
-
end
-
-
1
it "doesn't match any other path" do
-
1
expect(registry.get(path: "foo.tsv")).to be_nil
-
end
-
-
1
it "doesn't match extra args" do
-
1
expect(registry.get(2, path: "foo.csv")).to be_nil
-
end
-
end
-
-
1
describe "#initialize" do
-
3
let(:utf8_path) { fixture_path("csv/utf8.csv") }
-
5
let(:latin9_path) { fixture_path("csv/latin9.csv") }
-
-
1
let(:headers) do
-
4
[
-
"Matricule",
-
"Nom",
-
"Prénom",
-
"Email",
-
"Date de naissance",
-
"Entrée en entreprise",
-
"Administrateur",
-
"Bio",
-
"Service",
-
]
-
end
-
-
1
context "when no io nor path is given" do
-
1
it "fails" do
-
1
expect do
-
1
described_class.new
-
end.to raise_error(described_class::ArgumentError)
-
end
-
end
-
-
1
context "when both an io and a path are given" do
-
1
it "fails" do
-
1
expect do
-
1
described_class.new(io: double, path: double)
-
end.to raise_error(described_class::ArgumentError)
-
end
-
end
-
-
1
context "when only an io is given" do
-
4
let(:io) { File.new(io_path) }
-
-
1
context "when the default encoding is valid" do
-
1
alias_method :io_path, :utf8_path
-
-
1
it "can read CSV data" do
-
1
sheet = described_class.new(io: io)
-
1
expect(sheet.each_header.map(&:value)).to eq(headers)
-
end
-
end
-
-
1
context "when the default encoding is invalid" do
-
1
alias_method :io_path, :latin9_path
-
-
1
it "fails" do
-
1
expect do
-
1
described_class.new(io: io)
-
end.to raise_error(described_class::EncodingError)
-
end
-
-
1
it "can read CSV data once given a valid encoding" do
-
1
sheet = described_class.new(io: io, encoding: Encoding::ISO_8859_15)
-
1
expect(sheet.each_header.map(&:value)).to eq(headers)
-
end
-
end
-
end
-
-
1
context "when only a path is given" do
-
1
context "when the default encoding is valid" do
-
1
alias_method :path, :utf8_path
-
-
1
it "can read CSV data" do
-
1
sheet = described_class.new(path: path)
-
1
expect(sheet.each_header.map(&:value)).to eq(headers)
-
end
-
end
-
-
1
context "when the default encoding is invalid" do
-
1
alias_method :path, :latin9_path
-
-
1
it "fails" do
-
1
expect do
-
1
described_class.new(path: path)
-
end.to raise_error(described_class::EncodingError)
-
end
-
-
1
it "can read CSV data once given a valid encoding" do
-
1
sheet = described_class.new(path: path, encoding: Encoding::ISO_8859_15)
-
1
expect(sheet.each_header.map(&:value)).to eq(headers)
-
end
-
end
-
end
-
end
-
-
1
describe "#each_header" do
-
1
let(:expected_headers) do
-
[
-
2
header(value: raw_table[0][0], col: "A"),
-
header(value: raw_table[0][1], col: "B"),
-
header(value: raw_table[0][2], col: "C"),
-
header(value: raw_table[0][3], col: "D"),
-
]
-
end
-
-
1
context "with a block" do
-
1
it "yields each header, with its letter-based index" do
-
2
expect { |b| sheet.each_header(&b) }.to yield_successive_args(*expected_headers)
-
end
-
-
1
it "returns self" do
-
5
expect(sheet.each_header { double }).to be(sheet)
-
end
-
end
-
-
1
context "without a block" do
-
1
it "returns an enumerator" do
-
1
enum = sheet.each_header
-
-
1
expect(enum).to be_a(Enumerator)
-
1
expect(enum.size).to be(4)
-
1
expect(enum.to_a).to eq(expected_headers)
-
end
-
end
-
end
-
-
1
describe "#each_row" do
-
1
let(:expected_rows) do
-
[
-
2
row(row: 1, value: cells(raw_table[1], row: 1)),
-
row(row: 2, value: cells(raw_table[2], row: 2)),
-
row(row: 3, value: cells(raw_table[3], row: 3)),
-
]
-
end
-
-
1
context "with a block" do
-
1
it "yields each row, with its 1-based index" do
-
2
expect { |b| sheet.each_row(&b) }.to yield_successive_args(*expected_rows)
-
end
-
-
1
it "returns self" do
-
4
expect(sheet.each_row { double }).to be(sheet)
-
end
-
end
-
-
1
context "without a block" do
-
1
it "returns an enumerator" do
-
1
enum = sheet.each_row
-
-
1
expect(enum).to be_a(Enumerator)
-
1
expect(enum.size).to be_nil
-
1
expect(enum.to_a).to eq(expected_rows)
-
end
-
end
-
end
-
-
1
describe "#close" do
-
1
it "returns nil" do
-
1
expect(sheet.close).to be_nil
-
end
-
-
1
it "closes the underlying sheet" do
-
2
expect { sheet.close }.to change(raw_sheet, :closed?).from(false).to(true)
-
end
-
end
-
-
1
context "when the input table is odd" do
-
1
shared_examples "empty_sheet" do
-
2
it "doesn't enumerate any header" do
-
4
expect { |b| sheet.each_header(&b) }.not_to yield_control
-
end
-
-
2
it "doesn't enumerate any row" do
-
4
expect { |b| sheet.each_row(&b) }.not_to yield_control
-
end
-
end
-
-
1
context "when the input table is nil" do
-
1
it "raises an error" do
-
2
expect { new_sheet(nil) }.to raise_error(Sheetah::Sheet::Error)
-
end
-
end
-
-
1
context "when the input table is empty" do
-
3
let(:sheet) { new_sheet [] }
-
-
1
include_examples "empty_sheet"
-
end
-
-
1
context "when the input table headers are empty" do
-
3
let(:sheet) { new_sheet [[]] }
-
-
1
include_examples "empty_sheet"
-
end
-
end
-
-
1
describe "CSV options" do
-
1
it "requires a specific col_sep and quote_char" do
-
1
expect(CSV).to receive(:new).with(raw_sheet, col_sep: ",", quote_char: '"').and_call_original
-
-
1
sheet
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/backends/wrapper"
-
1
require "support/shared/sheet_factories"
-
-
1
RSpec.describe Sheetah::Backends::Wrapper do
-
1
include_context "sheet_factories"
-
-
1
let(:raw_table) do
-
7
Array.new(4) do |row|
-
28
Array.new(4) do |col|
-
112
instance_double(Object, "(#{row},#{col})")
-
end.freeze
-
end.freeze
-
end
-
-
1
let(:sheet) do
-
7
new_sheet(raw_table)
-
end
-
-
1
let(:table_interface) do
-
12
Module.new do
-
12
def [](_); end
-
12
def size; end
-
end
-
end
-
-
1
let(:headers_interface) do
-
9
Module.new do
-
9
def [](_); end
-
9
def size; end
-
end
-
end
-
-
1
let(:values_interfaces) do
-
7
Module.new do
-
7
def [](_); end
-
end
-
end
-
-
1
def stub_table(source, target = instance_double(table_interface)) # rubocop:disable Metrics/AbcSize
-
12
then: 1
else: 11
return if source.nil?
-
-
11
source.each_with_index do |source_row, y|
-
30
then: 9
else: 21
target_row = instance_double(y.zero? ? headers_interface : values_interfaces)
-
30
allow(target).to receive(:[]).with(y).and_return(target_row)
-
-
30
source_row.each_with_index do |source_cell, x|
-
112
allow(target_row).to receive(:[]).with(x).and_return(source_cell)
-
end
-
end
-
-
11
allow(target).to receive(:size).with(no_args).and_return(source.size)
-
11
else: 2
then: 9
allow(target[0]).to receive(:size).with(no_args).and_return(source[0].size) unless source.empty?
-
-
11
target
-
end
-
-
1
def new_sheet(...)
-
12
described_class.new(stub_table(...))
-
end
-
-
1
describe "#each_header" do
-
1
let(:expected_headers) do
-
[
-
2
header(value: raw_table[0][0], col: "A"),
-
header(value: raw_table[0][1], col: "B"),
-
header(value: raw_table[0][2], col: "C"),
-
header(value: raw_table[0][3], col: "D"),
-
]
-
end
-
-
1
context "with a block" do
-
1
it "yields each header, with its letter-based index" do
-
2
expect { |b| sheet.each_header(&b) }.to yield_successive_args(*expected_headers)
-
end
-
-
1
it "returns self" do
-
5
expect(sheet.each_header { double }).to be(sheet)
-
end
-
end
-
-
1
context "without a block" do
-
1
it "returns an enumerator" do
-
1
enum = sheet.each_header
-
-
1
expect(enum).to be_a(Enumerator)
-
1
expect(enum.size).to be(4)
-
1
expect(enum.to_a).to eq(expected_headers)
-
end
-
end
-
end
-
-
1
describe "#each_row" do
-
1
let(:expected_rows) do
-
[
-
2
row(row: 1, value: cells(raw_table[1], row: 1)),
-
row(row: 2, value: cells(raw_table[2], row: 2)),
-
row(row: 3, value: cells(raw_table[3], row: 3)),
-
]
-
end
-
-
1
context "with a block" do
-
1
it "yields each row, with its 1-based index" do
-
2
expect { |b| sheet.each_row(&b) }.to yield_successive_args(*expected_rows)
-
end
-
-
1
it "returns self" do
-
4
expect(sheet.each_row { double }).to be(sheet)
-
end
-
end
-
-
1
context "without a block" do
-
1
it "returns an enumerator" do
-
1
enum = sheet.each_row
-
-
1
expect(enum).to be_a(Enumerator)
-
1
expect(enum.size).to be_nil
-
1
expect(enum.to_a).to eq(expected_rows)
-
end
-
end
-
end
-
-
1
describe "#close" do
-
1
it "returns nil" do
-
1
expect(sheet.close).to be_nil
-
end
-
end
-
-
1
context "when the input table is odd" do
-
1
shared_examples "empty_sheet" do
-
2
it "doesn't enumerate any header" do
-
4
expect { |b| sheet.each_header(&b) }.not_to yield_control
-
end
-
-
2
it "doesn't enumerate any row" do
-
4
expect { |b| sheet.each_row(&b) }.not_to yield_control
-
end
-
end
-
-
1
context "when the input table is nil" do
-
1
it "raises an error" do
-
2
expect { new_sheet(nil) }.to raise_error(Sheetah::Sheet::Error)
-
end
-
end
-
-
1
context "when the input table is empty" do
-
3
let(:sheet) { new_sheet [] }
-
-
1
include_examples "empty_sheet"
-
end
-
-
1
context "when the input table headers are empty" do
-
3
let(:sheet) { new_sheet [[]] }
-
-
1
include_examples "empty_sheet"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/backends/xlsx"
-
1
require "support/shared/sheet_factories"
-
-
1
RSpec.describe Sheetah::Backends::Xlsx do
-
1
include_context "sheet_factories"
-
-
1
let(:sheet) do
-
7
new_sheet("xlsx/regular.xlsx")
-
end
-
-
1
def new_sheet(path)
-
14
described_class.new(path: path && fixture_path(path))
-
end
-
-
1
describe "::register" do
-
4
let(:registry) { Sheetah::BackendsRegistry.new }
-
-
1
before do
-
3
described_class.register(registry)
-
end
-
-
1
it "matches a XLSX path" do
-
1
expect(registry.get(path: "foo.xlsx")).to eq(described_class)
-
end
-
-
1
it "doesn't match any other path" do
-
1
expect(registry.get(path: "foo.xls")).to be_nil
-
end
-
-
1
it "doesn't match extra args" do
-
1
expect(registry.get(2, path: "foo.xlsx")).to be_nil
-
end
-
end
-
-
1
describe "#each_header" do
-
1
let(:expected_headers) do
-
[
-
2
header(value: "matricule", col: "A"),
-
header(value: "nom", col: "B"),
-
header(value: "prénom", col: "C"),
-
header(value: "date de naissance", col: "D"),
-
header(value: "email", col: "E"),
-
]
-
end
-
-
1
context "with a block" do
-
1
it "yields each header, with its letter-based index" do
-
2
expect { |b| sheet.each_header(&b) }.to yield_successive_args(*expected_headers)
-
end
-
-
1
it "returns self" do
-
6
expect(sheet.each_header { double }).to be(sheet)
-
end
-
end
-
-
1
context "without a block" do
-
1
it "returns an enumerator" do
-
1
enum = sheet.each_header
-
-
1
expect(enum).to be_a(Enumerator)
-
1
expect(enum.size).to be(5)
-
1
expect(enum.to_a).to eq(expected_headers)
-
end
-
end
-
end
-
-
1
describe "#each_row" do
-
1
let(:row1_cells) do
-
2
["004774", "Ytärd", "Glœuiçe", "28/04/1998", "foo@bar.com"]
-
end
-
-
1
let(:row2_cells) do
-
2
[664_623, "Goulijambon", "Carasmine", Date.new(1976, 1, 20), "foo@bar.com"]
-
end
-
-
1
let(:expected_rows) do
-
[
-
2
row(row: 1, value: cells(row1_cells, row: 1)),
-
row(row: 2, value: cells(row2_cells, row: 2)),
-
]
-
end
-
-
1
context "with a block" do
-
1
it "yields each row, with its 1-based index" do
-
2
expect { |b| sheet.each_row(&b) }.to yield_successive_args(*expected_rows)
-
end
-
-
1
it "returns self" do
-
3
expect(sheet.each_row { double }).to be(sheet)
-
end
-
end
-
-
1
context "without a block" do
-
1
it "returns an enumerator" do
-
1
enum = sheet.each_row
-
-
1
expect(enum).to be_a(Enumerator)
-
1
expect(enum.size).to be_nil
-
1
expect(enum.to_a).to eq(expected_rows)
-
end
-
end
-
end
-
-
1
describe "#close" do
-
1
it "returns nil" do
-
1
expect(sheet.close).to be_nil
-
end
-
end
-
-
1
context "when the input table is odd" do
-
1
shared_examples "empty_sheet" do
-
1
it "doesn't enumerate any header" do
-
2
expect { |b| sheet.each_header(&b) }.not_to yield_control
-
end
-
-
1
it "doesn't enumerate any row" do
-
2
expect { |b| sheet.each_row(&b) }.not_to yield_control
-
end
-
end
-
-
1
context "when the input table is nil" do
-
1
it "raises an error" do
-
2
expect { new_sheet(nil) }.to raise_error(Sheetah::Sheet::Error)
-
end
-
end
-
-
1
context "when the input table is empty" do
-
3
let(:sheet) { new_sheet("xlsx/empty.xlsx") }
-
-
1
include_examples "empty_sheet"
-
end
-
-
1
context "when the input table includes empty lines around the content" do
-
3
let(:sheet) { new_sheet("xlsx/empty_lines_around.xlsx") }
-
-
1
it "doesn't ignore them when detecting the headers" do
-
2
expect { |b| sheet.each_header(&b) }.to yield_control.exactly(5).times
-
1
expect(sheet.each_header.map(&:value)).to all(be_nil)
-
end
-
-
1
it "ignores them when detecting the rows" do
-
2
expect { |b| sheet.each_row(&b) }.to yield_control.exactly(3).times
-
end
-
end
-
-
1
context "when the input table includes empty lines within the content" do
-
3
let(:sheet) { new_sheet("xlsx/empty_lines_within.xlsx") }
-
-
1
it "doesn't impact the detection of headers" do
-
2
expect { |b| sheet.each_header(&b) }.to yield_control.exactly(5).times
-
end
-
-
1
it "doesn't ignore them when detecting the rows" do
-
2
expect { |b| sheet.each_row(&b) }.to yield_control.exactly(4).times
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/backends_registry"
-
-
1
RSpec.describe Sheetah::BackendsRegistry do
-
7
let(:registry) { described_class.new }
-
-
7
let(:backend0) { double(:backend0) }
-
7
let(:backend1) { double(:backend1) }
-
-
1
before do
-
6
registry.set(backend0) do |args, opts|
-
4
in: 1
else: 3
ok = (case args; in [[1, 2, Symbol]] then true; else false; end)
-
4
in: 1
else: 0
ok && (case opts; in { foo: Hash } then true; else false; end)
-
end
-
-
6
registry.set(backend1) do |args, opts|
-
3
in: 2
else: 1
ok = (case args; in [] then true; else false; end)
-
3
in: 2
else: 0
ok && (case opts; in { path: /\.csv$/ } then true; else false; end)
-
end
-
end
-
-
1
describe "#get / #set" do
-
1
it "can set a new backend with a matcher" do
-
1
expect(registry.get([1, 2, :ozij], foo: { 1 => 2 })).to be(backend0)
-
1
expect(registry.get(path: "file.csv")).to be(backend1)
-
1
expect(registry.get(double, path: "file.csv")).to be_nil
-
end
-
-
1
it "can overwrite a previous backend matcher" do
-
1
registry.set(backend0) do |args, opts|
-
1
in: 1
else: 0
ok = (case args; in ["foo"] then true; else false; end)
-
1
in: 1
else: 0
ok && (case opts; in {} then true; else false; end)
-
end
-
-
1
expect(registry.get("foo")).to be(backend0)
-
end
-
end
-
-
1
describe "#set" do
-
1
it "returns the registry itself" do
-
1
result = registry.set(backend0) {}
-
-
1
expect(result).to be(registry)
-
end
-
end
-
-
1
describe "#freeze" do
-
4
before { registry.freeze }
-
-
1
it "freezes the registry" do
-
1
expect(registry).to be_frozen
-
end
-
-
1
it "prevents further modifications" do
-
1
expect do
-
1
registry.set(backend0) {}
-
end.to raise_error(FrozenError)
-
end
-
-
1
it "doesn't prevent further readings" do
-
1
expect(registry.get(path: "foo.csv")).to be(backend1)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/backends"
-
-
1
RSpec.describe Sheetah::Backends do
-
1
describe "::registry" do
-
1
it "is a registry of backends" do
-
1
expect(described_class.registry).to be_a(Sheetah::BackendsRegistry)
-
end
-
end
-
-
1
describe "::open" do
-
1
let(:backend) do
-
2
double
-
end
-
-
4
let(:foo) { double }
-
4
let(:bar) { double }
-
3
let(:res) { double }
-
-
1
it "may open with an explicit backend" do
-
1
allow(backend).to receive(:open).with(foo, bar: bar).and_return(res)
-
1
expect(described_class.registry).not_to receive(:get)
-
-
1
expect(described_class.open(foo, backend: backend, bar: bar)).to be(res)
-
end
-
-
1
it "may open with an implicit backend" do
-
1
allow(backend).to receive(:open).with(foo, bar: bar).and_return(res)
-
1
allow(described_class.registry).to receive(:get).with(foo, bar: bar).and_return(backend)
-
-
1
expect(described_class.open(foo, bar: bar)).to be(res)
-
end
-
-
1
it "may miss a backend to open" do
-
1
allow(described_class.registry).to receive(:get).with(foo, bar: bar).and_return(nil)
-
-
1
result = described_class.open(foo, bar: bar)
-
1
expect(result).to be_failure
-
1
expect(result.failure).to have_attributes(msg_code: "no_applicable_backend")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/column"
-
-
1
RSpec.describe Sheetah::Column do
-
8
let(:key) { double }
-
8
let(:type) { double }
-
8
let(:index) { double }
-
8
let(:header) { double }
-
7
let(:header_pattern) { Object.new }
-
-
1
let(:col) do
-
6
described_class.new(
-
key: key,
-
type: type,
-
index: index,
-
header: header,
-
header_pattern: header_pattern
-
)
-
end
-
-
1
it "is frozen" do
-
1
expect(col).to be_frozen
-
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 a frozen attribute" do
-
1
expect(col.header_pattern).to be(header_pattern)
-
1
expect(col.header_pattern).to be_frozen
-
end
-
-
1
context "when the value is not given" do
-
2
let(:header_copy) { Object.new }
-
-
1
let(:col) do
-
1
described_class.new(
-
key: key,
-
type: type,
-
index: index,
-
header: header
-
)
-
end
-
-
1
before do
-
1
allow(header).to receive(:dup).and_return(header_copy)
-
end
-
-
1
it "defaults to a frozen copy of the header value" do
-
1
expect(col.header).not_to be_frozen
-
1
expect(col.header_pattern).to be(header_copy) & be_frozen
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/errors/error"
-
-
1
RSpec.describe Sheetah::Errors::Error do
-
1
it "is some kind of StandardError" do
-
1
expect(described_class.superclass).to be(StandardError)
-
end
-
-
1
describe "class msg_code" do
-
1
it "has a msg_code" do
-
1
expect(described_class.msg_code).to eq("sheetah.errors.error")
-
end
-
-
1
context "when inherited" do
-
1
context "when first defined anonymously" do
-
1
let(:subclass) do
-
6
Class.new(described_class)
-
end
-
-
1
context "when kept anonymous" do
-
1
it "doesn't have a msg_code by default" do
-
1
expect(subclass.msg_code).to be_nil
-
end
-
-
1
it "cannot deduce a msg_code" do
-
1
expect do
-
1
subclass.msg_code!
-
end.to raise_error(TypeError, /cannot build msg_code/i)
-
end
-
-
1
it "may have a custom msg_code" do
-
1
subclass.msg_code! "foo.bar.baz"
-
1
expect(subclass.msg_code).to eq("foo.bar.baz")
-
end
-
end
-
-
1
context "when named afterwards" do
-
1
before do
-
3
stub_const("Foizjeofijow::OIJDFO834", subclass)
-
end
-
-
1
it "doesn't have a msg_code by default" do
-
1
expect(subclass.msg_code).to be_nil
-
end
-
-
1
it "can deduce a msg_code" do
-
1
subclass.msg_code!
-
1
expect(subclass.msg_code).to eq("foizjeofijow.oijdfo834")
-
end
-
-
1
it "may have a custom msg_code" do
-
1
subclass.msg_code! "foo.bar.baz"
-
1
expect(subclass.msg_code).to eq("foo.bar.baz")
-
end
-
end
-
end
-
-
1
context "when first defined with a name" do
-
7
let(:namespace) { Module.new }
-
-
1
let(:subclass) do
-
6
class namespace::OIJDFO834 < described_class # rubocop:disable RSpec/LeakyConstantDeclaration,Lint/ConstantDefinitionInBlock,Style/ClassAndModuleChildren
-
6
self
-
end
-
end
-
-
1
context "when fully named" do
-
1
before do
-
3
stub_const("Foizjeofijow", namespace)
-
end
-
-
1
it "has a msg_code by default" do
-
1
expect(subclass.msg_code).to eq("foizjeofijow.oijdfo834")
-
end
-
-
1
it "can deduce the same msg_code" do
-
1
expect do
-
1
subclass.msg_code!
-
end.not_to change(subclass, :msg_code)
-
end
-
-
1
it "may have a custom msg_code" do
-
1
subclass.msg_code! "foo.bar.baz"
-
1
expect(subclass.msg_code).to eq("foo.bar.baz")
-
end
-
end
-
-
1
context "when not fully named" do
-
1
it "doesn't have a msg_code by default" do
-
1
expect(subclass.msg_code).to be_nil
-
end
-
-
1
it "cannot deduce a msg_code" do
-
1
expect do
-
1
subclass.msg_code!
-
end.to raise_error(TypeError, /cannot build msg_code/i)
-
end
-
-
1
it "may have a custom msg_code" do
-
1
subclass.msg_code! "foo.bar.baz"
-
1
expect(subclass.msg_code).to eq("foo.bar.baz")
-
end
-
end
-
end
-
end
-
end
-
-
1
describe "#msg_code" do
-
1
it "delegates to the class" do
-
1
allow(described_class).to receive(:msg_code).and_return(msg_code = double)
-
1
expect(subject.msg_code).to be(msg_code)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/errors/spec_error"
-
-
1
RSpec.describe Sheetah::Errors::SpecError do
-
1
it "has a msg_code" do
-
1
expect(described_class.msg_code).to eq("sheetah.errors.spec_error")
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/errors/type_error"
-
-
1
RSpec.describe Sheetah::Errors::TypeError do
-
1
it "is some kind of Error" do
-
1
expect(described_class.superclass).to be(Sheetah::Errors::Error)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/headers"
-
1
require "sheetah/column"
-
1
require "sheetah/messaging"
-
1
require "sheetah/sheet"
-
1
require "sheetah/specification"
-
-
1
RSpec.describe Sheetah::Headers, monadic_result: true do
-
1
let(:specification) do
-
7
instance_double(
-
Sheetah::Specification,
-
required_columns: [],
-
ignore_unspecified_columns?: false
-
)
-
end
-
-
1
let(:columns) do
-
7
Array.new(10) do
-
70
instance_double(Sheetah::Column)
-
end
-
end
-
-
1
let(:messenger) do
-
7
Sheetah::Messaging::Messenger.new
-
end
-
-
1
let(:sheet_headers) do
-
7
Array.new(5) do
-
35
instance_double(Sheetah::Sheet::Header, col: double, value: double)
-
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(
-
sheet_headers[0] => columns[4],
-
sheet_headers[1] => columns[1],
-
sheet_headers[2] => columns[7],
-
sheet_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(sheet_headers[1])
-
2
headers.add(sheet_headers[2])
-
2
headers.add(sheet_headers[0])
-
end
-
-
1
it "is a success and preserve #add order" do
-
1
expect(headers.result).to eq(
-
Success(
-
[
-
Sheetah::Headers::Header.new(sheet_headers[1], columns[1]),
-
Sheetah::Headers::Header.new(sheet_headers[2], columns[7]),
-
Sheetah::Headers::Header.new(sheet_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(sheet_headers[0])
-
2
headers.add(sheet_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 eq(
-
[
-
Sheetah::Messaging::Message.new(
-
severity: "ERROR",
-
code: "invalid_header",
-
code_data: sheet_headers[4].value,
-
scope: "COL",
-
scope_data: { col: sheet_headers[4].col }
-
),
-
]
-
)
-
end
-
end
-
-
1
context "when there is a duplicate" do
-
1
before do
-
2
headers.add(sheet_headers[0])
-
2
headers.add(sheet_headers[3])
-
2
headers.add(sheet_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 eq(
-
[
-
Sheetah::Messaging::Message.new(
-
severity: "ERROR",
-
code: "duplicated_header",
-
code_data: sheet_headers[1].value,
-
scope: "COL",
-
scope_data: { col: sheet_headers[1].col }
-
),
-
]
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/messaging"
-
-
1
RSpec.describe Sheetah::Messaging::Message do
-
5
let(:code) { double }
-
4
let(:code_data) { double }
-
4
let(:scope) { double }
-
5
let(:scope_data) { double }
-
4
let(:severity) { double }
-
-
1
let(:message) do
-
9
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/i)
-
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: Sheetah::Messaging::SCOPES::SHEET,
-
scope_data: nil,
-
severity: Sheetah::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
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 sheet" do
-
2
let(:scope) { Sheetah::Messaging::SCOPES::SHEET }
-
2
let(:scope_data) { nil }
-
-
1
it "can be reduced to a string" do
-
1
expect(message.to_s).to eq("[SHEET] ERROR: foo_is_bar")
-
end
-
end
-
-
1
context "when scoped to a row" do
-
2
let(:scope) { Sheetah::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) { Sheetah::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) { Sheetah::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) { Sheetah::Messaging::SCOPES::SHEET }
-
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 eq("[SHEET] ERROR: foo_is_bar {:foo=>\"bar\"}")
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/messaging"
-
-
1
RSpec.describe Sheetah::Messaging::Messenger do
-
18
let(:scopes) { Sheetah::Messaging::SCOPES }
-
6
let(:severities) { Sheetah::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::SHEET,
-
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, *args, &block)
-
18
allow_method_call_checking_block(receiver, :scoping!, *args, &block)
-
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, *args, &block)
-
2
allow_method_call_checking_block(receiver, :scope_row!, *args, &block)
-
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, *args, &block)
-
2
allow_method_call_checking_block(receiver, :scope_col!, *args, &block)
-
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
-
9
let(:scope) { Object.new }
-
9
let(:scope_data) { Object.new }
-
-
9
let(:code) { double }
-
5
let(:code_data) { double }
-
-
9
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(code, code_data)).to be(messenger)
-
end
-
-
1
it "adds the code & code_data as a warning" do
-
1
messenger.warn(code, code_data)
-
-
1
expect(messenger.messages).to contain_exactly(
-
Sheetah::Messaging::Message.new(
-
code: code,
-
code_data: code_data,
-
scope: scope,
-
scope_data: scope_data,
-
severity: severities::WARN
-
)
-
)
-
end
-
-
1
it "may do without code_data" do
-
1
messenger.warn(code)
-
-
1
expect(messenger.messages).to contain_exactly(
-
Sheetah::Messaging::Message.new(
-
code: code,
-
code_data: nil,
-
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(code, code_data)).to be(messenger)
-
end
-
-
1
it "adds the code & code_data as an error" do
-
1
messenger.error(code, code_data)
-
-
1
expect(messenger.messages).to contain_exactly(
-
Sheetah::Messaging::Message.new(
-
code: code,
-
code_data: code_data,
-
scope: scope,
-
scope_data: scope_data,
-
severity: severities::ERROR
-
)
-
)
-
end
-
-
1
it "may do without code_data" do
-
1
messenger.error(code)
-
-
1
expect(messenger.messages).to contain_exactly(
-
Sheetah::Messaging::Message.new(
-
code: code,
-
code_data: nil,
-
scope: scope,
-
scope_data: scope_data,
-
severity: severities::ERROR
-
)
-
)
-
end
-
end
-
-
1
describe "#exception" do
-
3
let(:e) { double }
-
-
1
before do
-
2
allow(e).to receive(:msg_code).and_return(code)
-
end
-
-
1
it "returns the receiver" do
-
1
expect(messenger.exception(e)).to be(messenger)
-
end
-
-
1
it "adds the exception's msg_code as an error" do
-
1
messenger.exception(e)
-
-
1
expect(messenger.messages).to contain_exactly(
-
Sheetah::Messaging::Message.new(
-
code: code,
-
code_data: nil,
-
scope: scope,
-
scope_data: scope_data,
-
severity: severities::ERROR
-
)
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/row_processor_result"
-
-
1
RSpec.describe Sheetah::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 "sheetah/messaging"
-
1
require "sheetah/headers"
-
1
require "sheetah/row_processor"
-
1
require "sheetah/sheet"
-
-
1
RSpec.describe Sheetah::RowProcessor, monadic_result: true do
-
1
let(:messenger) do
-
1
instance_double(Sheetah::Messaging::Messenger, dup: row_messenger)
-
end
-
-
1
let(:row_messenger) do
-
1
Sheetah::Messaging::Messenger.new
-
end
-
-
1
let(:headers) do
-
[
-
1
instance_double(Sheetah::Headers::Header, column: double, row_value_index: 0),
-
instance_double(Sheetah::Headers::Header, column: double, row_value_index: 1),
-
instance_double(Sheetah::Headers::Header, column: double, row_value_index: 2),
-
]
-
end
-
-
1
let(:cells) do
-
[
-
1
instance_double(Sheetah::Sheet::Cell, value: double, col: double),
-
instance_double(Sheetah::Sheet::Cell, value: double, col: double),
-
instance_double(Sheetah::Sheet::Cell, value: double, col: double),
-
]
-
end
-
-
1
let(:row) do
-
1
instance_double(Sheetah::Sheet::Row, row: double, value: cells)
-
end
-
-
1
let(:row_value_builder) do
-
1
instance_double(Sheetah::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: Sheetah::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(Sheetah::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(
-
Sheetah::RowProcessorResult.new(
-
row: row.row,
-
result: row_value_builder_result,
-
messages: row_messenger.messages
-
)
-
)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/row_value_builder"
-
1
require "sheetah/column"
-
1
require "sheetah/types/scalars/scalar"
-
-
1
RSpec.describe Sheetah::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(Sheetah::Types::Type, composite?: false) }
-
4
let(:scalar_key) { double }
-
-
5
let(:composite_type) { instance_double(Sheetah::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(Sheetah::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(Sheetah::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(Sheetah::Column, type: composite_type, key: composite_key, index: 2)
-
end
-
-
1
let(:column1) do
-
1
instance_double(Sheetah::Column, type: composite_type, key: composite_key, index: 1)
-
end
-
-
1
let(:column2) do
-
1
instance_double(Sheetah::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 "sheetah/sheet_processor_result"
-
-
1
RSpec.describe Sheetah::SheetProcessorResult 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 "sheetah/sheet_processor"
-
1
require "sheetah/specification"
-
-
1
RSpec.describe Sheetah::SheetProcessor, monadic_result: true do
-
1
let(:specification) do
-
6
instance_double(Sheetah::Specification)
-
end
-
-
1
let(:processor) do
-
6
described_class.new(specification)
-
end
-
-
1
let(:sheet_class) do
-
10
Class.new { include Sheetah::Sheet }
-
end
-
-
1
let(:backend_args) do
-
6
[double, double]
-
end
-
-
1
let(:backend_opts) do
-
6
{ foo: double, bar: double }
-
end
-
-
1
let(:sheet) do
-
3
instance_double(sheet_class)
-
end
-
-
1
def call(&block)
-
4
block ||= proc {} # stub a dummy proc
-
4
processor.call(*backend_args, backend: sheet_class, **backend_opts, &block)
-
end
-
-
1
def stub_sheet_open_ok(success = double)
-
3
allow(sheet_class).to(
-
receive(:open)
-
.with(*backend_args, **backend_opts)
-
.and_yield(sheet)
-
.and_return(Success(success))
-
)
-
-
3
success
-
end
-
-
1
def stub_sheet_open_ko(failure = double)
-
1
allow(sheet_class).to(
-
receive(:open)
-
.with(*backend_args, **backend_opts)
-
.and_return(Failure(failure))
-
)
-
-
1
failure
-
end
-
-
1
describe "backend detection" do
-
1
it "can rely on the explicit argument" do
-
1
actual_args = backend_args
-
1
actual_opts = backend_opts.merge(backend: sheet_class)
-
-
1
expect(Sheetah::Backends).to(
-
receive(:open)
-
.with(*actual_args, **actual_opts)
-
.and_return(Success())
-
)
-
-
1
result = processor.call(*actual_args, **actual_opts)
-
-
1
expect(result).to eq(
-
Sheetah::SheetProcessorResult.new(
-
result: Success(),
-
messages: []
-
)
-
)
-
end
-
-
1
it "can rely on the implicit detection" do
-
1
actual_args = backend_args
-
1
actual_opts = backend_opts
-
-
1
expect(Sheetah::Backends).to(
-
receive(:open)
-
.with(*actual_args, **actual_opts)
-
.and_return(Success())
-
)
-
-
1
result = processor.call(*actual_args, **actual_opts)
-
-
1
expect(result).to eq(
-
Sheetah::SheetProcessorResult.new(
-
result: Success(),
-
messages: []
-
)
-
)
-
end
-
end
-
-
1
context "when there is a sheet error" do
-
1
let(:error_class) do
-
1
klass = Class.new(Sheetah::Sheet::Error)
-
1
stub_const("Foo::Bar::BazError", klass)
-
1
klass.msg_code!
-
1
klass
-
end
-
-
1
let(:error) do
-
1
error_class.exception
-
end
-
-
1
before do
-
1
stub_sheet_open_ko(error)
-
end
-
-
1
it "is an empty failure, with messages" do
-
1
expect(call).to eq(
-
Sheetah::SheetProcessorResult.new(
-
result: Failure(),
-
messages: [
-
Sheetah::Messaging::Message.new(
-
code: "foo.bar.baz_error",
-
code_data: nil,
-
scope: "SHEET",
-
scope_data: nil,
-
severity: "ERROR"
-
),
-
]
-
)
-
)
-
end
-
end
-
-
1
shared_context "when there is no sheet error" do
-
2
let(:sheet_headers) do
-
9
Array.new(2) { instance_double(Sheetah::Sheet::Header) }
-
end
-
-
2
let(:sheet_rows) do
-
12
Array.new(3) { instance_double(Sheetah::Sheet::Row) }
-
end
-
-
2
let(:messenger) do
-
3
instance_double(Sheetah::Messaging::Messenger, messages: double)
-
end
-
-
2
let(:headers) do
-
3
instance_double(Sheetah::Headers)
-
end
-
-
2
def stub_messenger
-
3
allow(Sheetah::Messaging::Messenger).to(
-
receive(:new)
-
.with(no_args)
-
.and_return(messenger)
-
)
-
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(Sheetah::Headers).to(
-
receive(:new)
-
.with(specification: specification, messenger: messenger)
-
.and_return(headers)
-
)
-
end
-
-
2
def stub_headers_ops(result)
-
3
sheet_headers.each do |sheet_header|
-
6
expect(headers).to receive(:add).with(sheet_header).ordered
-
end
-
-
3
expect(headers).to receive(:result).and_return(result).ordered
-
end
-
-
2
before do
-
3
stub_messenger
-
3
stub_headers
-
-
3
stub_sheet_open_ok
-
-
3
stub_enumeration(sheet, :each_header, sheet_headers)
-
3
stub_enumeration(sheet, :each_row, sheet_rows)
-
end
-
end
-
-
1
context "when there is a header error" do
-
1
include_context "when there is no sheet 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(
-
Sheetah::SheetProcessorResult.new(
-
result: Failure(),
-
messages: messenger.messages
-
)
-
)
-
end
-
end
-
-
1
context "when there is no error" do
-
1
include_context "when there is no sheet error"
-
-
1
let(:headers_spec) do
-
2
double
-
end
-
-
1
let(:processed_rows) do
-
8
Array.new(sheet_rows.size) { double }
-
end
-
-
1
def stub_row_processing
-
2
allow(Sheetah::RowProcessor).to(
-
receive(:new)
-
.with(headers: headers_spec, messenger: messenger)
-
.and_return(row_processor = instance_double(Sheetah::RowProcessor))
-
)
-
-
2
sheet_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(
-
Sheetah::SheetProcessorResult.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 "sheetah/sheet"
-
-
1
RSpec.describe Sheetah::Sheet, monadic_result: true do
-
1
let(:sheet_class) do
-
21
c = Class.new do
-
21
def initialize(foo, bar:)
-
3
@foo = foo
-
3
@bar = bar
-
end
-
end
-
-
21
c.include(described_class)
-
-
21
stub_const("SheetClass", c)
-
-
21
c
-
end
-
-
1
let(:sheet) do
-
3
sheet_class.new(foo, bar: bar)
-
end
-
-
15
let(:foo) { double }
-
15
let(:bar) { double }
-
-
1
describe "::Error" do
-
2
subject { sheet_class::Error }
-
-
1
it "exposes some kind of Sheetah::Errors::Error" do
-
1
expect(subject.superclass).to be(Sheetah::Errors::Error)
-
end
-
end
-
-
1
describe "::Header" do
-
3
let(:col) { double }
-
3
let(:val) { double }
-
-
3
let(:wrapper) { sheet_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(sheet_class::Header.new(col: col, value: val))
-
1
expect(wrapper).not_to eq(sheet_class::Header.new(col: double, value: val))
-
end
-
end
-
-
1
describe "::Row" do
-
3
let(:row) { double }
-
3
let(:val) { double }
-
-
3
let(:wrapper) { sheet_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(sheet_class::Row.new(row: row, value: val))
-
1
expect(wrapper).not_to eq(sheet_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) { sheet_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(sheet_class::Cell.new(row: row, col: col, value: val))
-
1
expect(wrapper).not_to eq(sheet_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(:sheet) do
-
11
instance_double(sheet_class)
-
end
-
-
1
before do
-
11
allow(sheet_class).to receive(:new).with(foo, bar: bar).and_return(sheet)
-
end
-
-
1
context "without a block" do
-
1
it "returns a new sheet wrapped as a Success" do
-
1
expect(sheet_class.open(foo, bar: bar)).to eq(Success(sheet))
-
end
-
end
-
-
1
context "with a block" do
-
1
before do
-
10
allow(sheet).to receive(:close)
-
end
-
-
1
it "yields a new sheet" do
-
1
yielded = false
-
-
1
sheet_class.open(foo, bar: bar) do |opened_sheet|
-
1
yielded = true
-
1
expect(opened_sheet).to be(sheet)
-
end
-
-
1
expect(yielded).to be(true)
-
end
-
-
1
it "returns the value of the block, wrapped as a success" do
-
1
block_result = double
-
2
actual_block_result = sheet_class.open(foo, bar: bar) { block_result }
-
-
1
expect(actual_block_result).to eq(Success(block_result))
-
end
-
-
1
it "closes after yielding" do
-
1
sheet_class.open(foo, bar: bar) do
-
1
expect(sheet).not_to have_received(:close)
-
end
-
-
1
expect(sheet).to have_received(:close)
-
end
-
-
1
context "when an exception is raised" do
-
3
let(:exception) { Class.new(Exception) } # rubocop:disable Lint/InheritException
-
3
let(:error) { Class.new(StandardError) }
-
4
let(:sheet_error) { Class.new(Sheetah::Sheet::Error) }
-
-
1
context "without yielding control" do
-
1
it "doesn't rescue an exception" do
-
1
allow(sheet_class).to receive(:new).and_raise(exception)
-
-
1
expect do
-
1
sheet_class.open(foo, bar: bar)
-
end.to raise_error(exception)
-
end
-
-
1
it "doesn't rescue a standard error" do
-
1
allow(sheet_class).to receive(:new).and_raise(error)
-
-
1
expect do
-
1
sheet_class.open(foo, bar: bar)
-
end.to raise_error(error)
-
end
-
-
1
it "rescues and wraps a sheet error in a failure" do
-
1
allow(sheet_class).to receive(:new).and_raise(e = sheet_error.exception)
-
-
1
result = sheet_class.open(foo, bar: bar)
-
-
1
expect(result).to eq(Failure(e))
-
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
sheet_class.open(foo, bar: bar) do
-
1
expect(sheet).not_to have_received(:close)
-
1
raise exception
-
end
-
end.to raise_error(exception)
-
-
1
expect(sheet).to have_received(:close)
-
end
-
-
1
it "doesn't rescue but closes after a standard error is raised" do
-
1
expect do
-
1
sheet_class.open(foo, bar: bar) do
-
1
expect(sheet).not_to have_received(:close)
-
1
raise error
-
end
-
end.to raise_error(error)
-
-
1
expect(sheet).to have_received(:close)
-
end
-
-
1
it "rescues and closes after a sheet error is raised" do
-
1
sheet_class.open(foo, bar: bar) do
-
1
expect(sheet).not_to have_received(:close)
-
1
raise sheet_error
-
end
-
-
1
expect(sheet).to have_received(:close)
-
end
-
-
1
it "returns the exception, wrapped as a failure, after a sheet error is raised" do
-
1
e = sheet_error.exception # raise the instance directly to simplify result matching
-
-
1
result = sheet_class.open(foo, bar: bar) do
-
1
raise e
-
end
-
-
1
expect(result).to eq(Failure(e))
-
end
-
end
-
end
-
end
-
end
-
-
1
describe "#each_header" do
-
1
it "is abstract" do
-
2
expect { sheet.each_header }.to raise_error(
-
NoMethodError, "You must implement SheetClass#each_header => self"
-
)
-
end
-
end
-
-
1
describe "#each_row" do
-
1
it "is abstract" do
-
2
expect { sheet.each_row }.to raise_error(
-
NoMethodError, "You must implement SheetClass#each_row => self"
-
)
-
end
-
end
-
-
1
describe "#close" do
-
1
it "is abstract" do
-
2
expect { sheet.close }.to raise_error(
-
NoMethodError, "You must implement SheetClass#close => nil"
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/specification"
-
-
1
RSpec.describe Sheetah::Specification do
-
1
let(:spec) do
-
12
described_class.new
-
end
-
-
1
describe "#set" do
-
1
it "rejects nil patterns" do
-
1
pattern = nil
-
1
column = double
-
-
1
expect do
-
1
spec.set(pattern, column)
-
end.to raise_error(described_class::InvalidPatternError, "nil")
-
end
-
-
1
it "rejects mutable patterns" do
-
1
pattern = instance_double(Object, frozen?: false, inspect: "mutable_pattern_inspect")
-
1
column = double
-
-
1
expect do
-
1
spec.set(pattern, column)
-
end.to raise_error(described_class::MutablePatternError, "mutable_pattern_inspect")
-
end
-
-
1
it "rejects duplicated patterns" do
-
1
pattern = instance_double(Object, frozen?: true, inspect: "pattern_dup")
-
1
column0 = double
-
1
column1 = double
-
-
1
spec.set(pattern, column0)
-
-
1
expect do
-
1
spec.set(pattern, column0)
-
end.to raise_error(described_class::DuplicatedPatternError, "pattern_dup")
-
-
1
expect do
-
1
spec.set(pattern, column1)
-
end.to raise_error(described_class::DuplicatedPatternError, "pattern_dup")
-
end
-
-
1
it "accepts unique, frozen patterns" do
-
1
pattern1 = instance_double(Object, frozen?: true, inspect: "pattern1")
-
1
pattern2 = instance_double(Object, frozen?: true, inspect: "pattern2")
-
1
column = double
-
-
1
expect do
-
1
spec.set(pattern1, column)
-
1
spec.set(pattern2, column)
-
end.not_to raise_error
-
end
-
-
1
context "when frozen" do
-
1
it "cannot set new patterns" do
-
1
spec.freeze
-
-
1
pattern = instance_double(Object, frozen?: true)
-
1
column = double
-
-
1
expect do
-
1
spec.set(pattern, column)
-
end.to raise_error(FrozenError)
-
end
-
end
-
end
-
-
1
describe "#get" do
-
1
let(:regexp_pattern) do
-
7
/foo\d{3}bar/i.freeze
-
end
-
-
1
let(:string_pattern) do
-
7
"Doubitchou"
-
end
-
-
1
let(:other_pattern) do
-
7
instance_double(Object, frozen?: true)
-
end
-
-
1
let(:columns) do
-
28
Array.new(3) { double }
-
end
-
-
1
before do
-
7
spec.set(string_pattern, columns[0])
-
7
spec.set(regexp_pattern, columns[1])
-
7
spec.set(other_pattern, columns[2])
-
end
-
-
1
it "returns nil when header is nil" do
-
1
expect(spec.get(nil)).to be_nil
-
end
-
-
1
context "with a Regexp pattern" do
-
1
it "returns the matching column" do
-
1
expect(spec.get("foo123bar")).to eq(columns[1])
-
1
expect(spec.get("Foo480BAR")).to eq(columns[1])
-
end
-
end
-
-
1
context "with a String pattern" do
-
1
it "returns the matching column" do
-
1
expect(spec.get("Doubitchou")).to eq(columns[0])
-
end
-
-
1
it "matches case-sensitively" do
-
1
expect(spec.get("doubitchou")).to be_nil
-
end
-
end
-
-
1
context "with any other pattern" do
-
3
let(:header) { "boudoudou" }
-
-
1
it "matches an equivalent header" do
-
1
allow(other_pattern).to receive(:==).with(header).and_return(true)
-
1
expect(spec.get(header)).to eq(columns[2])
-
end
-
-
1
it "doesn't match a non-equivalent header" do
-
1
allow(other_pattern).to receive(:==).with(header).and_return(false)
-
1
expect(spec.get(header)).to be_nil
-
end
-
end
-
-
1
context "when frozen" do
-
1
it "can get existing patterns" do
-
1
spec.freeze
-
-
1
expect(spec.get("Doubitchou")).to eq(columns[0])
-
end
-
end
-
end
-
-
1
describe "errors" do
-
1
example "invalid pattern" do
-
1
expect(described_class::InvalidPatternError).to have_attributes(
-
superclass: Sheetah::Errors::SpecError,
-
msg_code: "sheetah.specification.invalid_pattern_error"
-
)
-
end
-
-
1
example "mutable pattern" do
-
1
expect(described_class::MutablePatternError).to have_attributes(
-
superclass: Sheetah::Errors::SpecError,
-
msg_code: "sheetah.specification.mutable_pattern_error"
-
)
-
end
-
-
1
example "duplicated pattern" do
-
1
expect(described_class::DuplicatedPatternError).to have_attributes(
-
superclass: Sheetah::Errors::SpecError,
-
msg_code: "sheetah.specification.duplicated_pattern_error"
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/template_config"
-
-
1
RSpec.describe Sheetah::TemplateConfig do
-
1
pending "TODO"
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/template"
-
-
1
RSpec.describe Sheetah::Template do
-
1
pending "TODO"
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/types/cast_chain"
-
-
1
RSpec.describe Sheetah::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 "sheetah/types/composites/array_compact"
-
1
require "support/shared/composite_type"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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 "sheetah/types/composites/array"
-
1
require "support/shared/composite_type"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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
-
2
expect { cast.call(value, messenger) }.to throw_symbol(:failure, "must_be_array")
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/types/composites/composite"
-
1
require "support/shared/composite_type"
-
-
1
RSpec.describe Sheetah::Types::Composites::Composite do
-
1
include_examples "composite_type"
-
-
1
it "inherits from the basic type" do
-
1
expect(described_class.superclass).to be(Sheetah::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 "sheetah/types/container"
-
-
1
RSpec.describe Sheetah::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
Sheetah::Types::Scalars::Scalar
-
end
-
-
1
let(:string_type) do
-
3
Sheetah::Types::Scalars::String
-
end
-
-
1
let(:email_type) do
-
3
Sheetah::Types::Scalars::Email
-
end
-
-
1
let(:boolsy_type) do
-
1
Sheetah::Types::Scalars::Boolsy
-
end
-
-
1
let(:date_string_type) do
-
1
Sheetah::Types::Scalars::DateString
-
end
-
-
1
def stub_new_type(klass, *args)
-
2
then: 0
else: 2
args << no_args if args.empty?
-
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(Sheetah::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(Sheetah::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(
-
Sheetah::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(
-
Sheetah::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(
-
Sheetah::Errors::TypeError,
-
"Invalid composite type: :foo"
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/types/scalars/boolsy_cast"
-
1
require "sheetah/messaging"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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(messenger).to receive(:error).with("must_be_boolsy", value: value.inspect)
-
2
expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil)
-
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 "sheetah/types/scalars/boolsy"
-
1
require "support/shared/scalar_type"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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 + [Sheetah::Types::Scalars::BoolsyCast]
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/types/scalars/date_string_cast"
-
1
require "sheetah/messaging"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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(messenger).to receive(:error).with("must_be_date", format: default_fmt)
-
2
expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil)
-
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(messenger).to receive(:error).with("must_be_date", format: date_fmt)
-
2
expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil)
-
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(messenger).to receive(:error).with("must_be_date", format: date_fmt)
-
2
expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil)
-
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(messenger).to receive(:error).with("must_be_date", format: default_fmt)
-
2
expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/types/scalars/date_string"
-
1
require "support/shared/scalar_type"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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 + [Sheetah::Types::Scalars::DateStringCast]
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/types/scalars/email_cast"
-
1
require "sheetah/messaging"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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(messenger).to receive(:error).with("must_be_email", value: value.inspect)
-
2
expect { cast.call(value, messenger) }.to throw_symbol(:failure, nil)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/types/scalars/email"
-
1
require "support/shared/scalar_type"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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 + [Sheetah::Types::Scalars::EmailCast]
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/types/scalars/scalar_cast"
-
1
require "sheetah/messaging"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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, "must_exist")
-
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("cleaned_string")
-
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 "sheetah/types/scalars/scalar"
-
1
require "support/shared/scalar_type"
-
-
1
RSpec.describe Sheetah::Types::Scalars::Scalar do
-
1
include_examples "scalar_type"
-
-
1
it "inherits from the basic type" do
-
1
expect(described_class.superclass).to be(Sheetah::Types::Type)
-
end
-
-
1
describe "::cast_classes" do
-
1
it "includes a basic cast class" do
-
1
expect(described_class.cast_classes).to eq(
-
[Sheetah::Types::Scalars::ScalarCast]
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/types/scalars/string"
-
1
require "support/shared/scalar_type"
-
1
require "support/shared/cast_class"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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
-
2
expect { cast.call(value, messenger) }.to throw_symbol(:failure, "must_be_string")
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/types/type"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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 "sheetah/utils/cell_string_cleaner"
-
-
1
RSpec.describe Sheetah::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 "sheetah/utils/monadic_result"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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 "sheetah/utils/monadic_result"
-
-
1
RSpec.describe Sheetah::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(Sheetah::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 "sheetah/utils/monadic_result"
-
-
1
RSpec.describe Sheetah::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 "sheetah/utils/monadic_result"
-
-
1
RSpec.describe Sheetah::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 "sheetah"
-
-
1
RSpec.describe Sheetah, monadic_result: true do
-
1
let(:types) do
-
24
reverse_string = Sheetah::Types::Scalars::String.cast { |v, _m| v.reverse }
-
-
9
Sheetah::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
Sheetah::Template.new(**template_opts)
-
end
-
-
1
let(:template_config) do
-
9
Sheetah::TemplateConfig.new(types: types)
-
end
-
-
1
let(:specification) do
-
9
template.apply(template_config)
-
end
-
-
1
let(:processor) do
-
9
Sheetah::SheetProcessor.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, backend: Sheetah::Backends::Wrapper, **opts, &block)
-
end
-
-
1
def process_to_a(*args, **opts)
-
4
a = []
-
16
processor.call(*args, backend: Sheetah::Backends::Wrapper, **opts) { |result| a << result }
-
4
a
-
end
-
-
1
context "when there is no sheet 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: Sheetah::Messaging::SCOPES::CELL,
-
scope_data: { row: 3, col: "B" },
-
severity: Sheetah::Messaging::SEVERITIES::ERROR
-
)
-
)
-
end
-
end
-
-
1
context "when there are unspecified columns in the sheet" 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: "oof",
-
scope: Sheetah::Messaging::SCOPES::COL,
-
scope_data: { col: "C" },
-
severity: Sheetah::Messaging::SEVERITIES::ERROR
-
),
-
have_attributes(
-
code: "invalid_header",
-
code_data: "rab",
-
scope: Sheetah::Messaging::SCOPES::COL,
-
scope_data: { col: "F" },
-
severity: Sheetah::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: "Foo",
-
scope: Sheetah::Messaging::SCOPES::SHEET,
-
scope_data: nil,
-
severity: Sheetah::Messaging::SEVERITIES::ERROR
-
),
-
have_attributes(
-
code: "missing_column",
-
code_data: "Bar 5",
-
scope: Sheetah::Messaging::SCOPES::SHEET,
-
scope_data: nil,
-
severity: Sheetah::Messaging::SEVERITIES::ERROR
-
)
-
)
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
mod = Module.new do
-
1
def fixtures_path
-
19
@fixtures_path ||= File.expand_path("./fixtures", __dir__)
-
end
-
-
1
def fixture_path(path)
-
19
File.join(fixtures_path, path)
-
end
-
end
-
-
1
RSpec.configure do |config|
-
1
config.include(mod)
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/utils/monadic_result"
-
-
1
RSpec.configure do |config|
-
1
config.include(Sheetah::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 "sheetah/types/type"
-
1
require "sheetah/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(Sheetah::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(Sheetah::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(
-
Sheetah::Errors::TypeError,
-
"Invalid index: #{type_index.inspect}"
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/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(Sheetah::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(
-
Sheetah::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(
-
Sheetah::Errors::TypeError, "A scalar type cannot be indexed"
-
)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sheetah/sheet"
-
-
1
RSpec.shared_context "sheet_factories" do
-
3
def header(...)
-
26
Sheetah::Sheet::Header.new(...)
-
end
-
-
3
def row(...)
-
16
Sheetah::Sheet::Row.new(...)
-
end
-
-
3
def cell(...)
-
68
Sheetah::Sheet::Cell.new(...)
-
end
-
-
3
def cells(values, row:, col: "A")
-
16
int = Sheetah::Sheet.col2int(col)
-
-
16
values.map.with_index(int) do |value, index|
-
68
cell(row: row, col: Sheetah::Sheet.int2col(index), value: value)
-
end
-
end
-
end