My own corner of monopam
README.md

csv #

Typed CSV codecs for OCaml, with streaming I/O and located errors.

Reading a CSV file into records usually means splitting lines by hand, looking columns up by index, and reporting a bad value as an exception with no row or column attached; writing the records back repeats the work in reverse. nox-csv describes a row once, as a codec in the style of jsont, and uses that description both to decode a file into records and to encode records into a file. Columns are matched by name against the header once per file, so each row is decoded without a lookup.

Streaming I/O is built on bytesrw: of_reader reads from a Bytes.Reader.t and to_writer writes to a Bytes.Writer.t, so CSV processing composes with other bytesrw codecs. of_file and of_string return the decoded rows, and fold_file and fold_reader process rows without keeping the list.

Decode errors carry a source location from loc and a path naming the failing row, by index, and column, by name.

Install #

$ opam install nox-csv

If opam cannot find the package, add the overlay repository first:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install nox-csv

Usage #

type point = { x : float; y : float; label : string }

let point_codec =
  Csv.(Row.(
    obj (fun x y label -> { x; y; label })
    |> col "x" Field.float ~enc:(fun p -> p.x)
    |> col "y" Field.float ~enc:(fun p -> p.y)
    |> col "label" Field.string ~enc:(fun p -> p.label)
    |> finish
  ))

(* Decode a string. Errors carry source locations and row/column context. *)
let csv = "x,y,label\n1.0,2.0,A\n3.5,4.5,B\n"

let points =
  match Csv.of_string point_codec csv with
  | Ok points -> points
  | Error e -> failwith (Csv.Error.to_string e)

let () =
  assert (points =
    [ { x = 1.0; y = 2.0; label = "A" };
      { x = 3.5; y = 4.5; label = "B" } ])

(* Streaming decode from a bytesrw reader. *)
let sum =
  let r = Bytesrw.Bytes.Reader.of_string csv in
  match Csv.of_reader point_codec r with
  | Ok rows -> List.fold_left (fun acc p -> acc +. p.x) 0.0 rows
  | Error e -> failwith (Csv.Error.to_string e)

let () = assert (sum = 4.5)

(* Encode to a bytesrw writer. Raises [Csv.Invalid_utf8_encode] if any
   field string contains malformed UTF-8. *)
let encoded =
  let buf = Buffer.create 256 in
  let w = Bytesrw.Bytes.Writer.of_buffer buf in
  Csv.to_writer point_codec points w;
  Buffer.contents buf

let () = assert (encoded = "x,y,label\n1,2,A\n3.5,4.5,B\n")

For programmatic inspection of errors (IDE diagnostics, error aggregation), decoding functions return structured Csv.Error.t values. Use Csv.Error.kind to inspect the error kind and Csv.Error.to_string to format the complete diagnostic. Reading functions with an _exn suffix raise Csv.Error instead:

let error_kind =
  match Csv.of_string (Csv.col "x" Csv.Field.float) "x\nbad\n" with
  | Ok _ -> failwith "expected an invalid float"
  | Error e -> Csv.Error.kind e

let () =
  assert (Csv.Error.string_of_kind error_kind =
    "invalid value \"bad\": not a float")

API overview #

  • Field codecs: Field.string, int, int64, float, bool, option, nullable_float and nullable_int. Field.v builds a custom codec and Field.map transforms one; Field.decode, Field.decode_exn and Field.encode run one on a single field.
  • The Row builder: obj, col, col_at, col_block and finish. Named and positional columns take a ?dec_absent default, and col_block reads strided groups of columns.
  • Reading: of_file, of_string and of_reader, with _exn variants. Writing: to_string and to_writer. Folding: fold_file and fold_reader.
  • decode, decode_exn and encode work on rows already split into fields, header first, and resolve, decode_row, encode_row and encode_header on one row at a time against a header.
  • col, update_col and delete_col query and update single columns, and col_names and col_count describe a codec.
  • Csv.Loc, Csv.Meta, Csv.Path and Csv.Error are the location and error modules of loc, and the Sort module names the three sorts of value (Row, Field, Header) that errors refer to.
  • The encoder raises Invalid_utf8_encode when a field is not well-formed UTF-8.

The byte-reading entry points and folds accept ?max_rows and ?max_cols to bound data-row count and header width on untrusted input. Both limits are unlimited by default. Byte readers and writers accept ?sep for a separator other than a comma.

A decode reads the columns its codec names and skips the rest, so a wide file costs only the columns the codec uses. UTF-8 well-formedness is checked on every header name, which resolving the codec reads, and in a data row only on the columns the codec reads.

Full signatures in csv.mli.

References #

  • RFC 4180, the CSV format.
  • jsont, the JSON codec library whose design this follows.
  • bytesrw, composable byte stream readers and writers.
  • loc, source locations and errors.

License #

ISC. See LICENSE.md.