1use foundationdb::{Database, FdbResult, RangeOption, Transaction};
2use foundationdb::directory::{DirectoryLayer, Directory, DirectoryOutput, DirectoryError};
3use foundationdb::tuple::{pack, unpack};
4use std::env;
5
6pub const ENV_DB_PATH: &str = "SNM_FDBCLI_DB_PATH";
7
8pub fn connect_db() -> FdbResult<Database> {
16 if let Ok(path) = env::var(ENV_DB_PATH) {
17 Database::from_path(&path)
18 } else {
19 Database::default()
20 }
21}
22
23pub async fn create_spaces(
27 trx: &Transaction,
28) -> (
29 DirectoryOutput,
30 DirectoryOutput,
31 DirectoryOutput,
32 DirectoryOutput,
33) {
34 let dir_layer = DirectoryLayer::default();
35
36 let users_path = vec!["srotas".to_string(), "users".to_string()];
37 let logins_path = vec!["srotas".to_string(), "logins".to_string()];
38 let orders_path = vec!["srotas".to_string(), "orders".to_string()];
39 let wallets_path = vec!["srotas".to_string(), "wallets".to_string()];
40
41 let users_dir = dir_layer
42 .create_or_open(trx, &users_path, None, None)
43 .await
44 .expect("create/open srotas/users failed");
45
46 let logins_dir = dir_layer
47 .create_or_open(trx, &logins_path, None, None)
48 .await
49 .expect("create/open srotas/logins failed");
50
51 let orders_dir = dir_layer
52 .create_or_open(trx, &orders_path, None, None)
53 .await
54 .expect("create/open srotas/orders failed");
55
56 let wallets_dir = dir_layer
57 .create_or_open(trx, &wallets_path, None, None)
58 .await
59 .expect("create/open srotas/wallets failed");
60
61 (users_dir, logins_dir, orders_dir, wallets_dir)
62}
63
64pub async fn open_spaces(
66 trx: &Transaction,
67) -> (
68 DirectoryOutput,
69 DirectoryOutput,
70 DirectoryOutput,
71 DirectoryOutput,
72) {
73 let dir_layer = DirectoryLayer::default();
74
75 let users_path = vec!["srotas".to_string(), "users".to_string()];
76 let logins_path = vec!["srotas".to_string(), "logins".to_string()];
77 let orders_path = vec!["srotas".to_string(), "orders".to_string()];
78 let wallets_path = vec!["srotas".to_string(), "wallets".to_string()];
79
80 let users_dir = dir_layer
81 .open(trx, &users_path, None)
82 .await
83 .expect("open srotas/users failed");
84 let logins_dir = dir_layer
85 .open(trx, &logins_path, None)
86 .await
87 .expect("open srotas/logins failed");
88 let orders_dir = dir_layer
89 .open(trx, &orders_path, None)
90 .await
91 .expect("open srotas/orders failed");
92 let wallets_dir = dir_layer
93 .open(trx, &wallets_path, None)
94 .await
95 .expect("open srotas/wallets failed");
96
97 (users_dir, logins_dir, orders_dir, wallets_dir)
98}
99
100pub fn prefix_range_for_user(
102 dir: &DirectoryOutput,
103 user_id: &str,
104) -> (Vec<u8>, Vec<u8>) {
105 let prefix = dir
106 .pack(&(user_id,))
107 .expect("pack user prefix");
108 let mut end = prefix.clone();
109 end.push(0xFF); (prefix, end)
112}
113
114pub async fn dump_dir(
116 trx: &Transaction,
117 dir: &DirectoryOutput,
118 limit: i32,
119) -> FdbResult<()> {
120 let (begin, end) = dir.range().expect("dir.range()");
121 let range = RangeOption::from((begin.as_slice(), end.as_slice()));
122 let kvs = trx
123 .get_range(&range, limit.try_into().unwrap(), false)
124 .await?;
125
126 if kvs.is_empty() {
127 println!(" (empty)");
128 } else {
129 for kv in kvs.iter() {
130 println!(
131 " key = {:?}, value = {}",
132 kv.key(),
133 String::from_utf8_lossy(kv.value())
134 );
135 }
136 }
137
138 Ok(())
139}
140
141pub async fn dir_create(
147 trx: &Transaction,
148 path: &[&str],
149) -> Result<DirectoryOutput, DirectoryError> {
150 let layer = DirectoryLayer::default();
151 let vec_path: Vec<String> = path.iter().map(|s| s.to_string()).collect();
152 let out = layer.create_or_open(trx, &vec_path, None, None).await?;
153 Ok(out)
154}
155
156pub async fn dir_open(
158 trx: &Transaction,
159 path: &[&str],
160) -> Result<DirectoryOutput, DirectoryError> {
161 let layer = DirectoryLayer::default();
162 let vec_path: Vec<String> = path.iter().map(|s| s.to_string()).collect();
163 let out = layer.open(trx, &vec_path, None).await?;
164 Ok(out)
165}
166
167pub async fn dir_list(
169 trx: &Transaction,
170 path: &[&str],
171) -> Result<Vec<String>, DirectoryError> {
172 let layer = DirectoryLayer::default();
173 let vec_path: Vec<String> = path.iter().map(|s| s.to_string()).collect();
174 let children = layer.list(trx, &vec_path).await?;
175 Ok(children)
176}
177
178fn parse_simple_tuple_str(tuple_str: &str) -> Result<String, String> {
183 let s = tuple_str.trim();
184 if !s.starts_with('(') || !s.ends_with(')') {
185 return Err(format!("Expected (value), got: {}", s));
186 }
187 let inner = &s[1..s.len() - 1]; Ok(inner.trim().to_string())
189}
190
191pub fn tuple_prefix_range(
194 dir: &DirectoryOutput,
195 tuple_str: &str,
196) -> Result<(Vec<u8>, Vec<u8>), String> {
197 let v = parse_simple_tuple_str(tuple_str)?;
198
199 let prefix = dir
200 .pack(&(v.as_str(),))
201 .map_err(|e| format!("pack error: {:?}", e))?;
202 let mut end = prefix.clone();
203 end.push(0xFF);
204
205 Ok((prefix, end))
206}
207
208pub fn tuple_key_from_string(
210 dir: &DirectoryOutput,
211 tuple_str: &str,
212) -> Result<Vec<u8>, String> {
213 let v = parse_simple_tuple_str(tuple_str)?;
214 let key = dir
215 .pack(&(v.as_str(),))
216 .map_err(|e| format!("pack error: {:?}", e))?;
217 Ok(key)
218}
219
220pub fn tuple_pack_from_string(tuple_str: &str) -> Result<Vec<u8>, String> {
223 let v = parse_simple_tuple_str(tuple_str)?;
224 let bytes = pack(&(v.as_str(),));
225 Ok(bytes)
226}
227
228pub fn tuple_unpack_to_string(bytes: &[u8]) -> Result<String, String> {
230 let t: (String,) =
231 unpack(bytes).map_err(|e| format!("unpack error: {:?}", e))?;
232 Ok(format!("({})", t.0))
233}
234
235
236
237
238
239
240
241
242#[cfg(test)]
247mod tests {
248 use super::*;
249 use foundationdb::api::FdbApiBuilder;
250
251 #[test]
256 fn tuple_pack_and_unpack_roundtrip() {
257 let input = "(alice)";
258 let bytes = tuple_pack_from_string(input).expect("pack should succeed");
259 let out = tuple_unpack_to_string(&bytes).expect("unpack should succeed");
260 assert_eq!(out, "(alice)");
261 }
262
263 #[test]
264 fn tuple_pack_rejects_invalid_format() {
265 assert!(tuple_pack_from_string("alice").is_err());
267 assert!(tuple_pack_from_string("(alice").is_err());
269 assert!(tuple_pack_from_string("alice)").is_err());
270 }
271
272 #[tokio::test]
281 #[ignore]
282 async fn fdb_end_to_end_directory_and_prefix() {
283 let _network = unsafe {
285 FdbApiBuilder::default()
286 .build()
287 .expect("build FDB API")
288 .boot()
289 .expect("boot network")
290 };
291
292 let db = connect_db().expect("connect_db()");
293
294 {
296 let trx = db.create_trx().expect("create_trx");
297 let path = ["test-root", "sub"];
298 let _dir = dir_create(&trx, &path).await.expect("dir_create");
299 trx.commit().await.expect("commit create");
300 }
301
302 {
303 let trx2 = db.create_trx().expect("create_trx 2");
304 let children = dir_list(&trx2, &["test-root"])
305 .await
306 .expect("dir_list");
307 assert!(
308 children.contains(&"sub".to_string()),
309 "expected 'sub' in children: {:?}",
310 children
311 );
312 trx2.commit().await.expect("commit list");
314 }
315
316 {
318 let trx3 = db.create_trx().expect("create_trx 3");
319 let dir = dir_open(&trx3, &["test-root", "sub"])
321 .await
322 .expect("dir_open for prefix test");
323
324 let (begin, end) = tuple_prefix_range(&dir, "(alice)")
325 .expect("tuple_prefix_range");
326 assert!(!begin.is_empty(), "begin should not be empty");
327 assert!(begin.len() < end.len(), "end should be longer than begin");
328 assert_eq!(end.last().copied(), Some(0xFF), "end should end with 0xFF");
329
330 trx3.commit().await.expect("commit prefix test");
331 }
332 }
333}