csv_plus_plus 0.1.1 → 0.1.3

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -63
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +17 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +112 -0
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +44 -17
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +42 -11
  9. data/lib/csv_plus_plus/compiler.rb +178 -0
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +50 -0
  11. data/lib/csv_plus_plus/entities/boolean.rb +40 -0
  12. data/lib/csv_plus_plus/entities/builtins.rb +58 -0
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +231 -0
  14. data/lib/csv_plus_plus/entities/date.rb +63 -0
  15. data/lib/csv_plus_plus/entities/entity.rb +50 -0
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +45 -0
  18. data/lib/csv_plus_plus/entities/function_call.rb +50 -0
  19. data/lib/csv_plus_plus/entities/number.rb +48 -0
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -0
  21. data/lib/csv_plus_plus/entities/string.rb +42 -0
  22. data/lib/csv_plus_plus/entities/variable.rb +37 -0
  23. data/lib/csv_plus_plus/entities.rb +40 -0
  24. data/lib/csv_plus_plus/error/error.rb +20 -0
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +37 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +75 -0
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +69 -0
  28. data/lib/csv_plus_plus/error/syntax_error.rb +71 -0
  29. data/lib/csv_plus_plus/error/writer_error.rb +17 -0
  30. data/lib/csv_plus_plus/error.rb +10 -2
  31. data/lib/csv_plus_plus/google_api_client.rb +11 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +17 -6
  34. data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
  35. data/lib/csv_plus_plus/lexer.rb +24 -0
  36. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +18 -0
  37. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  38. data/lib/csv_plus_plus/modifier/expand.rb +61 -0
  39. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  40. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  41. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  42. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  43. data/lib/csv_plus_plus/modifier.rb +82 -150
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +25 -25
  46. data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +86 -95
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +478 -0
  48. data/lib/csv_plus_plus/row.rb +53 -15
  49. data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
  50. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
  51. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  52. data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
  53. data/lib/csv_plus_plus/runtime/references.rb +110 -0
  54. data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
  55. data/lib/csv_plus_plus/runtime.rb +42 -0
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +63 -36
  58. data/lib/csv_plus_plus/version.rb +2 -1
  59. data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
  60. data/lib/csv_plus_plus/writer/csv.rb +11 -9
  61. data/lib/csv_plus_plus/writer/excel.rb +9 -2
  62. data/lib/csv_plus_plus/writer/file_backer_upper.rb +7 -4
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +88 -45
  64. data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
  65. data/lib/csv_plus_plus/writer/open_document.rb +6 -1
  66. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -33
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +41 -15
  69. metadata +44 -30
  70. data/lib/csv_plus_plus/code_section.rb +0 -101
  71. data/lib/csv_plus_plus/expand.rb +0 -18
  72. data/lib/csv_plus_plus/graph.rb +0 -62
  73. data/lib/csv_plus_plus/language/ast_builder.rb +0 -68
  74. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
  75. data/lib/csv_plus_plus/language/builtins.rb +0 -46
  76. data/lib/csv_plus_plus/language/compiler.rb +0 -152
  77. data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
  78. data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
  79. data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
  80. data/lib/csv_plus_plus/language/entities/function.rb +0 -35
  81. data/lib/csv_plus_plus/language/entities/function_call.rb +0 -37
  82. data/lib/csv_plus_plus/language/entities/number.rb +0 -36
  83. data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
  84. data/lib/csv_plus_plus/language/entities/string.rb +0 -31
  85. data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/language/entities.rb +0 -28
  87. data/lib/csv_plus_plus/language/references.rb +0 -70
  88. data/lib/csv_plus_plus/language/runtime.rb +0 -205
  89. data/lib/csv_plus_plus/language/scope.rb +0 -192
  90. data/lib/csv_plus_plus/language/syntax_error.rb +0 -66
  91. data/lib/csv_plus_plus/modifier.tab.rb +0 -907
  92. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -56
  93. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -1,205 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'entities'
4
- require_relative 'syntax_error'
5
- require 'tempfile'
6
-
7
- module CSVPlusPlus
8
- module Language
9
- # The runtime state of the compiler (the current +line_number+/+row_index+, +cell+ being processed, etc). We take
10
- # multiple runs through the input file for parsing so it's really convenient to have a central place for these
11
- # things to be managed.
12
- #
13
- # @attr_reader filename [String, nil] The filename that the input came from (mostly used for debugging since
14
- # +filename+ can be +nil+ if it's read from stdin.
15
- # @attr_reader length_of_code_section [Integer] The length (count of lines) of the code section part of the original
16
- # input.
17
- # @attr_reader length_of_csv_section [Integer] The length (count of lines) of the CSV part of the original csvpp
18
- # input.
19
- # @attr_reader length_of_original_file [Integer] The length (count of lines) of the original csvpp input.
20
- #
21
- # @attr cell [Cell] The current cell being processed
22
- # @attr cell_index [Integer] The index of the current cell being processed (starts at 0)
23
- # @attr row_index [Integer] The index of the current row being processed (starts at 0)
24
- # @attr line_number [Integer] The line number of the original csvpp template (starts at 1)
25
- class Runtime
26
- attr_reader :filename, :length_of_code_section, :length_of_csv_section, :length_of_original_file
27
-
28
- attr_accessor :cell, :cell_index, :row_index, :line_number
29
-
30
- # @param input [String] The input to be parsed
31
- # @param filename [String, nil] The filename that the input came from (mostly used for debugging since +filename+
32
- # can be +nil+ if it's read from stdin
33
- def initialize(input:, filename:)
34
- @filename = filename || 'stdin'
35
-
36
- init_input!(input)
37
- start!
38
- end
39
-
40
- # Map over an a csvpp file and keep track of line_number and row_index
41
- #
42
- # @param lines [Array]
43
- #
44
- # @return [Array]
45
- def map_lines(lines, &block)
46
- @line_number = 1
47
- lines.map do |line|
48
- block.call(line).tap { next_line! }
49
- end
50
- end
51
-
52
- # Map over a single row and keep track of the cell and it's index
53
- #
54
- # @param row [Array<Cell>] The row to map each cell over
55
- #
56
- # @return [Array]
57
- def map_row(row, &block)
58
- @cell_index = 0
59
- row.map.with_index do |cell, index|
60
- set_cell!(cell, index)
61
- block.call(cell, index)
62
- end
63
- end
64
-
65
- # Map over all rows and keep track of row and line numbers
66
- #
67
- # @param rows [Array<Row>] The rows to map over (and keep track of indexes)
68
- # @param cells_too [boolean] If the cells of each +row+ should be iterated over also.
69
- #
70
- # @return [Array]
71
- def map_rows(rows, cells_too: false, &block)
72
- @row_index = 0
73
- map_lines(rows) do |row|
74
- if cells_too
75
- # it's either CSV or a Row object
76
- map_row((row.is_a?(::CSVPlusPlus::Row) ? row.cells : row), &block)
77
- else
78
- block.call(row)
79
- end
80
- end
81
- end
82
-
83
- # Increment state to the next line
84
- #
85
- # @return [Integer]
86
- def next_line!
87
- @row_index += 1 unless @row_index.nil?
88
- @line_number += 1
89
- end
90
-
91
- # Return the current spreadsheet row number. It parallels +@row_index+ but starts at 1.
92
- #
93
- # @return [Integer, nil]
94
- def rownum
95
- return if @row_index.nil?
96
-
97
- @row_index + 1
98
- end
99
-
100
- # Set the current cell and index
101
- #
102
- # @param cell [Cell] The current cell
103
- # @param cell_index [Integer] The index of the cell
104
- def set_cell!(cell, cell_index)
105
- @cell = cell
106
- @cell_index = cell_index
107
- end
108
-
109
- # Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
110
- def start!
111
- @row_index = @cell_index = nil
112
- @line_number = 1
113
- end
114
-
115
- # Reset the runtime state starting at the CSV section
116
- def start_at_csv!
117
- # TODO: isn't the input re-written anyway without the code section? why do we need this?
118
- start!
119
- @line_number = @length_of_code_section || 1
120
- end
121
-
122
- # @return [String]
123
- def to_s
124
- "Runtime(cell: #{@cell}, row_index: #{@row_index}, cell_index: #{@cell_index})"
125
- end
126
-
127
- # Get the current (entity) value of a runtime value
128
- #
129
- # @param var_id [String, Symbol] The Variable#id of the variable being resolved.
130
- #
131
- # @return [Entity]
132
- def runtime_value(var_id)
133
- if runtime_variable?(var_id)
134
- ::CSVPlusPlus::Language::Builtins::VARIABLES[var_id.to_sym].resolve_fn.call(self)
135
- else
136
- raise_syntax_error('Undefined variable', var_id)
137
- end
138
- end
139
-
140
- # Is +var_id+ a runtime variable? (it's a static variable otherwise)
141
- #
142
- # @param var_id [String, Symbol] The Variable#id to check if it's a runtime variable
143
- #
144
- # @return [boolean]
145
- def runtime_variable?(var_id)
146
- ::CSVPlusPlus::Language::Builtins::VARIABLES.key?(var_id.to_sym)
147
- end
148
-
149
- # Called when an error is encoutered during parsing. It will construct a useful
150
- # error with the current +@row/@cell_index+, +@line_number+ and +@filename+
151
- #
152
- # @param message [String] A message relevant to why this error is being raised.
153
- # @param bad_input [String] The offending input that caused this error to be thrown.
154
- # @param wrapped_error [StandardError, nil] The underlying error that was raised (if it's not from our own logic)
155
- def raise_syntax_error(message, bad_input, wrapped_error: nil)
156
- raise(::CSVPlusPlus::Language::SyntaxError.new(message, bad_input, self, wrapped_error:))
157
- end
158
-
159
- # The currently available input for parsing. The tmp state will be re-written
160
- # between parsing the code section and the CSV section
161
- #
162
- # @return [String]
163
- def input
164
- @tmp
165
- end
166
-
167
- # We mutate the input over and over. It's ok because it's just a Tempfile
168
- #
169
- # @param data [String] The data to rewrite our input file to
170
- def rewrite_input!(data)
171
- @tmp.truncate(0)
172
- @tmp.write(data)
173
- @tmp.rewind
174
- end
175
-
176
- # Clean up the Tempfile we're using for parsing
177
- def cleanup!
178
- return unless @tmp
179
-
180
- @tmp.close
181
- @tmp.unlink
182
- @tmp = nil
183
- end
184
-
185
- private
186
-
187
- def count_code_section_lines(lines)
188
- eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
189
- lines.include?(eoc) ? (lines.take_while { |l| l != eoc }).length + 1 : 0
190
- end
191
-
192
- def init_input!(input)
193
- lines = (input || '').split(/\s*\n\s*/)
194
- @length_of_original_file = lines.length
195
- @length_of_code_section = count_code_section_lines(lines)
196
- @length_of_csv_section = @length_of_original_file - @length_of_code_section
197
-
198
- # we're gonna take our input file, write it to a tmp file then each
199
- # step is gonna mutate that tmp file
200
- @tmp = ::Tempfile.new
201
- rewrite_input!(input)
202
- end
203
- end
204
- end
205
- end
@@ -1,192 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../code_section'
4
- require_relative '../graph'
5
- require_relative './entities'
6
- require_relative './references'
7
- require_relative './syntax_error'
8
-
9
- module CSVPlusPlus
10
- module Language
11
- # A class representing the scope of the current Template and responsible for resolving variables
12
- #
13
- # @attr_reader code_section [CodeSection] The CodeSection containing variables and functions to be resolved
14
- # @attr_reader runtime [Runtime] The compiler's current runtime
15
- #
16
- # rubocop:disable Metrics/ClassLength
17
- class Scope
18
- attr_reader :code_section, :runtime
19
-
20
- # initialize with a +Runtime+ and optional +CodeSection+
21
- #
22
- # @param runtime [Runtime]
23
- # @param code_section [Runtime, nil]
24
- def initialize(runtime:, code_section: nil)
25
- @code_section = code_section if code_section
26
- @runtime = runtime
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::Language::References.extract(ast, @code_section)
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
- # Set the +code_section+ and resolve all inner dependencies in it's variables and functions.
48
- #
49
- # @param code_section [CodeSection] The code_section to be resolved
50
- def code_section=(code_section)
51
- @code_section = code_section
52
- resolve_static_variables!
53
- end
54
-
55
- # @return [String]
56
- def to_s
57
- "Scope(code_section: #{@code_section}, runtime: #{@runtime})"
58
- end
59
-
60
- private
61
-
62
- # Resolve all variable references defined statically in the code section
63
- # TODO: experiment with getting rid of this - does it even play correctly with runtime vars?
64
- def resolve_static_variables!
65
- variables = @code_section.variables
66
- last_var_dependencies = {}
67
- loop do
68
- var_dependencies, resolution_order = variable_resolution_order(only_static_vars(variables))
69
- return if var_dependencies == last_var_dependencies
70
-
71
- # TODO: make the contract better here where we're not seting the variables of another class
72
- @code_section.variables = resolve_dependencies(var_dependencies, resolution_order, variables)
73
- last_var_dependencies = var_dependencies.clone
74
- end
75
- end
76
-
77
- def only_static_vars(var_dependencies)
78
- var_dependencies.reject { |k| @runtime.runtime_variable?(k) }
79
- end
80
-
81
- def resolve_functions(ast, refs)
82
- refs.reduce(ast.dup) do |acc, elem|
83
- function_replace(acc, elem.id, resolve_function(elem.id))
84
- end
85
- end
86
-
87
- def resolve_variables(ast, refs)
88
- refs.reduce(ast.dup) do |acc, elem|
89
- variable_replace(acc, elem.id, resolve_variable(elem.id))
90
- end
91
- end
92
-
93
- # Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
94
- # rubocop:disable Metrics/MethodLength
95
- def function_replace(node, fn_id, replacement)
96
- if node.function_call? && node.id == fn_id
97
- call_function_or_runtime_value(replacement, node)
98
- elsif node.function_call?
99
- # not our function, but continue our depth first search on it
100
- ::CSVPlusPlus::Language::Entities::FunctionCall.new(
101
- node.id,
102
- node.arguments.map { |n| function_replace(n, fn_id, replacement) },
103
- infix: node.infix
104
- )
105
- else
106
- node
107
- end
108
- end
109
- # rubocop:enable Metrics/MethodLength
110
-
111
- def resolve_function(fn_id)
112
- id = fn_id.to_sym
113
- return @code_section.functions[id] if @code_section.defined_function?(id)
114
-
115
- ::CSVPlusPlus::Language::Builtins::FUNCTIONS[id]
116
- end
117
-
118
- def call_function_or_runtime_value(function_or_runtime_value, function_call)
119
- if function_or_runtime_value.function?
120
- call_function(function_or_runtime_value, function_call)
121
- else
122
- function_or_runtime_value.resolve_fn.call(@runtime, function_call.arguments)
123
- end
124
- end
125
-
126
- def call_function(function, function_call)
127
- i = 0
128
- function.arguments.reduce(function.body.dup) do |ast, argument|
129
- variable_replace(ast, argument, function_call.arguments[i]).tap do
130
- i += 1
131
- end
132
- end
133
- end
134
-
135
- # Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
136
- def variable_replace(node, var_id, replacement)
137
- if node.function_call?
138
- arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
139
- # TODO: refactor these places where we copy functions... it's brittle with the kwargs
140
- ::CSVPlusPlus::Language::Entities::FunctionCall.new(node.id, arguments, infix: node.infix)
141
- elsif node.variable? && node.id == var_id
142
- replacement
143
- else
144
- node
145
- end
146
- end
147
-
148
- def resolve_variable(var_id)
149
- id = var_id.to_sym
150
- return @code_section.variables[id] if @code_section.defined_variable?(id)
151
-
152
- # this will throw a syntax error if it doesn't exist (which is what we want)
153
- @runtime.runtime_value(id)
154
- end
155
-
156
- def check_unbound_vars(dependencies, variables)
157
- unbound_vars = dependencies.values.flatten - variables.keys
158
- return if unbound_vars.empty?
159
-
160
- @runtime.raise_syntax_error('Undefined variables', unbound_vars.map(&:to_s).join(', '))
161
- end
162
-
163
- def variable_resolution_order(variables)
164
- # we have a hash of variables => ASTs but they might have references to each other, so
165
- # we need to interpolate them first (before interpolating the cell values)
166
- var_dependencies = ::CSVPlusPlus::Graph.dependency_graph(variables, @runtime)
167
- # are there any references that we don't have variables for? (undefined variable)
168
- check_unbound_vars(var_dependencies, variables)
169
-
170
- # a topological sort will give us the order of dependencies
171
- [var_dependencies, ::CSVPlusPlus::Graph.topological_sort(var_dependencies)]
172
- # TODO: don't expose this exception directly to the caller
173
- rescue ::TSort::Cyclic
174
- @runtime.raise_syntax_error('Cyclic variable dependency detected', var_refs.keys)
175
- end
176
-
177
- def resolve_dependencies(var_dependencies, resolution_order, variables)
178
- {}.tap do |resolved_vars|
179
- # for each var and each dependency it has, build up and mutate resolved_vars
180
- resolution_order.each do |var|
181
- resolved_vars[var] = variables[var].dup
182
-
183
- var_dependencies[var].each do |dependency|
184
- resolved_vars[var] = variable_replace(resolved_vars[var], dependency, variables[dependency])
185
- end
186
- end
187
- end
188
- end
189
- end
190
- # rubocop:enable Metrics/ClassLength
191
- end
192
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CSVPlusPlus
4
- module Language
5
- # An error that can be thrown for various syntax errors
6
- class SyntaxError < ::CSVPlusPlus::Error
7
- # @param message [String] The primary message to be shown to the user
8
- # @param bad_input [String] The offending input that caused the error to be thrown
9
- # @param runtime [Runtime] The current runtime
10
- # @param wrapped_error [StandardError] The underlying error that caused the syntax error. For example a
11
- # Racc::ParseError that was thrown
12
- def initialize(message, bad_input, runtime, wrapped_error: nil)
13
- @bad_input = bad_input.to_s
14
- @runtime = runtime
15
- @wrapped_error = wrapped_error
16
- @message = message
17
-
18
- super(message)
19
- end
20
-
21
- # @return [String]
22
- def to_s
23
- to_trace
24
- end
25
-
26
- # Output a verbose user-helpful string that references the current runtime
27
- def to_verbose_trace
28
- warn(@wrapped_error.full_message) if @wrapped_error
29
- warn(@wrapped_error.backtrace) if @wrapped_error
30
- to_trace
31
- end
32
-
33
- # Output a user-helpful string that references the runtime state
34
- #
35
- # @return [String]
36
- def to_trace
37
- "#{message_prefix}#{cell_index} #{message_postfix}"
38
- end
39
-
40
- private
41
-
42
- def cell_index
43
- row_index = @runtime.row_index
44
- if @runtime.cell_index
45
- "[#{row_index},#{@runtime.cell_index}]"
46
- elsif row_index
47
- "[#{row_index}]"
48
- else
49
- ''
50
- end
51
- end
52
-
53
- def message_prefix
54
- line_number = @runtime.line_number
55
- filename = @runtime.filename
56
-
57
- line_str = line_number ? ":#{line_number}" : ''
58
- "csv++ #{filename}#{line_str}"
59
- end
60
-
61
- def message_postfix
62
- "#{@message}: \"#{@bad_input}\""
63
- end
64
- end
65
- end
66
- end