csv_plus_plus 0.1.1 → 0.1.3

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