Skip to main content

reliakit_csv/
lib.rs

1//! Strict, bounded, and deterministic CSV for reliability-sensitive Rust.
2//!
3//! `reliakit-csv` reads and writes a strict subset of [RFC 4180]. It is built
4//! for systems that process **untrusted** CSV or need **predictable** output:
5//! it rejects malformed quoting, enforces a rectangular shape (every record has
6//! the same number of fields), applies explicit [resource limits](CsvLimits),
7//! reports errors with a location, and serializes deterministically. It has no
8//! external dependencies, forbids unsafe code, and supports `no_std` (with
9//! `alloc`).
10//!
11//! It deliberately does **not** include type inference, configurable dialects,
12//! a streaming reader/writer over `std::io`, schema validation, or lenient
13//! recovery from malformed input.
14//!
15//! # Records
16//!
17//! At the lowest level a CSV document is a sequence of records, and each record
18//! is a sequence of UTF-8 string fields. [`read_str`] parses text into
19//! `Vec<Vec<String>>` and [`CsvWriter`] turns records back into text.
20//!
21//! ```
22//! use reliakit_csv::{read_str, CsvWriter};
23//!
24//! let records = read_str("name,city\nAda,London\n").unwrap();
25//! assert_eq!(records, [["name", "city"], ["Ada", "London"]]);
26//!
27//! // Serialization is deterministic: a field is quoted only when it must be,
28//! // and every record ends with CRLF.
29//! let mut writer = CsvWriter::new();
30//! writer.write_record(["a", "b,c"]);
31//! assert_eq!(writer.into_string(), "a,\"b,c\"\r\n");
32//! ```
33//!
34//! # Strictness
35//!
36//! The reader rejects input that lenient parsers would accept, so a successful
37//! parse is a strong guarantee:
38//!
39//! ```
40//! use reliakit_csv::read_str;
41//!
42//! // A quote inside an unquoted field is rejected, not absorbed.
43//! assert!(read_str("ab\"c\n").is_err());
44//! // Records must be rectangular: a short row is an error, not a `None` cell.
45//! assert!(read_str("a,b\nc\n").is_err());
46//! // A quoted field that never closes is rejected.
47//! assert!(read_str("\"oops\n").is_err());
48//! ```
49//!
50//! # Typed encoding
51//!
52//! [`CsvEncode`] maps your record type to a row of fields (with a header), and
53//! [`CsvDecode`] reads a row back, strictly. [`to_csv_string`] writes a header
54//! row followed by one row per value; [`from_csv_str`] validates the header and
55//! decodes the rest.
56//!
57//! ```
58//! use reliakit_csv::{
59//!     from_csv_str, to_csv_string, CsvDecode, CsvDecodeError, CsvEncode, CsvField,
60//! };
61//! use std::vec::Vec;
62//!
63//! #[derive(Debug, PartialEq)]
64//! struct Row {
65//!     id: u32,
66//!     name: String,
67//! }
68//!
69//! impl CsvEncode for Row {
70//!     fn header() -> Vec<&'static str> {
71//!         vec!["id", "name"]
72//!     }
73//!     fn encode_fields(&self, out: &mut Vec<String>) {
74//!         out.push(self.id.encode_field());
75//!         out.push(self.name.encode_field());
76//!     }
77//! }
78//!
79//! impl CsvDecode for Row {
80//!     fn decode_fields(fields: &[&str]) -> Result<Self, CsvDecodeError> {
81//!         if fields.len() != 2 {
82//!             return Err(CsvDecodeError::field_count());
83//!         }
84//!         Ok(Row {
85//!             id: u32::decode_field(fields[0]).map_err(|e| e.at_field(0))?,
86//!             name: String::decode_field(fields[1]).map_err(|e| e.at_field(1))?,
87//!         })
88//!     }
89//! }
90//!
91//! let rows = vec![Row { id: 1, name: "Ada".into() }];
92//! let text = to_csv_string(&rows);
93//! assert_eq!(text, "id,name\r\n1,Ada\r\n");
94//! assert_eq!(from_csv_str::<Row>(&text).unwrap(), rows);
95//! ```
96//!
97//! The per-field `encode_field`/`decode_field` calls above come from
98//! [`CsvField`], which is implemented for the integer types, `bool`, `String`,
99//! and `Option<T>`.
100//!
101//! # Limits
102//!
103//! [`read_str`] applies conservative [`CsvLimits`] by default. Use
104//! [`read_str_with_limits`] to choose a profile or tune individual limits:
105//!
106//! ```
107//! use reliakit_csv::{read_str_with_limits, CsvLimits};
108//!
109//! let limits = CsvLimits::conservative().with_max_fields_per_record(2);
110//! assert!(read_str_with_limits("a,b,c\n", &limits).is_err());
111//! ```
112//!
113//! # Feature flags
114//!
115//! - `std` (default) enables `std::error::Error` for the error types. The crate
116//!   is otherwise `no_std` and always uses `alloc`.
117//!
118//! [RFC 4180]: https://www.rfc-editor.org/rfc/rfc4180
119
120#![cfg_attr(not(feature = "std"), no_std)]
121#![forbid(unsafe_code)]
122#![warn(missing_docs)]
123
124extern crate alloc;
125
126mod error;
127mod field;
128mod limits;
129mod reader;
130mod record;
131mod writer;
132
133pub use error::{
134    CsvDecodeError, CsvDecodeErrorKind, CsvError, CsvErrorKind, CsvFromStrError, CsvLimitKind,
135};
136pub use field::CsvField;
137pub use limits::CsvLimits;
138pub use reader::{read_str, read_str_with_limits};
139pub use record::{
140    from_csv_str, from_csv_str_headerless, from_csv_str_headerless_with_limits,
141    from_csv_str_with_limits, to_csv_string, to_csv_string_headerless, CsvDecode, CsvEncode,
142};
143pub use writer::CsvWriter;
144
145/// Implementation details used by the `CsvEncode`/`CsvDecode` derives in
146/// `reliakit-derive`. Not part of the public API — do not use it directly; it
147/// may change at any time. It only re-exports `alloc` types so generated code
148/// can name them without assuming they are in scope on `no_std`.
149#[doc(hidden)]
150pub mod __private {
151    pub use alloc::string::String;
152    pub use alloc::vec::Vec;
153}