1#![allow(non_upper_case_globals)]
7#![allow(non_camel_case_types)]
8#![allow(non_snake_case)]
9#![cfg_attr(docsrs, feature(doc_cfg))]
10
11pub mod ffi {
12 #![allow(dead_code)]
13 include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
14}
15
16mod db;
17mod record;
18mod rset;
19mod selection_expression;
20
21#[cfg(feature = "arrow")]
22#[cfg_attr(docsrs, doc(cfg(feature = "arrow")))]
23pub mod arrow;
24
25pub use db::Db;
26pub use record::{FieldRef, Fields, Record, RecordRef};
27pub use rset::{OwnedRset, Records, Rset};
28pub use selection_expression::SelectionExpression;
29
30use std::ffi::CString;
31use std::fmt;
32use std::sync::Once;
33
34#[derive(Debug)]
35pub struct Error(String);
36
37impl Error {
38 pub(crate) fn new(msg: impl Into<String>) -> Self {
39 Error(msg.into())
40 }
41}
42
43impl fmt::Display for Error {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 f.write_str(&self.0)
46 }
47}
48
49impl std::error::Error for Error {}
50
51pub(crate) fn cstring(s: &str, what: &str) -> Result<CString, Error> {
52 CString::new(s).map_err(|_| Error::new(format!("{what} contains an interior NUL byte")))
53}
54
55pub(crate) fn ensure_init() {
56 static ONCE: Once = Once::new();
57 ONCE.call_once(|| unsafe { ffi::rec_init() });
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 const SAMPLE: &str = "\
65%rec: Book
66
67Title: Refactoring
68Author: Martin Fowler
69
70Title: Domain-Driven Design
71Author: Eric Evans
72";
73
74 #[test]
75 fn parse_and_count() {
76 let mut db = Db::parse_str(SAMPLE).unwrap();
77 assert_eq!(db.num_rsets(), 1);
78 let rset = db.rset_by_type("Book").unwrap();
79 assert_eq!(rset.num_records(), 2);
80 }
81
82 #[test]
83 fn selection_expression_filters() {
84 let mut db = Db::parse_str(SAMPLE).unwrap();
85 let rset = db.rset_by_type("Book").unwrap();
86 let selection_expression = SelectionExpression::compile("Author = 'Eric Evans'", false).unwrap();
87 let hits = rset.records().filter(|r| selection_expression.matches(r)).count();
88 assert_eq!(hits, 1);
89 }
90
91 #[test]
92 fn set_field_updates_matching() {
93 let mut db = Db::parse_str(SAMPLE).unwrap();
94 let selection_expression = SelectionExpression::compile("Author = 'Eric Evans'", false).unwrap();
95 let rset = db.rset_by_type("Book").unwrap();
96 let mut updated = 0;
97 for mut r in rset.records().filter(|r| selection_expression.matches(r)) {
98 assert!(r.set_field("Author", "Evans, Eric").unwrap());
99 updated += 1;
100 }
101 assert_eq!(updated, 1);
102 let s = db.to_rec_string().unwrap();
103 assert!(s.contains("Evans, Eric"));
104 assert!(!s.contains("Author: Eric Evans"));
105 }
106
107 #[test]
108 fn remove_matching_drops_records() {
109 let mut db = Db::parse_str(SAMPLE).unwrap();
110 let selection_expression = SelectionExpression::compile("Author = 'Martin Fowler'", false).unwrap();
111 let removed = {
112 let mut rset = db.rset_by_type("Book").unwrap();
113 rset.remove_matching(|r| selection_expression.matches(r))
114 };
115 assert_eq!(removed, 1);
116 assert_eq!(db.rset_by_type("Book").unwrap().num_records(), 1);
117 }
118
119 #[test]
120 fn build_db_from_scratch() {
121 let mut db = Db::new();
122 let mut rset = OwnedRset::new();
123
124 let mut desc = Record::new();
125 desc.append_field("%rec", "Book").unwrap();
126 desc.append_field("%type", "Year int").unwrap();
127 desc.append_field("%mandatory", "Title").unwrap();
128 rset.set_descriptor(desc);
129
130 let mut r = Record::new();
131 r.append_field("Title", "TDD").unwrap();
132 r.append_field("Year", "2002").unwrap();
133 rset.append_record(r).unwrap();
134
135 db.append_rset(rset).unwrap();
136 let text = db.to_rec_string().unwrap();
137 assert!(text.contains("%rec: Book"));
138 assert!(text.contains("%type: Year int"));
139 assert!(text.contains("%mandatory: Title"));
140 assert!(text.contains("Title: TDD"));
141
142 let mut db2 = Db::parse_str(&text).unwrap();
144 let rset2 = db2.rset_by_type("Book").unwrap();
145 assert_eq!(rset2.num_records(), 1);
146 }
147
148 #[test]
149 fn append_round_trip() {
150 let mut db = Db::parse_str(SAMPLE).unwrap();
151 {
152 let mut rset = db.rset_by_type("Book").unwrap();
153 let mut rec = Record::new();
154 rec.append_field("Title", "TDD").unwrap();
155 rec.append_field("Author", "Kent Beck").unwrap();
156 rset.append_record(rec).unwrap();
157 }
158 let serialized = db.to_rec_string().unwrap();
159 assert!(serialized.contains("Kent Beck"));
160 let mut db2 = Db::parse_str(&serialized).unwrap();
161 assert_eq!(db2.rset_by_type("Book").unwrap().num_records(), 3);
162 }
163}