dumb_delimited 1.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a4d7deecbbed56e6940058fdc548436cbdb0f081b43164bc841017601c27726
4
- data.tar.gz: b94ae4701ae2dec7cee54befe2e85c11ea20a9ae5906d0f7da6a8da1ef377eb6
3
+ metadata.gz: 98873351e7fa4aeb8624f716a4de4c2090e066b8b14b138d69bd01a5c7d6f4c4
4
+ data.tar.gz: 50595400f33304f7244c850cd72750dcbc70be0caabcbf95cee61dd671167cd1
5
5
  SHA512:
6
- metadata.gz: 9f2994715b89523b10c4fb1375b2a073e306cedfae565f502241a4ad8f82876b1ec02d1b453641842ca331bba2812ea73783c70af7cc1431b0bbaeec83e0ce09
7
- data.tar.gz: 36b9db417ddd340f695e1332ea005ad8ec03c3e8d31f5cc395b8479463497bee0e518e89f5916a90d0267ebfe9779abb21995d35e66f8da67b91f282e955b2b1
6
+ metadata.gz: 8797e92b1c578dfaa961b85d31eac7004539123fc3994f9b08ecbb05d7c6675ac01fa961e881795820b59218a27104f8ee60cd133a23a56364a77d77631bc5cc
7
+ data.tar.gz: 71795adf574729557f3524f4007c62baeee9bcb8953874362afbc5843a67c424c88dadf30531738ed57bcd56499ed5807e8fe230214bc9cee30910f0768ec514
data/CHANGELOG.md CHANGED
@@ -1,8 +1,35 @@
1
+ ## 3.0.0
2
+
3
+ * [BREAKING] Drop support for Ruby < 3.4
4
+ * Fix CSV keyword-argument compatibility
5
+
6
+
7
+ ## 2.0.0
8
+
9
+ * [BREAKING] Do not activate `options[:converters]` by default
10
+ * [BREAKING] Remove *pleasant_path* dependency. This is considered a
11
+ breaking change because *pleasant_path* adds extension methods to
12
+ several Ruby core classes that consumer code may depend on.
13
+ * [BREAKING] Remove `append_to_file` instance method
14
+ * [BREAKING] Rename `each_in_file` class method to `read_each`
15
+ * Add `parse_each` class method
16
+ * Rename `parse_file` class method to `read`, and alias as `parse_file`
17
+ * Rename `parse_text` class method to `parse`, and alias as `parse_text`
18
+ * Add `write` class method
19
+ * Add `append` class method
20
+ * Add `eol` parameter to `to_s` instance method
21
+ * Add `DumbDelimited.csv`, `.psv`, and `.tsv` convenience shortcuts
22
+ * Activate `options[:liberal_parsing]` by default
23
+ * Make `options` inheritable
24
+ * Fix behavior when `options[:headers]` is set
25
+ * Fix behavior when `options[:write_headers]` is set
26
+
27
+
1
28
  ## 1.1.0
2
29
 
3
- * Fixed gem load on case-sensitive file systems.
4
- * Added `parse_text` class method.
5
- * Added `append_to_file` instance method.
30
+ * Add `parse_text` class method
31
+ * Add `append_to_file` instance method
32
+ * Fix gem load on case-sensitive file systems
6
33
 
7
34
 
8
35
  ## 1.0.0
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in dumb_delimited.gemspec
4
4
  gemspec
5
+
6
+ gem "rake", "~> 13.0"
7
+ gem "minitest", "~> 6.0"
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dumb_delimited
2
2
 
3
- A library for unsophisticated delimited flat file IO. *dumb_delimited*
3
+ A library for unsophisticated delimited flat file IO. `dumb_delimited`
4
4
  mixes models and persistence in that "probably wrong but feels so right"
5
5
  kind of way.
6
6
 
@@ -33,7 +33,7 @@ used as a superclass or simply assigned to a constant.
33
33
  ```ruby
34
34
  class Product < DumbDelimited[:sku, :name, :base_price, :sale_price]
35
35
  def on_sale?
36
- sale_price < base_price
36
+ sale_price.to_f < base_price.to_f
37
37
  end
38
38
  end
39
39
 
@@ -42,80 +42,83 @@ Customer.delimiter = "|"
42
42
  ```
43
43
 
44
44
  Because "customers.psv" is pipe-delimited, we also set the delimiter
45
- for the Customer class. By default, model classes use comma (`","`) as
46
- the delimiter. Whenever a delimiter is set, it applies to all future
45
+ for the Customer class. By default, a model class uses a comma (`","`)
46
+ as its delimiter. Whenever a delimiter is set, it applies to all future
47
47
  IO operations for that model class.
48
48
 
49
- Now we can read each flat file, and recieve an array of model objects.
49
+ Convenience shortcuts that create a model class and set its delimiter
50
+ are also provided for a few common delimiters. Notably,
51
+ `DumbDelimited::psv` for a model class with a pipe (`"|"`) delimiter.
52
+ Thus, the `Customer` class could alternatively be written as:
50
53
 
51
54
  ```ruby
52
- products = Product.parse_file("products.csv")
53
- customers = Customer.parse_file("customers.psv")
55
+ Customer = DumbDelimited.psv(:name, :email, :address)
54
56
  ```
55
57
 
56
- This, however, will load the entire contents of each file into memory.
58
+ Using our model classes, we can read each flat file, and receive an
59
+ array of model objects:
60
+
61
+ ```ruby
62
+ products = Product.read("products.csv")
63
+ customers = Customer.read("customers.psv")
64
+ ```
65
+
66
+ However, this will load the entire contents of each file into memory.
57
67
  Let's say our customers file is very large, and we would prefer to
58
- iterate over it rather than load it all into memory at once. To do so,
59
- we can use the `each_in_file` method that the model class provides.
60
- Below is a complete example in which we load our product data, create a
61
- listing of products on sale, and iterate over our customers, notifying
62
- each customer of the sale products:
68
+ iterate over it one row at a time rather than load it all into memory at
69
+ once. To do so, we can use the `read_each` method. Below is a complete
70
+ example in which we load our product data, create a listing of products
71
+ on sale, and iterate over our customers, notifying each customer of the
72
+ sale products:
63
73
 
64
74
  ```ruby
65
- products = Product.parse_file("products.csv")
75
+ products = Product.read("products.csv")
66
76
 
67
77
  listing = products.select(&:on_sale?).map do |product|
68
78
  "* #{product.name} (#{product.sale_price})"
69
79
  end.join("\n")
70
80
 
71
- Customer.each_in_file("customers.psv") do |customer|
72
- message =
73
- "Hi #{customer.name}!\n\n" \
74
- "The following products are on sale:\n\n#{listing}"
81
+ Customer.read_each("customers.psv") do |customer|
82
+ message = <<~MESSAGE
83
+ Hi #{customer.name}!
84
+
85
+ The following products are on sale:
86
+
87
+ #{listing}
88
+ MESSAGE
75
89
 
76
90
  notify(customer.email, message)
77
91
  end
78
92
  ```
79
93
 
80
94
  Let's say the sale is now over, and we want to change our sale prices
81
- back to our base prices. *dumb_delimited* includes the
82
- [*pleasant_path*](https://rubygems.org/gems/pleasant_path) gem, which
83
- offers a fluent API for writing files. To finish our task, we use the
84
- `Array#write_to_file` method provided by *pleasant_path*, which in turn
85
- invokes `Product#to_s` (provided by *dumb_delimited*) on each model
86
- object.
95
+ back to our base prices. We can load our product data, modify it
96
+ directly, and finally persist it back with the `write` method:
87
97
 
88
98
  ```ruby
89
- Product.parse_file("products.csv").each do |product|
99
+ products = Product.read("products.csv")
100
+
101
+ products.each do |product|
90
102
  product.sale_price = product.base_price
91
- end.write_to_file("products.csv")
103
+ end
104
+
105
+ Product.write("products.csv", products)
92
106
  ```
93
107
 
94
- For a more detailed explanation of the *dumb_delimited* API, browse the
95
- [full documentation](http://www.rubydoc.info/gems/dumb_delimited/).
108
+ For a more detailed explanation of the `dumb_delimited` API, browse the
109
+ [API documentation](https://www.rubydoc.info/gems/dumb_delimited/).
96
110
 
97
111
 
98
112
  ## Installation
99
113
 
100
- Install from [Ruby Gems](https://rubygems.org/gems/dumb_delimited):
101
-
102
- ```bash
103
- $ gem install dumb_delimited
104
- ```
105
-
106
- Then require in your Ruby script:
107
-
108
- ```ruby
109
- require "dumb_delimited"
110
- ```
114
+ Install the [`dumb_delimited` gem](https://rubygems.org/gems/dumb_delimited).
111
115
 
112
116
 
113
117
  ## Contributing
114
118
 
115
- Run `rake test` to run the tests. You can also run `rake irb` for an
116
- interactive prompt that pre-loads the project code.
119
+ Run `rake test` to run the tests.
117
120
 
118
121
 
119
122
  ## License
120
123
 
121
- [MIT License](https://opensource.org/licenses/MIT)
124
+ [MIT License](LICENSE.txt)
data/Rakefile CHANGED
@@ -1,18 +1,5 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
- require "yard"
4
-
5
-
6
- YARD::Rake::YardocTask.new(:doc) do |t|
7
- end
8
-
9
- desc "Launch IRB with this gem pre-loaded"
10
- task :irb do
11
- require "dumb_delimited"
12
- require "irb"
13
- ARGV.clear
14
- IRB.start
15
- end
16
3
 
17
4
  Rake::TestTask.new(:test) do |t|
18
5
  t.libs << "test"
@@ -1,29 +1,27 @@
1
- # coding: utf-8
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "dumb_delimited/version"
1
+ require_relative "lib/dumb_delimited/version"
5
2
 
6
3
  Gem::Specification.new do |spec|
7
4
  spec.name = "dumb_delimited"
8
5
  spec.version = DumbDelimited::VERSION
9
6
  spec.authors = ["Jonathan Hefner"]
10
- spec.email = ["jonathan.hefner@gmail.com"]
7
+ spec.email = ["jonathan@hefner.pro"]
11
8
 
12
- spec.summary = %q{Library for unsophisticated delimited flat file IO}
9
+ spec.summary = %q{Unsophisticated delimited flat file IO}
13
10
  spec.homepage = "https://github.com/jonathanhefner/dumb_delimited"
14
11
  spec.license = "MIT"
12
+ spec.required_ruby_version = ">= 3.4"
15
13
 
16
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
- f.match(%r{^(test|spec|features)/})
14
+ spec.metadata["source_code_uri"] = spec.homepage
15
+ spec.metadata["changelog_uri"] = spec.metadata["source_code_uri"] + "/blob/master/CHANGELOG.md"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(__dir__) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.start_with?("test/", ".git") }
18
21
  end
19
22
  spec.bindir = "exe"
20
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
24
  spec.require_paths = ["lib"]
22
25
 
23
- spec.add_runtime_dependency "pleasant_path", "~> 1.0"
24
-
25
- spec.add_development_dependency "bundler", "~> 1.15"
26
- spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "minitest", "~> 5.0"
28
- spec.add_development_dependency "yard", "~> 0.9"
26
+ spec.add_dependency "csv"
29
27
  end
@@ -1,3 +1,3 @@
1
1
  module DumbDelimited
2
- VERSION = "1.1.0"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -1,28 +1,29 @@
1
1
  require "csv"
2
- require "pleasant_path"
3
2
  require_relative "dumb_delimited/version"
4
3
 
5
4
 
6
5
  module DumbDelimited
7
6
 
8
7
  # Returns a model class for delimited data consisting of the specified
9
- # columns. The returned class inherits from Ruby's
10
- # {https://ruby-doc.org/core/Struct.html +Struct+}, allowing data
11
- # manipulation via accessor methods, via indexing by column name, and
12
- # via indexing by column number. See {ClassMethods} and
13
- # {InstanceMethods} for the IO methods the returned class provides.
8
+ # +columns+. The returned class inherits from Ruby's
9
+ # {https://docs.ruby-lang.org/en/master/Struct.html +Struct+},
10
+ # allowing data manipulation via accessor methods, via indexing by
11
+ # column name, and via indexing by column number. See {ClassMethods}
12
+ # and {InstanceMethods} for the additional methods the returned class
13
+ # provides.
14
14
  #
15
15
  # @example
16
16
  # class Product < DumbDelimited[:sku, :name, :base_price, :sale_price]
17
17
  # def on_sale?
18
- # sale_price < base_price
18
+ # sale_price.to_f < base_price.to_f
19
19
  # end
20
20
  # end
21
21
  #
22
+ # @example
22
23
  # Customer = DumbDelimited[:name, :email, :address]
23
24
  #
24
- # @param columns [*Symbol]
25
- # @return [Class]
25
+ # @param columns [Array<Symbol>]
26
+ # @return [Class<Struct>]
26
27
  def self.[](*columns)
27
28
  Struct.new(*columns) do
28
29
  extend DumbDelimited::ClassMethods
@@ -30,42 +31,111 @@ module DumbDelimited
30
31
  end
31
32
  end
32
33
 
34
+ # Convenience shortcut to create a model class and set
35
+ # {ClassMethods#delimiter} to +","+.
36
+ #
37
+ # Note: This method exists mostly for parity with {psv} and {tsv}.
38
+ # Unless +CSV::DEFAULT_OPTIONS+ has been modified, the delimiter will
39
+ # already default to +","+.
40
+ #
41
+ # @example
42
+ # # This...
43
+ # Point = DumbDelimited.csv(:x, :y, :z)
44
+ #
45
+ # # ...is equivalent to:
46
+ # Point = DumbDelimited[:x, :y, :z]
47
+ # Point.delimiter = ","
48
+ #
49
+ # @param columns [Array<Symbol>]
50
+ # @return [Class<Struct>]
51
+ def self.csv(*columns)
52
+ klass = self[*columns]
53
+ klass.delimiter = ","
54
+ klass
55
+ end
56
+
57
+ # Convenience shortcut to create a model class and set
58
+ # {ClassMethods#delimiter} to +"|"+.
59
+ #
60
+ # @example
61
+ # # This...
62
+ # Point = DumbDelimited.psv(:x, :y, :z)
63
+ #
64
+ # # ...is equivalent to:
65
+ # Point = DumbDelimited[:x, :y, :z]
66
+ # Point.delimiter = "|"
67
+ #
68
+ # @param columns [Array<Symbol>]
69
+ # @return [Class<Struct>]
70
+ def self.psv(*columns)
71
+ klass = self[*columns]
72
+ klass.delimiter = "|"
73
+ klass
74
+ end
75
+
76
+ # Convenience shortcut to create a model class and set
77
+ # {ClassMethods#delimiter} to <code>"\t"</code>.
78
+ #
79
+ # @example
80
+ # # This...
81
+ # Point = DumbDelimited.tsv(:x, :y, :z)
82
+ #
83
+ # # ...is equivalent to:
84
+ # Point = DumbDelimited[:x, :y, :z]
85
+ # Point.delimiter = "\t"
86
+ #
87
+ # @param columns [Array<Symbol>]
88
+ # @return [Class<Struct>]
89
+ def self.tsv(*columns)
90
+ klass = self[*columns]
91
+ klass.delimiter = "\t"
92
+ klass
93
+ end
94
+
33
95
  end
34
96
 
35
97
 
36
98
  module DumbDelimited::ClassMethods
37
99
 
38
- # Returns the advanced options Hash. The Hash is not +dup+ed and can
39
- # be modified directly. Any modifications will be applied to all
40
- # future IO operations for the model class. For detailed information
41
- # about available options, see Ruby's
42
- # {http://ruby-doc.org/stdlib/libdoc/csv/rdoc/CSV.html#method-c-new
43
- # CSV module}.
100
+ # Returns the CSV options Hash. The Hash is not +dup+'d and can be
101
+ # modified directly. Any modifications will be applied to all future
102
+ # IO operations for the model class.
44
103
  #
45
- # @return [Hash]
104
+ # For detailed information about available options, see Ruby's
105
+ # {https://docs.ruby-lang.org/en/master/CSV.html#method-c-new CSV
106
+ # class}.
107
+ #
108
+ # @return [Hash{Symbol => Object}]
46
109
  def options
47
- @options ||= {
48
- col_sep: ",",
49
- skip_blanks: true,
50
- converters: :numeric,
51
- }
110
+ @options ||= if superclass == Struct
111
+ CSV::DEFAULT_OPTIONS.merge(
112
+ skip_blanks: true,
113
+ liberal_parsing: true,
114
+ )
115
+ else
116
+ superclass.options.dup
117
+ end
52
118
  end
53
119
 
54
- # Sets the advanced options Hash. The entire Hash is replaced, and
55
- # the new value will be applied to all future IO operations for the
56
- # model class. To set options individually, see {options}. For
57
- # detailed information about available options, see Ruby's
58
- # {http://ruby-doc.org/stdlib/libdoc/csv/rdoc/CSV.html#method-c-new
59
- # CSV module}.
120
+ # Sets the CSV options Hash. The entire Hash is replaced, and the new
121
+ # values will be applied to all future IO operations for the model
122
+ # class. To set options individually, see {options}.
60
123
  #
61
- # @param o [Hash]
62
- def options=(o)
63
- @options = o
124
+ # For detailed information about available options, see Ruby's
125
+ # {https://docs.ruby-lang.org/en/master/CSV.html#method-c-new CSV
126
+ # class}.
127
+ #
128
+ # @param opts [Hash{Symbol => Object}]
129
+ # @return [opts]
130
+ def options=(opts)
131
+ @options = opts
64
132
  end
65
133
 
66
134
  # Returns the column delimiter used in IO operations. Defaults to a
67
135
  # comma (<code>","</code>).
68
136
  #
137
+ # Equivalent to <code>options[:col_sep]</code>.
138
+ #
69
139
  # @return [String]
70
140
  def delimiter
71
141
  self.options[:col_sep]
@@ -76,6 +146,8 @@ module DumbDelimited::ClassMethods
76
146
  # delimiter can be safely chosen, and all IO operations will quote
77
147
  # field values as necessary.
78
148
  #
149
+ # Equivalent to <code>options[:col_sep] = delim</code>.
150
+ #
79
151
  # @example
80
152
  # Point = DumbDelimited[:x, :y, :z]
81
153
  # p = Point.new(1, 2, 3)
@@ -83,9 +155,10 @@ module DumbDelimited::ClassMethods
83
155
  # Point.delimiter = "|"
84
156
  # p.to_s # == "1|2|3"
85
157
  #
86
- # @param d [String]
87
- def delimiter=(d)
88
- self.options[:col_sep] = d
158
+ # @param delim [String]
159
+ # @return [delim]
160
+ def delimiter=(delim)
161
+ self.options[:col_sep] = delim
89
162
  end
90
163
 
91
164
  # Parses a single delimited line into a model object.
@@ -95,34 +168,53 @@ module DumbDelimited::ClassMethods
95
168
  # Point.parse_line("1,2,3") # == Point.new(1, 2, 3)
96
169
  #
97
170
  # @param line [String]
98
- # @return [self]
171
+ # @return [Struct]
99
172
  def parse_line(line)
100
- self.new(*CSV.parse_line(line, self.options))
173
+ parse_each(line).first
101
174
  end
102
175
 
103
- # Parses a string into an array of model objects.
176
+ # Parses a string or IO object into an array of model objects.
104
177
  #
105
178
  # @example
106
179
  # Point = DumbDelimited[:x, :y, :z]
107
- # Point.parse_text("1,2,3\n4,5,6\n7,8,9\n")
180
+ # Point.parse("1,2,3\n4,5,6\n7,8,9\n")
108
181
  # # == [
109
182
  # # Point.new(1, 2, 3),
110
183
  # # Point.new(4, 5, 6),
111
184
  # # Point.new(7, 8, 9)
112
185
  # # ]
113
186
  #
114
- # @param text [String]
115
- # @return [Array<self>]
116
- def parse_text(text)
117
- # using CSV.new.each instead of CSV.parse to avoid unnecessary mass
118
- # memory allocation and deallocation
119
- CSV.new(text, self.options).each.map{|row| self.new(*row) }
187
+ # @param data [String, IO]
188
+ # @return [Array<Struct>]
189
+ def parse(data)
190
+ parse_each(data).to_a
120
191
  end
121
192
 
122
- # Parses an entire delimited file into an array of model objects.
123
- # This will load the entire contents of the file into memory, and may
124
- # not be suitable for large files. To iterate over file contents
125
- # without loading it all into memory at once, use {each_in_file}.
193
+ alias_method :parse_text, :parse
194
+
195
+ # Parses a string or IO object one line at a time, yielding a model
196
+ # object for each line.
197
+ #
198
+ # If no block is given, an Enumerator is returned instead.
199
+ #
200
+ # @overload parse_each(data, &block)
201
+ # @param data [String, IO]
202
+ # @yieldparam model [Struct]
203
+ # @return [void]
204
+ #
205
+ # @overload parse_each(data)
206
+ # @param data [String, IO]
207
+ # @return [Enumerator<Struct>]
208
+ def parse_each(data, &block)
209
+ return to_enum(__method__, data) unless block_given?
210
+
211
+ csv_each(CSV.new(data, **self.options), &block)
212
+ end
213
+
214
+ # Parses a file into an array of model objects. This will load the
215
+ # entire contents of the file into memory, and may not be suitable for
216
+ # large files. To iterate over file contents without loading it all
217
+ # into memory at once, use {read_each}.
126
218
  #
127
219
  # @example
128
220
  # # CONTENTS OF FILE "points.csv":
@@ -131,7 +223,7 @@ module DumbDelimited::ClassMethods
131
223
  # # 7,8,9
132
224
  #
133
225
  # Point = DumbDelimited[:x, :y, :z]
134
- # Point.parse_file("points.csv")
226
+ # Point.read("points.csv")
135
227
  # # == [
136
228
  # # Point.new(1, 2, 3),
137
229
  # # Point.new(4, 5, 6),
@@ -139,27 +231,77 @@ module DumbDelimited::ClassMethods
139
231
  # # ]
140
232
  #
141
233
  # @param path [String, Pathname]
142
- # @return [Array<self>]
143
- def parse_file(path)
144
- each_in_file(path).to_a
234
+ # @return [Array<Struct>]
235
+ def read(path)
236
+ read_each(path).to_a
145
237
  end
146
238
 
147
- # Iterates over a delimited file, parsing one row at a time into model
148
- # objects. This avoids loading the entire contents of the file into
149
- # memory at once. If a block is given, it will be passed a model
150
- # object for each row in the file. Otherwise, if a block is not
151
- # given, an Enumerator will be returned. Note that some Enumerator
152
- # methods, such as +Enumerator#to_a+, will cause the entire contents
153
- # of the file to be loaded into memory regardless.
239
+ alias_method :parse_file, :read
240
+
241
+ # Parses a file one line at a time, yielding a model object for each
242
+ # line. This avoids loading the entire contents of the file into
243
+ # memory at once.
154
244
  #
155
- # @param path [String, Pathname]
156
- # @yieldparam [self] current model object
157
- # @return [Enumerator<self>, nil]
158
- def each_in_file(path)
245
+ # If no block is given, an Enumerator is returned instead. Note that
246
+ # some Enumerator methods, such as +Enumerator#to_a+, can cause the
247
+ # entire contents of the file to be loaded into memory.
248
+ #
249
+ # @overload read_each(path, &block)
250
+ # @param path [String, Pathname]
251
+ # @yieldparam model [Struct]
252
+ # @return [void]
253
+ #
254
+ # @overload read_each(path)
255
+ # @param path [String, Pathname]
256
+ # @return [Enumerator<Struct>]
257
+ def read_each(path, &block)
159
258
  return to_enum(__method__, path) unless block_given?
160
259
 
161
- CSV.foreach(path, self.options) do |row|
162
- yield self.new(*row)
260
+ CSV.open(path, **self.options) do |csv|
261
+ csv_each(csv, &block)
262
+ end
263
+ end
264
+
265
+ # Writes a collection of model objects to a file in delimited format.
266
+ # The previous contents of the file are overwritten, unless +append+
267
+ # is set to true.
268
+ #
269
+ # Column headers are written to the file if +:write_headers+ in
270
+ # {options} is set to true *and* either +append+ is false or the file
271
+ # is empty / non-existent. The column headers will be derived from
272
+ # either the value of +:headers+ in {options} if it is an Array, or
273
+ # otherwise from the columns defined by the model.
274
+ #
275
+ # @param path [String, Pathname]
276
+ # @param models [Enumerable<Struct>]
277
+ # @param append [Boolean]
278
+ # @return [void]
279
+ def write(path, models, append: false)
280
+ mode = append ? "a" : "w"
281
+ write_headers = options[:write_headers] && !(append && File.exist?(path) && File.size(path) > 0)
282
+ headers = (!options[:headers].is_a?(Array) && write_headers) ? members : options[:headers]
283
+
284
+ CSV.open(path, mode, **options, write_headers: write_headers, headers: headers) do |csv|
285
+ models.each{|model| csv << model }
286
+ end
287
+ end
288
+
289
+ # Appends a collection of model objects to a file in delimited format.
290
+ # Convenience shortcut for {write} with +append: true+.
291
+ #
292
+ # @param path [String, Pathname]
293
+ # @param models [Enumerable<Struct>]
294
+ # @return [void]
295
+ def append(path, models)
296
+ write(path, models, append: true)
297
+ end
298
+
299
+ private
300
+
301
+ def csv_each(csv, &block)
302
+ csv.each do |row|
303
+ row = row.fields if row.is_a?(CSV::Row)
304
+ block.call(self.new(*row))
163
305
  end
164
306
  end
165
307
 
@@ -169,33 +311,19 @@ end
169
311
  module DumbDelimited::InstanceMethods
170
312
 
171
313
  # Serializes a model object to a delimited string, using the delimiter
172
- # specified by {ClassMethods.delimiter}.
314
+ # specified by {ClassMethods#delimiter}. By default, the string will
315
+ # not end with a line terminator. To end the string with a line
316
+ # terminator designated by +:row_sep+ in {ClassMethods#options}, set
317
+ # +eol+ to true.
173
318
  #
319
+ # @param eol [Boolean]
174
320
  # @return [String]
175
- def to_s
176
- CSV.generate_line(self, self.class.options).chomp!
177
- end
321
+ def to_s(eol = false)
322
+ row_sep = eol ? self.class.options[:row_sep] : -""
178
323
 
179
- # Serializes a model object to a delimited string, using the delimiter
180
- # specified by {ClassMethods.delimiter}, and appends the string plus a
181
- # row separator to the specified file. Returns the model object.
182
- # This method is convenient when working with single model objects,
183
- # for example, appending a single entry to a log file. However, it is
184
- # not recommended for use with an array of model objects, due to the
185
- # overhead of opening and closing the file for each append.
186
- #
187
- # @example
188
- # Point = DumbDelimited[:x, :y, :z]
189
- # Point.new(1, 2, 3).append_to_file("out.txt") # == Point.new(1, 2, 3)
190
- # File.read("out.txt") # == "1,2,3\n"
191
- # Point.new(4, 5, 6).append_to_file("out.txt") # == Point.new(4, 5, 6)
192
- # File.read("out.txt") # == "1,2,3\n4,5,6\n"
193
- #
194
- # @param file [String, Pathname]
195
- # @return [self]
196
- def append_to_file(file)
197
- CSV.generate_line(self, self.class.options).append_to_file(file)
198
- self
324
+ CSV.generate(**self.class.options, row_sep: row_sep, write_headers: false) do |csv|
325
+ csv << self
326
+ end
199
327
  end
200
328
 
201
329
  end
metadata CHANGED
@@ -1,94 +1,34 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dumb_delimited
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Hefner
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2018-07-16 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: pleasant_path
13
+ name: csv
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '1.0'
18
+ version: '0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - "~>"
23
+ - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '1.0'
27
- - !ruby/object:Gem::Dependency
28
- name: bundler
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '1.15'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '1.15'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '10.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '10.0'
55
- - !ruby/object:Gem::Dependency
56
- name: minitest
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '5.0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '5.0'
69
- - !ruby/object:Gem::Dependency
70
- name: yard
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '0.9'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '0.9'
83
- description:
25
+ version: '0'
84
26
  email:
85
- - jonathan.hefner@gmail.com
27
+ - jonathan@hefner.pro
86
28
  executables: []
87
29
  extensions: []
88
30
  extra_rdoc_files: []
89
31
  files:
90
- - ".gitignore"
91
- - ".travis.yml"
92
32
  - CHANGELOG.md
93
33
  - Gemfile
94
34
  - LICENSE.txt
@@ -100,8 +40,9 @@ files:
100
40
  homepage: https://github.com/jonathanhefner/dumb_delimited
101
41
  licenses:
102
42
  - MIT
103
- metadata: {}
104
- post_install_message:
43
+ metadata:
44
+ source_code_uri: https://github.com/jonathanhefner/dumb_delimited
45
+ changelog_uri: https://github.com/jonathanhefner/dumb_delimited/blob/master/CHANGELOG.md
105
46
  rdoc_options: []
106
47
  require_paths:
107
48
  - lib
@@ -109,16 +50,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
109
50
  requirements:
110
51
  - - ">="
111
52
  - !ruby/object:Gem::Version
112
- version: '0'
53
+ version: '3.4'
113
54
  required_rubygems_version: !ruby/object:Gem::Requirement
114
55
  requirements:
115
56
  - - ">="
116
57
  - !ruby/object:Gem::Version
117
58
  version: '0'
118
59
  requirements: []
119
- rubyforge_project:
120
- rubygems_version: 2.7.6
121
- signing_key:
60
+ rubygems_version: 4.0.10
122
61
  specification_version: 4
123
- summary: Library for unsophisticated delimited flat file IO
62
+ summary: Unsophisticated delimited flat file IO
124
63
  test_files: []
data/.gitignore DELETED
@@ -1,9 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.2.5
5
- before_install: gem install bundler -v 1.15.1