use serde::de::DeserializeOwned;
use serde::Serialize;
use structfs_core_store::{Codec, Error, Format, Path, Reader, Record, Value, Writer};
use crate::convert::{from_value, to_value};
pub trait TypedReader: Reader {
fn read_as<T: DeserializeOwned>(
&mut self,
from: &Path,
codec: &dyn Codec,
) -> Result<Option<T>, Error> {
let Some(record) = self.read(from)? else {
return Ok(None);
};
let value = record.into_value(codec)?;
let typed = from_value(value)?;
Ok(Some(typed))
}
fn read_json(
&mut self,
from: &Path,
codec: &dyn Codec,
) -> Result<Option<serde_json::Value>, Error> {
self.read_as::<Value>(from, codec)?
.map(crate::value_to_json)
.transpose()
}
fn read_typed<T: DeserializeOwned>(&mut self, from: &Path) -> Result<Option<T>, Error> {
let Some(record) = self.read(from)? else {
return Ok(None);
};
match record {
Record::Parsed(value) => Ok(Some(from_value(value)?)),
Record::Raw { bytes, format, .. } if format == Format::JSON => crate::JsonCodec
.decode(&bytes, &format)
.and_then(from_value)
.map(Some),
Record::Raw { format, .. } => Err(Error::UnsupportedFormat(format)),
_ => Err(Error::store(
"typed_reader",
"read_typed",
"unsupported record variant",
)),
}
}
fn read_children_typed<T: DeserializeOwned>(
&mut self,
from: &Path,
) -> Result<Option<Vec<(String, T)>>, Error> {
let Some(children) = self.read_children(from)? else {
return Ok(None);
};
let mut result = Vec::with_capacity(children.len());
for name in children {
let child_path = from.child(structfs_core_store::PathComponent::try_new(&name)?);
if let Some(value) = self.read_typed(&child_path)? {
result.push((name, value));
}
}
Ok(Some(result))
}
}
impl<R: Reader + ?Sized> TypedReader for R {}
pub trait TypedWriter: Writer {
fn write_as<T: Serialize>(&mut self, to: &Path, data: &T) -> Result<Path, Error> {
let value = to_value(data)?;
self.write(to, Record::parsed(value))
}
fn write_json(&mut self, to: &Path, data: serde_json::Value) -> Result<Path, Error> {
self.write_as(to, &data)
}
fn write_typed<T: Serialize>(&mut self, to: &Path, data: &T) -> Result<Path, Error> {
self.write_as(to, data)
}
}
impl<W: Writer + ?Sized> TypedWriter for W {}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
struct TestStore {
data: HashMap<Path, Record>,
}
impl TestStore {
fn new() -> Self {
Self {
data: HashMap::new(),
}
}
}
impl Reader for TestStore {
fn read(&mut self, from: &Path) -> Result<Option<Record>, Error> {
Ok(self.data.get(from).cloned())
}
}
impl Writer for TestStore {
fn write(&mut self, to: &Path, data: Record) -> Result<Path, Error> {
self.data.insert(to.clone(), data);
Ok(to.clone())
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct TestUser {
name: String,
age: u32,
}
#[test]
fn typed_roundtrip() {
use structfs_core_store::path;
let mut store = TestStore::new();
let codec = crate::JsonCodec;
let user = TestUser {
name: "Alice".to_string(),
age: 30,
};
store.write_as(&path!("users/alice"), &user).unwrap();
let recovered: TestUser = store
.read_as(&path!("users/alice"), &codec)
.unwrap()
.unwrap();
assert_eq!(user, recovered);
}
#[test]
fn read_nonexistent_returns_none() {
use structfs_core_store::path;
let mut store = TestStore::new();
let codec = crate::JsonCodec;
let result: Option<TestUser> = store.read_as(&path!("nonexistent"), &codec).unwrap();
assert!(result.is_none());
}
#[test]
fn read_typed_codec_free_roundtrip() {
use structfs_core_store::path;
let mut store = TestStore::new();
let user = TestUser {
name: "Alice".to_string(),
age: 30,
};
store.write_typed(&path!("users/alice"), &user).unwrap();
let recovered: TestUser = store.read_typed(&path!("users/alice")).unwrap().unwrap();
assert_eq!(user, recovered);
let missing: Option<TestUser> = store.read_typed(&path!("nope")).unwrap();
assert!(missing.is_none());
}
#[test]
fn read_typed_parses_raw_json() {
use bytes::Bytes;
use structfs_core_store::path;
let mut store = TestStore::new();
store
.write(
&path!("raw"),
Record::raw(
Bytes::from_static(b"{\"name\":\"Bob\",\"age\":40}"),
Format::JSON,
),
)
.unwrap();
let user: TestUser = store.read_typed(&path!("raw")).unwrap().unwrap();
assert_eq!(user.name, "Bob");
}
#[test]
fn read_typed_rejects_non_json_raw() {
use bytes::Bytes;
use structfs_core_store::path;
let mut store = TestStore::new();
store
.write(
&path!("raw"),
Record::raw(Bytes::from_static(b"data"), Format::OCTET_STREAM),
)
.unwrap();
let result: Result<Option<TestUser>, _> = store.read_typed(&path!("raw"));
assert!(matches!(result, Err(Error::UnsupportedFormat(_))));
}
#[test]
fn read_children_typed_works() {
use structfs_core_store::{path, MemoryStore};
let mut store = MemoryStore::new();
store
.write_typed(
&path!("users/alice"),
&TestUser {
name: "Alice".to_string(),
age: 30,
},
)
.unwrap();
store
.write_typed(
&path!("users/bob"),
&TestUser {
name: "Bob".to_string(),
age: 40,
},
)
.unwrap();
let users: Vec<(String, TestUser)> =
store.read_children_typed(&path!("users")).unwrap().unwrap();
assert_eq!(users.len(), 2);
assert_eq!(users[0].0, "alice");
assert_eq!(users[0].1.name, "Alice");
assert_eq!(users[1].0, "bob");
let missing: Option<Vec<(String, TestUser)>> =
store.read_children_typed(&path!("nowhere")).unwrap();
assert!(missing.is_none());
}
#[test]
fn write_json_works() {
use structfs_core_store::path;
let mut store = TestStore::new();
let codec = crate::JsonCodec;
let json = serde_json::json!({
"key": "value",
"nested": {"a": 1, "b": 2}
});
store.write_json(&path!("config"), json.clone()).unwrap();
let recovered: serde_json::Value =
store.read_json(&path!("config"), &codec).unwrap().unwrap();
assert_eq!(json, recovered);
}
}