csv_plus_plus 0.1.3 → 0.2.0

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