csv_plus_plus 0.1.2 → 0.2.0

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