Skip to content

Commit cb7f54e

Browse files
committed
Emit an error if row and header size mismatch
1 parent fff5a0b commit cb7f54e

File tree

2 files changed

+47
-2
lines changed

2 files changed

+47
-2
lines changed

csv/shared/src/main/scala/fs2/data/csv/package.scala

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ package object csv {
198198
}
199199

200200
/** Encode a specified type into a CSV prepending the given headers. */
201+
@deprecated(
202+
message =
203+
"Emits incorrect data if rows have a different length than headers. Please use `encodeWithGivenHeaders` instead.",
204+
since = "fs2-data 1.11.1")
201205
def encodeGivenHeaders[T]: PartiallyAppliedEncodeGivenHeaders[T] =
202206
new PartiallyAppliedEncodeGivenHeaders[T](dummy = true)
203207

@@ -217,6 +221,27 @@ package object csv {
217221
}
218222
}
219223

224+
/** Encode a specified type into a CSV prepending the given headers. */
225+
def encodeWithGivenHeaders[T]: PartiallyAppliedEncodeWithGivenHeaders[T] =
226+
new PartiallyAppliedEncodeWithGivenHeaders[T](dummy = true)
227+
228+
@nowarn
229+
class PartiallyAppliedEncodeWithGivenHeaders[T](val dummy: Boolean) extends AnyVal {
230+
def apply[F[_], Header](headers: NonEmptyList[Header],
231+
fullRows: Boolean = false,
232+
separator: Char = ',',
233+
newline: String = "\n",
234+
escape: EscapeMode = EscapeMode.Auto)(implicit
235+
F: RaiseThrowable[F],
236+
T: RowEncoder[T],
237+
H: WriteableHeader[Header]): Pipe[F, T, String] = {
238+
val stringPipe =
239+
if (fullRows) lowlevel.toRowStrings[F](separator, newline, escape)
240+
else lowlevel.toStrings[F](separator, newline, escape)
241+
lowlevel.encode[F, T] andThen lowlevel.writeWithGivenHeaders(headers) andThen stringPipe
242+
}
243+
}
244+
220245
/** Encode a specified type into a CSV that contains the headers determined by encoding the first element. Empty if input is. */
221246
def encodeUsingFirstHeaders[T]: PartiallyAppliedEncodeUsingFirstHeaders[T] =
222247
new PartiallyAppliedEncodeUsingFirstHeaders(dummy = true)
@@ -316,10 +341,30 @@ package object csv {
316341
}
317342

318343
/** Encode a given type into CSV rows using a set of explicitly given headers. */
344+
@deprecated(
345+
message =
346+
"Emits incorrect data if rows have a different length than headers. Please use `writeWithGivenHeaders` instead.",
347+
since = "fs2-data 1.11.1")
319348
def writeWithHeaders[F[_], Header](headers: NonEmptyList[Header])(implicit
320349
H: WriteableHeader[Header]): Pipe[F, Row, NonEmptyList[String]] =
321350
Stream(H(headers)) ++ _.map(_.values)
322351

352+
/** Encode a given type into CSV rows using a set of explicitly given headers. */
353+
def writeWithGivenHeaders[F[_], Header](headers: NonEmptyList[Header])(implicit
354+
F: RaiseThrowable[F],
355+
H: WriteableHeader[Header]): Pipe[F, Row, NonEmptyList[String]] =
356+
attemptWriteWithGivenHeaders(headers).apply(_).rethrow
357+
358+
/** Encode a given type into CSV rows using a set of explicitly given headers, but signals errors as values. */
359+
def attemptWriteWithGivenHeaders[F[_], Header](headers: NonEmptyList[Header])(implicit
360+
H: WriteableHeader[Header]): Pipe[F, Row, Either[CsvException, NonEmptyList[String]]] = {
361+
val headerSize = headers.size
362+
Stream(Right(H(headers))) ++ _.map { row =>
363+
val rowSize = row.size
364+
if (rowSize == headerSize) Right(row.values) else Left(new HeaderSizeError(headerSize, rowSize, row.line))
365+
}
366+
}
367+
323368
/** Encode a given type into CSV rows without headers. */
324369
def writeWithoutHeaders[F[_]]: Pipe[F, Row, NonEmptyList[String]] =
325370
_.map(_.values)

site/documentation/csv/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ More high-level pipes are available for the following use cases:
5151
* `decodeGivenHeaders` for CSV parsing that requires headers, but they aren't present in the input
5252
* `decodeUsingHeaders` for CSV parsing that requires headers and they're present in the input
5353
* `encodeWithoutHeaders` for CSV encoding that works entirely without headers (Note: requires `RowEncoder` instead of `CsvRowEncoder`)
54-
* `encodeGivenHeaders` for CSV encoding that works without headers, but they should be added to the output
54+
* `encodeWithGivenHeaders` for CSV encoding that works without headers, but they should be added to the output
5555
* `encodeUsingFirstHeaders` for CSV encoding that works with headers. Uses the headers of the first row for the output.
5656

5757
### Dealing with erroneous files
@@ -219,7 +219,7 @@ testRows
219219
.string
220220
```
221221

222-
If you want to write headers, use `writeWithHeaders` or, in case you use `CsvRow`, `encodeRowWithFirstHeaders`. For writing non-String headers, you'll need to provide an instance of `WritableHeader`, a type class analog to `ParseableHeader`.
222+
If you want to write headers, use `writeWithGivenHeaders` or, in case you use `CsvRow`, `encodeRowWithFirstHeaders`. For writing non-String headers, you'll need to provide an instance of `WritableHeader`, a type class analog to `ParseableHeader`.
223223

224224
## The type classes: Decoders and Encoders
225225

0 commit comments

Comments
 (0)