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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +202 -0
  3. data/README.md +43 -0
  4. data/VERSION +1 -0
  5. data/lib/sheetah/attribute.rb +60 -0
  6. data/lib/sheetah/attribute_types/composite.rb +57 -0
  7. data/lib/sheetah/attribute_types/scalar.rb +58 -0
  8. data/lib/sheetah/attribute_types/value.rb +62 -0
  9. data/lib/sheetah/attribute_types/value.rb.orig +68 -0
  10. data/lib/sheetah/attribute_types.rb +49 -0
  11. data/lib/sheetah/backends/csv.rb +92 -0
  12. data/lib/sheetah/backends/wrapper.rb +57 -0
  13. data/lib/sheetah/backends/xlsx.rb +80 -0
  14. data/lib/sheetah/backends.rb +11 -0
  15. data/lib/sheetah/column.rb +31 -0
  16. data/lib/sheetah/errors/error.rb +8 -0
  17. data/lib/sheetah/errors/spec_error.rb +10 -0
  18. data/lib/sheetah/errors/type_error.rb +10 -0
  19. data/lib/sheetah/frozen.rb +9 -0
  20. data/lib/sheetah/headers.rb +96 -0
  21. data/lib/sheetah/messaging/config.rb +19 -0
  22. data/lib/sheetah/messaging/constants.rb +17 -0
  23. data/lib/sheetah/messaging/message.rb +70 -0
  24. data/lib/sheetah/messaging/message_variant.rb +47 -0
  25. data/lib/sheetah/messaging/messages/cleaned_string.rb +18 -0
  26. data/lib/sheetah/messaging/messages/duplicated_header.rb +21 -0
  27. data/lib/sheetah/messaging/messages/invalid_header.rb +21 -0
  28. data/lib/sheetah/messaging/messages/missing_column.rb +21 -0
  29. data/lib/sheetah/messaging/messages/must_be_array.rb +18 -0
  30. data/lib/sheetah/messaging/messages/must_be_boolsy.rb +21 -0
  31. data/lib/sheetah/messaging/messages/must_be_date.rb +21 -0
  32. data/lib/sheetah/messaging/messages/must_be_email.rb +21 -0
  33. data/lib/sheetah/messaging/messages/must_be_string.rb +18 -0
  34. data/lib/sheetah/messaging/messages/must_exist.rb +18 -0
  35. data/lib/sheetah/messaging/messages/sheet_error.rb +18 -0
  36. data/lib/sheetah/messaging/messenger.rb +133 -0
  37. data/lib/sheetah/messaging/validations/base_validator.rb +43 -0
  38. data/lib/sheetah/messaging/validations/dsl.rb +31 -0
  39. data/lib/sheetah/messaging/validations/invalid_message.rb +12 -0
  40. data/lib/sheetah/messaging/validations/mixins.rb +57 -0
  41. data/lib/sheetah/messaging/validations.rb +35 -0
  42. data/lib/sheetah/messaging.rb +22 -0
  43. data/lib/sheetah/row_processor.rb +41 -0
  44. data/lib/sheetah/row_processor_result.rb +20 -0
  45. data/lib/sheetah/row_value_builder.rb +53 -0
  46. data/lib/sheetah/sheet/col_converter.rb +62 -0
  47. data/lib/sheetah/sheet.rb +107 -0
  48. data/lib/sheetah/sheet_processor.rb +61 -0
  49. data/lib/sheetah/sheet_processor_result.rb +18 -0
  50. data/lib/sheetah/specification.rb +30 -0
  51. data/lib/sheetah/template.rb +85 -0
  52. data/lib/sheetah/template_config.rb +35 -0
  53. data/lib/sheetah/types/cast.rb +20 -0
  54. data/lib/sheetah/types/cast_chain.rb +49 -0
  55. data/lib/sheetah/types/composites/array.rb +16 -0
  56. data/lib/sheetah/types/composites/array_compact.rb +13 -0
  57. data/lib/sheetah/types/composites/composite.rb +32 -0
  58. data/lib/sheetah/types/container.rb +81 -0
  59. data/lib/sheetah/types/scalars/boolsy.rb +12 -0
  60. data/lib/sheetah/types/scalars/boolsy_cast.rb +35 -0
  61. data/lib/sheetah/types/scalars/date_string.rb +12 -0
  62. data/lib/sheetah/types/scalars/date_string_cast.rb +43 -0
  63. data/lib/sheetah/types/scalars/email.rb +12 -0
  64. data/lib/sheetah/types/scalars/email_cast.rb +28 -0
  65. data/lib/sheetah/types/scalars/scalar.rb +29 -0
  66. data/lib/sheetah/types/scalars/scalar_cast.rb +49 -0
  67. data/lib/sheetah/types/scalars/string.rb +18 -0
  68. data/lib/sheetah/types/type.rb +103 -0
  69. data/lib/sheetah/utils/cell_string_cleaner.rb +29 -0
  70. data/lib/sheetah/utils/monadic_result.rb +174 -0
  71. data/lib/sheetah.rb +31 -0
  72. metadata +118 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../message_variant"
4
+
5
+ module Sheetah
6
+ module Messaging
7
+ module Messages
8
+ class MustBeDate < MessageVariant
9
+ CODE = "must_be_date"
10
+
11
+ def_validator do
12
+ cell
13
+
14
+ def validate_code_data(message)
15
+ message.code_data in { format: String }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../message_variant"
4
+
5
+ module Sheetah
6
+ module Messaging
7
+ module Messages
8
+ class MustBeEmail < MessageVariant
9
+ CODE = "must_be_email"
10
+
11
+ def_validator do
12
+ cell
13
+
14
+ def validate_code_data(message)
15
+ message.code_data in { value: String }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../message_variant"
4
+
5
+ module Sheetah
6
+ module Messaging
7
+ module Messages
8
+ class MustBeString < MessageVariant
9
+ CODE = "must_be_string"
10
+
11
+ def_validator do
12
+ cell
13
+ nil_code_data
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../message_variant"
4
+
5
+ module Sheetah
6
+ module Messaging
7
+ module Messages
8
+ class MustExist < MessageVariant
9
+ CODE = "must_exist"
10
+
11
+ def_validator do
12
+ cell
13
+ nil_code_data
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../message_variant"
4
+
5
+ module Sheetah
6
+ module Messaging
7
+ module Messages
8
+ class SheetError < MessageVariant
9
+ CODE = "sheet_error"
10
+
11
+ def_validator do
12
+ sheet
13
+ nil_code_data
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+
5
+ module Sheetah
6
+ module Messaging
7
+ class Messenger
8
+ def initialize(
9
+ scope: SCOPES::SHEET,
10
+ scope_data: nil,
11
+ validate_messages: Messaging.config.validate_messages
12
+ )
13
+ @scope = scope.freeze
14
+ @scope_data = scope_data.freeze
15
+ @messages = []
16
+ @validate_messages = validate_messages
17
+ end
18
+
19
+ attr_reader :scope, :scope_data, :messages, :validate_messages
20
+
21
+ def ==(other)
22
+ other.is_a?(self.class) &&
23
+ scope == other.scope &&
24
+ scope_data == other.scope_data &&
25
+ messages == other.messages &&
26
+ validate_messages == other.validate_messages
27
+ end
28
+
29
+ def dup
30
+ self.class.new(
31
+ scope: @scope,
32
+ scope_data: @scope_data,
33
+ validate_messages: @validate_messages
34
+ )
35
+ end
36
+
37
+ def scoping!(scope, scope_data, &block)
38
+ scope = scope.freeze
39
+ scope_data = scope_data.freeze
40
+
41
+ if block
42
+ replace_scoping_block(scope, scope_data, &block)
43
+ else
44
+ replace_scoping_noblock(scope, scope_data)
45
+ end
46
+ end
47
+
48
+ def scoping(...)
49
+ dup.scoping!(...)
50
+ end
51
+
52
+ def scope_row!(row, &block)
53
+ scope = case @scope
54
+ when SCOPES::COL, SCOPES::CELL
55
+ SCOPES::CELL
56
+ else
57
+ SCOPES::ROW
58
+ end
59
+
60
+ scope_data = @scope_data.dup || {}
61
+ scope_data[:row] = row
62
+
63
+ scoping!(scope, scope_data, &block)
64
+ end
65
+
66
+ def scope_col!(col, &block)
67
+ scope = case @scope
68
+ when SCOPES::ROW, SCOPES::CELL
69
+ SCOPES::CELL
70
+ else
71
+ SCOPES::COL
72
+ end
73
+
74
+ scope_data = @scope_data.dup || {}
75
+ scope_data[:col] = col
76
+
77
+ scoping!(scope, scope_data, &block)
78
+ end
79
+
80
+ def scope_row(...)
81
+ dup.scope_row!(...)
82
+ end
83
+
84
+ def scope_col(...)
85
+ dup.scope_col!(...)
86
+ end
87
+
88
+ def warn(message)
89
+ add(message, severity: SEVERITIES::WARN)
90
+ end
91
+
92
+ def error(message)
93
+ add(message, severity: SEVERITIES::ERROR)
94
+ end
95
+
96
+ private
97
+
98
+ def add(message, severity:)
99
+ message.scope = @scope
100
+ message.scope_data = @scope_data
101
+ message.severity = severity
102
+
103
+ message.validate if @validate_messages
104
+
105
+ messages << message
106
+
107
+ self
108
+ end
109
+
110
+ def replace_scoping_noblock(new_scope, new_scope_data)
111
+ @scope = new_scope
112
+ @scope_data = new_scope_data
113
+
114
+ self
115
+ end
116
+
117
+ def replace_scoping_block(new_scope, new_scope_data)
118
+ prev_scope = @scope
119
+ prev_scope_data = @scope_data
120
+
121
+ @scope = new_scope
122
+ @scope_data = new_scope_data
123
+
124
+ begin
125
+ yield self
126
+ ensure
127
+ @scope = prev_scope
128
+ @scope_data = prev_scope_data
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dsl"
4
+ require_relative "invalid_message"
5
+
6
+ module Sheetah
7
+ module Messaging
8
+ module Validations
9
+ class BaseValidator
10
+ extend DSL
11
+
12
+ def validate(message)
13
+ errors = []
14
+
15
+ errors << "code" unless validate_code(message)
16
+ errors << "code_data" unless validate_code_data(message)
17
+ errors << "scope" unless validate_scope(message)
18
+ errors << "scope_data" unless validate_scope_data(message)
19
+
20
+ return if errors.empty?
21
+
22
+ raise InvalidMessage, "#{errors.join(", ")} <#{message.class}>#{message.to_h}"
23
+ end
24
+
25
+ def validate_code(_message)
26
+ true
27
+ end
28
+
29
+ def validate_code_data(_message)
30
+ true
31
+ end
32
+
33
+ def validate_scope(_message)
34
+ true
35
+ end
36
+
37
+ def validate_scope_data(_message)
38
+ true
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mixins"
4
+
5
+ module Sheetah
6
+ module Messaging
7
+ module Validations
8
+ module DSL
9
+ def cell
10
+ include Mixins::CellValidations
11
+ end
12
+
13
+ def col
14
+ include Mixins::ColValidations
15
+ end
16
+
17
+ def row
18
+ include Mixins::RowValidations
19
+ end
20
+
21
+ def sheet
22
+ include Mixins::SheetValidations
23
+ end
24
+
25
+ def nil_code_data
26
+ include Mixins::NilCodeData
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../errors/error"
4
+
5
+ module Sheetah
6
+ module Messaging
7
+ module Validations
8
+ class InvalidMessage < Errors::Error
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../constants"
4
+
5
+ module Sheetah
6
+ module Messaging
7
+ module Validations
8
+ module Mixins
9
+ module CellValidations
10
+ def validate_scope(message)
11
+ message.scope == SCOPES::CELL
12
+ end
13
+
14
+ def validate_scope_data(message)
15
+ message.scope_data in { col: String, row: Integer }
16
+ end
17
+ end
18
+
19
+ module ColValidations
20
+ def validate_scope(message)
21
+ message.scope == SCOPES::COL
22
+ end
23
+
24
+ def validate_scope_data(message)
25
+ message.scope_data in { col: String }
26
+ end
27
+ end
28
+
29
+ module RowValidations
30
+ def validate_scope(message)
31
+ message.scope == SCOPES::ROW
32
+ end
33
+
34
+ def validate_scope_data(message)
35
+ message.scope_data in { row: Integer }
36
+ end
37
+ end
38
+
39
+ module SheetValidations
40
+ def validate_scope(message)
41
+ message.scope == SCOPES::SHEET
42
+ end
43
+
44
+ def validate_scope_data(message)
45
+ message.scope_data.nil?
46
+ end
47
+ end
48
+
49
+ module NilCodeData
50
+ def validate_code_data(message)
51
+ message.code_data.nil?
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "validations/base_validator"
4
+
5
+ module Sheetah
6
+ module Messaging
7
+ module Validations
8
+ module ClassMethods
9
+ def def_validator(base: validator&.class || BaseValidator, &block)
10
+ @validator = Class.new(base, &block).new.freeze
11
+ end
12
+
13
+ def validator
14
+ if defined?(@validator)
15
+ @validator
16
+ elsif superclass.respond_to?(:validator)
17
+ superclass.validator
18
+ end
19
+ end
20
+
21
+ def validate(message)
22
+ validator&.validate(message)
23
+ end
24
+ end
25
+
26
+ def self.included(message_class)
27
+ message_class.extend(ClassMethods)
28
+ end
29
+
30
+ def validate
31
+ self.class.validate(self)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sheetah
4
+ module Messaging
5
+ require_relative "messaging/config"
6
+ require_relative "messaging/constants"
7
+ require_relative "messaging/message"
8
+ require_relative "messaging/messenger"
9
+
10
+ class << self
11
+ attr_accessor :config
12
+
13
+ def configure
14
+ config = self.config.dup
15
+ yield config
16
+ self.config = config.freeze
17
+ end
18
+ end
19
+
20
+ self.config = Config.new.freeze
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "row_processor_result"
4
+ require_relative "row_value_builder"
5
+
6
+ module Sheetah
7
+ class RowProcessor
8
+ def initialize(headers:, messenger:)
9
+ @headers = headers
10
+ @messenger = messenger
11
+ end
12
+
13
+ def call(row)
14
+ messenger = @messenger.dup
15
+
16
+ builder = RowValueBuilder.new(messenger)
17
+
18
+ messenger.scope_row!(row.row) do
19
+ @headers.each do |header|
20
+ cell = row.value[header.row_value_index]
21
+
22
+ messenger.scope_col!(cell.col) do
23
+ builder.add(header.column, cell.value)
24
+ end
25
+ end
26
+ end
27
+
28
+ build_result(row, builder, messenger)
29
+ end
30
+
31
+ private
32
+
33
+ def build_result(row, builder, messenger)
34
+ RowProcessorResult.new(
35
+ row: row.row,
36
+ result: builder.result,
37
+ messages: messenger.messages
38
+ )
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sheetah
4
+ class RowProcessorResult
5
+ def initialize(row:, result:, messages: [])
6
+ @row = row
7
+ @result = result
8
+ @messages = messages
9
+ end
10
+
11
+ attr_reader :row, :result, :messages
12
+
13
+ def ==(other)
14
+ other.is_a?(self.class) &&
15
+ row == other.row &&
16
+ result == other.result &&
17
+ messages == other.messages
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require_relative "utils/monadic_result"
5
+
6
+ module Sheetah
7
+ class RowValueBuilder
8
+ include Utils::MonadicResult
9
+
10
+ def initialize(messenger)
11
+ @messenger = messenger
12
+ @data = {}
13
+ @composites = Set.new
14
+ @failure = false
15
+ end
16
+
17
+ def add(column, value)
18
+ key = column.key
19
+ type = column.type
20
+ index = column.index
21
+
22
+ result = type.scalar(index, value, @messenger)
23
+
24
+ result.bind do |scalar|
25
+ if type.composite?
26
+ @composites << [key, type]
27
+ @data[key] ||= []
28
+ @data[key][index] = scalar
29
+ else
30
+ @data[key] = scalar
31
+ end
32
+ end
33
+
34
+ result.or { @failure = true }
35
+
36
+ result
37
+ end
38
+
39
+ def result
40
+ return Failure() if @failure
41
+
42
+ Do() do
43
+ @composites.each do |key, type|
44
+ value = type.composite(@data[key], @messenger).unwrap
45
+
46
+ @data[key] = value
47
+ end
48
+
49
+ Success(@data)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sheetah
4
+ module Sheet
5
+ class ColConverter
6
+ CHARSET = ("A".."Z").to_a.freeze
7
+ CHARSET_SIZE = CHARSET.size
8
+ CHAR_TO_INT = CHARSET.map.with_index(1).to_h.freeze
9
+ INT_TO_CHAR = CHAR_TO_INT.invert.freeze
10
+
11
+ def col2int(col)
12
+ raise ArgumentError unless col.is_a?(String) && !col.empty?
13
+
14
+ int = 0
15
+
16
+ col.each_char.reverse_each.with_index do |char, pow|
17
+ int += char2int(char) * (CHARSET_SIZE**pow)
18
+ end
19
+
20
+ int
21
+ end
22
+
23
+ def int2col(int)
24
+ raise ArgumentError unless int.is_a?(Integer) && int.positive?
25
+
26
+ x = int
27
+ y = CHARSET_SIZE
28
+ col = +""
29
+
30
+ until x.zero?
31
+ q, r = x.divmod(y)
32
+
33
+ if r.zero?
34
+ q -= 1
35
+ r = y
36
+ end
37
+
38
+ x = q
39
+
40
+ col << int2char(r)
41
+ end
42
+
43
+ col.reverse!
44
+ col.freeze
45
+ end
46
+
47
+ private
48
+
49
+ def char2int(char)
50
+ CHAR_TO_INT[char] || raise(ArgumentError, char.inspect)
51
+ end
52
+
53
+ def int2char(int)
54
+ INT_TO_CHAR[int] || raise(ArgumentError, int.inspect)
55
+ end
56
+ end
57
+
58
+ private_constant :ColConverter
59
+
60
+ COL_CONVERTER = ColConverter.new.freeze
61
+ end
62
+ end