Releases: guizmaii-opensource/csvzen
Release list
v0.6.0
What's Changed
- Bump actions/setup-java from 5 to 6 (#65) @dependabot[bot]
- Update sbt-scalafix to 0.14.8 (#68) @guizmaii
- Update scala3-library to 3.9.0 (#67) @guizmaii
- Bump sbt/setup-sbt from 1.5.7 to 1.5.8 (#66) @dependabot[bot]
- Bump release-drafter/release-drafter from 7.6.0 to 7.7.0 (#55) @dependabot[bot]
- Bump sbt/setup-sbt from 1.5.5 to 1.5.7 (#59) @dependabot[bot]
- Bump scala-steward-org/scala-steward-action from 2.92.0 to 2.96.0 (#60) @dependabot[bot]
- Update sbt-ci-release to 1.12.1 (#61) @guizmaii
- Update sbt, scripted-plugin to 1.13.0 (#62) @guizmaii
- Update sbt, scripted-plugin to 1.12.15 (#54) @guizmaii
- Bump release-drafter/release-drafter from 7.5.1 to 7.6.0 (#49) @dependabot[bot]
- Update scalafmt-core to 3.11.5 (#51) @guizmaii
- Bump sbt/setup-sbt from 1 to 1.5.5 (#52) @dependabot[bot]
- Update sbt, scripted-plugin to 1.12.14 (#45) @guizmaii
- Update sbt-scalafmt to 2.6.2 (#46) @guizmaii
- Update scalafmt-core to 3.11.4 (#47) @guizmaii
- Update sbt-ci-release to 1.12.0 (#41) @guizmaii, @Copilot
- Bump release-drafter/release-drafter from 7.4.0 to 7.5.1 (#37) @dependabot[bot]
- Bump scala-steward-org/scala-steward-action from 2.90.0 to 2.92.0 (#40) @dependabot[bot]
- Update scalafmt-core to 3.11.3 (#43) @guizmaii
- Update sbt-tpolecat to 0.5.7 (#36) @guizmaii
- Update sbt-tpolecat to 0.5.6 (#29) @guizmaii
- Update sbt-updates to 0.7.0 (#31) @guizmaii
- Bump actions/checkout from 6 to 7 (#32) @dependabot[bot]
- Bump release-drafter/release-drafter from 7.3.1 to 7.4.0 (#33) @dependabot[bot]
- Update sbt, scripted-plugin to 1.12.13 (#34) @guizmaii
v0.5.1
What's Changed
- Update scala3-library to 3.3.8 (#28) @guizmaii
- Update sbt-scalafix to 0.14.7 (#27) @guizmaii
- Bump scala-steward-org/scala-steward-action from 2.88.0 to 2.90.0 (#25) @dependabot[bot]
- Bump release-drafter/release-drafter from 7.3.0 to 7.3.1 (#26) @dependabot[bot]
- Update sbt-tpolecat to 0.5.5 (#24) @guizmaii
- Bump release-drafter/release-drafter from 7.2.1 to 7.3.0 (#20) @dependabot[bot]
- Update sbt-scalafmt to 2.6.1 (#22) @guizmaii
- Update scalafmt-core to 3.11.1 (#23) @guizmaii
- Update sbt-tpolecat to 0.5.4 (#21) @guizmaii
- Update zio, zio-streams, zio-test, ... to 2.1.26 (#19) @guizmaii
- Update sbt, scripted-plugin to 1.12.11 (#16) @guizmaii
- Bump release-drafter/release-drafter from 7.2.0 to 7.2.1 (#15) @dependabot[bot]
- Bump scala-steward-org/scala-steward-action from 2.86.0 to 2.88.0 (#14) @dependabot[bot]
- Update sbt, scripted-plugin to 1.12.10 (#12) @guizmaii
v0.5.0
v0.4.0
v0.3.0
csvzen 0.3.0 introduces csvzen-zio — a small, focused ZIO 2 integration on top of csvzen-core.
If you've been writing a CSV file from a ZStream[A] and gluing the resource handling together by hand, this is for you. Two helpers, no surprises:
openCsvWriter(path, config, …): ZIO[Scope, Throwable, CsvWriter]— opens aCsvWriterinside aScope, closing it automatically at scope exit (success, failure, or interruption).csvSink[A: CsvRowEncoder](path, config, …): ZSink[Any, Throwable, A, Nothing, Long]— writes a header row followed by one row per consumedA, returning the number of rows written. The underlying file handle is owned by the sink for its lifetime.
✨ Usage
libraryDependencies += "com.guizmaii" %% "csvzen-zio" % "0.3.0"import com.guizmaii.csvzen.core.*
import com.guizmaii.csvzen.zio.*
import zio.*
import zio.stream.ZStream
import java.nio.file.Paths
final case class Person(name: String, age: Int) derives CsvRowEncoder
val people: ZStream[Any, Throwable, Person] = ZStream.fromIterable(
Vector(Person("Ada", 36), Person("Linus", 55), Person("Grace", 85))
)
val rowsWritten: ZIO[Any, Throwable, Long] =
people.run(csvSink[Person](Paths.get("people.csv"), CsvConfig.default))The sink wraps CsvWriter lifecycle in a Scope, so abnormal terminations (failure, interruption) close the file handle deterministically. Setup runs inside a single ZIO.blocking shift; per-chunk writes use attemptBlocking. For tightly-locked execution on the blocking pool — no executor ping-pong between chunks — wrap the run call:
ZIO.blocking(stream.run(csvSink[Person](path, CsvConfig.default)))🔧 Implementation note (for the curious)
csvSink is hand-rolled as a ZChannel rather than built on ZSink.foldLeftChunksZIO. The recursion uses @threadUnsafe lazy val loop + a captured var count, so the channel value is built once and reused per chunk (no per-chunk ZChannel.ReadWithCause allocation), and the row count is a stack-local mutation rather than a parameter threaded through. Each stream.run(sink) allocates a fresh closure, so concurrent or repeated runs of the same sink instance are isolated.
🛠 Other changes
Repository reorganisation
All published sub-projects now live under modules/:
modules/
├── core/ (csvzen-core)
├── test-kit/ (csvzen-test-kit)
└── zio/ (csvzen-zio)
The root keeps configuration, docs and build files separate from the published artefacts. No source changes — pure layout. If you're contributing or browsing source, paths are now modules/<name>/src/….
📦 Installation
libraryDependencies += "com.guizmaii" %% "csvzen-core" % "0.3.0"
libraryDependencies += "com.guizmaii" %% "csvzen-test-kit" % "0.3.0" % Test
libraryDependencies += "com.guizmaii" %% "csvzen-zio" % "0.3.0" // optionalTargets Scala 3.3.7. JVM-only. csvzen-core has no runtime dependencies beyond the standard library; csvzen-zio pulls in dev.zio:zio + dev.zio:zio-streams.
Full diff: v0.2.0...v0.3.0
v0.2.0
csvzen 0.2.0 introduces csvzen-test-kit — a zio-test integration providing golden-file (snapshot) testing for CSV encoder output.
To the author's knowledge, no other Scala CSV library ships golden testing. scala-csv, kantan-csv and zio-blocks/schema-csv all leave it to you to roll your own. csvzen 0.2.0 closes that gap, and the API mirrors zio-json-golden so anyone coming from the JSON side gets a familiar workflow with no relearning.
🤔 Why golden tests for CSV?
CSV output is a wire format. Once consumers exist — a downstream pipeline, a partner who imports the file daily, an Excel sheet someone wired up two years ago — your encoder's output is a contract. Any change in the bytes the encoder produces is a change in that contract: a column reordered, a date format tweaked, a CRLF turned into LF, a None rendered as "" instead of "null". Each is the kind of "harmless cleanup" that quietly breaks a consumer in production three weeks later.
A golden test is a small, opinionated answer:
- You commit a reference file — the golden — that captures what the encoder produces today for a representative set of inputs.
- On every test run, the encoder is re-executed against the same inputs and the output is compared byte-for-byte to the golden.
- If anything in the encoder's output changes, the test fails and shows you the diff.
The value comes from what it forces:
- No silent format changes. Refactoring a
CsvFieldEncoder, swapping a date library, "fixing" a quoting rule — all of it surfaces as a failing test with a visible diff, not as a wire-format regression that ships. - Cheap to write, dense in coverage. One
csvGoldenTest(gen)call exercises 50 rows of randomised but stable input through the entire encoder stack. You don't write per-field assertions; you commit one file. - The diff is the spec. When the change is intentional, you open the
_changed.csvnext to the original, eyeball the diff to confirm the delta is what you wanted, and rename it over the original. The PR review then has a one-file diff that says exactly how the wire format moved. - Catches the boring stuff for free. Line-terminator drift, accidental quoting of a previously-unquoted column, an extra trailing newline, an
Instantformatter that started emitting+00:00instead ofZ— all surface immediately, not at 3 AM in production.
The cost is one checked-in file per encoder shape and one rename when the contract intentionally changes. Worth it.
✨ Usage
libraryDependencies += "com.guizmaii" %% "csvzen-test-kit" % "0.2.0" % Testimport com.guizmaii.csvzen.core.*
import com.guizmaii.csvzen.testkit.*
import zio.test.*
object UserSpec extends ZIOSpecDefault {
final case class User(id: Int, name: String, active: Boolean) derives CsvRowEncoder
val gen: Gen[Sized, User] =
for {
id <- Gen.int
name <- Gen.alphaNumericString
active <- Gen.boolean
} yield User(id, name, active)
override def spec = suite("UserSpec")(
csvGoldenTest(gen)
)
}Workflow
- First run → writes
src/test/resources/golden/User_new.csvand fails the test with "Remove_newfrom the suffix and re-run." That promotes the snapshot. - Subsequent runs → encoder output is compared byte-for-byte to the on-disk file. On mismatch a
<Name>_changed.csvis written next to the original so you can diff. If the change is intentional, overwrite the original; if not, the test caught a regression. - No env-var auto-update mode. Promotion is always an explicit file rename.
- On a passing run, leftover
_changed.csv/_new.csvfiles are best-effort deleted so the workspace converges to clean.
Configuration
csvGoldenTest(
gen,
GoldenConfiguration(
relativePath = "users", // golden lives at src/test/resources/golden/users/User.csv
sampleSize = 50, // default — bump for wider coverage
csvConfig = CsvConfig(delimiter = '\t', lineTerminator = "\n"),
),
)GoldenConfiguration is a regular default-valued parameter — no implicit-config juggling at the call site.
📚 Docs
- Project README → Golden tests — the "why you want them" preface plus a quickstart.
test-kit/README.md— full workflow, configuration knobs, and the gotchas aroundDeriveGen[String]+ Unicode bidi rendering.
🛠 Other changes
CsvRowEncoder.derivedscaladoc now flags the-Xmax-inlinesrequirement for case classes with ~25+ fields. Bump it in your build if you hit the "Maximal number of successive inlines (32) exceeded" compile error:scalacOptions ++= Seq("-Xmax-inlines:128")
- CI plumbing fix for the release-drafter workflow (#6).
📦 Installation
libraryDependencies += "com.guizmaii" %% "csvzen-core" % "0.2.0"
libraryDependencies += "com.guizmaii" %% "csvzen-test-kit" % "0.2.0" % TestTargets Scala 3.3.7. JVM-only. csvzen-core has no runtime dependencies beyond the standard library; csvzen-test-kit pulls in zio-test, zio-test-magnolia, and zio for its compile-scope contract.
🙏 Acknowledgements
csvzen-test-kit is modelled directly on zio-json-golden — same workflow, same _new.csv / _changed.csv suffix dance, same GoldenConfiguration shape. Credit to the zio-json contributors for the design; csvzen-test-kit is the CSV-shaped translation.
Full diff: v0.1.0...v0.2.0
v0.1.0
csvzen is a zero-allocation streaming CSV writer for Scala 3 (LTS). RFC 4180-compliant, java.io.Writer-based, with compile-time-derived row encoders and hand-rolled Int / Long digit conversion for primitive cells.
This is the first public release.
✨ Features
Streaming writer
CsvWriter.open(path, config, charset = UTF_8, options*)— buffered, file-backed. ConfigurableCharsetand anyOpenOption(APPEND,CREATE_NEW, …).- Implements
AutoCloseableandFlushable. Single-threaded by design. - Methods:
writeHeader[A](),writeHeader(IndexedSeq[String]),writeRow[A],writeRow(FieldEmitter => Unit)(escape hatch),writeAll[A](Iterable[A]),flush(),close().
Encoders
CsvRowEncoder[A]withderives CsvRowEncoderfor any flat case class — header names taken from field labels in declaration order.CsvRowEncoder.custom(headers)(encode)for hand-built encoders: project a subset of fields, reorder columns, rename headers.CsvFieldEncoder[A]shipped for primitives,BigInt,BigDecimal,UUID,Currency, everyjava.time.*codec (Instant,LocalDate*,OffsetDateTime,ZonedDateTime,Duration,Period,Year, …) andOption[A]for any of those.
Dialect
CsvConfig(delimiter, quoteChar, lineTerminator)— defaults to",","\"","\r\n". Validated in the constructor; invalid combinations throwIllegalArgumentException.
RFC 4180 escaping
- Quote-on-demand: only fields containing the delimiter, quote char,
\ror\nare wrapped in quotes; embedded quotes are doubled. - Plain fields take a fast path with zero allocations and one
Writer.write(s)call.
Zero-allocation hot path
- Hand-rolled
Int/Longdigit emission via a reusable per-writerscratch: Array[Char]. No per-row allocations forString, primitives,Boolean, orOption[None]. - Documented one-
String-per-cell carve-out forFloat,Double,BigInt,BigDecimal,UUID,Currency, andjava.time.*types — same trade-off as zio-blocks'schema-csv.
📦 Installation
libraryDependencies += "com.guizmaii" %% "csvzen-core" % "0.1.0"Targets Scala 3.3.7. JVM-only. No runtime dependencies beyond the standard library.
🙏 Acknowledgements
The codec concept and primitive-set are inspired by zio-blocks' schema-csv; the FieldEmitter design (owns-the-Writer, inline emit, two-pass escape scan) comes from a prior internal implementation.