csv_plus_plus 0.1.3 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -3
  3. data/docs/CHANGELOG.md +18 -0
  4. data/lib/csv_plus_plus/a1_reference.rb +202 -0
  5. data/lib/csv_plus_plus/benchmarked_compiler.rb +3 -3
  6. data/lib/csv_plus_plus/cell.rb +1 -35
  7. data/lib/csv_plus_plus/cli.rb +43 -80
  8. data/lib/csv_plus_plus/cli_flag.rb +77 -70
  9. data/lib/csv_plus_plus/color.rb +1 -1
  10. data/lib/csv_plus_plus/compiler.rb +31 -21
  11. data/lib/csv_plus_plus/entities/ast_builder.rb +11 -4
  12. data/lib/csv_plus_plus/entities/boolean.rb +16 -9
  13. data/lib/csv_plus_plus/entities/builtins.rb +68 -40
  14. data/lib/csv_plus_plus/entities/date.rb +14 -11
  15. data/lib/csv_plus_plus/entities/entity.rb +11 -29
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +18 -31
  17. data/lib/csv_plus_plus/entities/function.rb +22 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +35 -11
  19. data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
  20. data/lib/csv_plus_plus/entities/number.rb +15 -10
  21. data/lib/csv_plus_plus/entities/reference.rb +77 -0
  22. data/lib/csv_plus_plus/entities/runtime_value.rb +36 -23
  23. data/lib/csv_plus_plus/entities/string.rb +13 -10
  24. data/lib/csv_plus_plus/entities.rb +2 -18
  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 +18 -5
  28. data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -13
  29. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +10 -36
  30. data/lib/csv_plus_plus/error/modifier_validation_error.rb +6 -32
  31. data/lib/csv_plus_plus/error/positional_error.rb +15 -0
  32. data/lib/csv_plus_plus/error/writer_error.rb +1 -1
  33. data/lib/csv_plus_plus/error.rb +4 -1
  34. data/lib/csv_plus_plus/error_formatter.rb +111 -0
  35. data/lib/csv_plus_plus/google_api_client.rb +18 -8
  36. data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
  37. data/lib/csv_plus_plus/lexer/tokenizer.rb +53 -17
  38. data/lib/csv_plus_plus/lexer.rb +40 -1
  39. data/lib/csv_plus_plus/modifier/data_validation.rb +1 -1
  40. data/lib/csv_plus_plus/modifier/expand.rb +17 -0
  41. data/lib/csv_plus_plus/modifier.rb +6 -1
  42. data/lib/csv_plus_plus/options/file_options.rb +49 -0
  43. data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
  44. data/lib/csv_plus_plus/options/options.rb +102 -0
  45. data/lib/csv_plus_plus/options.rb +22 -110
  46. data/lib/csv_plus_plus/parser/cell_value.tab.rb +65 -66
  47. data/lib/csv_plus_plus/parser/code_section.tab.rb +92 -84
  48. data/lib/csv_plus_plus/parser/modifier.tab.rb +40 -30
  49. data/lib/csv_plus_plus/reader/csv.rb +50 -0
  50. data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
  51. data/lib/csv_plus_plus/reader/reader.rb +27 -0
  52. data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
  53. data/lib/csv_plus_plus/reader.rb +14 -0
  54. data/lib/csv_plus_plus/runtime/graph.rb +6 -6
  55. data/lib/csv_plus_plus/runtime/{position_tracker.rb → position.rb} +16 -5
  56. data/lib/csv_plus_plus/runtime/references.rb +32 -27
  57. data/lib/csv_plus_plus/runtime/runtime.rb +73 -67
  58. data/lib/csv_plus_plus/runtime/scope.rb +280 -0
  59. data/lib/csv_plus_plus/runtime.rb +9 -9
  60. data/lib/csv_plus_plus/source_code.rb +14 -9
  61. data/lib/csv_plus_plus/template.rb +17 -12
  62. data/lib/csv_plus_plus/version.rb +1 -1
  63. data/lib/csv_plus_plus/writer/csv.rb +32 -5
  64. data/lib/csv_plus_plus/writer/excel.rb +19 -6
  65. data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -14
  66. data/lib/csv_plus_plus/writer/google_sheets.rb +23 -129
  67. data/lib/csv_plus_plus/writer/{google_sheet_builder.rb → google_sheets_builder.rb} +39 -55
  68. data/lib/csv_plus_plus/writer/merger.rb +56 -0
  69. data/lib/csv_plus_plus/writer/open_document.rb +16 -2
  70. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +68 -43
  71. data/lib/csv_plus_plus/writer/writer.rb +42 -0
  72. data/lib/csv_plus_plus/writer.rb +58 -19
  73. data/lib/csv_plus_plus.rb +26 -14
  74. metadata +43 -18
  75. data/lib/csv_plus_plus/entities/cell_reference.rb +0 -231
  76. data/lib/csv_plus_plus/entities/variable.rb +0 -37
  77. data/lib/csv_plus_plus/error/syntax_error.rb +0 -71
  78. data/lib/csv_plus_plus/google_options.rb +0 -32
  79. data/lib/csv_plus_plus/lexer/lexer.rb +0 -89
  80. data/lib/csv_plus_plus/runtime/can_define_references.rb +0 -87
  81. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +0 -209
  82. data/lib/csv_plus_plus/writer/base_writer.rb +0 -45
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abee03db0f21f9d8c5d3cfb4ec81dbd748568f878458f355cab6db5fc458a509
4
- data.tar.gz: 6c4e95fa1c780a2d009eaeb58fd48b28c2dd96826b22be02f84d5fc8a06ad31e
3
+ metadata.gz: c29d586e678385367fe7a4a67fa66f4f0d111ceb1ace46125daaecbdd0bf7b03
4
+ data.tar.gz: aa13e49605c6448aa61cbf6d64ac39f3cfd9d8f84cb20370d8129ab5f1dd6f4b
5
5
  SHA512:
6
- metadata.gz: 59f2cb756bdfd20f95f252aa1c07fb4b994def21f07d8bc4e5a976b22f0b439fb950e1723fdb34753baf23527e5950d244f1386e9231c6318b35de3974305ba4
7
- data.tar.gz: 38b6d6d960aa17978adf2ee1724333754e7cdc86b8e41fa58522357448db8448d5441fb5961d3758af94d617b94b21b197b5e4d2d4eb2dba0cbc39da586c4413
6
+ metadata.gz: 6736fa3154f3ad44a4b3737a7d6e3b6571f6ba34226a8be4191a6cce3a53b1bcd09057b25bbc96da0ad01cc8c397e7b78df379b48fd510f881a810e8754aec65
7
+ data.tar.gz: 8160d63e5bda5417619d6a6e4e2a73b5cee744fc26b356d2c36efe3c56b93e2cfda8ed5cb128758079463542d0306d634c4804e02b7e4c56c14d9b98b0f3c1db
data/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ # Deprecated!!!
2
+
3
+ This version of csv++ is deprecated. For the current version please go to [github.com/patrickomatic/csv-plus-plus](https://github.com/patrickomatic/csv-plus-plus)
4
+
5
+ ![main](https://github.com/patrickomatic/csv-plus-plus/actions/workflows/rspec.yml/badge.svg)
1
6
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-community-brightgreen.svg)](https://rubystyle.guide)
2
7
  [![Gem Version](https://badge.fury.io/rb/csv_plus_plus.svg)](https://badge.fury.io/rb/csv_plus_plus)
3
8
 
@@ -19,8 +24,8 @@ quantity := celladjacent(D)
19
24
  def profit() (price * quantity) - fees
20
25
 
21
26
  ---
22
- ![[format=bold/align=center]]Date,Ticker,Price,Quantity,Total,Fees
23
- ![[expand]],[[format=bold]],,,"=profit()",$$fees
27
+ ![[format=bold/align=center]]Date ,Ticker ,Price ,Quantity ,Profit ,Fees
28
+ ![[expand]] ,[[format=italic]] , , ,"=profit()" ,=fees
24
29
  ```
25
30
 
26
31
  And can be compiled into a `.xlsx` file by:
@@ -45,7 +50,7 @@ or if you want the very latest changes, clone this repository and run:
45
50
 
46
51
  ## Examples
47
52
 
48
- Take a look at the [examples](./examples/) directory for a bunch of example `.csvpp` files.
53
+ Take a look at the [repository of examples](https://github.com/patrickomatic/csvpp-examples) repository for a bunch of example `.csvpp` files.
49
54
 
50
55
  ## CLI Arguments
51
56
 
@@ -58,7 +63,12 @@ Usage: csv++ [options]
58
63
  -k, --key-values KEY_VALUES A comma-separated list of key=values which will be made available to the template
59
64
  -n, --sheet-name SHEET_NAME The name of the sheet to apply the template to
60
65
  -o, --output OUTPUT_FILE The file to write to (must be .csv, .ods, .xls)
66
+ -s, --safe Do not overwrite values in the spreadsheet being written to. The default is to overwrite
61
67
  -v, --verbose Enable verbose output
62
68
  -x, --offset-columns OFFSET Apply the template offset by OFFSET cells
63
69
  -y, --offset-rows OFFSET Apply the template offset by OFFSET rows
64
70
  ```
71
+
72
+ ## See Also:
73
+
74
+ * [Supported features by output format](./docs/feature_matrix.csvpp)
data/docs/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## main
2
+
3
+ - Add a `-s`/`--safe` flag which changes the merge strategy to not overwrite existing values. If the spreadsheet being written to has values where the csvpp template wants to write, they will be overwritten otherwise.
4
+
5
+ ## v0.2.0
6
+
7
+ ### **Breaking Changes**
8
+
9
+ - Removal of the $$ operator - to dereference variables you can just reference them by name and they will be resolved if they are defined. Otherwise they will be left alone in the output
10
+
11
+ ### Non-breaking Changes
12
+
13
+ - Excel: fix the merging of existing values
14
+ - CSV: fix the merging of existing values
15
+ - Support merging in values from CSV (previously it would ignore/overwrite them)
16
+ - Allow for more generous spacing in the csv section (and reflect this in the examples)
17
+ - More type coverage
18
+
1
19
  ## v0.1.3
2
20
 
3
21
  - Proper scoping of variables defined within an expand modifier
@@ -0,0 +1,202 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ # A reference to a cell. Internally it is represented by a simple +cell_index+ and +row_index+ but there are
6
+ # functions for converting to and from A1-style formats. Supported formats are:
7
+ #
8
+ # * `1` - A reference to the entire first row
9
+ # * `A` - A reference to the entire first column
10
+ # * `A1` - A reference to the first cell (top left)
11
+ # * `A1:D10` - The range defined between A1 and D10
12
+ # * `Sheet1!B2` - Cell B2 on the sheet "Sheet1"
13
+ #
14
+ # @attr sheet_name [String, nil] The name of the sheet reference
15
+ # @attr_reader cell_index [Integer, nil] The cell index of the cell being referenced
16
+ # @attr_reader row_index [Integer, nil] The row index of the cell being referenced
17
+ # @attr_reader upper_cell_index [Integer, nil] If set, the cell reference is a range and this is the upper cell
18
+ # index of it
19
+ # @attr_reader upper_row_index [Integer, nil] If set, the cell reference is a range and this is the upper row index
20
+ # of in
21
+ # rubocop:disable Metrics/ClassLength
22
+ class A1Reference
23
+ extend ::T::Sig
24
+
25
+ sig { returns(::T.nilable(::String)) }
26
+ attr_accessor :sheet_name
27
+
28
+ sig { returns(::T.nilable(::Integer)) }
29
+ attr_reader :cell_index
30
+
31
+ sig { returns(::T.nilable(::Integer)) }
32
+ attr_reader :row_index
33
+
34
+ sig { returns(::T.nilable(::Integer)) }
35
+ attr_reader :upper_cell_index
36
+
37
+ sig { returns(::T.nilable(::Integer)) }
38
+ attr_reader :upper_row_index
39
+
40
+ sig { returns(::T.nilable(::CSVPlusPlus::Modifier::Expand)) }
41
+ attr_reader :scoped_to_expand
42
+
43
+ # TODO: this is getting gross, maybe define an actual parser
44
+ A1_NOTATION_REGEXP = /
45
+ ^
46
+ (?:
47
+ (?:
48
+ (?:'([^'\\]|\\.)*') # allow for a single-quoted sheet name
49
+ |
50
+ (\w+) # or if it's not quoted, just allow \w+
51
+ )
52
+ ! # if a sheet name is specified, it's always followed by a !
53
+ )?
54
+ ([a-zA-Z0-9]+) # the only part required - something alphanumeric
55
+ (?: :([a-zA-Z0-9]+))? # and they might make it a range
56
+ $
57
+ /x
58
+ public_constant :A1_NOTATION_REGEXP
59
+
60
+ ALPHA = ::T.let(('A'..'Z').to_a.freeze, ::T::Array[::String])
61
+ private_constant :ALPHA
62
+
63
+ sig { params(cell_reference_string: ::String).returns(::T::Boolean) }
64
+ # Does the given +cell_reference_string+ conform to a valid cell reference?
65
+ #
66
+ # {https://developers.google.com/sheets/api/guides/concepts}
67
+ #
68
+ # @param cell_reference_string [::String] The string to check if it is a valid cell reference (we assume it's in
69
+ # A1 notation but maybe can support R1C1)
70
+ #
71
+ # @return [::T::Boolean]
72
+ def self.valid_cell_reference?(cell_reference_string)
73
+ !(cell_reference_string =~ ::CSVPlusPlus::A1Reference::A1_NOTATION_REGEXP).nil?
74
+ end
75
+
76
+ sig do
77
+ params(
78
+ ref: ::T.nilable(::String),
79
+ cell_index: ::T.nilable(::Integer),
80
+ row_index: ::T.nilable(::Integer),
81
+ scoped_to_expand: ::T.nilable(::CSVPlusPlus::Modifier::Expand)
82
+ ).void
83
+ end
84
+ # Either +ref+, +cell_index+ or +row_index+ must be specified. If +ref+ is supplied it will be parsed to calculate
85
+ # +row_index+ and +cell_index.
86
+ #
87
+ # @param ref [String, nil] A raw user-inputted reference
88
+ # @param cell_index [Integer, nil] The index of the cell being referenced.
89
+ # @param row_index [Integer, nil] The index of the row being referenced.
90
+ # @param scoped_to_expand [Expand] The [[expand]] that this cell reference will be scoped to. In other words, it
91
+ # will only be able to be resolved if the position is within the bounds of the expand (it can't be referenced
92
+ # outside of the expand.)
93
+ def initialize(ref: nil, cell_index: nil, row_index: nil, scoped_to_expand: nil)
94
+ raise(::ArgumentError, 'Must specify :cell_index or :row_index') unless ref || cell_index || row_index
95
+
96
+ @scoped_to_expand = scoped_to_expand
97
+
98
+ if ref
99
+ from_a1_ref!(ref)
100
+ else
101
+ @cell_index = ::T.let(cell_index, ::T.nilable(::Integer))
102
+ @row_index = ::T.let(row_index, ::T.nilable(::Integer))
103
+
104
+ @upper_cell_index = ::T.let(nil, ::T.nilable(::Integer))
105
+ @upper_row_index = ::T.let(nil, ::T.nilable(::Integer))
106
+ end
107
+ end
108
+
109
+ sig { override.params(other: ::BasicObject).returns(::T::Boolean) }
110
+ # @param other [BasicObject]
111
+ #
112
+ # @return [boolean]
113
+ def ==(other)
114
+ case other
115
+ when self.class
116
+ @cell_index == other.cell_index && @row_index == other.row_index && @sheet_name == other.sheet_name \
117
+ && @upper_cell_index == other.upper_cell_index && @upper_row_index == other.upper_row_index
118
+ else
119
+ false
120
+ end
121
+ end
122
+
123
+ sig { params(position: ::CSVPlusPlus::Runtime::Position).returns(::T.nilable(::String)) }
124
+ # Turns index-based/X,Y coordinates into a A1 format
125
+ #
126
+ # @param position [Position]
127
+ #
128
+ # @return [::String, nil]
129
+ def to_a1_ref(position)
130
+ row_index = position_row_index(position)
131
+ return unless row_index || @cell_index
132
+
133
+ rowref = row_index ? (row_index + 1).to_s : ''
134
+ cellref = @cell_index ? to_a1_cell_ref : ''
135
+ [cellref, rowref].join
136
+ end
137
+
138
+ private
139
+
140
+ sig { params(position: ::CSVPlusPlus::Runtime::Position).returns(::T.nilable(::Integer)) }
141
+ def position_row_index(position)
142
+ @scoped_to_expand ? position.row_index : @row_index
143
+ end
144
+
145
+ sig { returns(::String) }
146
+ # Turns a cell index into an A1 reference (just the "A" part - for example 0 == 'A', 1 == 'B', 2 == 'C', etc.)
147
+ #
148
+ # @return [::String]
149
+ def to_a1_cell_ref
150
+ c = @cell_index.dup
151
+ ref = ''
152
+
153
+ while c >= 0
154
+ # rubocop:disable Lint/ConstantResolution
155
+ ref += ::T.must(ALPHA[c % 26])
156
+ # rubocop:enable Lint/ConstantResolution
157
+ c = (c / 26).floor - 1
158
+ end
159
+
160
+ ref.reverse
161
+ end
162
+
163
+ sig { params(ref: ::String).void }
164
+ def from_a1_ref!(ref)
165
+ quoted_sheet_name, unquoted_sheet_name, lower_range, upper_range = ::T.must(
166
+ ref.strip.match(
167
+ ::CSVPlusPlus::A1Reference::A1_NOTATION_REGEXP
168
+ )
169
+ ).captures
170
+
171
+ @sheet_name = quoted_sheet_name || unquoted_sheet_name
172
+
173
+ parse_lower_range!(lower_range) if lower_range
174
+ parse_upper_range!(upper_range) if upper_range
175
+ end
176
+
177
+ sig { params(lower_range: ::String).void }
178
+ def parse_lower_range!(lower_range)
179
+ cell_ref, row_ref = ::T.must(lower_range.match(/^([a-zA-Z]+)?(\d+)?$/)).captures
180
+ @cell_index = from_a1_cell_ref!(cell_ref) if cell_ref
181
+ @row_index = Integer(row_ref, 10) - 1 if row_ref
182
+ end
183
+
184
+ sig { params(upper_range: ::String).void }
185
+ # TODO: make this less redundant with the above function
186
+ def parse_upper_range!(upper_range)
187
+ cell_ref, row_ref = ::T.must(upper_range.match(/^([a-zA-Z]+)?(\d+)?$/)).captures
188
+ @upper_cell_index = from_a1_cell_ref!(cell_ref) if cell_ref
189
+ @upper_row_index = Integer(row_ref, 10) - 1 if row_ref
190
+ end
191
+
192
+ sig { params(cell_ref: ::String).returns(::Integer) }
193
+ def from_a1_cell_ref!(cell_ref)
194
+ (cell_ref.upcase.chars.reduce(0) do |cell_index, letter|
195
+ # rubocop:disable Lint/ConstantResolution
196
+ (cell_index * 26) + ::T.must(ALPHA.find_index(letter)) + 1
197
+ # rubocop:enable Lint/ConstantResolution
198
+ end) - 1
199
+ end
200
+ end
201
+ # rubocop:enable Metrics/ClassLength
202
+ end
@@ -18,7 +18,7 @@ module CSVPlusPlus
18
18
 
19
19
  sig do
20
20
  params(
21
- options: ::CSVPlusPlus::Options,
21
+ options: ::CSVPlusPlus::Options::Options,
22
22
  runtime: ::CSVPlusPlus::Runtime::Runtime,
23
23
  block: ::T.proc.params(compiler: ::CSVPlusPlus::Compiler).void
24
24
  ).void
@@ -40,7 +40,7 @@ module CSVPlusPlus
40
40
  sig do
41
41
  params(
42
42
  benchmark: ::Benchmark::Report,
43
- options: ::CSVPlusPlus::Options,
43
+ options: ::CSVPlusPlus::Options::Options,
44
44
  runtime: ::CSVPlusPlus::Runtime::Runtime
45
45
  ).void
46
46
  end
@@ -52,7 +52,7 @@ module CSVPlusPlus
52
52
  @timings = ::T.let([], ::T::Array[::Benchmark::Tms])
53
53
  end
54
54
 
55
- sig { override.params(block: ::T.proc.params(runtime: ::CSVPlusPlus::Runtime::Runtime).void).void }
55
+ sig { params(block: ::T.proc.params(position: ::CSVPlusPlus::Runtime::Position).void).void }
56
56
  # Time the Compiler#outputting! stage
57
57
  # rubocop:disable Naming/BlockForwarding
58
58
  def outputting!(&block)
@@ -4,7 +4,7 @@
4
4
  module CSVPlusPlus
5
5
  # A cell of a template
6
6
  #
7
- # @attr ast [Entity]
7
+ # @attr ast [Entity, nil] The AST of the formula in the cell (if there is one)
8
8
  # @attr row_index [Integer] The cell's row index (starts at 0)
9
9
  # @attr_reader index [Integer] The cell's index (starts at 0)
10
10
  # @attr_reader modifier [Modifier] The modifier for this cell
@@ -23,26 +23,6 @@ module CSVPlusPlus
23
23
  sig { returns(::CSVPlusPlus::Modifier::Modifier) }
24
24
  attr_reader :modifier
25
25
 
26
- sig do
27
- params(
28
- value: ::T.nilable(::String),
29
- runtime: ::CSVPlusPlus::Runtime::Runtime,
30
- modifier: ::CSVPlusPlus::Modifier::Modifier
31
- ).returns(::CSVPlusPlus::Cell)
32
- end
33
- # Parse a +value+ into a Cell object.
34
- #
35
- # @param value [String] A string value which should already have been processed through a CSV parser
36
- # @param runtime [Runtime]
37
- # @param modifier [Modifier]
38
- #
39
- # @return [Cell]
40
- def self.parse(value, runtime:, modifier:)
41
- new(value:, row_index: runtime.row_index, index: runtime.cell_index, modifier:).tap do |c|
42
- c.ast = ::T.unsafe(::CSVPlusPlus::Parser::CellValue.new).parse(value, runtime)
43
- end
44
- end
45
-
46
26
  sig do
47
27
  params(
48
28
  index: ::Integer,
@@ -66,24 +46,10 @@ module CSVPlusPlus
66
46
  # The +@value+ (cleaned up some)
67
47
  #
68
48
  # @return [::String]
69
- # TODO: is this used?
70
49
  def value
71
50
  stripped = @value&.strip
72
51
 
73
52
  stripped&.empty? ? nil : stripped
74
53
  end
75
-
76
- sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T.nilable(::String)) }
77
- # A compiled final representation of the cell. This can only happen after all cell have had variables and functions
78
- # resolved.
79
- #
80
- # @param runtime [Runtime]
81
- #
82
- # @return [::String]
83
- def evaluate(runtime)
84
- return value unless @ast
85
-
86
- "=#{@ast.evaluate(runtime)}"
87
- end
88
54
  end
89
55
  end
@@ -2,111 +2,46 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module CSVPlusPlus
5
- # Handle running the application with the given CLI flags
5
+ # Handle running the application with the supported +CLIFlag+s
6
6
  #
7
- # @attr options [Options, nil] The parsed CLI options
7
+ # @attr options [Options] The parsed CLI options
8
8
  class CLI
9
9
  extend ::T::Sig
10
10
 
11
- sig { returns(::CSVPlusPlus::Options) }
11
+ sig { returns(::CSVPlusPlus::Options::Options) }
12
12
  attr_accessor :options
13
13
 
14
+ sig { returns(::CSVPlusPlus::SourceCode) }
15
+ attr_accessor :source_code
16
+
14
17
  sig { void }
15
18
  # Handle CLI flags and launch the compiler
16
19
  #
17
20
  # @return [CLI]
18
21
  def self.launch_compiler!
19
- cli = new
20
- cli.main
22
+ new.main
21
23
  rescue ::StandardError => e
22
- ::T.must(cli).handle_error(e)
24
+ warn(e.message)
23
25
  exit(1)
24
26
  end
25
27
 
26
28
  sig { void }
27
29
  # Initialize and parse the CLI flags provided to the program
28
30
  def initialize
29
- @options = ::T.let(::CSVPlusPlus::Options.new, ::CSVPlusPlus::Options)
30
- parse_options!
31
+ opts = parse_options
32
+
33
+ @source_code = ::T.let(::CSVPlusPlus::SourceCode.new(source_code_filename), ::CSVPlusPlus::SourceCode)
34
+ @options = ::T.let(apply_options(opts), ::CSVPlusPlus::Options::Options)
31
35
  end
32
36
 
33
37
  sig { void }
34
38
  # Compile the given template using the given CLI flags
35
39
  def main
36
- ::CSVPlusPlus.apply_template_to_sheet!(::ARGF.read, ::ARGF.filename, @options)
37
- end
38
-
39
- sig { params(error: ::StandardError).void }
40
- # Nicely handle a given error. How it's handled depends on if it's our error and if @options.verbose
41
- #
42
- # @param error [CSVPlusPlus::Error, Google::Apis::ClientError, StandardError]
43
- def handle_error(error)
44
- # make sure that we're on a newline (verbose mode might be in the middle of printing a benchmark)
45
- puts("\n\n") if @options.verbose
46
-
47
- case error
48
- when ::CSVPlusPlus::Error::Error
49
- handle_internal_error(error)
50
- when ::Google::Apis::ClientError
51
- handle_google_error(error)
52
- else
53
- unhandled_error(error)
54
- end
40
+ ::CSVPlusPlus.cli_compile(source_code, options)
55
41
  end
56
42
 
57
43
  private
58
44
 
59
- sig { void }
60
- # Handle the supplied command line options, setting +@options+ or throw an error if anything is invalid
61
- def parse_options!
62
- option_parser.parse!
63
- validate_options
64
- rescue ::OptionParser::InvalidOption => e
65
- raise(::CSVPlusPlus::Error::Error, e.message)
66
- end
67
-
68
- sig { params(error: ::StandardError).void }
69
- # An error was thrown that we weren't planning on
70
- def unhandled_error(error)
71
- warn(
72
- <<~ERROR_MESSAGE)
73
- An unexpected error was encountered. Please try running again with --verbose and
74
- reporting the error at: https://github.com/patrickomatic/csv-plus-plus/issues/new'
75
- ERROR_MESSAGE
76
-
77
- return unless @options.verbose
78
-
79
- warn(error.full_message)
80
- warn("Cause: #{error.cause}") if error.cause
81
- end
82
-
83
- sig { params(error: ::CSVPlusPlus::Error::Error).void }
84
- def handle_internal_error(error)
85
- case error
86
- when ::CSVPlusPlus::Error::SyntaxError
87
- warn(@options.verbose ? error.to_verbose_trace : error.to_trace)
88
- else
89
- warn(error.message)
90
- end
91
- end
92
-
93
- sig { params(error: ::Google::Apis::ClientError).void }
94
- def handle_google_error(error)
95
- warn("Error making Google Sheets API request: #{error.message}")
96
- return unless @options.verbose
97
-
98
- warn("#{error.status_code} Error making Google API request [#{error.message}]: #{error.body}")
99
- end
100
-
101
- sig { void }
102
- def validate_options
103
- error_message = @options.validate
104
- return if error_message.nil?
105
-
106
- puts(option_parser)
107
- raise(::CSVPlusPlus::Error::Error, error_message)
108
- end
109
-
110
45
  sig { returns(::OptionParser) }
111
46
  def option_parser
112
47
  ::OptionParser.new do |parser|
@@ -115,10 +50,38 @@ module CSVPlusPlus
115
50
  exit
116
51
  end
117
52
 
118
- ::SUPPORTED_CSVPP_FLAGS.each do |f|
119
- parser.on(f.short_flag, f.long_flag, f.description) { |v| f.handler.call(@options, v) }
53
+ ::CSVPlusPlus::SUPPORTED_CSVPP_FLAGS.each do |f|
54
+ parser.on(f.short_flag, f.long_flag, f.description)
55
+ end
56
+ end
57
+ end
58
+
59
+ sig { params(opts: ::T::Hash[::Symbol, ::String]).returns(::CSVPlusPlus::Options::Options) }
60
+ def apply_options(opts)
61
+ ::CSVPlusPlus::Options.from_cli_flags(opts, source_code.filename).tap do |options|
62
+ opts.each do |key, value|
63
+ ::T.must(::CSVPlusPlus::FLAG_HANDLERS[key]).call(options, value) if ::CSVPlusPlus::FLAG_HANDLERS.key?(key)
120
64
  end
121
65
  end
122
66
  end
67
+
68
+ sig { returns(::T::Hash[::Symbol, ::String]) }
69
+ def parse_options
70
+ {}.tap do |opts|
71
+ option_parser.parse!(into: opts)
72
+ end
73
+ rescue ::OptionParser::InvalidOption => e
74
+ puts(option_parser)
75
+ raise(::CSVPlusPlus::Error::CLIError, e.message)
76
+ end
77
+
78
+ sig { returns(::String) }
79
+ # NOTE: this must be called after #parse_options, since #parse_options modifiers +ARGV+
80
+ def source_code_filename
81
+ ::ARGV.pop || raise(
82
+ ::CSVPlusPlus::Error::CLIError,
83
+ 'You must specify a source (.csvpp) file to compile as the last argument'
84
+ )
85
+ end
123
86
  end
124
87
  end