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
@@ -0,0 +1,280 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Runtime
6
+ # Responsible for storing and resolving variables and function references
7
+ # rubocop:disable Metrics/ClassLength
8
+ class Scope
9
+ extend ::T::Sig
10
+
11
+ sig { returns(::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function]) }
12
+ attr_reader :functions
13
+
14
+ sig { returns(::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]) }
15
+ attr_reader :variables
16
+
17
+ sig do
18
+ params(
19
+ functions: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function],
20
+ variables: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]
21
+ ).void
22
+ end
23
+ # @param functions [Hash<Symbol, Function>] Pre-defined functions
24
+ # @param variables [Hash<Symbol, Entity>] Pre-defined variables
25
+ def initialize(functions: {}, variables: {})
26
+ @functions = functions
27
+ @variables = variables
28
+ end
29
+
30
+ sig { params(id: ::Symbol, entity: ::CSVPlusPlus::Entities::Entity).returns(::CSVPlusPlus::Entities::Entity) }
31
+ # Define a (or re-define an existing) variable
32
+ #
33
+ # @param id [String, Symbol] The identifier for the variable
34
+ # @param entity [Entity] The value (entity) the variable holds
35
+ #
36
+ # @return [Entity] The value of the variable (+entity+)
37
+ def def_variable(id, entity)
38
+ @variables[id] = entity
39
+ end
40
+
41
+ sig { params(vars: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]).void }
42
+ # Define (or re-define existing) variables
43
+ #
44
+ # @param vars [Hash<Symbol, Variable>] Variables to define
45
+ def def_variables(vars)
46
+ vars.each { |id, entity| def_variable(id, entity) }
47
+ end
48
+
49
+ sig do
50
+ params(id: ::Symbol, function: ::CSVPlusPlus::Entities::Function).returns(::CSVPlusPlus::Entities::Function)
51
+ end
52
+ # Define a (or re-define an existing) function
53
+ #
54
+ # @param id [Symbol] The identifier for the function
55
+ # @param function [Entities::Function] The defined function
56
+ #
57
+ # @return [Entities::Function] The defined function
58
+ def def_function(id, function)
59
+ @functions[id.to_sym] = function
60
+ end
61
+
62
+ sig { params(var_id: ::Symbol, position: ::CSVPlusPlus::Runtime::Position).returns(::T::Boolean) }
63
+ # Variables outside of an ![[expand=...] are always in scope. If it's defined within an expand then things
64
+ # get trickier because the variable is only in scope while we're processing cells within that expand.
65
+ #
66
+ # @param var_id [Symbol] The variable's identifier that we are checking if it's in scope
67
+ # @param position [Position]
68
+ #
69
+ # @return [boolean]
70
+ def in_scope?(var_id, position)
71
+ value = @variables[var_id]
72
+
73
+ return false unless value
74
+
75
+ expand = value.is_a?(::CSVPlusPlus::Entities::Reference) && value.a1_ref.scoped_to_expand
76
+ !expand || expand.position_within?(position)
77
+ end
78
+
79
+ sig { returns(::String) }
80
+ # Provide a summary of the functions and variables compiled (to show in verbose mode)
81
+ #
82
+ # @return [::String]
83
+ def verbose_summary
84
+ <<~SUMMARY
85
+ # Code Section Summary
86
+
87
+ ## Resolved Variables
88
+
89
+ #{variable_summary}
90
+
91
+ ## Functions
92
+
93
+ #{function_summary}
94
+ SUMMARY
95
+ end
96
+
97
+ sig do
98
+ params(
99
+ position: ::CSVPlusPlus::Runtime::Position,
100
+ ast: ::CSVPlusPlus::Entities::Entity,
101
+ refs: ::T::Enumerable[::CSVPlusPlus::Entities::FunctionCall]
102
+ ).returns(::CSVPlusPlus::Entities::Entity)
103
+ end
104
+ # @param position [Position
105
+ # @param ast [Entity]
106
+ # @param refs [Array<FunctionCall>]
107
+ #
108
+ # @return [Entity]
109
+ def resolve_functions(position, ast, refs)
110
+ refs.reduce(ast.dup) do |acc, elem|
111
+ function_replace(position, acc, elem.id, resolve_function(elem.id))
112
+ end
113
+ end
114
+
115
+ sig do
116
+ params(
117
+ position: ::CSVPlusPlus::Runtime::Position,
118
+ ast: ::CSVPlusPlus::Entities::Entity,
119
+ refs: ::T::Enumerable[::CSVPlusPlus::Entities::Reference]
120
+ ).returns(::CSVPlusPlus::Entities::Entity)
121
+ end
122
+ # @param position [Position]
123
+ # @param ast [Entity]
124
+ # @param refs [Array<Variable>]
125
+ #
126
+ # @return [Entity]
127
+ def resolve_variables(position, ast, refs)
128
+ refs.reduce(ast.dup) do |acc, elem|
129
+ next acc unless (id = elem.id)
130
+
131
+ variable_replace(acc, id, resolve_variable(position, id))
132
+ end
133
+ end
134
+
135
+ sig do
136
+ params(
137
+ position: ::CSVPlusPlus::Runtime::Position,
138
+ node: ::CSVPlusPlus::Entities::Entity,
139
+ fn_id: ::Symbol,
140
+ replacement: ::T.any(::CSVPlusPlus::Entities::Function, ::CSVPlusPlus::Entities::RuntimeValue)
141
+ ).returns(::CSVPlusPlus::Entities::Entity)
142
+ end
143
+ # Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
144
+ # rubocop:disable Metrics/MethodLength
145
+ def function_replace(position, node, fn_id, replacement)
146
+ if node.is_a?(::CSVPlusPlus::Entities::FunctionCall) && node.id == fn_id
147
+ call_function_or_builtin(position, replacement, node)
148
+ elsif node.is_a?(::CSVPlusPlus::Entities::FunctionCall)
149
+ # not our function, but continue our depth first search on it
150
+ ::CSVPlusPlus::Entities::FunctionCall.new(
151
+ node.id,
152
+ node.arguments.map { |n| function_replace(position, n, fn_id, replacement) },
153
+ infix: node.infix
154
+ )
155
+ else
156
+ node
157
+ end
158
+ end
159
+ # rubocop:enable Metrics/MethodLength
160
+
161
+ sig do
162
+ params(fn_id: ::Symbol)
163
+ .returns(::T.any(::CSVPlusPlus::Entities::Function, ::CSVPlusPlus::Entities::RuntimeValue))
164
+ end
165
+ # @param fn_id [Symbol]
166
+ #
167
+ # @return [Entities::Function]
168
+ def resolve_function(fn_id)
169
+ return ::T.must(@functions[fn_id]) if @functions.key?(fn_id)
170
+
171
+ builtin = ::CSVPlusPlus::Entities::Builtins::FUNCTIONS[fn_id]
172
+ raise(::CSVPlusPlus::Error::FormulaSyntaxError.new('Undefined function', bad_input: fn_id.to_s)) unless builtin
173
+
174
+ builtin
175
+ end
176
+
177
+ sig do
178
+ params(
179
+ position: ::CSVPlusPlus::Runtime::Position,
180
+ function_or_builtin: ::T.any(::CSVPlusPlus::Entities::RuntimeValue, ::CSVPlusPlus::Entities::Function),
181
+ function_call: ::CSVPlusPlus::Entities::FunctionCall
182
+ ).returns(::CSVPlusPlus::Entities::Entity)
183
+ end
184
+ # @param position [Position]
185
+ # @param function_or_builtin [Entities::Function, Entities::RuntimeValue]
186
+ # @param function_call [Entities::FunctionCall]
187
+ #
188
+ # @return [Entities::Entity]
189
+ def call_function_or_builtin(position, function_or_builtin, function_call)
190
+ if function_or_builtin.is_a?(::CSVPlusPlus::Entities::RuntimeValue)
191
+ function_or_builtin.call(position, function_call.arguments)
192
+ else
193
+ call_function(function_or_builtin, function_call)
194
+ end
195
+ end
196
+
197
+ sig do
198
+ params(
199
+ function: ::CSVPlusPlus::Entities::Function,
200
+ function_call: ::CSVPlusPlus::Entities::FunctionCall
201
+ ).returns(::CSVPlusPlus::Entities::Entity)
202
+ end
203
+ # Since functions are just built up from existing functions, a "call" is effectively replacing the variable
204
+ # references in the +@body+ with the ones being passed as arguments
205
+ #
206
+ # @param function [Entities::Function] The function being called
207
+ # @param function_call [Entities::FunctionCall] The actual call of the function
208
+ #
209
+ # @return [Entities::Entity]
210
+ def call_function(function, function_call)
211
+ i = 0
212
+ function.arguments.reduce(function.body.dup) do |ast, argument|
213
+ variable_replace(ast, argument, ::T.must(function_call.arguments[i])).tap do
214
+ i += 1
215
+ end
216
+ end
217
+ end
218
+
219
+ sig do
220
+ params(
221
+ node: ::CSVPlusPlus::Entities::Entity,
222
+ var_id: ::Symbol,
223
+ replacement: ::CSVPlusPlus::Entities::Entity
224
+ ).returns(::CSVPlusPlus::Entities::Entity)
225
+ end
226
+ # Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
227
+ def variable_replace(node, var_id, replacement)
228
+ if node.is_a?(::CSVPlusPlus::Entities::FunctionCall)
229
+ arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
230
+ # TODO: refactor these places where we copy functions... it's brittle with the kwargs
231
+ ::CSVPlusPlus::Entities::FunctionCall.new(node.id, arguments, infix: node.infix)
232
+ elsif node.is_a?(::CSVPlusPlus::Entities::Reference) && node.id == var_id
233
+ replacement
234
+ else
235
+ node
236
+ end
237
+ end
238
+
239
+ sig do
240
+ params(position: ::CSVPlusPlus::Runtime::Position, var_id: ::Symbol).returns(::CSVPlusPlus::Entities::Entity)
241
+ end
242
+ # @param position [Position]
243
+ # @param var_id [Symbol]
244
+ #
245
+ # @return [Entities::Entity]
246
+ def resolve_variable(position, var_id)
247
+ return ::T.must(@variables[var_id]) if @variables.key?(var_id)
248
+
249
+ unless ::CSVPlusPlus::Entities::Builtins.builtin_variable?(var_id)
250
+ raise(::CSVPlusPlus::Error::FormulaSyntaxError.new('Undefined variable', bad_input: var_id.to_s))
251
+ end
252
+
253
+ ::T.must(::CSVPlusPlus::Entities::Builtins::VARIABLES[var_id]).call(position, [])
254
+ end
255
+
256
+ sig { returns(::String) }
257
+ # Create a summary of all currently defined variables
258
+ #
259
+ # @return [String]
260
+ def variable_summary
261
+ return '(no variables defined)' if @variables.empty?
262
+
263
+ @variables.map { |k, v| "#{k} := #{v}" }
264
+ .join("\n")
265
+ end
266
+
267
+ sig { returns(::String) }
268
+ # Create a summary of all currently defined functions
269
+ #
270
+ # @return [String]
271
+ def function_summary
272
+ return '(no functions defined)' if @functions.empty?
273
+
274
+ @functions.map { |k, f| "#{k}: #{f}" }
275
+ .join("\n")
276
+ end
277
+ end
278
+ # rubocop:enable Metrics/ClassLength
279
+ end
280
+ end
@@ -1,199 +1,42 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
4
+ require_relative './runtime/graph'
5
+ require_relative './runtime/position'
6
+ require_relative './runtime/references'
7
+ require_relative './runtime/runtime'
8
+ require_relative './runtime/scope'
9
+
3
10
  module CSVPlusPlus
4
- # The runtime state of the compiler (the current +line_number+/+row_index+, +cell+ being processed, etc) for parsing
5
- # a given file. We take multiple runs through the input file for parsing so it's really convenient to have a
6
- # central place for these things to be managed.
11
+ # All functionality needed to keep track of the runtime AKA execution context. This module has a lot of
12
+ # reponsibilities:
7
13
  #
8
- # @attr_reader filename [String, nil] The filename that the input came from (mostly used for debugging since
9
- # +filename+ can be +nil+ if it's read from stdin.
10
- # @attr_reader length_of_code_section [Integer] The length (count of lines) of the code section part of the original
11
- # input.
12
- # @attr_reader length_of_csv_section [Integer] The length (count of lines) of the CSV part of the original csvpp
13
- # input.
14
- # @attr_reader length_of_original_file [Integer] The length (count of lines) of the original csvpp input.
14
+ # - variables and function resolution and scoping
15
+ # - variable & function definitions
16
+ # - keeping track of the runtime state (the current cell being processed)
17
+ # - rewriting the input file that's being parsed
15
18
  #
16
- # @attr cell [Cell] The current cell being processed
17
- # @attr cell_index [Integer] The index of the current cell being processed (starts at 0)
18
- # @attr row_index [Integer] The index of the current row being processed (starts at 0)
19
- # @attr line_number [Integer] The line number of the original csvpp template (starts at 1)
20
- class Runtime
21
- attr_reader :filename, :length_of_code_section, :length_of_csv_section, :length_of_original_file
22
-
23
- attr_accessor :cell, :cell_index, :row_index, :line_number
24
-
25
- # @param input [String] The input to be parsed
26
- # @param filename [String, nil] The filename that the input came from (mostly used for debugging since +filename+
27
- # can be +nil+ if it's read from stdin
28
- def initialize(input:, filename:)
29
- @filename = filename || 'stdin'
30
-
31
- init_input!(input)
32
- start!
33
- end
34
-
35
- # Map over an a csvpp file and keep track of line_number and row_index
36
- #
37
- # @param lines [Array]
38
- #
39
- # @return [Array]
40
- def map_lines(lines, &block)
41
- @line_number = 1
42
- lines.map do |line|
43
- block.call(line).tap { next_line! }
44
- end
45
- end
46
-
47
- # Map over a single row and keep track of the cell and it's index
48
- #
49
- # @param row [Array<Cell>] The row to map each cell over
50
- #
51
- # @return [Array]
52
- def map_row(row, &block)
53
- @cell_index = 0
54
- row.map.with_index do |cell, index|
55
- set_cell!(cell, index)
56
- block.call(cell, index)
57
- end
58
- end
59
-
60
- # Map over all rows and keep track of row and line numbers
61
- #
62
- # @param rows [Array<Row>] The rows to map over (and keep track of indexes)
63
- # @param cells_too [boolean] If the cells of each +row+ should be iterated over also.
64
- #
65
- # @return [Array]
66
- def map_rows(rows, cells_too: false, &block)
67
- @row_index = 0
68
- map_lines(rows) do |row|
69
- if cells_too
70
- # it's either CSV or a Row object
71
- map_row((row.is_a?(::CSVPlusPlus::Row) ? row.cells : row), &block)
72
- else
73
- block.call(row)
74
- end
75
- end
76
- end
77
-
78
- # Increment state to the next line
79
- #
80
- # @return [Integer]
81
- def next_line!
82
- @row_index += 1 unless @row_index.nil?
83
- @line_number += 1
84
- end
85
-
86
- # Return the current spreadsheet row number. It parallels +@row_index+ but starts at 1.
87
- #
88
- # @return [Integer, nil]
89
- def rownum
90
- return if @row_index.nil?
91
-
92
- @row_index + 1
93
- end
94
-
95
- # Set the current cell and index
96
- #
97
- # @param cell [Cell] The current cell
98
- # @param cell_index [Integer] The index of the cell
99
- def set_cell!(cell, cell_index)
100
- @cell = cell
101
- @cell_index = cell_index
102
- end
103
-
104
- # Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
105
- def start!
106
- @row_index = @cell_index = nil
107
- @line_number = 1
108
- end
109
-
110
- # Reset the runtime state starting at the CSV section
111
- def start_at_csv!
112
- # TODO: isn't the input re-written anyway without the code section? why do we need this?
113
- start!
114
- @line_number = @length_of_code_section || 1
115
- end
116
-
117
- # @return [String]
118
- def to_s
119
- "Runtime(cell: #{@cell}, row_index: #{@row_index}, cell_index: #{@cell_index})"
120
- end
121
-
122
- # Get the current (entity) value of a runtime value
123
- #
124
- # @param var_id [String, Symbol] The Variable#id of the variable being resolved.
125
- #
126
- # @return [Entity]
127
- def runtime_value(var_id)
128
- if runtime_variable?(var_id)
129
- ::CSVPlusPlus::Entities::Builtins::VARIABLES[var_id.to_sym].resolve_fn.call(self)
130
- else
131
- raise_formula_syntax_error('Undefined variable', var_id)
132
- end
133
- end
134
-
135
- # Is +var_id+ a runtime variable? (it's a static variable otherwise)
136
- #
137
- # @param var_id [String, Symbol] The Variable#id to check if it's a runtime variable
138
- #
139
- # @return [boolean]
140
- def runtime_variable?(var_id)
141
- ::CSVPlusPlus::Entities::Builtins::VARIABLES.key?(var_id.to_sym)
142
- end
143
-
144
- # Called when an error is encoutered during parsing. It will construct a useful
145
- # error with the current +@row/@cell_index+, +@line_number+ and +@filename+
146
- #
147
- # @param message [String] A message relevant to why this error is being raised.
148
- # @param bad_input [String] The offending input that caused this error to be thrown.
149
- # @param wrapped_error [StandardError, nil] The underlying error that was raised (if it's not from our own logic)
150
- def raise_formula_syntax_error(message, bad_input, wrapped_error: nil)
151
- raise(::CSVPlusPlus::Error::FormulaSyntaxError.new(message, bad_input, self, wrapped_error:))
152
- end
153
-
154
- # The currently available input for parsing. The tmp state will be re-written
155
- # between parsing the code section and the CSV section
156
- #
157
- # @return [String]
158
- def input
159
- @tmp
160
- end
161
-
162
- # We mutate the input over and over. It's ok because it's just a Tempfile
163
- #
164
- # @param data [String] The data to rewrite our input file to
165
- def rewrite_input!(data)
166
- @tmp.truncate(0)
167
- @tmp.write(data)
168
- @tmp.rewind
169
- end
170
-
171
- # Clean up the Tempfile we're using for parsing
172
- def cleanup!
173
- return unless @tmp
174
-
175
- @tmp.close
176
- @tmp.unlink
177
- @tmp = nil
178
- end
179
-
180
- private
181
-
182
- def count_code_section_lines(lines)
183
- eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
184
- lines.include?(eoc) ? (lines.take_while { |l| l != eoc }).length + 1 : 0
185
- end
186
-
187
- def init_input!(input)
188
- lines = (input || '').split(/\s*\n\s*/)
189
- @length_of_original_file = lines.length
190
- @length_of_code_section = count_code_section_lines(lines)
191
- @length_of_csv_section = @length_of_original_file - @length_of_code_section
192
-
193
- # we're gonna take our input file, write it to a tmp file then each
194
- # step is gonna mutate that tmp file
195
- @tmp = ::Tempfile.new
196
- rewrite_input!(input)
19
+ module Runtime
20
+ extend ::T::Sig
21
+
22
+ sig do
23
+ params(
24
+ source_code: ::CSVPlusPlus::SourceCode,
25
+ position: ::T.nilable(::CSVPlusPlus::Runtime::Position),
26
+ scope: ::T.nilable(::CSVPlusPlus::Runtime::Scope)
27
+ ).returns(::CSVPlusPlus::Runtime::Runtime)
28
+ end
29
+ # Initialize a runtime instance with all the functionality we need. A runtime is one-to-one with a file being
30
+ # compiled.
31
+ #
32
+ # @param source_code [SourceCode] The csv++ source code to be compiled
33
+ # @param position [Position, nil]
34
+ # @param scope [Scope, nil]
35
+ #
36
+ # @return [Runtime::Runtime]
37
+ def self.new(source_code:, position: nil, scope: nil)
38
+ position ||= ::CSVPlusPlus::Runtime::Position.new(source_code.input)
39
+ ::CSVPlusPlus::Runtime::Runtime.new(source_code:, position:, scope:)
197
40
  end
198
41
  end
199
42
  end
@@ -0,0 +1,71 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ # Information about the unparsed source code
6
+ class SourceCode
7
+ extend ::T::Sig
8
+
9
+ sig { returns(::String) }
10
+ attr_reader :input
11
+
12
+ sig { returns(::Pathname) }
13
+ attr_reader :filename
14
+
15
+ sig { returns(::Integer) }
16
+ attr_reader :length_of_csv_section
17
+
18
+ sig { returns(::Integer) }
19
+ attr_reader :length_of_code_section
20
+
21
+ sig { returns(::Integer) }
22
+ attr_reader :length_of_file
23
+
24
+ sig { params(filename: ::String, input: ::T.nilable(::String)).void }
25
+ # @param filename [::String] The name of the file the source came from.
26
+ def initialize(filename, input: nil)
27
+ @filename = ::T.let(::Pathname.new(filename), ::Pathname)
28
+ @input = ::T.let(input || read_file, ::String)
29
+
30
+ lines = @input.split(/[\r\n]/)
31
+ @length_of_file = ::T.let(lines.length, ::Integer)
32
+ @length_of_code_section = ::T.let(count_code_section_lines(lines), ::Integer)
33
+ @length_of_csv_section = ::T.let(@length_of_file - @length_of_code_section, ::Integer)
34
+ end
35
+
36
+ sig { params(line_number: ::Integer).returns(::T::Boolean) }
37
+ # Does the given +line_number+ land in the code section of the file? (which includes the --- separator)
38
+ #
39
+ # @param line_number [Integer]
40
+ #
41
+ # @return [T::Boolean]
42
+ def in_code_section?(line_number)
43
+ line_number <= @length_of_code_section
44
+ end
45
+
46
+ sig { params(line_number: ::Integer).returns(::T::Boolean) }
47
+ # Does the given +line_number+ land in the CSV section of the file?
48
+ #
49
+ # @param line_number [Integer]
50
+ #
51
+ # @return [T::Boolean]
52
+ def in_csv_section?(line_number)
53
+ line_number > @length_of_code_section
54
+ end
55
+
56
+ private
57
+
58
+ sig { returns(::String) }
59
+ def read_file
60
+ raise(::CSVPlusPlus::Error::CLIError, "Source file #{@filename} does not exist") unless ::File.exist?(@filename)
61
+
62
+ ::File.read(@filename)
63
+ end
64
+
65
+ sig { params(lines: ::T::Array[::String]).returns(::Integer) }
66
+ def count_code_section_lines(lines)
67
+ eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
68
+ lines.include?(eoc) ? (lines.take_while { |l| l != eoc }).length + 1 : 0
69
+ end
70
+ end
71
+ end