tabulard 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|