csv_plus_plus 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/README.md +18 -62
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +62 -0
  5. data/lib/csv_plus_plus/can_define_references.rb +88 -0
  6. data/lib/csv_plus_plus/can_resolve_references.rb +8 -0
  7. data/lib/csv_plus_plus/cell.rb +3 -3
  8. data/lib/csv_plus_plus/cli.rb +24 -7
  9. data/lib/csv_plus_plus/color.rb +12 -6
  10. data/lib/csv_plus_plus/compiler.rb +156 -0
  11. data/lib/csv_plus_plus/data_validation.rb +138 -0
  12. data/lib/csv_plus_plus/{language → entities}/ast_builder.rb +5 -7
  13. data/lib/csv_plus_plus/entities/boolean.rb +31 -0
  14. data/lib/csv_plus_plus/{language → entities}/builtins.rb +2 -4
  15. data/lib/csv_plus_plus/entities/cell_reference.rb +60 -0
  16. data/lib/csv_plus_plus/entities/date.rb +30 -0
  17. data/lib/csv_plus_plus/entities/entity.rb +84 -0
  18. data/lib/csv_plus_plus/entities/function.rb +33 -0
  19. data/lib/csv_plus_plus/entities/function_call.rb +35 -0
  20. data/lib/csv_plus_plus/entities/number.rb +34 -0
  21. data/lib/csv_plus_plus/entities/runtime_value.rb +26 -0
  22. data/lib/csv_plus_plus/entities/string.rb +29 -0
  23. data/lib/csv_plus_plus/entities/variable.rb +25 -0
  24. data/lib/csv_plus_plus/entities.rb +33 -0
  25. data/lib/csv_plus_plus/error/error.rb +10 -0
  26. data/lib/csv_plus_plus/error/formula_syntax_error.rb +36 -0
  27. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +27 -0
  28. data/lib/csv_plus_plus/error/modifier_validation_error.rb +49 -0
  29. data/lib/csv_plus_plus/{language → error}/syntax_error.rb +6 -14
  30. data/lib/csv_plus_plus/error/writer_error.rb +9 -0
  31. data/lib/csv_plus_plus/error.rb +9 -2
  32. data/lib/csv_plus_plus/expand.rb +3 -1
  33. data/lib/csv_plus_plus/google_api_client.rb +4 -0
  34. data/lib/csv_plus_plus/lexer/lexer.rb +19 -11
  35. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +17 -0
  36. data/lib/csv_plus_plus/modifier.rb +73 -70
  37. data/lib/csv_plus_plus/options.rb +3 -0
  38. data/lib/csv_plus_plus/parser/cell_value.tab.rb +305 -0
  39. data/lib/csv_plus_plus/parser/code_section.tab.rb +410 -0
  40. data/lib/csv_plus_plus/parser/modifier.tab.rb +484 -0
  41. data/lib/csv_plus_plus/references.rb +68 -0
  42. data/lib/csv_plus_plus/row.rb +0 -3
  43. data/lib/csv_plus_plus/runtime.rb +199 -0
  44. data/lib/csv_plus_plus/scope.rb +196 -0
  45. data/lib/csv_plus_plus/template.rb +21 -5
  46. data/lib/csv_plus_plus/validated_modifier.rb +164 -0
  47. data/lib/csv_plus_plus/version.rb +1 -1
  48. data/lib/csv_plus_plus/writer/file_backer_upper.rb +6 -4
  49. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +24 -29
  50. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +33 -12
  51. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +3 -6
  52. data/lib/csv_plus_plus.rb +41 -16
  53. metadata +34 -24
  54. data/lib/csv_plus_plus/code_section.rb +0 -68
  55. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
  56. data/lib/csv_plus_plus/language/cell_value.tab.rb +0 -332
  57. data/lib/csv_plus_plus/language/code_section.tab.rb +0 -442
  58. data/lib/csv_plus_plus/language/compiler.rb +0 -157
  59. data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
  60. data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
  61. data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
  62. data/lib/csv_plus_plus/language/entities/function.rb +0 -35
  63. data/lib/csv_plus_plus/language/entities/function_call.rb +0 -26
  64. data/lib/csv_plus_plus/language/entities/number.rb +0 -36
  65. data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
  66. data/lib/csv_plus_plus/language/entities/string.rb +0 -31
  67. data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
  68. data/lib/csv_plus_plus/language/entities.rb +0 -28
  69. data/lib/csv_plus_plus/language/references.rb +0 -70
  70. data/lib/csv_plus_plus/language/runtime.rb +0 -205
  71. data/lib/csv_plus_plus/language/scope.rb +0 -188
  72. data/lib/csv_plus_plus/modifier.tab.rb +0 -907
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a93c3ee93f2320dc717ee330c55cd3f300cfa4d155a4502b9a433cdafdcc389
4
- data.tar.gz: aa95dca43a374d57f2217030e0aefef680341549613b0388cb5df1122cb5f815
3
+ metadata.gz: d0fb79a1f7a00d60f77289518fb9c6ccac6b059024d5cf66a3f083ec92e77def
4
+ data.tar.gz: a35796b982b01171636e27b1d1a9d19d6d414249b0e4634947d5a754cb240146
5
5
  SHA512:
6
- metadata.gz: 19a827c9a5fc8f0f9fe9d58c4e6b85de821c9e09627465673c2813c2690fadc886c7d8f24e3da5402783b27347a79f7832b13c82abe4e204026d12839d6f4691
7
- data.tar.gz: b157b531d9ae9e2fda9c93ed466976deacfe639b1fbc1527b60c706be9d97792946341a767f00c32b6d35cbc6e388711980767d9ef6b5c20b4a2f44ebe7fb121
6
+ metadata.gz: 45dd804f7889d65ac5f5c63ccc131d774e7ea24097bfe9449af63a48345ca0b9f16ea9e84c17a73151c9ee8d9d90ce4b01d67b7ced817e831e66781d2e7141e0
7
+ data.tar.gz: 21cda263af6d05d5ee396a9d00b4c1c78f1a043d91787dcf0d822e1adb6a97a2326da836cdc3bbc3c78e43e5c342c1bdb9da966884f1d9d8df364e574f334ded
data/CHANGELOG.md CHANGED
@@ -1,8 +1,23 @@
1
+ ## v0.1.2
2
+
3
+ - var=... modifier which allows binding a variable to a cell
4
+ - Improved error handling and messages
5
+ - Moving in a direction that allows for the context-dependent aspects of modifiers
6
+ - Fixes a bug with creating a new excel spreadsheet
7
+ - Docs & tests
8
+
9
+ ## v0.1.1
10
+
11
+ - Better support for the various infix operators (+,-,/,*,^,%,=,<,etc)
12
+ * Previously we were converting them to their prefix equivalent (multiply, minus, concat, etc) but excel doesn't support most of those. So we keep them infix
13
+ * Didn't support some infix operators (^, %, </>/<=/>=/<>)
14
+ * Proper support for operator precedence
15
+ - When in verbose mode, print a summary of compiled functions and variables
16
+ - docs & tests
1
17
 
2
18
  ## v0.1.0
3
19
 
4
20
  - revamp of builtin functions
5
-
6
21
  - docs & tests
7
22
 
8
23
  ## v0.0.5
data/README.md CHANGED
@@ -9,101 +9,57 @@ A tool that allows you to programatically author spreadsheets in your favorite t
9
9
 
10
10
  A `csvpp` file consists of a (optional) code section and a CSV section separated by `---`. In the code section you can define variables and functions that can be used in the CSV below it. For example:
11
11
 
12
+ ###### **`mystocks.csvpp`**
12
13
  ```
13
14
  fees := 0.50 # my broker charges $0.50 a trade
14
15
 
15
- price := cellref(C)
16
- quantity := cellref(D)
16
+ price := celladjacent(C)
17
+ quantity := celladjacent(D)
17
18
 
18
19
  def profit() (price * quantity) - fees
19
20
 
20
21
  ---
21
22
  ![[format=bold/align=center]]Date,Ticker,Price,Quantity,Total,Fees
22
- ![[expand]],[[format=bold]],,,"=PROFIT()",$$fees
23
+ ![[expand]],[[format=bold]],,,"=profit()",$$fees
23
24
  ```
24
25
 
25
- ## Variables
26
-
27
- Variables can be defined in the code section by giving a name (a combination of letters, numbers and underscores ) the expression `:=` and followed with a value.
28
-
29
- ### Built-in Variables
30
-
31
- * `$$rownum` - The current row number. The first row of the spreadsheet starts at 1. Can be used anywhere and it's value will evaluate to the current row being processed.
32
-
33
- ## Functions
34
-
35
- ### Built-in Functions
36
- * `cellref(CELL)` - Returns a reference to the `CELL` relative to the current row. If the current `$$rownum` is `2`, then `CELLREF("C")` returns a reference to cell `C2`.
37
-
38
- ## Modifiers
39
-
40
- Modifiers can change the formatting of a cell or row, apply validation, change alignment, etc. All of the normal rules of CSV apply, with the addition that each cell can have modifiers (specified in `[[`/`]]` for cells and `![[`/`]]` for rows):
26
+ And can be compiled into a `.xlsx` file by:
41
27
 
42
28
  ```
43
- foo,[[...]]bar,baz
29
+ $ csv++ -n 'My Stock Tracker' -o mystocks.xlsx mystocks.csvpp
44
30
  ```
45
31
 
46
- specifying formatting or various other modifiers to the cell. Additionally a row can start with:
47
32
 
48
- ```
49
- ![[...]]foo,bar,baz
50
- ```
33
+ See the [Language Reference](./docs/LANGUAGE_REFERENCE.md) for a full explanation of features.
51
34
 
52
- which will apply that modifier to all cells in the row.
35
+ ## Installing
53
36
 
54
- ### Examples
55
-
56
- * Align the second cell left, align the last cell to the center and make it bold and italicized:
57
-
58
- ```
59
- Date,[[align=left]]Amount,Quantity,[[align=center/format=bold italic]]Price
60
- ```
61
-
62
- * Underline and center-align an entire row:
63
-
64
- ```
65
- ![[align=center/format=underline]]Date,Amount,Quantity,Price
66
- ```
67
-
68
- * A header for the first row, then some formulas that repeat for each row for the rest of the spreadsheet:
37
+ Just install it via rubygems (homebrew and debian packages are in the works):
69
38
 
70
- ```
71
- ![[align=center/format=bold]]Date,Price,Quantity,Profit
72
- ![[expand=1:]],,,"=MULTIPLY(cellref(B), cellref(C))"
73
- ```
39
+ `$ gem install csv_plus_plus`
74
40
 
75
- ## Setup (Google Sheets)
41
+ or if you want the very latest changes, clone the repository and run:
76
42
 
77
- Just install it via rubygems (homebrew and debian packages are in the works):
43
+ `$ rake gem:install`
78
44
 
79
- `$ gem install csv_plus_plus`
45
+ ### [Setting Up Google Sheets](./docs/README_GOOGLE_SHEETS.md)
80
46
 
81
- ### Publishing to Google Sheets
47
+ ## Examples
82
48
 
83
- * Go to the [GCP developers console](https://console.cloud.google.com/projectselector2/apis/credentials?pli=1&supportedpurview=project), create a service account and export keys for it to `~/.config/gcloud/application_default_credentials.json`
84
- * "Share" the spreadsheet with the email associated with the service account
49
+ Take a look at the [examples](./examples/) directory for a bunch of example `.csvpp` files.
85
50
 
86
51
  ## CLI Arguments
87
52
 
88
53
  ```
89
54
  Usage: csv++ [options]
55
+ -h, --help Show help information
90
56
  -b, --backup Create a backup of the spreadsheet before applying changes.
91
- -g, --google-sheet-id SHEET_ID The id of the sheet - you can extract this from the URL: https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0
92
57
  -c, --create Create the sheet if it doesn't exist. It will use --sheet-name if specified
58
+ -g, --google-sheet-id SHEET_ID The id of the sheet - you can extract this from the URL: https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0
93
59
  -k, --key-values KEY_VALUES A comma-separated list of key=values which will be made available to the template
94
60
  -n, --sheet-name SHEET_NAME The name of the sheet to apply the template to
61
+ -o, --output OUTPUT_FILE The file to write to (must be .csv, .ods, .xls)
95
62
  -v, --verbose Enable verbose output
96
63
  -x, --offset-columns OFFSET Apply the template offset by OFFSET cells
97
64
  -y, --offset-rows OFFSET Apply the template offset by OFFSET rows
98
- -h, --help Show help information
99
- ```
100
-
101
- ## Usage Examples
102
-
103
- ```
104
- # apply my_taxes_template.csvpp to an existing Google Sheet with name "Taxes 2022"
105
- $ csv++ --sheet-name "Taxes 2022" --sheet-id "[...]" my_taxes_template.csvpp
106
-
107
- # take input from stdin, supply a variable ($$rate = 1) and apply to the "Stocks" spreadsheet
108
- $ cat stocks.csvpp | csv++ -k "rate=1" -n "Stocks" -i "[...]"
109
65
  ```
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ # Extend a +Compiler+ class and add benchmark timings
5
+ #
6
+ # @attr_reader timings [Array<Benchmark::Tms>] +Benchmark+ timings that have been accumulated by each step of
7
+ # compilation
8
+ # @attr_reader benchmark [Benchmark] A +Benchmark+ instance
9
+ module BenchmarkedCompiler
10
+ attr_reader :benchmark, :timings
11
+
12
+ # Wrap a +Compiler+ with our instance methods that add benchmarks
13
+ def self.with_benchmarks(compiler, &block)
14
+ ::Benchmark.benchmark(::Benchmark::CAPTION, 25, ::Benchmark::FORMAT, '> Total') do |x|
15
+ # compiler = new(options:, runtime:, benchmark: x)
16
+ compiler.extend(self)
17
+ compiler.benchmark = x
18
+
19
+ block.call(compiler)
20
+
21
+ [compiler.timings.reduce(:+)]
22
+ end
23
+ end
24
+
25
+ # @param benchmark [Benchmark] A +Benchmark+ instance
26
+ def benchmark=(benchmark)
27
+ @benchmark = benchmark
28
+ @timings = []
29
+ end
30
+
31
+ # Time the Compiler#outputting! stage
32
+ def outputting!
33
+ time_stage('Writing the spreadsheet') { super }
34
+ end
35
+
36
+ protected
37
+
38
+ def parse_code_section!
39
+ time_stage('Parsing code section') { super }
40
+ end
41
+
42
+ def parse_csv_section!
43
+ time_stage('Parsing CSV section') { super }
44
+ end
45
+
46
+ def expanding
47
+ time_stage('Expanding rows') { super }
48
+ end
49
+
50
+ def resolve_all_cells!(template)
51
+ time_stage('Resolving each cell') { super(template) }
52
+ end
53
+
54
+ private
55
+
56
+ def time_stage(stage, &block)
57
+ ret = nil
58
+ @timings << @benchmark.report(stage) { ret = block.call }
59
+ ret
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ # Methods for classes that need to manage +@variables+ and +@functions+
5
+ module CanDefineReferences
6
+ # Define a (or re-define an existing) variable
7
+ #
8
+ # @param id [String, Symbol] The identifier for the variable
9
+ # @param entity [Entity] The value (entity) the variable holds
10
+ def def_variable(id, entity)
11
+ variables[id.to_sym] = entity
12
+ end
13
+
14
+ # Define (or re-define existing) variables
15
+ #
16
+ # @param variables [Hash<Symbol, Variable>] Variables to define
17
+ def def_variables(vars)
18
+ vars.each { |id, entity| def_variable(id, entity) }
19
+ end
20
+
21
+ # Define a (or re-define an existing) function
22
+ #
23
+ # @param id [String, Symbol] The identifier for the function
24
+ # @param entity [Entities::Function] The defined function
25
+ def def_function(id, entity)
26
+ functions[id.to_sym] = entity
27
+ end
28
+
29
+ # Is the variable defined?
30
+ #
31
+ # @param var_id [Symbol, String] The identifier of the variable
32
+ #
33
+ # @return [boolean]
34
+ def defined_variable?(var_id)
35
+ variables.key?(var_id.to_sym)
36
+ end
37
+
38
+ # Is the function defined?
39
+ #
40
+ # @param fn_id [Symbol, String] The identifier of the function
41
+ #
42
+ # @return [boolean]
43
+ def defined_function?(fn_id)
44
+ functions.key?(fn_id.to_sym)
45
+ end
46
+
47
+ # Provide a summary of the functions and variables compiled (to show in verbose mode)
48
+ #
49
+ # @return [String]
50
+ def verbose_summary
51
+ <<~SUMMARY
52
+ # Code Section Summary
53
+
54
+ ## Resolved Variables
55
+
56
+ #{variable_summary}
57
+
58
+ ## Functions
59
+
60
+ #{function_summary}
61
+ SUMMARY
62
+ end
63
+
64
+ private
65
+
66
+ def variables
67
+ @variables ||= {}
68
+ end
69
+
70
+ def functions
71
+ @functions ||= {}
72
+ end
73
+
74
+ def variable_summary
75
+ return '(no variables defined)' if variables.empty?
76
+
77
+ variables.map { |k, v| "#{k} := #{v}" }
78
+ .join("\n")
79
+ end
80
+
81
+ def function_summary
82
+ return '(no functions defined)' if functions.empty?
83
+
84
+ functions.map { |k, f| "#{k}: #{f}" }
85
+ .join("\n")
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ # Methods for classes that need to resolve references
5
+ module CanResolveReferences
6
+ # TODO
7
+ end
8
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './language/cell_value.tab'
4
- require_relative './modifier'
3
+ require_relative 'modifier'
4
+ require_relative 'parser/cell_value.tab'
5
5
 
6
6
  module CSVPlusPlus
7
7
  # A cell of a template
@@ -23,7 +23,7 @@ module CSVPlusPlus
23
23
  # @return [Cell]
24
24
  def self.parse(value, runtime:, modifier:)
25
25
  new(value:, row_index: runtime.row_index, index: runtime.cell_index, modifier:).tap do |c|
26
- c.ast = ::CSVPlusPlus::Language::CellValueParser.new.parse(value, runtime)
26
+ c.ast = ::CSVPlusPlus::Parser::CellValue.new.parse(value, runtime)
27
27
  end
28
28
  end
29
29
 
@@ -31,14 +31,16 @@ module CSVPlusPlus
31
31
  #
32
32
  # @param error [CSVPlusPlus::Error, Google::Apis::ClientError, StandardError]
33
33
  def handle_error(error)
34
+ # make sure that we're on a newline (verbose mode might be in the middle of printing a benchmark)
35
+ puts("\n\n") if @options.verbose
36
+
34
37
  case error
35
- when ::CSVPlusPlus::Error
38
+ when ::CSVPlusPlus::Error::Error
36
39
  handle_internal_error(error)
37
40
  when ::Google::Apis::ClientError
38
41
  handle_google_error(error)
39
42
  else
40
- # TODO: more if verbose?
41
- warn(error.message)
43
+ unhandled_error(error)
42
44
  end
43
45
  end
44
46
 
@@ -48,18 +50,33 @@ module CSVPlusPlus
48
50
  option_parser.parse!
49
51
  validate_options
50
52
  rescue ::OptionParser::InvalidOption => e
51
- raise(::CSVPlusPlus::Error, e.message)
53
+ raise(::CSVPlusPlus::Error::Error, e.message)
52
54
  end
53
55
 
54
- # @return [String]
56
+ # @return [::String]
55
57
  def to_s
56
58
  "CLI(options: #{options})"
57
59
  end
58
60
 
59
61
  private
60
62
 
63
+ # An error was thrown that we weren't planning on
64
+ def unhandled_error(error)
65
+ warn(
66
+ <<~ERROR_MESSAGE)
67
+ An unexpected error was encountered. Please try running again with --verbose and
68
+ reporting the error at: https://github.com/patrickomatic/csv-plus-plus/issues/new'
69
+ ERROR_MESSAGE
70
+
71
+ return unless @options.verbose
72
+
73
+ warn(error.full_message)
74
+ warn("Cause: #{error.cause}") if error.cause
75
+ end
76
+
61
77
  def handle_internal_error(error)
62
- if error.is_a?(::CSVPlusPlus::Language::SyntaxError)
78
+ case error
79
+ when ::CSVPlusPlus::Error::SyntaxError
63
80
  warn(@options.verbose ? error.to_verbose_trace : error.to_trace)
64
81
  else
65
82
  warn(error.message)
@@ -78,7 +95,7 @@ module CSVPlusPlus
78
95
  return if error_message.nil?
79
96
 
80
97
  puts(option_parser)
81
- raise(::CSVPlusPlus::Error, error_message)
98
+ raise(::CSVPlusPlus::Error::Error, error_message)
82
99
  end
83
100
 
84
101
  def option_parser
@@ -9,13 +9,19 @@ module CSVPlusPlus
9
9
  class Color
10
10
  attr_reader :red_hex, :green_hex, :blue_hex
11
11
 
12
- # create an instance from a string like "#FFF" or "#FFFFFF"
12
+ HEX_STRING_REGEXP = /^#?([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/i
13
+ public_constant :HEX_STRING_REGEXP
14
+
15
+ # @return [boolean]
16
+ def self.valid_hex_string?(hex_string)
17
+ !(hex_string.strip =~ ::CSVPlusPlus::Color::HEX_STRING_REGEXP).nil?
18
+ end
19
+
20
+ # Create an instance from a string like "#FFF" or "#FFFFFF"
13
21
  #
14
22
  # @param hex_string [String] The hex string input to parse
15
23
  def initialize(hex_string)
16
- @red_hex, @green_hex, @blue_hex = hex_string
17
- .gsub(/^#?/, '')
18
- .match(/([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/i)
24
+ @red_hex, @green_hex, @blue_hex = hex_string.strip.match(::CSVPlusPlus::Color::HEX_STRING_REGEXP)
19
25
  &.captures
20
26
  &.map { |s| s.length == 1 ? s + s : s }
21
27
  end
@@ -43,12 +49,12 @@ module CSVPlusPlus
43
49
 
44
50
  # Create a hex representation of the color (without a '#')
45
51
  #
46
- # @return [String]
52
+ # @return [::String]
47
53
  def to_hex
48
54
  [@red_hex, @green_hex, @blue_hex].join
49
55
  end
50
56
 
51
- # @return [String]
57
+ # @return [::String]
52
58
  def to_s
53
59
  "Color(r: #{@red_hex}, g: #{@green_hex}, b: #{@blue_hex})"
54
60
  end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'benchmarked_compiler'
4
+ require_relative 'entities'
5
+ require_relative 'parser/code_section.tab'
6
+ require_relative 'runtime'
7
+ require_relative 'scope'
8
+
9
+ module CSVPlusPlus
10
+ # Encapsulates the parsing and building of objects (+Template+ -> +Row+ -> +Cell+). Variable resolution is delegated
11
+ # to the +Scope+
12
+ #
13
+ # @attr_reader options [Options] The +Options+ to compile with
14
+ # @attr_reader runtime [Runtime] The runtime execution
15
+ # @attr_reader scope [Scope] +Scope+ for variable resolution
16
+ class Compiler
17
+ attr_reader :options, :runtime, :scope
18
+
19
+ # Create a compiler and make sure it gets cleaned up
20
+ #
21
+ # @param runtime [Runtime] The initial +Runtime+ for the compiler
22
+ # @param options [Options]
23
+ def self.with_compiler(runtime:, options:, &block)
24
+ compiler = new(options:, runtime:)
25
+ if options.verbose
26
+ ::CSVPlusPlus::BenchmarkedCompiler.with_benchmarks(compiler) do |c|
27
+ block.call(c)
28
+ end
29
+ else
30
+ yield(compiler)
31
+ end
32
+ ensure
33
+ runtime.cleanup!
34
+ end
35
+
36
+ # @param runtime [Runtime]
37
+ # @param options [Options]
38
+ # @param scope [Scope, nil]
39
+ def initialize(runtime:, options:, scope: nil)
40
+ @options = options
41
+ @runtime = runtime
42
+ @scope = scope || ::CSVPlusPlus::Scope.new(runtime:)
43
+
44
+ # TODO: infer a type
45
+ # allow user-supplied key/values to override anything global or from the code section
46
+ @scope.def_variables(
47
+ options.key_values.transform_values { |v| ::CSVPlusPlus::Entities::String.new(v.to_s) }
48
+ )
49
+ end
50
+
51
+ # Write the compiled results
52
+ def outputting!
53
+ @runtime.start_at_csv!
54
+ yield
55
+ end
56
+
57
+ # Compile a template and return a +::CSVPlusPlus::Template+ instance ready to be written with a +Writer+
58
+ #
59
+ # @return [Template]
60
+ def compile_template
61
+ parse_code_section!
62
+ rows = parse_csv_section!
63
+
64
+ ::CSVPlusPlus::Template.new(rows:, scope: @scope).tap do |t|
65
+ t.validate_infinite_expands(@runtime)
66
+ expanding { t.expand_rows! }
67
+ resolve_all_cells!(t)
68
+ end
69
+ end
70
+
71
+ # @return [String]
72
+ def to_s
73
+ "Compiler(options: #{@options}, runtime: #{@runtime}, scope: #{@scope})"
74
+ end
75
+
76
+ protected
77
+
78
+ # Parses the input file and returns a +CodeSection+
79
+ #
80
+ # @return [CodeSection]
81
+ def parse_code_section!
82
+ @runtime.start!
83
+
84
+ # TODO: this flow can probably be refactored, it used to have more needs back when we had to
85
+ # parse and save the code_section
86
+ parsing_code_section do |input|
87
+ csv_section = ::CSVPlusPlus::Parser::CodeSection.new(@scope).parse(input, @runtime)
88
+ # TODO: call scope.resolve_static_variables?? or maybe it doesn't matter
89
+
90
+ # return the csv_section to the caller because they're gonna re-write input with it
91
+ next csv_section
92
+ end
93
+ # @scope.code_section
94
+ end
95
+
96
+ # Parse the CSV section and return an array of +Row+s
97
+ #
98
+ # @return [Array<Row>]
99
+ def parse_csv_section!
100
+ @runtime.start_at_csv!
101
+ @runtime.map_rows(::CSV.new(runtime.input)) do |csv_row|
102
+ parse_row(csv_row)
103
+ end
104
+ ensure
105
+ # we're done with the file and everything is in memory
106
+ @runtime.cleanup!
107
+ end
108
+
109
+ # Iterates through each cell of each row and resolves it's variable and function references.
110
+ #
111
+ # @param template [Template]
112
+ # @return [Array<Entity>]
113
+ def resolve_all_cells!(template)
114
+ @runtime.start_at_csv!
115
+ @runtime.map_rows(template.rows, cells_too: true) do |cell|
116
+ cell.ast = @scope.resolve_cell_value if cell.ast
117
+ end
118
+ end
119
+
120
+ # Expanding rows
121
+ def expanding
122
+ @runtime.start_at_csv!
123
+ yield
124
+ end
125
+
126
+ private
127
+
128
+ def parsing_code_section
129
+ csv_section = yield(@runtime.input.read)
130
+ @runtime.rewrite_input!(csv_section)
131
+ end
132
+
133
+ # Using the current +@runtime+ and the given +csv_row+ parse it into a +Row+ of +Cell+s
134
+ # +csv_row+ should have already been run through a CSV parser and is an array of strings
135
+ #
136
+ # @param csv_row [Array<Array<String>>]
137
+ # @return [Row]
138
+ def parse_row(csv_row)
139
+ row_modifier = ::CSVPlusPlus::ValidatedModifier.new(row_level: true)
140
+
141
+ cells = @runtime.map_row(csv_row) { |value, _cell_index| parse_cell(value, row_modifier) }
142
+
143
+ ::CSVPlusPlus::Row.new(@runtime.row_index, cells, row_modifier)
144
+ end
145
+
146
+ def parse_cell(value, row_modifier)
147
+ cell_modifier = ::CSVPlusPlus::ValidatedModifier.new
148
+ parsed_value = ::CSVPlusPlus::Parser::Modifier.new(cell_modifier:, row_modifier:, scope: @scope).parse(
149
+ value,
150
+ @runtime
151
+ )
152
+
153
+ ::CSVPlusPlus::Cell.parse(parsed_value, runtime:, modifier: cell_modifier)
154
+ end
155
+ end
156
+ end