csv_plus_plus 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -3
  3. data/docs/CHANGELOG.md +16 -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 +71 -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 +97 -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 +31 -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 +37 -12
  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: a4462a2a82490271a95479970e10246ed7d8b1e8a62d21928a5f2b710b2bd511
4
+ data.tar.gz: 31ce301945c5cc4d3154395dfa62271637baba4af78d2efed9b13d779036e015
5
5
  SHA512:
6
- metadata.gz: 59f2cb756bdfd20f95f252aa1c07fb4b994def21f07d8bc4e5a976b22f0b439fb950e1723fdb34753baf23527e5950d244f1386e9231c6318b35de3974305ba4
7
- data.tar.gz: 38b6d6d960aa17978adf2ee1724333754e7cdc86b8e41fa58522357448db8448d5441fb5961d3758af94d617b94b21b197b5e4d2d4eb2dba0cbc39da586c4413
6
+ metadata.gz: a968bc15a47a6ac129b0a4a13df3dfb1997991548f13af9ac28598091b5562be8d7d1943a03f86a1a501533ff2eb974d1e2cc788f43fca27f09be455edcdba9f
7
+ data.tar.gz: f091403bede1fb2e751fad9b6937a836f0676e5ab284690fa2c8983fa746d90e9780c701d3a7ca1161fa9aca04b84263a1b7e7789dda866d6fe1caf975593287
data/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ ![main](https://github.com/patrickomatic/csv-plus-plus/actions/workflows/rspec.yml/badge.svg)
1
2
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-community-brightgreen.svg)](https://rubystyle.guide)
2
3
  [![Gem Version](https://badge.fury.io/rb/csv_plus_plus.svg)](https://badge.fury.io/rb/csv_plus_plus)
3
4
 
@@ -19,8 +20,8 @@ quantity := celladjacent(D)
19
20
  def profit() (price * quantity) - fees
20
21
 
21
22
  ---
22
- ![[format=bold/align=center]]Date,Ticker,Price,Quantity,Total,Fees
23
- ![[expand]],[[format=bold]],,,"=profit()",$$fees
23
+ ![[format=bold/align=center]]Date ,Ticker ,Price ,Quantity ,Profit ,Fees
24
+ ![[expand]] ,[[format=italic]] , , ,"=profit()" ,=fees
24
25
  ```
25
26
 
26
27
  And can be compiled into a `.xlsx` file by:
@@ -45,7 +46,7 @@ or if you want the very latest changes, clone this repository and run:
45
46
 
46
47
  ## Examples
47
48
 
48
- Take a look at the [examples](./examples/) directory for a bunch of example `.csvpp` files.
49
+ Take a look at the [repository of examples](https://github.com/patrickomatic/csvpp-examples) repository for a bunch of example `.csvpp` files.
49
50
 
50
51
  ## CLI Arguments
51
52
 
@@ -62,3 +63,7 @@ Usage: csv++ [options]
62
63
  -x, --offset-columns OFFSET Apply the template offset by OFFSET cells
63
64
  -y, --offset-rows OFFSET Apply the template offset by OFFSET rows
64
65
  ```
66
+
67
+ ## See Also:
68
+
69
+ * [Supported features by output format](./docs/feature_matrix.csvpp)
data/docs/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## main/upcoming
2
+
3
+ ## v0.2.0
4
+
5
+ ### **Breaking Changes**
6
+
7
+ - 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
8
+
9
+ ### Non-breaking Changes
10
+
11
+ - Excel: fix the merging of existing values
12
+ - CSV: fix the merging of existing values
13
+ - Support merging in values from CSV (previously it would ignore/overwrite them)
14
+ - Allow for more generous spacing in the csv section (and reflect this in the examples)
15
+ - More type coverage
16
+
1
17
  ## v0.1.3
2
18
 
3
19
  - 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
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module CSVPlusPlus
@@ -8,83 +8,84 @@ module CSVPlusPlus
8
8
  # @attr_reader long_flag [String] A definition of the long/word-based flag
9
9
  # @attr_reader description [String] A description of what the flag does
10
10
  # @attr_reader handler [Proc(Options, String)] A proc which is called to handle when this flag is seen
11
- class CliFlag
12
- attr_reader :short_flag, :long_flag, :description, :handler
11
+ class CLIFlag
12
+ extend ::T::Sig
13
13
 
14
+ sig { returns(::String) }
15
+ attr_reader :description
16
+
17
+ sig { returns(::String) }
18
+ attr_reader :long_flag
19
+
20
+ sig { returns(::String) }
21
+ attr_reader :short_flag
22
+
23
+ sig { params(short_flag: ::String, long_flag: ::String, description: ::String).void }
14
24
  # @param short_flag [String] A definition of the short/single-character flag
15
25
  # @param long_flag [String] A definition of the long/word-based flag
16
26
  # @param description [String] A description of what the flag does
17
- # @param handler [Proc(Options, String)] A proc which is called to handle when this flag is seen
18
- def initialize(short_flag, long_flag, description, handler)
27
+ def initialize(short_flag, long_flag, description)
19
28
  @short_flag = short_flag
20
29
  @long_flag = long_flag
21
30
  @description = description
22
- @handler = handler
23
- end
24
-
25
- # @return [String]
26
- def to_s
27
- "#{@short_flag}, #{@long_flag} #{@description}"
28
31
  end
29
32
  end
30
- end
31
33
 
32
- SUPPORTED_CSVPP_FLAGS = [
33
- ::CSVPlusPlus::CliFlag.new(
34
- '-b',
35
- '--backup',
36
- 'Create a backup of the spreadsheet before applying changes.',
37
- ->(options, _v) { options.backup = true }
38
- ),
39
- ::CSVPlusPlus::CliFlag.new(
40
- '-c',
41
- '--create',
42
- "Create the sheet if it doesn't exist. It will use --sheet-name if specified",
43
- ->(options, _v) { options.create_if_not_exists = true }
44
- ),
45
- ::CSVPlusPlus::CliFlag.new(
46
- '-g SHEET_ID',
47
- '--google-sheet-id SHEET_ID',
48
- 'The id of the sheet - you can extract this from the URL: ' \
49
- 'https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0',
50
- ->(options, v) { options.google_sheet_id = v }
51
- ),
52
- ::CSVPlusPlus::CliFlag.new(
53
- '-k',
54
- '--key-values KEY_VALUES',
55
- 'A comma-separated list of key=values which will be made available to the template',
56
- lambda do |options, v|
57
- options.key_values =
58
- begin
59
- [v.split('=')].to_h
60
- rescue ::StandardError
61
- {}
62
- end
63
- end
64
- ),
65
- ::CSVPlusPlus::CliFlag.new(
66
- '-n SHEET_NAME',
67
- '--sheet-name SHEET_NAME',
68
- 'The name of the sheet to apply the template to',
69
- ->(options, v) { options.sheet_name = v }
70
- ),
71
- ::CSVPlusPlus::CliFlag.new(
72
- '-o OUTPUT_FILE',
73
- '--output OUTPUT_FILE',
74
- 'The file to write to (must be .csv, .ods, .xls)',
75
- ->(options, v) { options.output_filename = v }
76
- ),
77
- ::CSVPlusPlus::CliFlag.new('-v', '--verbose', 'Enable verbose output', ->(options, _v) { options.verbose = true }),
78
- ::CSVPlusPlus::CliFlag.new(
79
- '-x OFFSET',
80
- '--offset-columns OFFSET',
81
- 'Apply the template offset by OFFSET cells',
82
- ->(options, v) { options.offset[0] = v }
83
- ),
84
- ::CSVPlusPlus::CliFlag.new(
85
- '-y OFFSET',
86
- '--offset-rows OFFSET',
87
- 'Apply the template offset by OFFSET rows',
88
- ->(options, v) { options.offset[1] = v }
34
+ FLAG_HANDLERS = ::T.let(
35
+ {
36
+ backup: ->(options, _v) { options.backup = true },
37
+ create: ->(options, _v) { options.create_if_not_exists = true },
38
+ 'key-values': lambda { |options, v|
39
+ options.key_values =
40
+ begin
41
+ [v.split('=')].to_h
42
+ rescue ::StandardError
43
+ {}
44
+ end
45
+ },
46
+ 'offset-columns': ->(options, v) { options.offset[0] = v },
47
+ 'offset-rows': ->(options, v) { options.offset[1] = v },
48
+ output: ->(options, v) { options.output_filename = ::Pathname.new(v) },
49
+ verbose: ->(options, _v) { options.verbose = true }
50
+ },
51
+ ::T::Hash[::Symbol, ::T.proc.params(options: ::CSVPlusPlus::Options::Options, v: ::String).void]
89
52
  )
90
- ].freeze
53
+ public_constant :FLAG_HANDLERS
54
+
55
+ SUPPORTED_CSVPP_FLAGS = ::T.let(
56
+ [
57
+ ::CSVPlusPlus::CLIFlag.new('-b', '--backup', 'Create a backup of the spreadsheet before applying changes.'),
58
+ ::CSVPlusPlus::CLIFlag.new(
59
+ '-c',
60
+ '--create',
61
+ "Create the sheet if it doesn't exist. It will use --sheet-name if specified"
62
+ ),
63
+ ::CSVPlusPlus::CLIFlag.new(
64
+ '-g SHEET_ID',
65
+ '--google-sheet-id SHEET_ID',
66
+ 'The id of the sheet - you can extract this from the URL: ' \
67
+ 'https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0'
68
+ ),
69
+ ::CSVPlusPlus::CLIFlag.new(
70
+ '-k',
71
+ '--key-values KEY_VALUES',
72
+ 'A comma-separated list of key=values which will be made available to the template'
73
+ ),
74
+ ::CSVPlusPlus::CLIFlag.new(
75
+ '-n SHEET_NAME',
76
+ '--sheet-name SHEET_NAME',
77
+ 'The name of the sheet to apply the template to'
78
+ ),
79
+ ::CSVPlusPlus::CLIFlag.new(
80
+ '-o OUTPUT_FILE',
81
+ '--output OUTPUT_FILE',
82
+ 'The file to write to (must be .csv, .ods, .xls)'
83
+ ),
84
+ ::CSVPlusPlus::CLIFlag.new('-v', '--verbose', 'Enable verbose output'),
85
+ ::CSVPlusPlus::CLIFlag.new('-x OFFSET', '--offset-columns OFFSET', 'Apply the template offset by OFFSET cells'),
86
+ ::CSVPlusPlus::CLIFlag.new('-y OFFSET', '--offset-rows OFFSET', 'Apply the template offset by OFFSET rows')
87
+ ].freeze,
88
+ ::T::Array[::CSVPlusPlus::CLIFlag]
89
+ )
90
+ public_constant :SUPPORTED_CSVPP_FLAGS
91
+ end