csv_plus_plus 0.1.2 → 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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -5
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +25 -0
  4. data/lib/csv_plus_plus/a1_reference.rb +202 -0
  5. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  6. data/lib/csv_plus_plus/cell.rb +29 -41
  7. data/lib/csv_plus_plus/cli.rb +53 -80
  8. data/lib/csv_plus_plus/cli_flag.rb +71 -71
  9. data/lib/csv_plus_plus/color.rb +32 -7
  10. data/lib/csv_plus_plus/compiler.rb +98 -66
  11. data/lib/csv_plus_plus/entities/ast_builder.rb +30 -39
  12. data/lib/csv_plus_plus/entities/boolean.rb +26 -10
  13. data/lib/csv_plus_plus/entities/builtins.rb +66 -24
  14. data/lib/csv_plus_plus/entities/date.rb +42 -6
  15. data/lib/csv_plus_plus/entities/entity.rb +17 -69
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +44 -0
  17. data/lib/csv_plus_plus/entities/function.rb +34 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +49 -10
  19. data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
  20. data/lib/csv_plus_plus/entities/number.rb +30 -11
  21. data/lib/csv_plus_plus/entities/reference.rb +77 -0
  22. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -13
  23. data/lib/csv_plus_plus/entities/string.rb +23 -7
  24. data/lib/csv_plus_plus/entities.rb +7 -16
  25. data/lib/csv_plus_plus/error/cli_error.rb +17 -0
  26. data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
  27. data/lib/csv_plus_plus/error/error.rb +25 -2
  28. data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -12
  29. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +34 -12
  30. data/lib/csv_plus_plus/error/modifier_validation_error.rb +21 -27
  31. data/lib/csv_plus_plus/error/positional_error.rb +15 -0
  32. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  33. data/lib/csv_plus_plus/error.rb +5 -1
  34. data/lib/csv_plus_plus/error_formatter.rb +111 -0
  35. data/lib/csv_plus_plus/google_api_client.rb +25 -10
  36. data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
  37. data/lib/csv_plus_plus/lexer/tokenizer.rb +58 -17
  38. data/lib/csv_plus_plus/lexer.rb +64 -1
  39. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  40. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  41. data/lib/csv_plus_plus/modifier/expand.rb +78 -0
  42. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  43. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  44. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  45. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  46. data/lib/csv_plus_plus/modifier.rb +89 -160
  47. data/lib/csv_plus_plus/options/file_options.rb +49 -0
  48. data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
  49. data/lib/csv_plus_plus/options/options.rb +97 -0
  50. data/lib/csv_plus_plus/options.rb +34 -77
  51. data/lib/csv_plus_plus/parser/cell_value.tab.rb +66 -67
  52. data/lib/csv_plus_plus/parser/code_section.tab.rb +86 -83
  53. data/lib/csv_plus_plus/parser/modifier.tab.rb +57 -53
  54. data/lib/csv_plus_plus/reader/csv.rb +50 -0
  55. data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
  56. data/lib/csv_plus_plus/reader/reader.rb +27 -0
  57. data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
  58. data/lib/csv_plus_plus/reader.rb +14 -0
  59. data/lib/csv_plus_plus/row.rb +53 -12
  60. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  61. data/lib/csv_plus_plus/runtime/position.rb +242 -0
  62. data/lib/csv_plus_plus/runtime/references.rb +115 -0
  63. data/lib/csv_plus_plus/runtime/runtime.rb +132 -0
  64. data/lib/csv_plus_plus/runtime/scope.rb +280 -0
  65. data/lib/csv_plus_plus/runtime.rb +34 -191
  66. data/lib/csv_plus_plus/source_code.rb +71 -0
  67. data/lib/csv_plus_plus/template.rb +71 -39
  68. data/lib/csv_plus_plus/version.rb +2 -1
  69. data/lib/csv_plus_plus/writer/csv.rb +37 -8
  70. data/lib/csv_plus_plus/writer/excel.rb +25 -5
  71. data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -13
  72. data/lib/csv_plus_plus/writer/google_sheets.rb +29 -85
  73. data/lib/csv_plus_plus/writer/google_sheets_builder.rb +179 -0
  74. data/lib/csv_plus_plus/writer/merger.rb +31 -0
  75. data/lib/csv_plus_plus/writer/open_document.rb +21 -2
  76. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +140 -42
  77. data/lib/csv_plus_plus/writer/writer.rb +42 -0
  78. data/lib/csv_plus_plus/writer.rb +79 -10
  79. data/lib/csv_plus_plus.rb +47 -18
  80. metadata +50 -21
  81. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  82. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  83. data/lib/csv_plus_plus/data_validation.rb +0 -138
  84. data/lib/csv_plus_plus/entities/cell_reference.rb +0 -60
  85. data/lib/csv_plus_plus/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/error/syntax_error.rb +0 -58
  87. data/lib/csv_plus_plus/expand.rb +0 -20
  88. data/lib/csv_plus_plus/google_options.rb +0 -27
  89. data/lib/csv_plus_plus/graph.rb +0 -62
  90. data/lib/csv_plus_plus/lexer/lexer.rb +0 -85
  91. data/lib/csv_plus_plus/references.rb +0 -68
  92. data/lib/csv_plus_plus/scope.rb +0 -196
  93. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  94. data/lib/csv_plus_plus/writer/base_writer.rb +0 -20
  95. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +0 -147
  96. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  97. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CSVPlusPlus
4
- # The Google-specific options a user can supply
5
- #
6
- # attr sheet_id [String] The ID of the Google Sheet to write to
7
- GoogleOptions =
8
- ::Struct.new(:sheet_id) do
9
- # Format a string with a verbose description of what we're doing with the options
10
- #
11
- # @return [String]
12
- def verbose_summary
13
- <<~SUMMARY
14
- ## Google Sheets Options
15
-
16
- > Sheet ID | #{sheet_id}
17
- SUMMARY
18
- end
19
-
20
- # @return [String]
21
- def to_s
22
- "GoogleOptions(sheet_id: #{sheet_id})"
23
- end
24
- end
25
-
26
- public_constant :GoogleOptions
27
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'tsort'
4
-
5
- module CSVPlusPlus
6
- # Graph ordering and searching functions
7
- module Graph
8
- # Get a list of all variables references in a given +ast+
9
- # TODO: this is only used in one place - refactor it
10
- def self.variable_references(ast, runtime, include_runtime_variables: false)
11
- depth_first_search(ast) do |node|
12
- next unless node.variable?
13
-
14
- node.id if !runtime.runtime_variable?(node.id) || include_runtime_variables
15
- end
16
- end
17
-
18
- # Create a dependency graph of +variables+
19
- def self.dependency_graph(variables, runtime)
20
- ::CSVPlusPlus::Graph::DependencyGraph[
21
- variables.map { |var_id, ast| [var_id, variable_references(ast, runtime)] }
22
- ]
23
- end
24
-
25
- # Perform a topological sort on a +DependencyGraph+. A toplogical sort is noteworthy
26
- # because it will give us the order in which we need to resolve our variable dependencies.
27
- #
28
- # Given this dependency graph:
29
- #
30
- # { a: [b c], b: [c], c: [d], d: [] }
31
- #
32
- # it will return:
33
- #
34
- # [d, c, b, a]
35
- #
36
- def self.topological_sort(dependencies)
37
- dependencies.tsort
38
- end
39
-
40
- # Do a DFS on an AST starting at +node+
41
- def self.depth_first_search(node, accum = [], &)
42
- ret = yield(node)
43
- accum << ret unless ret.nil?
44
-
45
- return accum unless node.function_call?
46
-
47
- node.arguments.each { |n| depth_first_search(n, accum, &) }
48
- accum
49
- end
50
-
51
- # A dependency graph represented as a +Hash+ which will be used by our +topological_sort+ function
52
- class DependencyGraph < Hash
53
- include ::TSort
54
- alias tsort_each_node each_key
55
-
56
- # sort each child
57
- def tsort_each_child(node, &)
58
- fetch(node).each(&)
59
- end
60
- end
61
- end
62
- end
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../color'
4
-
5
- module CSVPlusPlus
6
- # Common methods to be mixed into the Racc parsers
7
- #
8
- # @attr_reader tokens [Array]
9
- module Lexer
10
- attr_reader :tokens
11
-
12
- # Initialize a lexer instance with an empty +@tokens+
13
- def initialize(tokens: [])
14
- @tokens = tokens
15
- end
16
-
17
- # Used by racc to iterate each token
18
- #
19
- # @return [Array<(String, String)>]
20
- def next_token
21
- @tokens.shift
22
- end
23
-
24
- # Orchestate the tokenizing, parsing and error handling of parsing input. Each instance will implement their own
25
- # #tokenizer method
26
- #
27
- # @return [Lexer#return_value] Each instance will define it's own +return_value+ with the result of parsing
28
- def parse(input, runtime)
29
- return if input.nil?
30
-
31
- return return_value unless anything_to_parse?(input)
32
-
33
- tokenize(input, runtime)
34
- do_parse
35
- return_value
36
- rescue ::Racc::ParseError => e
37
- runtime.raise_formula_syntax_error("Error parsing #{parse_subject}", e.message, wrapped_error: e)
38
- rescue ::CSVPlusPlus::Error::ModifierValidationError => e
39
- raise(::CSVPlusPlus::Error::ModifierSyntaxError.new(runtime, wrapped_error: e))
40
- end
41
-
42
- TOKEN_LIBRARY = {
43
- A1_NOTATION: [::CSVPlusPlus::Entities::CellReference::A1_NOTATION_REGEXP, :A1_NOTATION],
44
- FALSE: [/false/i, :FALSE],
45
- HEX_COLOR: [::CSVPlusPlus::Color::HEX_STRING_REGEXP, :HEX_COLOR],
46
- ID: [/[$!\w:]+/, :ID],
47
- INFIX_OP: [%r{\^|\+|-|\*|/|&|<|>|<=|>=|<>}, :INFIX_OP],
48
- NUMBER: [/-?[\d.]+/, :NUMBER],
49
- STRING: [%r{"(?:[^"\\]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"}, :STRING],
50
- TRUE: [/true/i, :TRUE],
51
- VAR_REF: [/\$\$/, :VAR_REF]
52
- }.freeze
53
- public_constant :TOKEN_LIBRARY
54
-
55
- private
56
-
57
- def tokenize(input, runtime)
58
- return if input.nil?
59
-
60
- t = tokenizer.scan(input)
61
-
62
- until t.scanner.empty?
63
- next if t.matches_ignore?
64
-
65
- return if t.stop?
66
-
67
- t.scan_tokens!
68
- consume_token(t, runtime)
69
- end
70
-
71
- @tokens << %i[EOL EOL]
72
- end
73
-
74
- def consume_token(tokenizer, runtime)
75
- if tokenizer.last_token
76
- @tokens << [tokenizer.last_token, tokenizer.last_match]
77
- elsif tokenizer.scan_catchall
78
- @tokens << [tokenizer.last_match, tokenizer.last_match]
79
- else
80
- # TODO: this should raise a modifier_syntax_error if we're on the modifier parser
81
- runtime.raise_formula_syntax_error("Unable to parse #{parse_subject} starting at", tokenizer.peek)
82
- end
83
- end
84
- end
85
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'graph'
4
- require_relative 'scope'
5
-
6
- module CSVPlusPlus
7
- # References in an AST that need to be resolved
8
- #
9
- # @attr functions [Array<Entities::Function>] Functions references
10
- # @attr variables [Array<Entities::Variable>] Variable references
11
- class References
12
- attr_accessor :functions, :variables
13
-
14
- # Extract references from an AST and return them in a new +References+ object
15
- #
16
- # @param ast [Entity] An +Entity+ to do a depth first search on for references. Entities can be
17
- # infinitely deep because they can contain other function calls as params to a function call
18
- # @param scope [Scope] The +CodeSection+ containing all currently defined functions & variables
19
- #
20
- # @return [References]
21
- def self.extract(ast, scope)
22
- new.tap do |refs|
23
- ::CSVPlusPlus::Graph.depth_first_search(ast) do |node|
24
- next unless node.function_call? || node.variable?
25
-
26
- refs.functions << node if function_reference?(node, scope)
27
- refs.variables << node if node.variable?
28
- end
29
- end
30
- end
31
-
32
- # Is the node a resolvable reference?
33
- #
34
- # @param node [Entity] The node to check if it's resolvable
35
- #
36
- # @return [boolean]
37
- # TODO: move this into the Entity subclasses
38
- def self.function_reference?(node, scope)
39
- node.function_call? && (scope.defined_function?(node.id) \
40
- || ::CSVPlusPlus::Entities::Builtins::FUNCTIONS.key?(node.id))
41
- end
42
-
43
- private_class_method :function_reference?
44
-
45
- # Create an object with empty references. The caller will build them up as it depth-first-searches
46
- def initialize
47
- @functions = []
48
- @variables = []
49
- end
50
-
51
- # Are there any references to be resolved?
52
- #
53
- # @return [boolean]
54
- def empty?
55
- @functions.empty? && @variables.empty?
56
- end
57
-
58
- # @return [String]
59
- def to_s
60
- "References(functions: #{@functions}, variables: #{@variables})"
61
- end
62
-
63
- # @return [boolean]
64
- def ==(other)
65
- @functions == other.functions && @variables == other.variables
66
- end
67
- end
68
- end
@@ -1,196 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'can_define_references'
4
- require_relative 'entities'
5
- require_relative 'graph'
6
- require_relative 'references'
7
-
8
- module CSVPlusPlus
9
- # A class representing the scope of the current Template and responsible for resolving variables
10
- #
11
- # @attr_reader functions [Hash<Symbol, Function>] The currently functions defined
12
- # @attr_reader runtime [Runtime] The compiler's current runtime
13
- # @attr_reader variables [Hash<Symbol, Entity>] The currently defined variables
14
- #
15
- # rubocop:disable Metrics/ClassLength
16
- class Scope
17
- include ::CSVPlusPlus::CanDefineReferences
18
- # TODO: split out a CanResolveReferences
19
-
20
- attr_reader :functions, :runtime, :variables
21
-
22
- # @param runtime [Runtime]
23
- def initialize(runtime:, functions: {}, variables: {})
24
- @runtime = runtime
25
- @functions = functions
26
- @variables = variables
27
- end
28
-
29
- # Resolve all values in the ast of the current cell being processed
30
- #
31
- # @return [Entity]
32
- def resolve_cell_value
33
- return unless (ast = @runtime.cell&.ast)
34
-
35
- last_round = nil
36
- loop do
37
- refs = ::CSVPlusPlus::References.extract(ast, self)
38
- return ast if refs.empty?
39
-
40
- # TODO: throw an error here instead I think - basically we did a round and didn't make progress
41
- return ast if last_round == refs
42
-
43
- ast = resolve_functions(resolve_variables(ast, refs.variables), refs.functions)
44
- end
45
- end
46
-
47
- # Bind +var_id+ to the current cell (based on where +@runtime+ is currently pointing).
48
- #
49
- # @param var_id [Symbol] The name of the variable to bind the cell reference to
50
- #
51
- # @return [CellReference]
52
- def bind_variable_to_cell(var_id)
53
- ::CSVPlusPlus::Entities::CellReference.from_index(
54
- cell_index: runtime.cell_index,
55
- row_index: runtime.row_index
56
- ).tap do |cell_reference|
57
- def_variable(var_id, cell_reference)
58
- end
59
- end
60
-
61
- # @return [String]
62
- def to_s
63
- "Scope(functions: #{@functions}, runtime: #{@runtime}, variables: #{@variables})"
64
- end
65
-
66
- private
67
-
68
- # Resolve all variable references defined statically in the code section
69
- # TODO: experiment with getting rid of this - does it even play correctly with runtime vars?
70
- def resolve_static_variables!
71
- last_var_dependencies = {}
72
- loop do
73
- var_dependencies, resolution_order = variable_resolution_order(only_static_vars(variables))
74
- return if var_dependencies == last_var_dependencies
75
-
76
- # TODO: make the contract better here
77
- @variables = resolve_dependencies(var_dependencies, resolution_order, variables)
78
- last_var_dependencies = var_dependencies.clone
79
- end
80
- end
81
-
82
- def only_static_vars(var_dependencies)
83
- var_dependencies.reject { |k| @runtime.runtime_variable?(k) }
84
- end
85
-
86
- def resolve_functions(ast, refs)
87
- refs.reduce(ast.dup) do |acc, elem|
88
- function_replace(acc, elem.id, resolve_function(elem.id))
89
- end
90
- end
91
-
92
- def resolve_variables(ast, refs)
93
- refs.reduce(ast.dup) do |acc, elem|
94
- variable_replace(acc, elem.id, resolve_variable(elem.id))
95
- end
96
- end
97
-
98
- # Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
99
- # rubocop:disable Metrics/MethodLength
100
- def function_replace(node, fn_id, replacement)
101
- if node.function_call? && node.id == fn_id
102
- call_function_or_runtime_value(replacement, node)
103
- elsif node.function_call?
104
- # not our function, but continue our depth first search on it
105
- ::CSVPlusPlus::Entities::FunctionCall.new(
106
- node.id,
107
- node.arguments.map { |n| function_replace(n, fn_id, replacement) },
108
- infix: node.infix
109
- )
110
- else
111
- node
112
- end
113
- end
114
- # rubocop:enable Metrics/MethodLength
115
-
116
- def resolve_function(fn_id)
117
- id = fn_id.to_sym
118
- return functions[id] if defined_function?(id)
119
-
120
- ::CSVPlusPlus::Entities::Builtins::FUNCTIONS[id]
121
- end
122
-
123
- def call_function_or_runtime_value(function_or_runtime_value, function_call)
124
- if function_or_runtime_value.function?
125
- call_function(function_or_runtime_value, function_call)
126
- else
127
- function_or_runtime_value.resolve_fn.call(@runtime, function_call.arguments)
128
- end
129
- end
130
-
131
- def call_function(function, function_call)
132
- i = 0
133
- function.arguments.reduce(function.body.dup) do |ast, argument|
134
- variable_replace(ast, argument, function_call.arguments[i]).tap do
135
- i += 1
136
- end
137
- end
138
- end
139
-
140
- # Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
141
- def variable_replace(node, var_id, replacement)
142
- if node.function_call?
143
- arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
144
- # TODO: refactor these places where we copy functions... it's brittle with the kwargs
145
- ::CSVPlusPlus::Entities::FunctionCall.new(node.id, arguments, infix: node.infix)
146
- elsif node.variable? && node.id == var_id
147
- replacement
148
- else
149
- node
150
- end
151
- end
152
-
153
- def resolve_variable(var_id)
154
- id = var_id.to_sym
155
- return variables[id] if defined_variable?(id)
156
-
157
- # this will throw a syntax error if it doesn't exist (which is what we want)
158
- @runtime.runtime_value(id)
159
- end
160
-
161
- def check_unbound_vars(dependencies, variables)
162
- unbound_vars = dependencies.values.flatten - variables.keys
163
- return if unbound_vars.empty?
164
-
165
- @runtime.raise_formula_syntax_error('Undefined variables', unbound_vars.map(&:to_s).join(', '))
166
- end
167
-
168
- def variable_resolution_order(variables)
169
- # we have a hash of variables => ASTs but they might have references to each other, so
170
- # we need to interpolate them first (before interpolating the cell values)
171
- var_dependencies = ::CSVPlusPlus::Graph.dependency_graph(variables, @runtime)
172
- # are there any references that we don't have variables for? (undefined variable)
173
- check_unbound_vars(var_dependencies, variables)
174
-
175
- # a topological sort will give us the order of dependencies
176
- [var_dependencies, ::CSVPlusPlus::Graph.topological_sort(var_dependencies)]
177
- # TODO: don't expose this exception directly to the caller
178
- rescue ::TSort::Cyclic
179
- @runtime.raise_formula_syntax_error('Cyclic variable dependency detected', var_refs.keys)
180
- end
181
-
182
- def resolve_dependencies(var_dependencies, resolution_order, variables)
183
- {}.tap do |resolved_vars|
184
- # for each var and each dependency it has, build up and mutate resolved_vars
185
- resolution_order.each do |var|
186
- resolved_vars[var] = variables[var].dup
187
-
188
- var_dependencies[var].each do |dependency|
189
- resolved_vars[var] = variable_replace(resolved_vars[var], dependency, variables[dependency])
190
- end
191
- end
192
- end
193
- end
194
- end
195
- # rubocop:enable Metrics/ClassLength
196
- end
@@ -1,164 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'data_validation'
4
- require_relative 'modifier'
5
-
6
- module CSVPlusPlus
7
- # Validates and coerces modifier values as they are parsed.
8
- #
9
- # Previously this logic was handled in the parser's grammar, but with the introduction of variable binding, the
10
- # grammar is no longer context free so we need the parser to be a little looser on what it accepts and validate it
11
- # here. Having this layer is also nice because we can provide better error messages to the user for what went
12
- # wrong during the parse.
13
- class ValidatedModifier < ::CSVPlusPlus::Modifier
14
- # Validates that +border+ is 'all', 'top', 'bottom', 'left' or 'right'.
15
- #
16
- # @param value [String] The unvalidated user input
17
- def border=(value)
18
- super(one_of(:border, value, %i[all top bottom left right]))
19
- end
20
-
21
- # Validates that +bordercolor+ is a hex color.
22
- #
23
- # @param value [String] The unvalidated user input
24
- def bordercolor=(value)
25
- super(color_value(:bordercolor, value))
26
- end
27
-
28
- # Validates that +borderstyle+ is 'dashed', 'dotted', 'double', 'solid', 'solid_medium' or 'solid_thick'.
29
- #
30
- # @param value [String] The unvalidated user input
31
- def borderstyle=(value)
32
- super(one_of(:borderstyle, value, %i[dashed dotted double solid solid_medium solid_thick]))
33
- end
34
-
35
- # Validates that +color+ is a hex color.
36
- #
37
- # @param value [String] The unvalidated user input
38
- def color=(value)
39
- super(color_value(:color, value))
40
- end
41
-
42
- # Validates that +expand+ is a positive integer.
43
- #
44
- # @param value [String] The unvalidated user input
45
- def expand=(value)
46
- super(::CSVPlusPlus::Expand.new(positive_integer(:expand, value)))
47
- end
48
-
49
- # Validates that +fontcolor+ is a hex color.
50
- #
51
- # @param value [String] The unvalidated user input
52
- def fontcolor=(value)
53
- super(color_value(:fontcolor, value))
54
- end
55
-
56
- # Validates that +fontcolor+ is a hex color.
57
- def fontfamily=(value)
58
- super(matches_regexp(:fontfamily, unquote(value), /^[\w\s]+$/, 'It is not a valid font family.'))
59
- end
60
-
61
- # Validates that +fontsize+ is a positive integer
62
- #
63
- # @param value [String] The unvalidated user input
64
- def fontsize=(value)
65
- super(positive_integer(:fontsize, value))
66
- end
67
-
68
- # Validates that +format+ is 'bold', 'italic', 'strikethrough' or 'underline'.
69
- #
70
- # @param value [String] The unvalidated user input
71
- def format=(value)
72
- super(one_of(:format, value, %i[bold italic strikethrough underline]))
73
- end
74
-
75
- # Validates that +halign+ is 'left', 'center' or 'right'.
76
- #
77
- # @param value [String] The unvalidated user input
78
- def halign=(value)
79
- super(one_of(:halign, value, %i[left center right]))
80
- end
81
-
82
- # Validates that +note+ is a quoted string.
83
- #
84
- # @param value [String] The unvalidated user input
85
-
86
- # Validates that +numberformat+ is 'currency', 'date', 'date_time', 'number', 'percent', 'text', 'time' or
87
- # 'scientific'.
88
- #
89
- # @param value [String] The unvalidated user input
90
- def numberformat=(value)
91
- super(one_of(:nubmerformat, value, %i[currency date date_time number percent text time scientific]))
92
- end
93
-
94
- # Validates that +valign+ is 'top', 'center' or 'bottom'.
95
- #
96
- # @param value [String] The unvalidated user input
97
- def valign=(value)
98
- super(one_of(:valign, value, %i[top center bottom]))
99
- end
100
-
101
- # Validates that the conditional validating rules are well-formed.
102
- #
103
- # Pretty much based off of the Google Sheets API spec here:
104
- #
105
- # @param value [String] The unvalidated user input
106
- def validation=(value)
107
- super(a_data_validation(:validation, value))
108
- end
109
-
110
- # Validates +variable+ is a valid variable identifier.
111
- #
112
- # @param value [String] The unvalidated user input
113
- def var=(value)
114
- # TODO: I need a shared definition of what a variable can be (I guess the :ID token)
115
- super(matches_regexp(:var, value, /^\w+$/, 'It must be a sequence of letters, numbers and _.').to_sym)
116
- end
117
-
118
- private
119
-
120
- # XXX centralize this :(((
121
- def unquote(str)
122
- # TODO: I'm pretty sure this isn't sufficient and we need to deal with the backslashes
123
- str.gsub(/^['\s]*|['\s]*$/, '')
124
- end
125
-
126
- def a_data_validation(modifier, value)
127
- data_validation = ::CSVPlusPlus::DataValidation.new(value)
128
- return data_validation unless data_validation.valid?
129
-
130
- raise_error(modifier, value, message: data_validation.invalid_reason)
131
- end
132
-
133
- def color_value(modifier, value)
134
- unless ::CSVPlusPlus::Color.valid_hex_string?(value)
135
- raise_error(modifier, value, message: 'It must be a 3 or 6 digit hex code.')
136
- end
137
-
138
- ::CSVPlusPlus::Color.new(value)
139
- end
140
-
141
- def matches_regexp(modifier, value, regexp, message)
142
- raise_error(modifier, value, message:) unless value =~ regexp
143
- value
144
- end
145
-
146
- def one_of(modifier, value, choices)
147
- value.downcase.to_sym.tap do |v|
148
- raise_error(modifier, value, choices:) unless choices.include?(v)
149
- end
150
- end
151
-
152
- def positive_integer(modifier, value)
153
- Integer(value, 10).tap do |i|
154
- raise_error(modifier, value, message: 'It must be positive and greater than 0.') unless i.positive?
155
- end
156
- rescue ::ArgumentError
157
- raise_error(modifier, value, message: 'It must be a valid (whole) number.')
158
- end
159
-
160
- def raise_error(modifier, bad_input, choices: nil, message: nil)
161
- raise(::CSVPlusPlus::Error::ModifierValidationError.new(modifier, bad_input, choices:, message:))
162
- end
163
- end
164
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CSVPlusPlus
4
- module Writer
5
- # Some shared functionality that all Writers should build on
6
- class BaseWriter
7
- attr_accessor :options
8
-
9
- protected
10
-
11
- # Open a CSV outputter to +filename+
12
- def initialize(options)
13
- @options = options
14
- load_requires
15
- end
16
-
17
- def load_requires; end
18
- end
19
- end
20
- end