tabulard 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +43 -0
- data/VERSION +1 -0
- data/lib/sheetah/attribute.rb +60 -0
- data/lib/sheetah/attribute_types/composite.rb +57 -0
- data/lib/sheetah/attribute_types/scalar.rb +58 -0
- data/lib/sheetah/attribute_types/value.rb +62 -0
- data/lib/sheetah/attribute_types/value.rb.orig +68 -0
- data/lib/sheetah/attribute_types.rb +49 -0
- data/lib/sheetah/backends/csv.rb +92 -0
- data/lib/sheetah/backends/wrapper.rb +57 -0
- data/lib/sheetah/backends/xlsx.rb +80 -0
- data/lib/sheetah/backends.rb +11 -0
- data/lib/sheetah/column.rb +31 -0
- data/lib/sheetah/errors/error.rb +8 -0
- data/lib/sheetah/errors/spec_error.rb +10 -0
- data/lib/sheetah/errors/type_error.rb +10 -0
- data/lib/sheetah/frozen.rb +9 -0
- data/lib/sheetah/headers.rb +96 -0
- data/lib/sheetah/messaging/config.rb +19 -0
- data/lib/sheetah/messaging/constants.rb +17 -0
- data/lib/sheetah/messaging/message.rb +70 -0
- data/lib/sheetah/messaging/message_variant.rb +47 -0
- data/lib/sheetah/messaging/messages/cleaned_string.rb +18 -0
- data/lib/sheetah/messaging/messages/duplicated_header.rb +21 -0
- data/lib/sheetah/messaging/messages/invalid_header.rb +21 -0
- data/lib/sheetah/messaging/messages/missing_column.rb +21 -0
- data/lib/sheetah/messaging/messages/must_be_array.rb +18 -0
- data/lib/sheetah/messaging/messages/must_be_boolsy.rb +21 -0
- data/lib/sheetah/messaging/messages/must_be_date.rb +21 -0
- data/lib/sheetah/messaging/messages/must_be_email.rb +21 -0
- data/lib/sheetah/messaging/messages/must_be_string.rb +18 -0
- data/lib/sheetah/messaging/messages/must_exist.rb +18 -0
- data/lib/sheetah/messaging/messages/sheet_error.rb +18 -0
- data/lib/sheetah/messaging/messenger.rb +133 -0
- data/lib/sheetah/messaging/validations/base_validator.rb +43 -0
- data/lib/sheetah/messaging/validations/dsl.rb +31 -0
- data/lib/sheetah/messaging/validations/invalid_message.rb +12 -0
- data/lib/sheetah/messaging/validations/mixins.rb +57 -0
- data/lib/sheetah/messaging/validations.rb +35 -0
- data/lib/sheetah/messaging.rb +22 -0
- data/lib/sheetah/row_processor.rb +41 -0
- data/lib/sheetah/row_processor_result.rb +20 -0
- data/lib/sheetah/row_value_builder.rb +53 -0
- data/lib/sheetah/sheet/col_converter.rb +62 -0
- data/lib/sheetah/sheet.rb +107 -0
- data/lib/sheetah/sheet_processor.rb +61 -0
- data/lib/sheetah/sheet_processor_result.rb +18 -0
- data/lib/sheetah/specification.rb +30 -0
- data/lib/sheetah/template.rb +85 -0
- data/lib/sheetah/template_config.rb +35 -0
- data/lib/sheetah/types/cast.rb +20 -0
- data/lib/sheetah/types/cast_chain.rb +49 -0
- data/lib/sheetah/types/composites/array.rb +16 -0
- data/lib/sheetah/types/composites/array_compact.rb +13 -0
- data/lib/sheetah/types/composites/composite.rb +32 -0
- data/lib/sheetah/types/container.rb +81 -0
- data/lib/sheetah/types/scalars/boolsy.rb +12 -0
- data/lib/sheetah/types/scalars/boolsy_cast.rb +35 -0
- data/lib/sheetah/types/scalars/date_string.rb +12 -0
- data/lib/sheetah/types/scalars/date_string_cast.rb +43 -0
- data/lib/sheetah/types/scalars/email.rb +12 -0
- data/lib/sheetah/types/scalars/email_cast.rb +28 -0
- data/lib/sheetah/types/scalars/scalar.rb +29 -0
- data/lib/sheetah/types/scalars/scalar_cast.rb +49 -0
- data/lib/sheetah/types/scalars/string.rb +18 -0
- data/lib/sheetah/types/type.rb +103 -0
- data/lib/sheetah/utils/cell_string_cleaner.rb +29 -0
- data/lib/sheetah/utils/monadic_result.rb +174 -0
- data/lib/sheetah.rb +31 -0
- metadata +118 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "sheet/col_converter"
|
4
|
+
require_relative "errors/error"
|
5
|
+
require_relative "messaging/messages/sheet_error"
|
6
|
+
require_relative "utils/monadic_result"
|
7
|
+
|
8
|
+
module Sheetah
|
9
|
+
module Sheet
|
10
|
+
def self.included(mod)
|
11
|
+
mod.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.col2int(...)
|
15
|
+
COL_CONVERTER.col2int(...)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.int2col(...)
|
19
|
+
COL_CONVERTER.int2col(...)
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
def open(*args, **opts)
|
24
|
+
handle_sheet_error do
|
25
|
+
sheet = new(*args, **opts)
|
26
|
+
next sheet unless block_given?
|
27
|
+
|
28
|
+
begin
|
29
|
+
yield sheet
|
30
|
+
ensure
|
31
|
+
sheet.close
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def handle_sheet_error
|
39
|
+
Utils::MonadicResult::Success.new(yield)
|
40
|
+
rescue Error => e
|
41
|
+
Utils::MonadicResult::Failure.new(e)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Error < Errors::Error
|
46
|
+
def to_message
|
47
|
+
Messaging::Messages::SheetError.new
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Header
|
52
|
+
def initialize(col:, value:)
|
53
|
+
@col = col
|
54
|
+
@value = value
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :col, :value
|
58
|
+
|
59
|
+
def ==(other)
|
60
|
+
other.is_a?(self.class) && col == other.col && value == other.value
|
61
|
+
end
|
62
|
+
|
63
|
+
def row_value_index
|
64
|
+
Sheet.col2int(col) - 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Row
|
69
|
+
def initialize(row:, value:)
|
70
|
+
@row = row
|
71
|
+
@value = value
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_reader :row, :value
|
75
|
+
|
76
|
+
def ==(other)
|
77
|
+
other.is_a?(self.class) && row == other.row && value == other.value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Cell
|
82
|
+
def initialize(row:, col:, value:)
|
83
|
+
@row = row
|
84
|
+
@col = col
|
85
|
+
@value = value
|
86
|
+
end
|
87
|
+
|
88
|
+
attr_reader :row, :col, :value
|
89
|
+
|
90
|
+
def ==(other)
|
91
|
+
other.is_a?(self.class) && row == other.row && col == other.col && value == other.value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def each_header
|
96
|
+
raise NoMethodError, "You must implement #{self.class}#each_header => self"
|
97
|
+
end
|
98
|
+
|
99
|
+
def each_row
|
100
|
+
raise NoMethodError, "You must implement #{self.class}#each_row => self"
|
101
|
+
end
|
102
|
+
|
103
|
+
def close
|
104
|
+
raise NoMethodError, "You must implement #{self.class}#close => nil"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "backends"
|
4
|
+
require_relative "headers"
|
5
|
+
require_relative "messaging"
|
6
|
+
require_relative "row_processor"
|
7
|
+
require_relative "sheet"
|
8
|
+
require_relative "sheet_processor_result"
|
9
|
+
require_relative "utils/monadic_result"
|
10
|
+
|
11
|
+
module Sheetah
|
12
|
+
class SheetProcessor
|
13
|
+
include Utils::MonadicResult
|
14
|
+
|
15
|
+
def initialize(specification)
|
16
|
+
@specification = specification
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(*args, **opts)
|
20
|
+
messenger = Messaging::Messenger.new
|
21
|
+
|
22
|
+
result = Do() do
|
23
|
+
Backends.open(*args, **opts) do |sheet|
|
24
|
+
row_processor = build_row_processor(sheet, messenger)
|
25
|
+
|
26
|
+
sheet.each_row do |row|
|
27
|
+
yield row_processor.call(row)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
handle_result(result, messenger)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def parse_headers(sheet, messenger)
|
38
|
+
headers = Headers.new(specification: @specification, messenger: messenger)
|
39
|
+
|
40
|
+
sheet.each_header do |header|
|
41
|
+
headers.add(header)
|
42
|
+
end
|
43
|
+
|
44
|
+
headers.result
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_row_processor(sheet, messenger)
|
48
|
+
headers = parse_headers(sheet, messenger).unwrap
|
49
|
+
|
50
|
+
RowProcessor.new(headers: headers, messenger: messenger)
|
51
|
+
end
|
52
|
+
|
53
|
+
def handle_result(result, messenger)
|
54
|
+
result.or do |failure|
|
55
|
+
messenger.error(failure.to_message) if failure.respond_to?(:to_message)
|
56
|
+
end
|
57
|
+
|
58
|
+
SheetProcessorResult.new(result: result.discard, messages: messenger.messages)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sheetah
|
4
|
+
class SheetProcessorResult
|
5
|
+
def initialize(result:, messages: [])
|
6
|
+
@result = result
|
7
|
+
@messages = messages
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :result, :messages
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
other.is_a?(self.class) &&
|
14
|
+
result == other.result &&
|
15
|
+
messages == other.messages
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sheetah
|
4
|
+
class Specification
|
5
|
+
def initialize(columns:, ignore_unspecified_columns: false)
|
6
|
+
@columns = columns
|
7
|
+
@ignore_unspecified_columns = ignore_unspecified_columns
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(header)
|
11
|
+
return if header.nil?
|
12
|
+
|
13
|
+
@columns.find do |column|
|
14
|
+
column.header_pattern.match?(header)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def required_columns
|
19
|
+
@columns.select(&:required?)
|
20
|
+
end
|
21
|
+
|
22
|
+
def optional_columns
|
23
|
+
@columns.reject(&:required?)
|
24
|
+
end
|
25
|
+
|
26
|
+
def ignore_unspecified_columns?
|
27
|
+
@ignore_unspecified_columns
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require_relative "attribute"
|
5
|
+
require_relative "specification"
|
6
|
+
require_relative "errors/spec_error"
|
7
|
+
|
8
|
+
module Sheetah
|
9
|
+
# A {Template} represents the abstract structure of a tabular document.
|
10
|
+
#
|
11
|
+
# The main component of the structure is the object obtained by processing a
|
12
|
+
# row. A template therefore specifies all possible attributes of that object
|
13
|
+
# as a list of (key, abstract type) pairs.
|
14
|
+
#
|
15
|
+
# Each attribute will eventually be compiled into as many concrete columns as
|
16
|
+
# necessary with the help of a {TemplateConfig config} to produce a
|
17
|
+
# {Specification specification}.
|
18
|
+
#
|
19
|
+
# In other words, a {Template} specifies the structure of the processing
|
20
|
+
# result (its attributes), whereas a {Specification} specifies the columns
|
21
|
+
# that may be involved into building the processing result.
|
22
|
+
#
|
23
|
+
# {Attribute Attributes} may either be _composite_ (their value is a
|
24
|
+
# composition of multiple values) or _scalar_ (their value is a single
|
25
|
+
# value). Scalar attributes will thus produce a single column in the
|
26
|
+
# specification, and composite attributes will produce as many columns as
|
27
|
+
# required by the number of scalar values they hold.
|
28
|
+
class Template
|
29
|
+
def self.build(attributes:, **kwargs)
|
30
|
+
attributes = attributes.map { |attribute| Attribute.build(**attribute) }
|
31
|
+
attributes.freeze
|
32
|
+
|
33
|
+
template = new(attributes: attributes, **kwargs)
|
34
|
+
template.freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(attributes:, ignore_unspecified_columns: false)
|
38
|
+
ensure_attributes_unicity(attributes)
|
39
|
+
|
40
|
+
@attributes = attributes
|
41
|
+
@ignore_unspecified_columns = ignore_unspecified_columns
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply(config)
|
45
|
+
columns = []
|
46
|
+
|
47
|
+
attributes.each do |attribute|
|
48
|
+
attribute.each_column(config) do |column|
|
49
|
+
columns << column.freeze
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
specification = Specification.new(
|
54
|
+
columns: columns.freeze,
|
55
|
+
ignore_unspecified_columns: ignore_unspecified_columns
|
56
|
+
)
|
57
|
+
|
58
|
+
specification.freeze
|
59
|
+
end
|
60
|
+
|
61
|
+
def ==(other)
|
62
|
+
other.is_a?(self.class) &&
|
63
|
+
attributes == other.attributes &&
|
64
|
+
ignore_unspecified_columns == other.ignore_unspecified_columns
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
attr_reader :attributes, :ignore_unspecified_columns
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def ensure_attributes_unicity(attributes)
|
74
|
+
keys = Set.new
|
75
|
+
|
76
|
+
duplicate = attributes.find do |attribute|
|
77
|
+
!keys.add?(attribute.key)
|
78
|
+
end
|
79
|
+
|
80
|
+
return unless duplicate
|
81
|
+
|
82
|
+
raise Errors::SpecError, "Duplicated key: #{duplicate.key.inspect}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "types/container"
|
4
|
+
|
5
|
+
module Sheetah
|
6
|
+
class TemplateConfig
|
7
|
+
def initialize(types: Types::Container.new)
|
8
|
+
@types = types
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :types
|
12
|
+
|
13
|
+
# Given an attribute key and a possibily-nil column index, return the header and header pattern
|
14
|
+
# for that column.
|
15
|
+
#
|
16
|
+
# The return value should be an array with two items:
|
17
|
+
#
|
18
|
+
# 1. The first item is the header, as a String.
|
19
|
+
# 2. The second item is the header pattern, and should respond to `#match?` with a boolean
|
20
|
+
# value. Instances of Regexp will obviously do, but the requirement is really about the
|
21
|
+
# `#match?` method.
|
22
|
+
#
|
23
|
+
# @param key [Symbol, String]
|
24
|
+
# @param index [Integer, nil]
|
25
|
+
# @return [Array(String, #match?)]
|
26
|
+
def header(key, index)
|
27
|
+
header = key.to_s.capitalize
|
28
|
+
header = "#{header} #{index + 1}" if index
|
29
|
+
|
30
|
+
pattern = /^#{Regexp.escape(header)}$/i
|
31
|
+
|
32
|
+
[header, pattern]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sheetah
|
4
|
+
module Types
|
5
|
+
# @private
|
6
|
+
module Cast
|
7
|
+
def ==(other)
|
8
|
+
other.is_a?(self.class) && other.config == config
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def config
|
14
|
+
instance_variables.each_with_object({}) do |ivar, acc|
|
15
|
+
acc[ivar] = instance_variable_get(ivar)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../utils/monadic_result"
|
4
|
+
|
5
|
+
module Sheetah
|
6
|
+
module Types
|
7
|
+
class CastChain
|
8
|
+
include Utils::MonadicResult
|
9
|
+
|
10
|
+
def initialize(casts = [])
|
11
|
+
@casts = casts
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :casts
|
15
|
+
|
16
|
+
def prepend(cast)
|
17
|
+
@casts.unshift(cast)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def append(cast)
|
22
|
+
@casts.push(cast)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def freeze
|
27
|
+
@casts.each(&:freeze)
|
28
|
+
@casts.freeze
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(value, messenger)
|
33
|
+
failure = catch(:failure) do
|
34
|
+
success = catch(:success) do
|
35
|
+
@casts.reduce(value) do |prev_value, cast|
|
36
|
+
cast.call(prev_value, messenger)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
return Success(success)
|
41
|
+
end
|
42
|
+
|
43
|
+
messenger.error(failure) if failure
|
44
|
+
|
45
|
+
Failure()
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "composite"
|
4
|
+
require_relative "../../messaging/messages/must_be_array"
|
5
|
+
|
6
|
+
module Sheetah
|
7
|
+
module Types
|
8
|
+
module Composites
|
9
|
+
Array = Composite.cast do |value, _messenger|
|
10
|
+
throw :failure, Messaging::Messages::MustBeArray.new unless value.is_a?(::Array)
|
11
|
+
|
12
|
+
value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../errors/type_error"
|
4
|
+
require_relative "../type"
|
5
|
+
|
6
|
+
module Sheetah
|
7
|
+
module Types
|
8
|
+
module Composites
|
9
|
+
class Composite < Type
|
10
|
+
def initialize(types, **opts)
|
11
|
+
super(**opts)
|
12
|
+
|
13
|
+
@types = types
|
14
|
+
end
|
15
|
+
|
16
|
+
def composite?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def scalar(index, value, messenger)
|
21
|
+
if (type = @types[index])
|
22
|
+
type.scalar(nil, value, messenger)
|
23
|
+
else
|
24
|
+
raise Errors::TypeError, "Invalid index: #{index.inspect}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
alias composite cast
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../errors/type_error"
|
4
|
+
|
5
|
+
require_relative "scalars/scalar"
|
6
|
+
require_relative "scalars/string"
|
7
|
+
require_relative "scalars/email"
|
8
|
+
require_relative "scalars/boolsy"
|
9
|
+
require_relative "scalars/date_string"
|
10
|
+
require_relative "composites/array"
|
11
|
+
require_relative "composites/array_compact"
|
12
|
+
|
13
|
+
module Sheetah
|
14
|
+
module Types
|
15
|
+
class Container
|
16
|
+
scalar = Scalars::Scalar.new!
|
17
|
+
string = Scalars::String.new!
|
18
|
+
email = Scalars::Email.new!
|
19
|
+
boolsy = Scalars::Boolsy.new!
|
20
|
+
date_string = Scalars::DateString.new!
|
21
|
+
|
22
|
+
DEFAULTS = {
|
23
|
+
scalars: {
|
24
|
+
scalar: -> { scalar },
|
25
|
+
string: -> { string },
|
26
|
+
email: -> { email },
|
27
|
+
boolsy: -> { boolsy },
|
28
|
+
date_string: -> { date_string },
|
29
|
+
}.freeze,
|
30
|
+
composites: {
|
31
|
+
array: ->(types) { Composites::Array.new!(types) },
|
32
|
+
array_compact: ->(types) { Composites::ArrayCompact.new!(types) },
|
33
|
+
}.freeze,
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
def initialize(scalars: nil, composites: nil, defaults: DEFAULTS)
|
37
|
+
@scalars =
|
38
|
+
(scalars ? defaults[:scalars].merge(scalars) : defaults[:scalars]).freeze
|
39
|
+
|
40
|
+
@composites =
|
41
|
+
(composites ? defaults[:composites].merge(composites) : defaults[:composites]).freeze
|
42
|
+
end
|
43
|
+
|
44
|
+
def scalars
|
45
|
+
@scalars.keys
|
46
|
+
end
|
47
|
+
|
48
|
+
def composites
|
49
|
+
@composites.keys
|
50
|
+
end
|
51
|
+
|
52
|
+
def scalar(scalar_name)
|
53
|
+
builder = fetch_scalar_builder(scalar_name)
|
54
|
+
|
55
|
+
builder.call
|
56
|
+
end
|
57
|
+
|
58
|
+
def composite(composite_name, scalar_names)
|
59
|
+
builder = fetch_composite_builder(composite_name)
|
60
|
+
|
61
|
+
scalars = scalar_names.map { |scalar_name| scalar(scalar_name) }
|
62
|
+
|
63
|
+
builder.call(scalars)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def fetch_scalar_builder(type)
|
69
|
+
@scalars.fetch(type) do
|
70
|
+
raise Errors::TypeError, "Invalid scalar type: #{type.inspect}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def fetch_composite_builder(type)
|
75
|
+
@composites.fetch(type) do
|
76
|
+
raise Errors::TypeError, "Invalid composite type: #{type.inspect}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../messaging/messages/must_be_boolsy"
|
4
|
+
require_relative "../cast"
|
5
|
+
|
6
|
+
module Sheetah
|
7
|
+
module Types
|
8
|
+
module Scalars
|
9
|
+
class BoolsyCast
|
10
|
+
include Cast
|
11
|
+
|
12
|
+
TRUTHY = [].freeze
|
13
|
+
FALSY = [].freeze
|
14
|
+
private_constant :TRUTHY, :FALSY
|
15
|
+
|
16
|
+
def initialize(truthy: TRUTHY, falsy: FALSY, **)
|
17
|
+
@truthy = truthy
|
18
|
+
@falsy = falsy
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(value, _messenger)
|
22
|
+
if @truthy.include?(value)
|
23
|
+
true
|
24
|
+
elsif @falsy.include?(value)
|
25
|
+
false
|
26
|
+
else
|
27
|
+
throw :failure, Messaging::Messages::MustBeBoolsy.new(
|
28
|
+
code_data: { value: value.inspect }
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require_relative "../../messaging/messages/must_be_date"
|
5
|
+
require_relative "../cast"
|
6
|
+
|
7
|
+
module Sheetah
|
8
|
+
module Types
|
9
|
+
module Scalars
|
10
|
+
class DateStringCast
|
11
|
+
include Cast
|
12
|
+
|
13
|
+
DATE_FMT = "%Y-%m-%d"
|
14
|
+
private_constant :DATE_FMT
|
15
|
+
|
16
|
+
def initialize(date_fmt: DATE_FMT, accept_date: true, **)
|
17
|
+
@date_fmt = date_fmt
|
18
|
+
@accept_date = accept_date
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(value, _messenger)
|
22
|
+
case value
|
23
|
+
when ::Date
|
24
|
+
return value if @accept_date
|
25
|
+
when ::String
|
26
|
+
date = parse_date_string(value)
|
27
|
+
return date if date
|
28
|
+
end
|
29
|
+
|
30
|
+
throw :failure, Messaging::Messages::MustBeDate.new(code_data: { format: @date_fmt })
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def parse_date_string(value)
|
36
|
+
::Date.strptime(value, @date_fmt)
|
37
|
+
rescue ::TypeError, ::Date::Error
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|