-
-
Notifications
You must be signed in to change notification settings - Fork 25
/
package.scala
455 lines (393 loc) · 22.5 KB
/
package.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
/*
* Copyright 2024 fs2-data Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fs2
package data
import text._
import cats._
import csv.internals._
import cats.data._
import cats.syntax.all._
import scala.annotation.{implicitNotFound, unused}
import scala.annotation.nowarn
package object csv {
/** Higher kinded version of [[scala.None]]. Ignores the type param.
*/
type NoneF[+A] = None.type
/** A CSV row without headers.
*/
type Row = RowF[NoneF, Nothing]
/** A CSV row with headers, that can be used to access the cell values.
*
* '''Note:''' the following invariant holds when using this class: `values` and `headers` have the same size.
*/
type CsvRow[Header] = RowF[Some, Header]
type HeaderResult[T] = Either[HeaderError, NonEmptyList[T]]
type DecoderResult[T] = Either[DecoderError, T]
/** Describes how a row can be decoded to the given type.
*
* `RowDecoder` provides convenient methods such as `map`, `emap`, or `flatMap`
* to build new decoders out of more basic one.
*
* Actually, `RowDecoder` has a [[https://typelevel.org/cats/api/cats/MonadError.html cats `MonadError`]]
* instance. To get the full power of it, import `cats.syntax.all._`.
*/
@implicitNotFound(
"No implicit RowDecoder found for type ${T}.\nYou can define one using RowDecoder.instance, by calling map on another RowDecoder or by using generic derivation for product types like case classes.\nFor that, add the fs2-data-csv-generic module to your dependencies and use either full-automatic derivation:\nimport fs2.data.csv.generic.auto._\nor the recommended semi-automatic derivation:\nimport fs2.data.csv.generic.semiauto._\nimplicit val rowDecoder: RowDecoder[${T}] = deriveRowDecoder\nMake sure to have instances of CellDecoder for every member type in scope.\n")
type RowDecoder[T] = RowDecoderF[NoneF, T, Nothing]
/** Describes how a row can be encoded from a value of the given type.
*/
@implicitNotFound(
"No implicit RowEncoder found for type ${T}.\nYou can define one using RowEncoder.instance, by calling contramap on another RowEncoder or by using generic derivation for product types like case classes.\nFor that, add the fs2-data-csv-generic module to your dependencies and use either full-automatic derivation:\nimport fs2.data.csv.generic.auto._\nor the recommended semi-automatic derivation:\nimport fs2.data.csv.generic.semiauto._\nimplicit val rowEncoder: RowEncoder[${T}] = deriveRowEncoder\nMake sure to have instances of CellEncoder for every member type in scope.\n")
type RowEncoder[T] = RowEncoderF[NoneF, T, Nothing]
/** Describes how a row can be decoded to the given type.
*
* `CsvRowDecoder` provides convenient methods such as `map`, `emap`, or `flatMap`
* to build new decoders out of more basic one.
*
* Actually, `CsvRowDecoder` has a [[https://typelevel.org/cats/api/cats/MonadError.html cats `MonadError`]]
* instance. To get the full power of it, import `cats.syntax.all._`.
*/
@implicitNotFound(
"No implicit CsvRowDecoder found for type ${T}.\nYou can define one using CsvRowDecoder.instance, by calling map on another CsvRowDecoder or by using generic derivation for product types like case classes.\nFor that, add the fs2-data-csv-generic module to your dependencies and use either full-automatic derivation:\nimport fs2.data.csv.generic.auto._\nor the recommended semi-automatic derivation:\nimport fs2.data.csv.generic.semiauto._\nimplicit val csvRowDecoder: CsvRowDecoder[${T}] = deriveCsvRowDecoder\nMake sure to have instances of CellDecoder for every member type in scope.\n")
type CsvRowDecoder[T, Header] = RowDecoderF[Some, T, Header]
/** Describes how a row can be encoded from a value of the given type.
*/
@implicitNotFound(
"No implicit CsvRowEncoderF[H, found for type ${T}.\nYou can define one using CsvRowEncoderF[H, .instance, by calling contramap on another CsvRowEncoderF[H, or by using generic derivation for product types like case classes.\nFor that, add the fs2-data-csv-generic module to your dependencies and use either full-automatic derivation:\nimport fs2.data.csv.generic.auto._\nor the recommended semi-automatic derivation:\nimport fs2.data.csv.generic.semiauto._\nimplicit val csvRowEncoder: CsvRowEncoderF[H, [${T}] = deriveCsvRowEncoderF[H, \nMake sure to have instances of CellEncoder for every member type in scope.\n")
type CsvRowEncoder[T, Header] = RowEncoderF[Some, T, Header]
@nowarn
sealed trait QuoteHandling
object QuoteHandling {
/** Treats quotation marks as the start of a quoted value if the first
* character of a value is a quotation mark, otherwise treats the value
* literally (this is the historic and default behavior)
*
* For example, "hello, world" would be parsed as unquoted `hello, world`
*/
case object RFCCompliant extends QuoteHandling
/** Treats values as raw strings and does not treat quotation marks with
* any particular meaning
*
* For example, "hello, world" would be parsed as the still-quoted
* `"hello, world"`
*/
case object Literal extends QuoteHandling
}
/** Decode a char-like stream (see [[fs2.data.text.CharLikeChunks]]) into a specified type,
* assuming the file neither contains headers nor are they needed for decoding.
*/
def decodeWithoutHeaders[T]: PartiallyAppliedDecodeWithoutHeaders[T] =
new PartiallyAppliedDecodeWithoutHeaders[T](dummy = true)
@nowarn
class PartiallyAppliedDecodeWithoutHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_], C](separator: Char = ',', quoteHandling: QuoteHandling = QuoteHandling.RFCCompliant)(implicit
F: RaiseThrowable[F],
C: CharLikeChunks[F, C],
T: RowDecoder[T]): Pipe[F, C, T] =
lowlevel.rows(separator, quoteHandling) andThen lowlevel.noHeaders andThen lowlevel.decode
}
/** Decode a char-like stream (see [[fs2.data.text.CharLikeChunks]]) into a specified type,
* assuming the file contains headers, but they shall be skipped for decoding.
*/
def decodeSkippingHeaders[T]: PartiallyAppliedDecodeSkippingHeaders[T] =
new PartiallyAppliedDecodeSkippingHeaders[T](dummy = true)
@nowarn
class PartiallyAppliedDecodeSkippingHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_], C](separator: Char = ',', quoteHandling: QuoteHandling = QuoteHandling.RFCCompliant)(implicit
F: RaiseThrowable[F],
C: CharLikeChunks[F, C],
T: RowDecoder[T]
): Pipe[F, C, T] =
lowlevel.rows(separator, quoteHandling) andThen lowlevel.skipHeaders andThen lowlevel.decode
}
/** Decode a char-like stream (see [[fs2.data.text.CharLikeChunks]]) into a specified type,
* assuming the file contains headers and they need to be taken into account for decoding.
*/
def decodeUsingHeaders[T]: PartiallyAppliedDecodeUsingHeaders[T] =
new PartiallyAppliedDecodeUsingHeaders[T](dummy = true)
@nowarn
class PartiallyAppliedDecodeUsingHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_], C, Header](separator: Char = ',', quoteHandling: QuoteHandling = QuoteHandling.RFCCompliant)(
implicit
F: RaiseThrowable[F],
C: CharLikeChunks[F, C],
T: CsvRowDecoder[T, Header],
H: ParseableHeader[Header]): Pipe[F, C, T] =
lowlevel.rows(separator, quoteHandling) andThen lowlevel.headers andThen lowlevel.decodeRow
}
/** Decode a char-like stream (see [[fs2.data.text.CharLikeChunks]]) into a specified type.
*
* Scenarios:
* - If skipHeaders is false, then the file contains no headers.
* - If skipHeaders is true, then the headers in the file will be skipped.
*
* For both scenarios the file is assumed to be compliant with the set of headers given.
*/
def decodeGivenHeaders[T]: PartiallyAppliedDecodeGivenHeaders[T] =
new PartiallyAppliedDecodeGivenHeaders(dummy = true)
@nowarn
class PartiallyAppliedDecodeGivenHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_], C, Header](headers: NonEmptyList[Header],
skipHeaders: Boolean = false,
separator: Char = ',',
quoteHandling: QuoteHandling = QuoteHandling.RFCCompliant)(implicit
F: RaiseThrowable[F],
C: CharLikeChunks[F, C],
T: CsvRowDecoder[T, Header]): Pipe[F, C, T] = {
if (skipHeaders)
lowlevel.rows(separator, quoteHandling) andThen lowlevel.skipHeaders andThen
lowlevel.withHeaders(headers) andThen lowlevel.decodeRow
else
lowlevel.rows(separator, quoteHandling) andThen lowlevel.withHeaders(headers) andThen lowlevel.decodeRow
}
}
/** Encode a specified type into a CSV that contains no headers. */
def encodeWithoutHeaders[T]: PartiallyAppliedEncodeWithoutHeaders[T] =
new PartiallyAppliedEncodeWithoutHeaders[T](dummy = true)
@nowarn
class PartiallyAppliedEncodeWithoutHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_]](fullRows: Boolean = false,
separator: Char = ',',
newline: String = "\n",
escape: EscapeMode = EscapeMode.Auto)(implicit T: RowEncoder[T]): Pipe[F, T, String] = {
val stringPipe =
if (fullRows) lowlevel.toRowStrings[F](separator, newline, escape)
else lowlevel.toStrings[F](separator, newline, escape)
lowlevel.encode[F, T] andThen lowlevel.writeWithoutHeaders andThen stringPipe
}
}
/** Encode a specified type into a CSV prepending the given headers. */
def encodeGivenHeaders[T]: PartiallyAppliedEncodeGivenHeaders[T] =
new PartiallyAppliedEncodeGivenHeaders[T](dummy = true)
@nowarn
class PartiallyAppliedEncodeGivenHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_], Header](headers: NonEmptyList[Header],
fullRows: Boolean = false,
separator: Char = ',',
newline: String = "\n",
escape: EscapeMode = EscapeMode.Auto)(implicit
T: RowEncoder[T],
H: WriteableHeader[Header]): Pipe[F, T, String] = {
val stringPipe =
if (fullRows) lowlevel.toRowStrings[F](separator, newline, escape)
else lowlevel.toStrings[F](separator, newline, escape)
lowlevel.encode[F, T] andThen lowlevel.writeWithHeaders(headers) andThen stringPipe
}
}
/** Encode a specified type into a CSV that contains the headers determined by encoding the first element. Empty if input is. */
def encodeUsingFirstHeaders[T]: PartiallyAppliedEncodeUsingFirstHeaders[T] =
new PartiallyAppliedEncodeUsingFirstHeaders(dummy = true)
@nowarn
class PartiallyAppliedEncodeUsingFirstHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_], Header](fullRows: Boolean = false,
separator: Char = ',',
newline: String = "\n",
escape: EscapeMode = EscapeMode.Auto)(implicit
T: CsvRowEncoder[T, Header],
H: WriteableHeader[Header]): Pipe[F, T, String] = {
val stringPipe =
if (fullRows) lowlevel.toRowStrings[F](separator, newline, escape)
else lowlevel.toStrings[F](separator, newline, escape)
lowlevel.encodeRow[F, Header, T] andThen lowlevel.encodeRowWithFirstHeaders[F, Header] andThen stringPipe
}
}
/** Low level pipes for CSV handling. All pipes only perform one step in a CSV (de)serialization pipeline,
* so use these if you want to customise. All standard use cases should be covered by the higher level pipes directly
* on the csv package which are composed of the lower level ones here.
*/
object lowlevel {
/** Transforms a stream of characters into a stream of CSV rows.
*
* @param separator character to use to separate fields in the CSV
* @param quoteHandling use [[QuoteHandling.RFCCompliant]] for RFC-4180
* handling of quotation marks (optionally quoted
* if the value begins with a quotation mark; the
* default) or [[QuoteHandling.Literal]] if quotation
* marks should be treated literally
*/
def rows[F[_], T](separator: Char = ',', quoteHandling: QuoteHandling = QuoteHandling.RFCCompliant)(implicit
F: RaiseThrowable[F],
T: CharLikeChunks[F, T]): Pipe[F, T, Row] =
RowParser.pipe[F, T](separator, quoteHandling)
/** Transforms a stream of raw CSV rows into parsed CSV rows with headers. */
def headers[F[_], Header](implicit
F: RaiseThrowable[F],
Header: ParseableHeader[Header]): Pipe[F, Row, CsvRow[Header]] =
CsvRowParser.pipe[F, Header]
/** Transforms a stream of raw CSV rows into parsed CSV rows with headers, with failures at the element level instead of failing the stream */
def headersAttempt[F[_], Header](implicit
Header: ParseableHeader[Header]): Pipe[F, Row, Either[CsvException, CsvRow[Header]]] =
CsvRowParser.pipeAttempt[F, Header]
// left here for bincompat
private[csv] def headersAttempt[F[_], Header](implicit
@unused F: RaiseThrowable[F],
Header: ParseableHeader[Header]): Pipe[F, Row, Either[CsvException, CsvRow[Header]]] =
CsvRowParser.pipeAttempt[F, Header]
/** Transforms a stream of raw CSV rows into parsed CSV rows with given headers. */
def withHeaders[F[_], Header](headers: NonEmptyList[Header])(implicit
F: RaiseThrowable[F]): Pipe[F, Row, CsvRow[Header]] =
_.map(CsvRow.liftRow(headers)).rethrow
/** Transforms a stream of raw CSV rows into parsed CSV rows with given headers. */
def attemptWithHeaders[F[_], Header](
headers: NonEmptyList[Header]): Pipe[F, Row, Either[CsvException, CsvRow[Header]]] =
_.map(CsvRow.liftRow(headers))
/** Transforms a stream of raw CSV rows into rows. */
def noHeaders[F[_]]: Pipe[F, Row, Row] = identity
/** Transforms a stream of raw CSV rows into rows, skipping the first row to ignore the headers. */
def skipHeaders[F[_]]: Pipe[F, Row, Row] =
_.tail
/** Decodes simple rows (without headers) into a specified type using a suitable [[RowDecoder]]. */
def decode[F[_], R](implicit F: RaiseThrowable[F], R: RowDecoder[R]): Pipe[F, Row, R] =
_.map(R(_)).rethrow
/** Decodes simple rows (without headers) into a specified type using a suitable [[RowDecoder]], but signal errors as values. */
def attemptDecode[F[_], R](implicit R: RowDecoder[R]): Pipe[F, Row, DecoderResult[R]] =
_.map(R(_))
/** Decodes [[CsvRow]]s (with headers) into a specified type using a suitable [[CsvRowDecoder]]. */
def decodeRow[F[_], Header, R](implicit
F: RaiseThrowable[F],
R: CsvRowDecoder[R, Header]): Pipe[F, CsvRow[Header], R] =
_.map(R(_)).rethrow
/** Decodes [[CsvRow]]s (with headers) into a specified type using a suitable [[CsvRowDecoder]], but signal errors as values. */
def attemptDecodeRow[F[_], Header, R](implicit
R: CsvRowDecoder[R, Header]): Pipe[F, CsvRow[Header], DecoderResult[R]] =
_.map(R(_))
/** Decodes [[CsvRow]]s (with headers) into a specified type using a suitable [[CsvRowDecoder]], but signal errors as values from both header as well as rows. */
def attemptFlatMapDecodeRow[F[_], Header, R](implicit R: CsvRowDecoder[R, Header])
: Stream[F, Either[CsvException, CsvRow[Header]]] => Stream[F, Either[CsvException, R]] = {
_.map(_.flatMap(R(_)))
}
/** Encode a given type into CSV rows using a set of explicitly given headers. */
def writeWithHeaders[F[_], Header](headers: NonEmptyList[Header])(implicit
H: WriteableHeader[Header]): Pipe[F, Row, NonEmptyList[String]] =
Stream(H(headers)) ++ _.map(_.values)
/** Encode a given type into CSV rows without headers. */
def writeWithoutHeaders[F[_]]: Pipe[F, Row, NonEmptyList[String]] =
_.map(_.values)
/** Serialize a CSV row to Strings. No guarantees are given on how the resulting Strings are cut. */
def toStrings[F[_]](separator: Char = ',',
newline: String = "\n",
escape: EscapeMode = EscapeMode.Auto): Pipe[F, NonEmptyList[String], String] = {
_.flatMap(nel =>
Stream
.emits(nel.toList)
.map(RowWriter.encodeColumn(separator, escape))
.intersperse(separator.toString) ++ Stream(newline))
}
/** Serialize a CSV row to Strings. Guaranteed to emit one String per CSV row (= one line if no quoted newlines are contained in the value). */
def toRowStrings[F[_]](separator: Char = ',',
newline: String = "\n",
escape: EscapeMode = EscapeMode.Auto): Pipe[F, NonEmptyList[String], String] = {
// explicit Show avoids mapping the NEL before
val showColumn: Show[String] =
Show.show(RowWriter.encodeColumn(separator, escape))
_.map(_.mkString_("", separator.toString, newline)(showColumn, implicitly))
}
/** Encode a given type into simple CSV rows without headers. */
def encode[F[_], R](implicit R: RowEncoder[R]): Pipe[F, R, Row] =
_.map(R(_))
/** Encode a given type into CSV rows with headers. */
def encodeRow[F[_], Header, R](implicit R: CsvRowEncoder[R, Header]): Pipe[F, R, CsvRow[Header]] =
_.map(R(_))
/** Encode a given type into CSV row with headers taken from the first element.
* If the input stream is empty, the output is as well.
*/
def encodeRowWithFirstHeaders[F[_], Header](implicit
H: WriteableHeader[Header]): Pipe[F, CsvRow[Header], NonEmptyList[String]] =
_.pull.peek1
.flatMap {
case Some((CsvRow(_, headers), stream)) =>
Pull.output1(H(headers)) >> stream.map(_.values).pull.echo
case None => Pull.done
}
.stream
}
object lenient {
/** Decode a char-like stream (see [[fs2.data.text.CharLikeChunks]]) into a specified type, with failures at the
* element level instead of failing the stream.
*
* Scenarios:
* - If skipHeaders is false, then the file contains no headers.
* - If skipHeaders is true, then the headers in the file will be skipped.
*
* For both scenarios the file is assumed to be compliant with the set of headers given.
*/
def attemptDecodeGivenHeaders[T]: PartiallyAppliedDecodeAttemptGivenHeaders[T] =
new PartiallyAppliedDecodeAttemptGivenHeaders[T](dummy = true)
class PartiallyAppliedDecodeAttemptGivenHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_], C, Header](headers: NonEmptyList[Header],
skipHeaders: Boolean = false,
separator: Char = ',',
quoteHandling: QuoteHandling = QuoteHandling.RFCCompliant)(implicit
F: RaiseThrowable[F],
C: CharLikeChunks[F, C],
T: CsvRowDecoder[T, Header]): Pipe[F, C, Either[CsvException, T]] = {
if (skipHeaders)
lowlevel.rows(separator, quoteHandling) andThen lowlevel.skipHeaders andThen
lowlevel.attemptWithHeaders(headers) andThen lowlevel.attemptFlatMapDecodeRow
else
lowlevel.rows(separator, quoteHandling) andThen lowlevel.attemptWithHeaders(
headers) andThen lowlevel.attemptFlatMapDecodeRow
}
}
/** Decode a char-like stream (see [[fs2.data.text.CharLikeChunks]]) into a specified type, with failures at the
* element level instead of failing the stream.
*
* This function assumes the file contains headers and they need to be taken into account for decoding.
*/
def attemptDecodeUsingHeaders[T]: PartiallyAppliedDecodeAttemptUsingHeaders[T] =
new PartiallyAppliedDecodeAttemptUsingHeaders[T](dummy = true)
class PartiallyAppliedDecodeAttemptUsingHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_], C, Header](separator: Char = ',', quoteHandling: QuoteHandling = QuoteHandling.RFCCompliant)(
implicit
F: RaiseThrowable[F],
C: CharLikeChunks[F, C],
T: CsvRowDecoder[T, Header],
H: ParseableHeader[Header]): Pipe[F, C, Either[CsvException, T]] = {
lowlevel.rows(separator, quoteHandling) andThen lowlevel.headersAttempt(
H) andThen lowlevel.attemptFlatMapDecodeRow
}
}
/** Decode a char-like stream (see [[fs2.data.text.CharLikeChunks]]) into a specified type, with failures at the
* element level instead of failing the stream.
*
* This function assumes the file contains headers, but they shall be skipped for decoding.
*/
def attemptDecodeSkippingHeaders[T]: PartiallyAppliedDecodeAttemptSkippingHeaders[T] =
new PartiallyAppliedDecodeAttemptSkippingHeaders[T](dummy = true)
class PartiallyAppliedDecodeAttemptSkippingHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_], C](separator: Char = ',', quoteHandling: QuoteHandling = QuoteHandling.RFCCompliant)(implicit
F: RaiseThrowable[F],
C: CharLikeChunks[F, C],
T: RowDecoder[T]): Pipe[F, C, Either[CsvException, T]] = {
lowlevel.rows(separator, quoteHandling) andThen lowlevel.skipHeaders andThen lowlevel.attemptDecode
}
}
/** Decode a char-like stream (see [[fs2.data.text.CharLikeChunks]]) into a specified type, with failures at the
* element level instead of failing the stream.
*
* This function assumes the file neither contains headers nor are they needed for decoding.
*/
def attemptDecodeWithoutHeaders[T]: PartiallyAppliedDecodeAttemptWithoutHeaders[T] =
new PartiallyAppliedDecodeAttemptWithoutHeaders[T](dummy = true)
class PartiallyAppliedDecodeAttemptWithoutHeaders[T](val dummy: Boolean) extends AnyVal {
def apply[F[_], C](separator: Char = ',', quoteHandling: QuoteHandling = QuoteHandling.RFCCompliant)(implicit
F: RaiseThrowable[F],
C: CharLikeChunks[F, C],
T: RowDecoder[T]): Pipe[F, C, Either[CsvException, T]] =
lowlevel.rows(separator, quoteHandling) andThen lowlevel.noHeaders andThen lowlevel.attemptDecode
}
}
}