tabulard 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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