essential_read/
lib.rs

1//! Library to read predicates and solutions.
2//!
3//! Provides functions to read and optionally deserialize predicates and solutions in JSON format.
4
5#![deny(missing_docs)]
6#![deny(unsafe_code)]
7
8use anyhow::{anyhow, Result};
9use essential_types::{contract::Contract, solution::Solution};
10use std::{
11    fs::DirEntry,
12    path::{Path, PathBuf},
13};
14use tokio::io::{AsyncReadExt, BufReader};
15
16/// Read and deserialize predicates from a file.
17///
18/// Calls `check_path_json` via call to `read_bytes`.
19pub async fn read_contract(path: PathBuf) -> Result<Contract> {
20    let bytes = read_bytes(path).await?;
21    let contract = deserialize_contract(bytes).await?;
22    Ok(contract)
23}
24
25/// Read and deserialize contracts in a directory.
26///
27/// Calls `check_path_json` for every entry in directory with a valid UTF-8 file name.
28pub async fn read_contracts(path: PathBuf) -> Result<Vec<Contract>> {
29    let mut contracts: Vec<Contract> = vec![];
30    for entry in path.read_dir()? {
31        let file_path = dir_entry_to_path(&path, entry?)
32            .inspect_err(|err| println!("skipping file: {}", err))?;
33        check_path_json(&path)?;
34        let bytes = read_bytes(file_path).await?;
35        let contract = deserialize_contract(bytes).await?;
36        contracts.push(contract);
37    }
38    Ok(contracts)
39}
40
41/// Read and deserialize a solution from a file.
42///
43/// Calls `check_path_json` via call to `read_bytes`.
44pub async fn read_solution(path: PathBuf) -> Result<Solution> {
45    let bytes = read_bytes(path).await?;
46    let solution = deserialize_solution(bytes).await?;
47    Ok(solution)
48}
49
50/// Read and deserialize solutions in a directory.
51///
52/// Calls `check_path_json` for every entry in directory with a valid UTF-8 file name.
53pub async fn read_solutions(path: PathBuf) -> Result<Vec<Solution>> {
54    let mut solutions: Vec<Solution> = vec![];
55    for entry in path.read_dir()? {
56        #[cfg(feature = "tracing")]
57        let file_path = dir_entry_to_path(&path, entry?)
58            .inspect_err(|err| tracing::warn!("skipping file: {}", err));
59
60        #[cfg(not(feature = "tracing"))]
61        let file_path = dir_entry_to_path(&path, entry?);
62
63        let Ok(file_path) = file_path else {
64            continue;
65        };
66
67        #[cfg(not(feature = "tracing"))]
68        if check_path_json(&path).is_err() {
69            continue;
70        }
71
72        #[cfg(feature = "tracing")]
73        if let Err(e) = check_path_json(&path) {
74            tracing::warn!("skipping file because it's not json: {}", e);
75            continue;
76        }
77
78        let bytes = read_bytes_inner(file_path).await?;
79        let solution = deserialize_solution(bytes).await?;
80        solutions.push(solution);
81    }
82    Ok(solutions)
83}
84
85/// Read the contents of a file as bytes.
86///
87/// Calls `check_path_json`.
88pub async fn read_bytes(path: PathBuf) -> Result<Vec<u8>> {
89    check_path_json(&path)?;
90    read_bytes_inner(path).await
91}
92
93async fn read_bytes_inner(path: PathBuf) -> Result<Vec<u8>> {
94    let file = tokio::fs::File::open(path).await?;
95    let mut bytes = Vec::new();
96    let mut reader = BufReader::new(file);
97    reader.read_to_end(&mut bytes).await?;
98    Ok(bytes)
99}
100
101/// Read the contents of files in a directory as a vector of bytes.
102///
103/// Calls `check_path_json` for every entry in directory with a valid UTF-8 file name.
104pub async fn read_bytes_dir(path: PathBuf) -> Result<Vec<Vec<u8>>> {
105    let mut all_bytes: Vec<Vec<u8>> = vec![];
106    for entry in path.read_dir()? {
107        #[cfg(feature = "tracing")]
108        let file_path = dir_entry_to_path(&path, entry?)
109            .inspect_err(|err| tracing::warn!("skipping file: {}", err));
110
111        #[cfg(not(feature = "tracing"))]
112        let file_path = dir_entry_to_path(&path, entry?);
113
114        let Ok(file_path) = file_path else {
115            continue;
116        };
117
118        #[cfg(not(feature = "tracing"))]
119        if check_path_json(&path).is_err() {
120            continue;
121        }
122
123        #[cfg(feature = "tracing")]
124        if let Err(e) = check_path_json(&path) {
125            tracing::warn!("skipping file because it's not json: {}", e);
126            continue;
127        }
128
129        let bytes = read_bytes_inner(file_path).await?;
130        all_bytes.push(bytes);
131    }
132    Ok(all_bytes)
133}
134
135/// Deserialize a contract from bytes.
136pub async fn deserialize_contract(bytes: Vec<u8>) -> Result<Contract> {
137    let contract = serde_json::from_slice::<Contract>(&bytes)?;
138    Ok(contract)
139}
140
141/// Deserialize a solution from bytes.
142pub async fn deserialize_solution(bytes: Vec<u8>) -> Result<Solution> {
143    let solution = serde_json::from_slice::<Solution>(&bytes)?;
144    Ok(solution)
145}
146
147/// Convert a `DirEntry` in directory with given path to a `PathBuf`.
148///
149/// Returns an error if the file name is not valid UTF-8
150fn dir_entry_to_path(path: &Path, entry: DirEntry) -> Result<PathBuf> {
151    let name = entry.file_name();
152    let name = name
153        .to_str()
154        .ok_or_else(|| anyhow!("file name is invalid UTF-8"))?;
155    let path = path.join(name);
156    Ok(path)
157}
158
159/// Check if a path is a JSON file.
160///
161/// Returns an error if:
162/// - The path is not a file.
163/// - The path does not have a `.json` extension.
164fn check_path_json(path: &Path) -> Result<()> {
165    if !path.is_file() {
166        return Err(anyhow!("path is not a file: {:?}", path));
167    }
168    if !path.extension().map_or(false, |ext| ext == "json") {
169        return Err(anyhow!("path is not a JSON file: {:?}", path));
170    }
171    Ok(())
172}