Rust implementation of the SSV format.
[dependencies]
ssv = { path = "..." }The crate is a workspace of two packages: ssv (the library) and ssv_derive (the proc-macros). You only depend on ssv; the derive macros are re-exported from it.
fn from_str<T: FromRow>(src: &str) -> Result<Tables<T>, Error>
fn from_file<T: FromRow>(path: impl AsRef<Path>) -> Result<Tables<T>, Error>Derive FromRow on a struct that mirrors the SSV columns:
use ssv::FromRow;
#[derive(Debug, FromRow)]
struct Record {
name: String,
age: i32,
score: f32,
active: bool,
tags: Vec<String>,
}
let tables = ssv::from_str::<Record>(src)?;
let (rows, _config) = tables.into_first();
// rows: Vec<Record>from_str always returns Tables<T>, which holds every table in the document.
// Index access
let first = &tables[0];
// Named access (requires #! TABLE name in the source)
let colors = &tables["colors"];
// Single-table shorthand — consumes Tables
let (rows, config) = tables.into_first();Table<T> fields:
pub struct Table<T> {
pub name: Option<String>,
pub rows: Vec<T>,
pub config: Config,
pub pre_comments: Vec<String>,
pub inline_comments: BTreeMap<usize, Vec<String>>, // keyed by row index
pub trailing_comments: Vec<String>,
}fn to_string<T: ToRow>(tables: &Tables<T>) -> Result<String, Error>
fn to_file<T: ToRow>(path: impl AsRef<Path>, tables: &Tables<T>) -> Result<(), Error>Derive ToRow alongside FromRow, parse the file, edit rows, write it back:
#[derive(Debug, FromRow, ToRow)]
struct Record { /* ... */ }
let mut tables = ssv::from_file::<Record>("data.ssv")?;
tables[0].rows.push(Record { name: "Carol".into(), age: 28, score: 8.1, active: true, tags: vec![] });
ssv::to_file("data.ssv", &tables)?;| Macro | Purpose |
|---|---|
FromRow |
Deserialize a struct from an SSV row |
ToRow |
Serialize a struct to an SSV row; generates the header |
SsvType |
Use a struct as a named-tuple field in another type |
A struct that derives SsvType can be used as a field type inside ToRow/FromRow structs. It maps to an SSV named tuple:
#[derive(SsvType)]
struct Point { x: f32, y: f32 }
#[derive(FromRow, ToRow)]
struct Path { label: String, origin: Point }| Rust type | SSV type |
|---|---|
String |
string |
bool |
bool |
i32 |
int |
i8 / i16 / i64 / i128 |
int8 / int16 / int64 / int128 |
u32 |
uint |
u8 / u16 / u64 / u128 |
uint8 / uint16 / uint64 / uint128 |
f32 |
float |
f64 |
float64 |
Option<T> |
T? |
Vec<T> |
T[] |
(A, B) … (A, …, T) |
[A, B] … [A, …, T] |
All element types must implement SsvType.
All public functions return Result<_, ssv::Error>. Error implements std::error::Error and Display.
| Variant | When |
|---|---|
IllegalDelimiter |
Delimiter character is forbidden (alphanumeric, \, #, …) |
IllegalPrimaryDelimiter |
Primary delimiter is :, ,, [, or ] |
EmptyDelimiterList |
#! DELIMITERS with no characters |
InvalidHeader |
Malformed header line |
UnknownType |
Unrecognised type name in header |
UnknownField |
Struct field not present in the parsed header |
ConversionError |
Value cannot be parsed as its declared type |
UnescapedDelimiter |
Primitive field contains a bare delimiter character |
UnknownEscapeSequence |
\X where X is not a recognised escape |
NotEnoughDelimiters |
Nesting depth exceeds the number of declared delimiters |
TooManyFields |
Data row has more non-empty fields than the header |
Io |
File read/write failure |
It's generally simpler to just write an SSV file, import and edit its data, but you may also generate them with this crate:
Use #[ssv(range = "min..max")] to attach a range constraint to a numeric field in the generated header:
#[derive(ToRow, FromRow)]
struct Player {
name: String,
#[ssv(range = "0..100")]
score: u8,
#[ssv(range = "18..")]
age: u8,
}
// Generated header: name:string | score:uint8(0..100) | age:uint8(18..)All parser-comment settings from the SSV spec are reflected in Config. When parsing, Config is populated from the source document and returned alongside the rows. Build one manually when generating from scratch:
pub struct Config {
pub delimiters: Vec<char>, // default: ['|', ';']
pub disable_binary_numbers: bool,
pub disable_octal_numbers: bool,
pub disable_hex_numbers: bool,
pub disable_exponential_numbers: bool,
pub disable_regex_check: bool,
pub decimal_separator: char, // default: '.'
pub numeric_separator: Option<char>,
pub parenthetical_negatives: bool,
pub null_char: Option<char>,
pub escape_char: char, // default: '\\'
pub disable_markdown_support: bool,
pub isolated_tables: bool,
pub header: Vec<(String, String)>, // raw header fields
pub type_aliases: Vec<(String, FieldType)>,
}Use Tables::single(rows, config) to build a Tables without parsing anything:
let mut config = Config::default();
config.delimiters = vec!['|', ';', ':'];
let rows = vec![
Record { name: "Alice".into(), age: 30, score: 9.5, active: true, tags: vec![] },
];
let tables = Tables::single(rows, config);
let output = ssv::to_string(&tables)?;This project is licensed under your choice of any of the following licenses:
- 0BSD License (LICENSE-0BSD or https://opensource.org/license/0bsd)
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT License (LICENSE-MIT or https://opensource.org/licenses/MIT)
Personally I suggest 0BSD as it doesn't even require keeping the license file. Do whatever you want. I don't care. If your legal department cares, the other two are available.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.