csv_plus_plus 0.1.0 → 0.1.2

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