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
@@ -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