xlsxrb 0.1.5 → 0.1.7
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.
- checksums.yaml +4 -4
- data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
- data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
- data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
- data/CHANGELOG.md +28 -0
- data/README.md +66 -44
- data/Rakefile +2 -1
- data/Steepfile +1 -2
- data/benchmark.rb +376 -0
- data/docs/ARCHITECTURE.md +12 -5
- data/docs/visual/VisualGallery.md +8 -0
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/ruby_lsp/xlsxrb/addon.rb +43 -0
- data/lib/ruby_lsp/xlsxrb/completion_listener.rb +769 -0
- data/lib/xlsxrb/elements/cell.rb +137 -11
- data/lib/xlsxrb/elements/column.rb +22 -0
- data/lib/xlsxrb/elements/row.rb +98 -5
- data/lib/xlsxrb/elements/types.rb +40 -5
- data/lib/xlsxrb/elements/workbook.rb +73 -5
- data/lib/xlsxrb/elements/worksheet.rb +110 -1
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +104 -40
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +144 -23
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +160 -145
- data/lib/xlsxrb/ooxml/xml_builder.rb +16 -10
- data/lib/xlsxrb/ooxml/zip_reader.rb +104 -18
- data/lib/xlsxrb/ooxml/zip_writer.rb +63 -25
- data/lib/xlsxrb/style_builder.rb +158 -30
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb.rb +892 -336
- data/rbs_collection.lock.yaml +28 -0
- data/sig/generated/xlsxrb/elements/cell.rbs +39 -0
- data/sig/generated/xlsxrb/elements/column.rbs +35 -0
- data/sig/generated/xlsxrb/elements/row.rbs +39 -0
- data/sig/generated/xlsxrb/elements/types.rbs +82 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +32 -0
- data/sig/generated/xlsxrb/elements/worksheet.rbs +34 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
- data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
- data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
- data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
- data/sig/generated/xlsxrb/style_builder.rbs +212 -50
- data/sig/generated/xlsxrb.rbs +1646 -0
- data/sig/rexml.rbs +4 -0
- metadata +78 -1
data/README.md
CHANGED
|
@@ -6,17 +6,22 @@ A Ruby library for reading and writing XLSX files with streaming support.
|
|
|
6
6
|
|
|
7
7
|
The Ruby ecosystem already has great XLSX libraries. Each is well-designed for its purpose:
|
|
8
8
|
|
|
9
|
-
| Library | Read | Write |
|
|
10
|
-
| -------------------------------------------------- | ---- | ----- |
|
|
11
|
-
| [roo](https://rubygems.org/gems/roo) | ✅ | ❌ |
|
|
12
|
-
| [creek](https://rubygems.org/gems/creek) | ✅ | ❌ |
|
|
13
|
-
| [xsv](https://rubygems.org/gems/xsv) | ✅ | ❌ |
|
|
14
|
-
| [caxlsx / axlsx](https://rubygems.org/gems/caxlsx) | ❌ | ✅ |
|
|
15
|
-
| [xlsxtream](https://rubygems.org/gems/xlsxtream) | ❌ | ✅ |
|
|
16
|
-
| [
|
|
17
|
-
| [
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
| Library | Read | Write | Model | Write String Storage | Rich Formatting |
|
|
10
|
+
| -------------------------------------------------- | ---- | ----- | --------------------- | -------------------- | --------------- |
|
|
11
|
+
| [roo](https://rubygems.org/gems/roo) | ✅ | ❌ | Streaming | N/A (Read-only) | ⚠️ (Formulas, Basic styles) |
|
|
12
|
+
| [creek](https://rubygems.org/gems/creek) | ✅ | ❌ | Streaming | N/A (Read-only) | ❌ (Raw cell values) |
|
|
13
|
+
| [xsv](https://rubygems.org/gems/xsv) | ✅ | ❌ | Streaming | N/A (Read-only) | ❌ (Fast plain text) |
|
|
14
|
+
| [caxlsx / axlsx](https://rubygems.org/gems/caxlsx) | ❌ | ✅ | In-Memory | Inline (opt: SST) | ✅ (Charts, Styles) |
|
|
15
|
+
| [xlsxtream](https://rubygems.org/gems/xlsxtream) | ❌ | ✅ | Streaming | Inline (opt: SST) | ❌ (Plain data only) |
|
|
16
|
+
| [fast_excel](https://rubygems.org/gems/fast_excel) | ❌ | ✅ | Streaming (C Ext) | SST (opt: Inline) | ⚠️ (Basic styles) |
|
|
17
|
+
| [rubyXL](https://rubygems.org/gems/rubyXL) | ✅ | ✅ | In-Memory | Inline / Direct | ✅ (DOM editing) |
|
|
18
|
+
| **[xlsxrb](https://github.com/niku/xlsxrb)** | ✅ | ✅ | **Streaming / In-Memory** | **SST** | ✅ **(Full Features)** |
|
|
19
|
+
|
|
20
|
+
Each of these libraries makes deliberate tradeoffs, and they do so thoughtfully:
|
|
21
|
+
* **Memory & Execution Model (Streaming vs In-Memory)**: Streaming libraries write or read rows sequentially on-the-fly to maintain a constant, low-memory footprint regardless of row count. In-memory libraries build complete document object trees, offering flexible random access and cell updates at the cost of high RAM usage on large sheets.
|
|
22
|
+
* **String Storage Architecture (SST vs Inline Strings)**:
|
|
23
|
+
* **SST (Shared String Table)**: De-duplicates strings into a central dictionary (`xl/sharedStrings.xml`), referencing them by numeric IDs in cell entries (`<c t="s"><v>0</v></c>`). This is standard Microsoft Excel behavior, producing significantly smaller raw XML documents (50–100% smaller) and reducing Excel's memory footprint when opening spreadsheets.
|
|
24
|
+
* **Inline Strings**: Writes text directly into cell payloads (`<c t="inlineStr"><is><t>...</t></is></c>`). Bypassing the dictionary enables blazing-fast raw throughput for simple data exports, but inflates uncompressed XML size and limits advanced formatting (e.g. styling, cell merges, charts).
|
|
20
25
|
|
|
21
26
|
Traditionally, attempting to build a "complete package" that offers both reading and writing, rich features, high performance, strict compatibility, and comprehensive documentation presents an inherent open-source challenge: the cumulative maintenance overhead often exceeds the capacity of individual human maintainers.
|
|
22
27
|
|
|
@@ -77,8 +82,8 @@ Generate large files efficiently by writing data directly to the file stream:
|
|
|
77
82
|
```ruby
|
|
78
83
|
require "xlsxrb"
|
|
79
84
|
|
|
80
|
-
Xlsxrb.generate("large_output.xlsx") do |
|
|
81
|
-
|
|
85
|
+
Xlsxrb.generate("large_output.xlsx") do |stream_writer|
|
|
86
|
+
stream_writer.sheet("Sales Data") do |sheet|
|
|
82
87
|
sheet.row(["Date", "Amount", "Status"])
|
|
83
88
|
sheet.row([Date.today, 100, true])
|
|
84
89
|
sheet.column(0, width: 15.5)
|
|
@@ -92,7 +97,7 @@ Read rows one at a time without loading the entire file into memory:
|
|
|
92
97
|
require "xlsxrb"
|
|
93
98
|
|
|
94
99
|
Xlsxrb.foreach("large_file.xlsx") do |sheet|
|
|
95
|
-
sheet.
|
|
100
|
+
sheet.each_row do |row|
|
|
96
101
|
puts "Row #{row.index}: #{row.cells.map(&:value).join(', ')}"
|
|
97
102
|
end
|
|
98
103
|
end
|
|
@@ -103,17 +108,17 @@ end
|
|
|
103
108
|
`xlsxrb` provides a powerful, immutable-by-default API for modifying existing Excel files or building templates in-memory.
|
|
104
109
|
|
|
105
110
|
#### Modifying an Existing File
|
|
106
|
-
You can update specific cells or sheets using the functional `Xlsxrb.modify` API, which yields the parsed `Workbook`.
|
|
111
|
+
You can update specific cells or sheets using the functional `Xlsxrb.modify` API, which yields the parsed `Elements::Workbook`.
|
|
107
112
|
|
|
108
113
|
```ruby
|
|
109
114
|
require "xlsxrb"
|
|
110
115
|
|
|
111
|
-
# Create a
|
|
112
|
-
Xlsxrb.build { |
|
|
116
|
+
# Create a template.xlsx for this example
|
|
117
|
+
Xlsxrb.build { |builder| builder.sheet("Invoice") }.write("template.xlsx")
|
|
113
118
|
|
|
114
|
-
Xlsxrb.modify("template.xlsx", "output.xlsx") do |
|
|
115
|
-
|
|
116
|
-
# Update
|
|
119
|
+
Xlsxrb.modify("template.xlsx", "output.xlsx") do |workbook|
|
|
120
|
+
workbook.update_sheet("Invoice") do |sheet|
|
|
121
|
+
# Update specific cells (returns updated sheet)
|
|
117
122
|
sheet = sheet.update_cell("C4", value: "INV-10042")
|
|
118
123
|
sheet = sheet.update_cell("C5", value: Date.today)
|
|
119
124
|
|
|
@@ -129,16 +134,31 @@ end
|
|
|
129
134
|
You can directly apply inline styles or use Ranges for multiple columns without boilerplate:
|
|
130
135
|
|
|
131
136
|
```ruby
|
|
132
|
-
Xlsxrb.build do |
|
|
137
|
+
Xlsxrb.build do |builder|
|
|
133
138
|
# Use [] accessor for sheets
|
|
134
|
-
|
|
139
|
+
builder["Report"].row(
|
|
135
140
|
["ID", "Name", "Score", "Rank"],
|
|
136
141
|
# Apply 'header' style to first two columns, and bold inline style to the third
|
|
137
142
|
styles: { 0..1 => "header", 2 => { font: { bold: true, color: "red" } } }
|
|
138
143
|
)
|
|
139
144
|
|
|
140
145
|
# Set multiple column widths at once using Ranges
|
|
141
|
-
|
|
146
|
+
builder["Report"].column("A".."D", width: 15.0)
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### IDE Autocompletion & Ruby LSP Support
|
|
151
|
+
|
|
152
|
+
`xlsxrb` bundles a native **Ruby LSP Add-on** (`RubyLsp::Xlsxrb::Addon`) and full **RBS signatures**, enabling zero-configuration method autocompletion and rich Markdown documentation in VS Code and other LSP-enabled editors.
|
|
153
|
+
|
|
154
|
+
Whether you use standard descriptive block variable names (`|stream_writer|`, `|sheet|`, `|workbook|`) or short names (`|wb|`, `|s|`), your editor will automatically provide complete method suggestions and parameter hints:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
Xlsxrb.generate("output.xlsx") do |stream_writer| # or |wb|
|
|
158
|
+
stream_writer.sheet("Data") do |sheet| # or |s|
|
|
159
|
+
sheet.row(["Product", "Price"], styles: :bold)
|
|
160
|
+
sheet.auto_filter("A1:B100")
|
|
161
|
+
end
|
|
142
162
|
end
|
|
143
163
|
```
|
|
144
164
|
|
|
@@ -156,37 +176,39 @@ For detailed specification references and policies, see [SPEC_SOURCES.md](docs/S
|
|
|
156
176
|
|
|
157
177
|
## Benchmarks
|
|
158
178
|
|
|
159
|
-
The following benchmarks measure the time
|
|
179
|
+
The following benchmarks measure the time, peak memory, and GC count required to process a 1,000,000 cells (100,000 rows × 10 columns) spreadsheet across popular Ruby Excel libraries. Each test is executed across 3 independent runs in isolated subprocesses; median values are reported along with the mean execution time.
|
|
160
180
|
|
|
161
181
|
### Write Performance (1,000,000 cells)
|
|
162
182
|
|
|
163
|
-
| Library | Time
|
|
164
|
-
| ---------------------- |
|
|
165
|
-
| xlsxtream
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
| xlsxrb (
|
|
169
|
-
|
|
|
170
|
-
| rubyXL
|
|
183
|
+
| Library | Model | Write String Storage | Time (Median) | Time (Mean) | Peak Memory | GC Count |
|
|
184
|
+
| ---------------------- | ----------- | -------------------- | ------------- | ----------- | ----------- | -------- |
|
|
185
|
+
| xlsxtream 3.1.0 | Streaming | Inline String | 1.23 s | 1.25 s | 18.1 MB | 1061.0 |
|
|
186
|
+
| xlsxrb (Streaming) | Streaming | SST (Shared) | 1.62 s | 1.62 s | 94.5 MB | 39.0 |
|
|
187
|
+
| fast_excel 0.5.0 (C) | Streaming | SST (Shared) | 2.03 s | 2.03 s | 147.9 MB | 263.0 |
|
|
188
|
+
| xlsxrb (In-Memory) | In-Memory | SST (Shared) | 4.06 s | 4.06 s | 280.3 MB | 32.0 |
|
|
189
|
+
| caxlsx 4.5.0 | In-Memory | Inline String | 5.36 s | 5.35 s | 188.7 MB | 23.0 |
|
|
190
|
+
| rubyXL 3.4.38 | In-Memory | Inline String | 38.02 s | 38.03 s | 2166.0 MB | 104.0 |
|
|
191
|
+
|
|
192
|
+
> **Note**: All libraries are evaluated in their **default, out-of-the-box configuration**. Under the same Microsoft Excel-standard Shared String Table (SST) architecture, Pure Ruby `xlsxrb` (Streaming: 1.62s, In-Memory: 4.06s) writes 1,000,000 cells faster than the C-extension `fast_excel` (2.03s) and in-memory gems like `caxlsx` (5.36s).
|
|
171
193
|
|
|
172
194
|
### Read Performance (1,000,000 cells)
|
|
173
195
|
|
|
174
|
-
| Library
|
|
175
|
-
|
|
|
176
|
-
| xlsxrb (Streaming) |
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
| xsv
|
|
181
|
-
| rubyXL
|
|
196
|
+
| Library | Model | Time (Median) | Time (Mean) | Peak Memory | GC Count |
|
|
197
|
+
| ---------------------- | ----------- | ------------- | ----------- | ----------- | -------- |
|
|
198
|
+
| xlsxrb (Streaming) | Streaming | 3.38 s | 3.37 s | 90.6 MB | 40.0 |
|
|
199
|
+
| xlsxrb (In-Memory) | In-Memory | 5.75 s | 5.94 s | 250.9 MB | 55.0 |
|
|
200
|
+
| creek 2.6.3 | Streaming | 7.90 s | 7.93 s | 835.7 MB | 481.0 |
|
|
201
|
+
| roo 3.0.0 | Streaming | 10.34 s | 10.28 s | 139.0 MB | 107.0 |
|
|
202
|
+
| xsv 1.4.1 | Streaming | 16.39 s | 17.28 s | 75.4 MB | 2215.0 |
|
|
203
|
+
| rubyXL 3.4.38 | In-Memory | 35.45 s | 35.38 s | 2281.3 MB | 146.0 |
|
|
182
204
|
|
|
183
|
-
### Running the Benchmarks Locally
|
|
205
|
+
### Running the Benchmarks Locally (Reproducibility)
|
|
184
206
|
|
|
185
|
-
The benchmark
|
|
207
|
+
The benchmark suite leverages [`bundler/inline`](https://bundler.io/v2.5/guides/bundler_in_a_single_file_ruby_script.html) to automatically manage and download all peer ecosystem gems without modifying the project's core `Gemfile` or requiring manual global `gem install` steps. Each library is executed in an isolated subprocess (`Bundler.with_unbundled_env`) across multiple runs with standard business dataset rows (integers, strings, floats, booleans, dates) to ensure clean memory and GC measurements without cross-contamination.
|
|
186
208
|
|
|
187
|
-
To run the benchmark
|
|
209
|
+
To run the complete benchmark suite:
|
|
188
210
|
```bash
|
|
189
|
-
ruby benchmark.rb 100000
|
|
211
|
+
ruby benchmark.rb 100000 10
|
|
190
212
|
```
|
|
191
213
|
|
|
192
214
|
## Security (Protection against CSV/Excel Injection)
|
data/Rakefile
CHANGED
|
@@ -527,7 +527,8 @@ namespace :doc do
|
|
|
527
527
|
end
|
|
528
528
|
desc "Generate RBS signature files from inline annotations"
|
|
529
529
|
task :sig do
|
|
530
|
-
|
|
530
|
+
files = FileList["lib/**/*.rb"].to_a
|
|
531
|
+
sh "bundle", "exec", "rbs-inline", "--output=sig/generated", "--base=lib", *files
|
|
531
532
|
end
|
|
532
533
|
|
|
533
534
|
desc "Run static type checking with Steep"
|
data/Steepfile
CHANGED
data/benchmark.rb
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rubocop:disable all
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "bundler/inline"
|
|
8
|
+
|
|
9
|
+
puts "Ensuring benchmark peer ecosystem gems are available (bundler/inline)..."
|
|
10
|
+
gemfile(true) do
|
|
11
|
+
source "https://rubygems.org"
|
|
12
|
+
gem "caxlsx", "4.5.0"
|
|
13
|
+
gem "xlsxtream", "3.1.0"
|
|
14
|
+
gem "fast_excel", "0.5.0", platform: :mri
|
|
15
|
+
gem "rubyXL", "3.4.38"
|
|
16
|
+
gem "roo", "3.0.0"
|
|
17
|
+
gem "creek", "2.6.3"
|
|
18
|
+
gem "xsv", "1.4.1"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
RUNS = (ENV["RUNS"] || "3").to_i
|
|
22
|
+
ROWS = (ARGV[0] || "100000").to_i
|
|
23
|
+
COLS = (ARGV[1] || "10").to_i
|
|
24
|
+
|
|
25
|
+
AVAILABLE_GEMS = {
|
|
26
|
+
"xlsxrb_stream" => true,
|
|
27
|
+
"xlsxrb_inmemory" => true,
|
|
28
|
+
"xlsxtream" => true,
|
|
29
|
+
"fast_excel" => true,
|
|
30
|
+
"caxlsx" => true,
|
|
31
|
+
"rubyXL" => true,
|
|
32
|
+
"creek" => true,
|
|
33
|
+
"roo" => true,
|
|
34
|
+
"xsv" => true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
puts "=" * 80
|
|
38
|
+
puts "Benchmarking Excel Libraries (#{ROWS} rows x #{COLS} cols = #{ROWS * COLS} cells)"
|
|
39
|
+
puts "Runs per benchmark: #{RUNS} (Median reported, Mean calculated)"
|
|
40
|
+
puts "Ruby: #{RUBY_DESCRIPTION}"
|
|
41
|
+
puts "=" * 80
|
|
42
|
+
|
|
43
|
+
RUNNER_SCRIPT = <<~'RUBY'
|
|
44
|
+
require "json"
|
|
45
|
+
require "stringio"
|
|
46
|
+
|
|
47
|
+
def measure
|
|
48
|
+
gc_before = GC.stat[:count]
|
|
49
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
50
|
+
|
|
51
|
+
yield
|
|
52
|
+
|
|
53
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
54
|
+
gc_after = GC.stat[:count]
|
|
55
|
+
|
|
56
|
+
# Peak memory in MB via /proc/self/status or getrusage
|
|
57
|
+
peak_mb = 0.0
|
|
58
|
+
if File.exist?("/proc/self/status")
|
|
59
|
+
status = File.read("/proc/self/status")
|
|
60
|
+
if status =~ /VmHWM:\s+(\d+)\s+kB/i
|
|
61
|
+
peak_mb = $1.to_f / 1024.0
|
|
62
|
+
elsif status =~ /VmRSS:\s+(\d+)\s+kB/i
|
|
63
|
+
peak_mb = $1.to_f / 1024.0
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
time: (t1 - t0),
|
|
69
|
+
peak_memory_mb: peak_mb,
|
|
70
|
+
gc_count: (gc_after - gc_before)
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def generate_row(r, cols)
|
|
75
|
+
base = [r + 1, "User #{r + 1}", 123.45, true, "Active", (r + 1) * 10, "Tokyo", 99.9, false, "Item #{r % 50}"]
|
|
76
|
+
if cols <= base.size
|
|
77
|
+
base.first(cols)
|
|
78
|
+
else
|
|
79
|
+
base + Array.new(cols - base.size) { |c| "col_#{c}_#{r}" }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
lib = ARGV[0]
|
|
84
|
+
mode = ARGV[1] # "write" or "read"
|
|
85
|
+
rows = ARGV[2].to_i
|
|
86
|
+
cols = ARGV[3].to_i
|
|
87
|
+
filename = ARGV[4]
|
|
88
|
+
|
|
89
|
+
result = case [lib, mode]
|
|
90
|
+
when ["xlsxtream", "write"]
|
|
91
|
+
require "xlsxtream"
|
|
92
|
+
measure do
|
|
93
|
+
Xlsxtream::Workbook.open(filename) do |wb|
|
|
94
|
+
wb.write_worksheet("Data") do |sheet|
|
|
95
|
+
rows.times do |r|
|
|
96
|
+
sheet << generate_row(r, cols)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
when ["fast_excel", "write"]
|
|
102
|
+
require "fast_excel"
|
|
103
|
+
measure do
|
|
104
|
+
wb = FastExcel.open(filename)
|
|
105
|
+
sheet = wb.add_worksheet("Data")
|
|
106
|
+
rows.times do |r|
|
|
107
|
+
sheet.append_row(generate_row(r, cols))
|
|
108
|
+
end
|
|
109
|
+
wb.close
|
|
110
|
+
end
|
|
111
|
+
when ["caxlsx", "write"]
|
|
112
|
+
require "caxlsx"
|
|
113
|
+
measure do
|
|
114
|
+
p = Axlsx::Package.new
|
|
115
|
+
wb = p.workbook
|
|
116
|
+
wb.add_worksheet(name: "Data") do |sheet|
|
|
117
|
+
rows.times do |r|
|
|
118
|
+
sheet.add_row(generate_row(r, cols))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
p.serialize(filename)
|
|
122
|
+
end
|
|
123
|
+
when ["rubyXL", "write"]
|
|
124
|
+
require "rubyXL"
|
|
125
|
+
measure do
|
|
126
|
+
wb = RubyXL::Workbook.new
|
|
127
|
+
sheet = wb[0]
|
|
128
|
+
sheet.sheet_name = "Data"
|
|
129
|
+
rows.times do |r|
|
|
130
|
+
row_data = generate_row(r, cols)
|
|
131
|
+
row_data.each_with_index do |val, c|
|
|
132
|
+
sheet.add_cell(r, c, val)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
wb.write(filename)
|
|
136
|
+
end
|
|
137
|
+
when ["xlsxrb_stream", "write"]
|
|
138
|
+
require_relative "lib/xlsxrb"
|
|
139
|
+
measure do
|
|
140
|
+
Xlsxrb.generate(filename) do |wb|
|
|
141
|
+
wb.sheet("Data") do |sheet|
|
|
142
|
+
rows.times do |r|
|
|
143
|
+
sheet.row(generate_row(r, cols))
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
when ["xlsxrb_inmemory", "write"]
|
|
149
|
+
require_relative "lib/xlsxrb"
|
|
150
|
+
measure do
|
|
151
|
+
wb = Xlsxrb.build do |b|
|
|
152
|
+
b.sheet("Data") do |s|
|
|
153
|
+
rows.times do |r|
|
|
154
|
+
s.row(generate_row(r, cols))
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
Xlsxrb.write(filename, wb)
|
|
159
|
+
end
|
|
160
|
+
when ["xlsxrb_stream", "read"]
|
|
161
|
+
require_relative "lib/xlsxrb"
|
|
162
|
+
measure do
|
|
163
|
+
count = 0
|
|
164
|
+
Xlsxrb.foreach(filename) do |sheet|
|
|
165
|
+
sheet.each do |row|
|
|
166
|
+
row.cells.each do |cell|
|
|
167
|
+
_val = cell.value
|
|
168
|
+
count += 1
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
when ["xlsxrb_inmemory", "read"]
|
|
174
|
+
require_relative "lib/xlsxrb"
|
|
175
|
+
measure do
|
|
176
|
+
wb = Xlsxrb.read(filename)
|
|
177
|
+
count = 0
|
|
178
|
+
wb.sheets.each do |sheet|
|
|
179
|
+
sheet.rows.each do |row|
|
|
180
|
+
row.cells.each do |cell|
|
|
181
|
+
_val = cell.value
|
|
182
|
+
count += 1
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
when ["creek", "read"]
|
|
188
|
+
require "creek"
|
|
189
|
+
measure do
|
|
190
|
+
creek = Creek::Book.new(filename)
|
|
191
|
+
count = 0
|
|
192
|
+
creek.sheets.each do |sheet|
|
|
193
|
+
sheet.rows.each do |row|
|
|
194
|
+
row.each_value do |_val|
|
|
195
|
+
count += 1
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
when ["roo", "read"]
|
|
201
|
+
require "roo"
|
|
202
|
+
measure do
|
|
203
|
+
xlsx = Roo::Excelx.new(filename)
|
|
204
|
+
count = 0
|
|
205
|
+
xlsx.each_row_streaming do |row|
|
|
206
|
+
row.each do |cell|
|
|
207
|
+
_val = cell&.value
|
|
208
|
+
count += 1
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
when ["xsv", "read"]
|
|
213
|
+
require "xsv"
|
|
214
|
+
measure do
|
|
215
|
+
x = Xsv.open(filename)
|
|
216
|
+
count = 0
|
|
217
|
+
x.sheets.each do |sheet|
|
|
218
|
+
sheet.each do |row|
|
|
219
|
+
row.each do |_val|
|
|
220
|
+
count += 1
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
when ["rubyXL", "read"]
|
|
226
|
+
require "rubyXL"
|
|
227
|
+
measure do
|
|
228
|
+
wb = RubyXL::Parser.parse(filename)
|
|
229
|
+
count = 0
|
|
230
|
+
wb.worksheets.each do |sheet|
|
|
231
|
+
sheet.each do |row|
|
|
232
|
+
next unless row
|
|
233
|
+
row.cells.each do |cell|
|
|
234
|
+
_val = cell&.value
|
|
235
|
+
count += 1
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
else
|
|
241
|
+
raise "Unknown benchmark target: #{lib} #{mode}"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
puts result.to_json
|
|
245
|
+
RUBY
|
|
246
|
+
|
|
247
|
+
runner_file = "benchmark_runner.rb"
|
|
248
|
+
File.write(runner_file, RUNNER_SCRIPT)
|
|
249
|
+
|
|
250
|
+
def run_isolated(lib, mode, rows, cols, filename)
|
|
251
|
+
cmd = ["ruby", "-Ilib", "benchmark_runner.rb", lib, mode, rows.to_s, cols.to_s, filename]
|
|
252
|
+
stdout, stderr, status = Bundler.with_unbundled_env do
|
|
253
|
+
Open3.capture3(*cmd)
|
|
254
|
+
end
|
|
255
|
+
unless status.success?
|
|
256
|
+
warn "Failed to run #{lib} #{mode}: #{stderr}"
|
|
257
|
+
return nil
|
|
258
|
+
end
|
|
259
|
+
JSON.parse(stdout.strip, symbolize_names: true)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def run_benchmark_series(name, lib, mode, rows, cols, filename, runs)
|
|
263
|
+
unless AVAILABLE_GEMS[lib]
|
|
264
|
+
puts "Skipping #{name} (gem not installed)"
|
|
265
|
+
return nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
print "Running #{name} (#{runs} runs)... "
|
|
269
|
+
$stdout.flush
|
|
270
|
+
results = []
|
|
271
|
+
runs.times do |_i|
|
|
272
|
+
File.delete(filename) if File.exist?(filename) && mode == "write"
|
|
273
|
+
res = run_isolated(lib, mode, rows, cols, filename)
|
|
274
|
+
if res
|
|
275
|
+
results << res
|
|
276
|
+
print "#{res[:time].round(2)}s "
|
|
277
|
+
$stdout.flush
|
|
278
|
+
else
|
|
279
|
+
print "ERR "
|
|
280
|
+
$stdout.flush
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
puts
|
|
284
|
+
|
|
285
|
+
return nil if results.empty?
|
|
286
|
+
|
|
287
|
+
times = results.map { |r| r[:time] }.sort
|
|
288
|
+
mems = results.map { |r| r[:peak_memory_mb] }.sort
|
|
289
|
+
gcs = results.map { |r| r[:gc_count] }.sort
|
|
290
|
+
|
|
291
|
+
median_time = times[times.size / 2]
|
|
292
|
+
mean_time = times.sum / times.size
|
|
293
|
+
median_mem = mems[mems.size / 2]
|
|
294
|
+
median_gc = gcs[gcs.size / 2]
|
|
295
|
+
|
|
296
|
+
{
|
|
297
|
+
name: name,
|
|
298
|
+
median_time: median_time,
|
|
299
|
+
mean_time: mean_time,
|
|
300
|
+
median_mem: median_mem,
|
|
301
|
+
median_gc: median_gc
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# 1. Generate a standard reference file for reading benchmarks
|
|
306
|
+
ref_file = "bench_reference_data.xlsx"
|
|
307
|
+
puts "\n[Setup] Generating reference file (#{ROWS} x #{COLS}) for read benchmarks..."
|
|
308
|
+
run_isolated("xlsxrb_stream", "write", ROWS, COLS, ref_file)
|
|
309
|
+
|
|
310
|
+
# 2. Benchmark Write
|
|
311
|
+
puts "\n=== Benchmarking Write Performance ==="
|
|
312
|
+
write_targets = [
|
|
313
|
+
["xlsxtream 3.1.0", "xlsxtream", "Streaming", "Inline String"],
|
|
314
|
+
["xlsxrb (Streaming)", "xlsxrb_stream", "Streaming", "SST (Shared)"],
|
|
315
|
+
["fast_excel 0.5.0 (C)", "fast_excel", "Streaming", "SST (Shared)"],
|
|
316
|
+
["caxlsx 4.5.0", "caxlsx", "In-Memory", "Inline String"],
|
|
317
|
+
["xlsxrb (In-Memory)", "xlsxrb_inmemory", "In-Memory", "SST (Shared)"],
|
|
318
|
+
["rubyXL 3.4.38", "rubyXL", "In-Memory", "Inline String"]
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
write_results = []
|
|
322
|
+
write_targets.each do |name, lib, model, storage|
|
|
323
|
+
target_file = "bench_write_#{lib}.xlsx"
|
|
324
|
+
res = run_benchmark_series(name, lib, "write", ROWS, COLS, target_file, RUNS)
|
|
325
|
+
if res
|
|
326
|
+
res[:model] = model
|
|
327
|
+
res[:storage] = storage
|
|
328
|
+
write_results << res
|
|
329
|
+
end
|
|
330
|
+
FileUtils.rm_f(target_file)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# 3. Benchmark Read
|
|
334
|
+
puts "\n=== Benchmarking Read Performance ==="
|
|
335
|
+
read_targets = [
|
|
336
|
+
["xlsxrb (Streaming)", "xlsxrb_stream", "Streaming"],
|
|
337
|
+
["xlsxrb (In-Memory)", "xlsxrb_inmemory", "In-Memory"],
|
|
338
|
+
["creek 2.6.3", "creek", "Streaming"],
|
|
339
|
+
["roo 3.0.0", "roo", "Streaming"],
|
|
340
|
+
["xsv 1.4.1", "xsv", "Streaming"],
|
|
341
|
+
["rubyXL 3.4.38", "rubyXL", "In-Memory"]
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
read_results = []
|
|
345
|
+
read_targets.each do |name, lib, model|
|
|
346
|
+
res = run_benchmark_series(name, lib, "read", ROWS, COLS, ref_file, RUNS)
|
|
347
|
+
if res
|
|
348
|
+
res[:model] = model
|
|
349
|
+
read_results << res
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Cleanup
|
|
354
|
+
FileUtils.rm_f(ref_file)
|
|
355
|
+
FileUtils.rm_f(runner_file)
|
|
356
|
+
|
|
357
|
+
# Print Tables
|
|
358
|
+
puts "\n" + ("=" * 80)
|
|
359
|
+
puts "### Write Performance (#{ROWS * COLS} cells: #{ROWS} rows x #{COLS} cols)"
|
|
360
|
+
puts ""
|
|
361
|
+
puts "| Library | Model | Write String Storage | Time (Median) | Time (Mean) | Peak Memory | GC Count |"
|
|
362
|
+
puts "| ---------------------- | ----------- | -------------------- | ------------- | ----------- | ----------- | -------- |"
|
|
363
|
+
write_results.sort_by { |r| r[:median_time] }.each do |r|
|
|
364
|
+
printf "| %-22s | %-11s | %-20s | %6.2f s | %6.2f s | %7.1f MB | %6.1f |\n",
|
|
365
|
+
r[:name], r[:model], r[:storage], r[:median_time], r[:mean_time], r[:median_mem], r[:median_gc]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
puts "\n### Read Performance (#{ROWS * COLS} cells: #{ROWS} rows x #{COLS} cols)"
|
|
369
|
+
puts ""
|
|
370
|
+
puts "| Library | Model | Time (Median) | Time (Mean) | Peak Memory | GC Count |"
|
|
371
|
+
puts "| ---------------------- | ----------- | ------------- | ----------- | ----------- | -------- |"
|
|
372
|
+
read_results.sort_by { |r| r[:median_time] }.each do |r|
|
|
373
|
+
printf "| %-22s | %-11s | %6.2f s | %6.2f s | %7.1f MB | %6.1f |\n",
|
|
374
|
+
r[:name], r[:model], r[:median_time], r[:mean_time], r[:median_mem], r[:median_gc]
|
|
375
|
+
end
|
|
376
|
+
puts "=" * 80
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -26,12 +26,19 @@ As a strict rule, **we do not accept dynamic method definitions using `method_mi
|
|
|
26
26
|
2. **Developer Experience**: IDE autocompletion, jump-to-definition, and YARD documentation work perfectly.
|
|
27
27
|
3. **Traceability**: If a method exists, you can `grep` for it.
|
|
28
28
|
|
|
29
|
-
Even in cases where proxy patterns (e.g. `WorksheetProxy`) or OOXML builder mappings (e.g. `ChartBuilder`, `SeriesBuilder`) would traditionally benefit from dynamic
|
|
29
|
+
Even in cases where proxy patterns (e.g. `WorksheetProxy`) or OOXML builder mappings (e.g. `ChartBuilder`, `SeriesBuilder`) would traditionally benefit from dynamic delegation to avoid boilerplate, we explicitly generate and write out those delegations in the source code.
|
|
30
30
|
|
|
31
|
-
###
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
### 1. **`Xlsxrb` Module is the ONLY Entrypoint:**
|
|
32
|
+
The `Xlsxrb` module provides the top-level methods: `generate`, `build`, `read`, `foreach`, and `modify`. Users should **never** instantiate internal classes (like `Xlsxrb::Ooxml::WorkbookWriter`) directly.
|
|
33
|
+
|
|
34
|
+
2. **The `@api public` Contract (SemVer Guarantee):**
|
|
35
|
+
Any module, class, or method tagged with `# @api public` in its YARD documentation is guaranteed to follow Semantic Versioning.
|
|
36
|
+
- Patch versions (0.1.x -> 0.1.y) will not break these APIs.
|
|
37
|
+
- Minor versions (0.x.0 -> 0.y.0) will not break these APIs once 1.0.0 is released (during 0.x.x, it is a best-effort promise).
|
|
38
|
+
- Major versions (1.x -> 2.x) are the only time breaking changes to `@api public` components are permitted.
|
|
39
|
+
|
|
40
|
+
3. **Block-Yielded Objects are Public APIs:**
|
|
41
|
+
All builder objects yielded into blocks (e.g., `writer` in `Xlsxrb.generate { |writer| }`, `sheet` in `writer.sheet { |sheet| }`, `chart` in `sheet.chart { |chart| }`) are explicitly marked as `@api public`. Their exposed methods constitute the DSL and are strictly protected by the SemVer contract.
|
|
35
42
|
|
|
36
43
|
---
|
|
37
44
|
|
|
@@ -1426,6 +1426,14 @@ sheet.rows.each do |row|
|
|
|
1426
1426
|
end
|
|
1427
1427
|
```
|
|
1428
1428
|
|
|
1429
|
+
### Console Output
|
|
1430
|
+
|
|
1431
|
+
```text
|
|
1432
|
+
=== Read Validation ===
|
|
1433
|
+
Row 0: A1: "Format" (String), B1: "Value" (String)
|
|
1434
|
+
Row 1: A2: "Rich Text Cell" (String), B2: "Normal BOLD RED ITALIC BLUE" (String)
|
|
1435
|
+
```
|
|
1436
|
+
|
|
1429
1437
|
<hr/>
|
|
1430
1438
|
|
|
1431
1439
|
## Cell Times
|
data/docs/wasm/ruby.wasm
CHANGED
|
Binary file
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_lsp/addon"
|
|
4
|
+
require_relative "completion_listener"
|
|
5
|
+
require_relative "../../xlsxrb/version"
|
|
6
|
+
|
|
7
|
+
module RubyLsp
|
|
8
|
+
module Xlsxrb
|
|
9
|
+
# Ruby LSP Add-on for xlsxrb.
|
|
10
|
+
#
|
|
11
|
+
# [Context & Lifecycle Note]
|
|
12
|
+
# This add-on serves as a bridge/polyfill for current Ruby LSP environments.
|
|
13
|
+
# While xlsxrb ships with complete RBS signatures (`sig/generated/`), Ruby LSP's
|
|
14
|
+
# type inferrer does not yet perform automatic static type inference from method
|
|
15
|
+
# block signatures to block parameters (e.g., `Xlsxrb.generate do |wb|`).
|
|
16
|
+
#
|
|
17
|
+
# This add-on enables immediate out-of-the-box autocompletion and rich Markdown
|
|
18
|
+
# documentation across all public block arguments.
|
|
19
|
+
#
|
|
20
|
+
# As the Ruby tooling ecosystem evolves and Ruby LSP gains native block argument
|
|
21
|
+
# type resolution from gem RBS signatures in the future, this add-on will become
|
|
22
|
+
# redundant and can eventually be deprecated or removed.
|
|
23
|
+
class Addon < ::RubyLsp::Addon
|
|
24
|
+
def activate(global_state, _message_queue)
|
|
25
|
+
@global_state = global_state
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def deactivate; end
|
|
29
|
+
|
|
30
|
+
def name
|
|
31
|
+
"xlsxrb"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def version
|
|
35
|
+
::Xlsxrb::VERSION
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_completion_listener(response_builder, node_context, dispatcher, _uri = nil)
|
|
39
|
+
CompletionListener.new(response_builder, node_context, dispatcher, @global_state)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|