csv_plus_plus 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -63
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +17 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +112 -0
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +44 -17
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +42 -11
  9. data/lib/csv_plus_plus/compiler.rb +178 -0
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +50 -0
  11. data/lib/csv_plus_plus/entities/boolean.rb +40 -0
  12. data/lib/csv_plus_plus/entities/builtins.rb +58 -0
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +231 -0
  14. data/lib/csv_plus_plus/entities/date.rb +63 -0
  15. data/lib/csv_plus_plus/entities/entity.rb +50 -0
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +45 -0
  18. data/lib/csv_plus_plus/entities/function_call.rb +50 -0
  19. data/lib/csv_plus_plus/entities/number.rb +48 -0
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -0
  21. data/lib/csv_plus_plus/entities/string.rb +42 -0
  22. data/lib/csv_plus_plus/entities/variable.rb +37 -0
  23. data/lib/csv_plus_plus/entities.rb +40 -0
  24. data/lib/csv_plus_plus/error/error.rb +20 -0
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +37 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +75 -0
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +69 -0
  28. data/lib/csv_plus_plus/error/syntax_error.rb +71 -0
  29. data/lib/csv_plus_plus/error/writer_error.rb +17 -0
  30. data/lib/csv_plus_plus/error.rb +10 -2
  31. data/lib/csv_plus_plus/google_api_client.rb +11 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +17 -6
  34. data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
  35. data/lib/csv_plus_plus/lexer.rb +24 -0
  36. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +18 -0
  37. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  38. data/lib/csv_plus_plus/modifier/expand.rb +61 -0
  39. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  40. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  41. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  42. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  43. data/lib/csv_plus_plus/modifier.rb +82 -150
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +25 -25
  46. data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +86 -95
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +478 -0
  48. data/lib/csv_plus_plus/row.rb +53 -15
  49. data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
  50. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
  51. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  52. data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
  53. data/lib/csv_plus_plus/runtime/references.rb +110 -0
  54. data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
  55. data/lib/csv_plus_plus/runtime.rb +42 -0
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +63 -36
  58. data/lib/csv_plus_plus/version.rb +2 -1
  59. data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
  60. data/lib/csv_plus_plus/writer/csv.rb +11 -9
  61. data/lib/csv_plus_plus/writer/excel.rb +9 -2
  62. data/lib/csv_plus_plus/writer/file_backer_upper.rb +7 -4
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +88 -45
  64. data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
  65. data/lib/csv_plus_plus/writer/open_document.rb +6 -1
  66. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -33
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +41 -15
  69. metadata +44 -30
  70. data/lib/csv_plus_plus/code_section.rb +0 -101
  71. data/lib/csv_plus_plus/expand.rb +0 -18
  72. data/lib/csv_plus_plus/graph.rb +0 -62
  73. data/lib/csv_plus_plus/language/ast_builder.rb +0 -68
  74. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
  75. data/lib/csv_plus_plus/language/builtins.rb +0 -46
  76. data/lib/csv_plus_plus/language/compiler.rb +0 -152
  77. data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
  78. data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
  79. data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
  80. data/lib/csv_plus_plus/language/entities/function.rb +0 -35
  81. data/lib/csv_plus_plus/language/entities/function_call.rb +0 -37
  82. data/lib/csv_plus_plus/language/entities/number.rb +0 -36
  83. data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
  84. data/lib/csv_plus_plus/language/entities/string.rb +0 -31
  85. data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/language/entities.rb +0 -28
  87. data/lib/csv_plus_plus/language/references.rb +0 -70
  88. data/lib/csv_plus_plus/language/runtime.rb +0 -205
  89. data/lib/csv_plus_plus/language/scope.rb +0 -192
  90. data/lib/csv_plus_plus/language/syntax_error.rb +0 -66
  91. data/lib/csv_plus_plus/modifier.tab.rb +0 -907
  92. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -56
  93. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ef86258faa82b75114acc715e9cb68bb312dc55194d533270fce0a796e02362
4
- data.tar.gz: 9316f43dd4169a779efc6d8f447002434c4fc5984064b1f22f1448baee0f4522
3
+ metadata.gz: abee03db0f21f9d8c5d3cfb4ec81dbd748568f878458f355cab6db5fc458a509
4
+ data.tar.gz: 6c4e95fa1c780a2d009eaeb58fd48b28c2dd96826b22be02f84d5fc8a06ad31e
5
5
  SHA512:
6
- metadata.gz: 8637e6c3dec41bf747e5fe1f6eb4d3227247e196ef530bbac96e964f6cdbe911d39bed9941ab7e116f00ea2547cf5050a609f078af8dab8f28a6e0f7746e776d
7
- data.tar.gz: 57acb6edef384a31a41ea244841d2977d31817d87735944336688f5961d6eb22c97e5b88683d753c1163239f397936411a7836eafe1aef6e9ed6a612e846fd68
6
+ metadata.gz: 59f2cb756bdfd20f95f252aa1c07fb4b994def21f07d8bc4e5a976b22f0b439fb950e1723fdb34753baf23527e5950d244f1386e9231c6318b35de3974305ba4
7
+ data.tar.gz: 38b6d6d960aa17978adf2ee1724333754e7cdc86b8e41fa58522357448db8448d5441fb5961d3758af94d617b94b21b197b5e4d2d4eb2dba0cbc39da586c4413
data/README.md CHANGED
@@ -9,101 +9,56 @@ 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):
41
-
42
- ```
43
- foo,[[...]]bar,baz
44
- ```
45
-
46
- specifying formatting or various other modifiers to the cell. Additionally a row can start with:
47
-
48
- ```
49
- ![[...]]foo,bar,baz
50
- ```
51
-
52
- which will apply that modifier to all cells in the row.
53
-
54
- ### Examples
55
-
56
- * Align the second cell left, align the last cell to the center and make it bold and italicized:
26
+ And can be compiled into a `.xlsx` file by:
57
27
 
58
28
  ```
59
- Date,[[align=left]]Amount,Quantity,[[align=center/format=bold italic]]Price
29
+ $ csv++ -n 'My Stock Tracker' -o mystocks.xlsx mystocks.csvpp
60
30
  ```
61
31
 
62
- * Underline and center-align an entire row:
32
+ See the [Language Reference](./docs/LANGUAGE_REFERENCE.md) for a full explanation of features.
63
33
 
64
- ```
65
- ![[align=center/format=underline]]Date,Amount,Quantity,Price
66
- ```
34
+ ## Installing
67
35
 
68
- * A header for the first row, then some formulas that repeat for each row for the rest of the spreadsheet:
36
+ Just install it via rubygems (homebrew and debian packages are in the works):
69
37
 
70
- ```
71
- ![[align=center/format=bold]]Date,Price,Quantity,Profit
72
- ![[expand=1:]],,,"=MULTIPLY(cellref(B), cellref(C))"
73
- ```
38
+ `$ gem install csv_plus_plus`
74
39
 
75
- ## Setup (Google Sheets)
40
+ or if you want the very latest changes, clone this repository and run:
76
41
 
77
- Just install it via rubygems (homebrew and debian packages are in the works):
42
+ `$ rake gem:install`
78
43
 
79
- `$ gem install csv_plus_plus`
44
+ ### [Setting Up Google Sheets](./docs/README_GOOGLE_SHEETS.md)
80
45
 
81
- ### Publishing to Google Sheets
46
+ ## Examples
82
47
 
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
48
+ Take a look at the [examples](./examples/) directory for a bunch of example `.csvpp` files.
85
49
 
86
50
  ## CLI Arguments
87
51
 
88
52
  ```
89
53
  Usage: csv++ [options]
54
+ -h, --help Show help information
90
55
  -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
56
  -c, --create Create the sheet if it doesn't exist. It will use --sheet-name if specified
57
+ -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
58
  -k, --key-values KEY_VALUES A comma-separated list of key=values which will be made available to the template
94
59
  -n, --sheet-name SHEET_NAME The name of the sheet to apply the template to
60
+ -o, --output OUTPUT_FILE The file to write to (must be .csv, .ods, .xls)
95
61
  -v, --verbose Enable verbose output
96
62
  -x, --offset-columns OFFSET Apply the template offset by OFFSET cells
97
63
  -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
64
  ```
@@ -1,3 +1,20 @@
1
+ ## v0.1.3
2
+
3
+ - Proper scoping of variables defined within an expand modifier
4
+ - Types via Sorbet
5
+ - Fix formula insertion on Excel
6
+ - Fix modifier string quoting
7
+ - Fix broken Yard doc generation
8
+ - Fix: multiple modifiers on the same row weren't being handled
9
+
10
+ ## v0.1.2
11
+
12
+ - var=... modifier which allows binding a variable to a cell
13
+ - Improved error handling and messages
14
+ - Moving in a direction that allows for the context-dependent aspects of modifiers
15
+ - Fixes a bug with creating a new excel spreadsheet
16
+ - Docs & tests
17
+
1
18
  ## v0.1.1
2
19
 
3
20
  - Better support for the various infix operators (+,-,/,*,^,%,=,<,etc)
@@ -0,0 +1,112 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ # Extend a +Compiler+ class and add benchmark timings
6
+ #
7
+ # @attr_reader timings [Array<Benchmark::Tms>] +Benchmark+ timings that have been accumulated by each step of
8
+ # compilation
9
+ # @attr_reader benchmark [Benchmark::Report] A +Benchmark+ instance
10
+ class BenchmarkedCompiler < ::CSVPlusPlus::Compiler
11
+ extend ::T::Sig
12
+
13
+ sig { returns(::Benchmark::Report) }
14
+ attr_reader :benchmark
15
+
16
+ sig { returns(::T::Array[::Benchmark::Tms]) }
17
+ attr_reader :timings
18
+
19
+ sig do
20
+ params(
21
+ options: ::CSVPlusPlus::Options,
22
+ runtime: ::CSVPlusPlus::Runtime::Runtime,
23
+ block: ::T.proc.params(compiler: ::CSVPlusPlus::Compiler).void
24
+ ).void
25
+ end
26
+ # Instantiate a +::Compiler+ that can benchmark (time) it's stages. For better or worse, the only way that they
27
+ # Benchmark library exposes it's +::Benchmark::Report+ is via a block, so this code also has to wrap with one
28
+ #
29
+ # @param options [Options]
30
+ # @param runtime [Runtime]
31
+ def self.with_benchmarks(options:, runtime:, &block)
32
+ ::Benchmark.benchmark(::Benchmark::CAPTION, 25, ::Benchmark::FORMAT, '> Total') do |x|
33
+ # compiler.extend(self)
34
+ compiler = new(benchmark: x, options:, runtime:)
35
+ block.call(compiler)
36
+ [compiler.timings.reduce(:+)]
37
+ end
38
+ end
39
+
40
+ sig do
41
+ params(
42
+ benchmark: ::Benchmark::Report,
43
+ options: ::CSVPlusPlus::Options,
44
+ runtime: ::CSVPlusPlus::Runtime::Runtime
45
+ ).void
46
+ end
47
+ # @param benchmark [::Benchmark::Report]
48
+ def initialize(benchmark:, options:, runtime:)
49
+ super(options:, runtime:)
50
+
51
+ @benchmark = ::T.let(benchmark, ::Benchmark::Report)
52
+ @timings = ::T.let([], ::T::Array[::Benchmark::Tms])
53
+ end
54
+
55
+ sig { override.params(block: ::T.proc.params(runtime: ::CSVPlusPlus::Runtime::Runtime).void).void }
56
+ # Time the Compiler#outputting! stage
57
+ # rubocop:disable Naming/BlockForwarding
58
+ def outputting!(&block)
59
+ time_stage('Writing the spreadsheet') { super(&block) }
60
+ end
61
+ # rubocop:enable Naming/BlockForwarding
62
+
63
+ protected
64
+
65
+ sig { override.void }
66
+ def parse_code_section!
67
+ time_stage('Parsing code section') { super }
68
+ end
69
+
70
+ sig { override.returns(::T::Array[::CSVPlusPlus::Row]) }
71
+ def parse_csv_section!
72
+ time_stage('Parsing CSV section') { super }
73
+ end
74
+
75
+ sig { override.params(block: ::T.proc.void).void }
76
+ # rubocop:disable Naming/BlockForwarding
77
+ def expanding!(&block)
78
+ time_stage('Expanding rows') { super(&block) }
79
+ end
80
+ # rubocop:enable Naming/BlockForwarding
81
+
82
+ sig { override.params(block: ::T.proc.void).void }
83
+ # rubocop:disable Naming/BlockForwarding
84
+ def bind_all_vars!(&block)
85
+ time_stage('Binding [[var=]]') { super(&block) }
86
+ end
87
+ # rubocop:enable Naming/BlockForwarding
88
+
89
+ sig do
90
+ override
91
+ .params(template: ::CSVPlusPlus::Template)
92
+ .returns(::T::Array[::T::Array[::CSVPlusPlus::Entities::Entity]])
93
+ end
94
+ def resolve_all_cells!(template)
95
+ time_stage('Resolving each cell') { super(template) }
96
+ end
97
+
98
+ private
99
+
100
+ sig do
101
+ type_parameters(:R).params(
102
+ stage: ::String,
103
+ block: ::T.proc.returns(::T.type_parameter(:R))
104
+ ).returns(::T.nilable(::T.type_parameter(:R)))
105
+ end
106
+ def time_stage(stage, &block)
107
+ ret = ::T.let(nil, ::T.nilable(::T.type_parameter(:R)))
108
+ @timings << ::T.unsafe(@benchmark.report(stage) { ret = block.call })
109
+ ret
110
+ end
111
+ end
112
+ end
@@ -1,8 +1,6 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative './language/cell_value.tab'
4
- require_relative './modifier'
5
-
6
4
  module CSVPlusPlus
7
5
  # A cell of a template
8
6
  #
@@ -11,9 +9,27 @@ module CSVPlusPlus
11
9
  # @attr_reader index [Integer] The cell's index (starts at 0)
12
10
  # @attr_reader modifier [Modifier] The modifier for this cell
13
11
  class Cell
14
- attr_accessor :ast, :row_index
15
- attr_reader :index, :modifier
12
+ extend ::T::Sig
13
+
14
+ sig { returns(::T.nilable(::CSVPlusPlus::Entities::Entity)) }
15
+ attr_accessor :ast
16
+
17
+ sig { returns(::Integer) }
18
+ attr_accessor :row_index
19
+
20
+ sig { returns(::Integer) }
21
+ attr_reader :index
16
22
 
23
+ sig { returns(::CSVPlusPlus::Modifier::Modifier) }
24
+ attr_reader :modifier
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
17
33
  # Parse a +value+ into a Cell object.
18
34
  #
19
35
  # @param value [String] A string value which should already have been processed through a CSV parser
@@ -23,45 +39,51 @@ module CSVPlusPlus
23
39
  # @return [Cell]
24
40
  def self.parse(value, runtime:, modifier:)
25
41
  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)
42
+ c.ast = ::T.unsafe(::CSVPlusPlus::Parser::CellValue.new).parse(value, runtime)
27
43
  end
28
44
  end
29
45
 
30
- # @param row_index [Integer] The cell's row index (starts at 0)
46
+ sig do
47
+ params(
48
+ index: ::Integer,
49
+ modifier: ::CSVPlusPlus::Modifier::Modifier,
50
+ row_index: ::Integer,
51
+ value: ::T.nilable(::String)
52
+ ).void
53
+ end
31
54
  # @param index [Integer] The cell's index (starts at 0)
32
- # @param value [String] A string value which should already have been processed through a CSV parser
33
55
  # @param modifier [Modifier] A modifier to apply to this cell
34
- def initialize(row_index:, index:, value:, modifier:)
56
+ # @param row_index [Integer] The cell's row index (starts at 0)
57
+ # @param value [String] A string value which should already have been processed through a CSV parser
58
+ def initialize(index:, modifier:, row_index:, value:)
35
59
  @value = value
36
60
  @modifier = modifier
37
61
  @index = index
38
62
  @row_index = row_index
39
63
  end
40
64
 
65
+ sig { returns(::T.nilable(::String)) }
41
66
  # The +@value+ (cleaned up some)
42
67
  #
43
- # @return [String]
68
+ # @return [::String]
69
+ # TODO: is this used?
44
70
  def value
45
- return if @value.nil? || @value.strip.empty?
71
+ stripped = @value&.strip
46
72
 
47
- @value.strip
73
+ stripped&.empty? ? nil : stripped
48
74
  end
49
75
 
50
- # @return [String]
51
- def to_s
52
- "Cell(index: #{@index}, row_index: #{@row_index}, value: #{@value}, modifier: #{@modifier})"
53
- end
54
-
55
- # A compiled final representation of the cell. This can only happen after all cell have had
56
- # variables and functions resolved.
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]
57
81
  #
58
- # @return [String]
59
- def to_csv
82
+ # @return [::String]
83
+ def evaluate(runtime)
60
84
  return value unless @ast
61
85
 
62
- # This looks really simple but we're relying on each node of the AST to define #to_s such that calling
63
- # this at the top will recursively print the tree (as a well-formatted spreadsheet formula)
64
- "=#{@ast}"
86
+ "=#{@ast.evaluate(runtime)}"
65
87
  end
66
88
  end
67
89
  end
@@ -1,71 +1,96 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require 'optparse'
4
-
5
4
  module CSVPlusPlus
6
5
  # Handle running the application with the given CLI flags
7
6
  #
8
7
  # @attr options [Options, nil] The parsed CLI options
9
8
  class CLI
9
+ extend ::T::Sig
10
+
11
+ sig { returns(::CSVPlusPlus::Options) }
10
12
  attr_accessor :options
11
13
 
14
+ sig { void }
12
15
  # Handle CLI flags and launch the compiler
13
16
  #
14
17
  # @return [CLI]
15
18
  def self.launch_compiler!
16
19
  cli = new
17
- cli.parse_options!
18
20
  cli.main
19
21
  rescue ::StandardError => e
20
- cli.handle_error(e)
22
+ ::T.must(cli).handle_error(e)
21
23
  exit(1)
22
24
  end
23
25
 
26
+ sig { void }
27
+ # Initialize and parse the CLI flags provided to the program
28
+ def initialize
29
+ @options = ::T.let(::CSVPlusPlus::Options.new, ::CSVPlusPlus::Options)
30
+ parse_options!
31
+ end
32
+
33
+ sig { void }
24
34
  # Compile the given template using the given CLI flags
25
35
  def main
26
- parse_options! unless @options
27
36
  ::CSVPlusPlus.apply_template_to_sheet!(::ARGF.read, ::ARGF.filename, @options)
28
37
  end
29
38
 
39
+ sig { params(error: ::StandardError).void }
30
40
  # Nicely handle a given error. How it's handled depends on if it's our error and if @options.verbose
31
41
  #
32
42
  # @param error [CSVPlusPlus::Error, Google::Apis::ClientError, StandardError]
33
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
+
34
47
  case error
35
- when ::CSVPlusPlus::Error
48
+ when ::CSVPlusPlus::Error::Error
36
49
  handle_internal_error(error)
37
50
  when ::Google::Apis::ClientError
38
51
  handle_google_error(error)
39
52
  else
40
- # TODO: more if verbose?
41
- warn(error.message)
53
+ unhandled_error(error)
42
54
  end
43
55
  end
44
56
 
57
+ private
58
+
59
+ sig { void }
45
60
  # Handle the supplied command line options, setting +@options+ or throw an error if anything is invalid
46
61
  def parse_options!
47
- @options = ::CSVPlusPlus::Options.new
48
62
  option_parser.parse!
49
63
  validate_options
50
64
  rescue ::OptionParser::InvalidOption => e
51
- raise(::CSVPlusPlus::Error, e.message)
65
+ raise(::CSVPlusPlus::Error::Error, e.message)
52
66
  end
53
67
 
54
- # @return [String]
55
- def to_s
56
- "CLI(options: #{options})"
57
- end
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
58
76
 
59
- private
77
+ return unless @options.verbose
60
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 }
61
84
  def handle_internal_error(error)
62
- if error.is_a?(::CSVPlusPlus::Language::SyntaxError)
85
+ case error
86
+ when ::CSVPlusPlus::Error::SyntaxError
63
87
  warn(@options.verbose ? error.to_verbose_trace : error.to_trace)
64
88
  else
65
89
  warn(error.message)
66
90
  end
67
91
  end
68
92
 
93
+ sig { params(error: ::Google::Apis::ClientError).void }
69
94
  def handle_google_error(error)
70
95
  warn("Error making Google Sheets API request: #{error.message}")
71
96
  return unless @options.verbose
@@ -73,14 +98,16 @@ module CSVPlusPlus
73
98
  warn("#{error.status_code} Error making Google API request [#{error.message}]: #{error.body}")
74
99
  end
75
100
 
101
+ sig { void }
76
102
  def validate_options
77
103
  error_message = @options.validate
78
104
  return if error_message.nil?
79
105
 
80
106
  puts(option_parser)
81
- raise(::CSVPlusPlus::Error, error_message)
107
+ raise(::CSVPlusPlus::Error::Error, error_message)
82
108
  end
83
109
 
110
+ sig { returns(::OptionParser) }
84
111
  def option_parser
85
112
  ::OptionParser.new do |parser|
86
113
  parser.on('-h', '--help', 'Show help information') do
@@ -1,7 +1,6 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative './google_options'
4
-
5
4
  module CSVPlusPlus
6
5
  # Individual CLI flags that a user can supply
7
6
  #
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
@@ -7,19 +8,49 @@ module CSVPlusPlus
7
8
  # attr_reader green_hex [String] The green value in hex ("FF", "00", "AF", etc)
8
9
  # attr_reader red_hex [String] The red value in hex ("FF", "00", "AF", etc)
9
10
  class Color
10
- attr_reader :red_hex, :green_hex, :blue_hex
11
+ extend ::T::Sig
11
12
 
12
- # create an instance from a string like "#FFF" or "#FFFFFF"
13
+ sig { returns(::String) }
14
+ attr_reader :red_hex
15
+
16
+ sig { returns(::String) }
17
+ attr_reader :green_hex
18
+
19
+ sig { returns(::String) }
20
+ attr_reader :blue_hex
21
+
22
+ HEX_STRING_REGEXP = /^#?([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/i
23
+ public_constant :HEX_STRING_REGEXP
24
+
25
+ sig { params(hex_string: ::String).returns(::T::Boolean) }
26
+ # Is +hex_string+ a valid hexadecimal color code? This function will accept input like the 6-digit format: #FF00FF,
27
+ # 00AABB and the shorter 3-digit format: #FFF, 0FA.
28
+ #
29
+ # @param hex_string [::String] The string to see if it's valid hex string
30
+ #
31
+ # @return [boolean]
32
+ def self.valid_hex_string?(hex_string)
33
+ !(hex_string.strip =~ ::CSVPlusPlus::Color::HEX_STRING_REGEXP).nil?
34
+ end
35
+
36
+ sig { params(hex_string: ::String).void }
37
+ # Create an instance from a string like "#FFF" or "#FFFFFF"
13
38
  #
14
39
  # @param hex_string [String] The hex string input to parse
40
+ # rubocop:disable Metrics/CyclomaticComplexity
15
41
  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)
42
+ red_hex, green_hex, blue_hex = hex_string.strip.match(::CSVPlusPlus::Color::HEX_STRING_REGEXP)
19
43
  &.captures
20
44
  &.map { |s| s.length == 1 ? s + s : s }
45
+ raise(::CSVPlusPlus::Error::Error, "Invalid color: #{hex_string}") unless red_hex && green_hex && blue_hex
46
+
47
+ @red_hex = ::T.let(red_hex, ::String)
48
+ @green_hex = ::T.let(green_hex, ::String)
49
+ @blue_hex = ::T.let(blue_hex, ::String)
21
50
  end
51
+ # rubocop:enable Metrics/CyclomaticComplexity
22
52
 
53
+ sig { returns(::Float) }
23
54
  # The percent (decimal between 0-1) of red
24
55
  #
25
56
  # @return [Numeric]
@@ -27,6 +58,7 @@ module CSVPlusPlus
27
58
  hex_to_percent(@red_hex)
28
59
  end
29
60
 
61
+ sig { returns(::Float) }
30
62
  # The percent (decimal between 0-1) of green
31
63
  #
32
64
  # @return [Numeric]
@@ -34,6 +66,7 @@ module CSVPlusPlus
34
66
  hex_to_percent(@green_hex)
35
67
  end
36
68
 
69
+ sig { returns(::Float) }
37
70
  # The percent (decimal between 0-1) of blue
38
71
  #
39
72
  # @return [Numeric]
@@ -41,18 +74,15 @@ module CSVPlusPlus
41
74
  hex_to_percent(@blue_hex)
42
75
  end
43
76
 
77
+ sig { returns(::String) }
44
78
  # Create a hex representation of the color (without a '#')
45
79
  #
46
- # @return [String]
80
+ # @return [::String]
47
81
  def to_hex
48
82
  [@red_hex, @green_hex, @blue_hex].join
49
83
  end
50
84
 
51
- # @return [String]
52
- def to_s
53
- "Color(r: #{@red_hex}, g: #{@green_hex}, b: #{@blue_hex})"
54
- end
55
-
85
+ sig { params(other: ::Object).returns(::T::Boolean) }
56
86
  # @return [boolean]
57
87
  def ==(other)
58
88
  other.is_a?(self.class) &&
@@ -63,6 +93,7 @@ module CSVPlusPlus
63
93
 
64
94
  private
65
95
 
96
+ sig { params(hex: ::String).returns(::Float) }
66
97
  def hex_to_percent(hex)
67
98
  hex.to_i(16) / 255.0
68
99
  end