Skip to main content

rings_node/
util.rs

1//! Utilities for configuration and build.
2
3use std::io::Read;
4
5use flate2::read::GzDecoder;
6use flate2::write::GzEncoder;
7use flate2::Compression;
8use serde::de::DeserializeOwned;
9use serde::Deserialize;
10use serde::Deserializer;
11use serde::Serialize;
12use serde::Serializer;
13
14#[cfg(feature = "node")]
15use crate::error::Error;
16
17#[allow(dead_code)]
18/// Serialize a value through Serde without an intermediate wrapper.
19pub fn serialize_forward<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
20where
21    T: Serialize,
22    S: Serializer,
23{
24    value.serialize(serializer)
25}
26
27#[allow(dead_code)]
28/// Deserialize a value through Serde without an intermediate wrapper.
29pub fn deserialize_forward<'de, T, D>(deserializer: D) -> Result<T, D::Error>
30where
31    T: Deserialize<'de>,
32    D: Deserializer<'de>,
33{
34    T::deserialize(deserializer)
35}
36
37#[allow(dead_code)]
38/// Serialize a value as gzipped JSON bytes.
39pub fn serialize_gzip<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
40where
41    T: Serialize,
42    S: Serializer,
43{
44    let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
45    serde_json::to_writer(&mut encoder, value).map_err(serde::ser::Error::custom)?;
46    let compressed_data = encoder.finish().map_err(serde::ser::Error::custom)?;
47    serializer.serialize_bytes(&compressed_data)
48}
49
50#[allow(dead_code)]
51/// Deserialize gzipped JSON bytes into a value.
52pub fn deserialize_gzip<'de, D, T>(deserializer: D) -> Result<T, D::Error>
53where
54    T: DeserializeOwned,
55    D: Deserializer<'de>,
56{
57    let bytes = Vec::<u8>::deserialize(deserializer)?;
58    let mut decoder = GzDecoder::new(&bytes[..]);
59    let mut decompressed_data = Vec::new();
60    decoder
61        .read_to_end(&mut decompressed_data)
62        .map_err(serde::de::Error::custom)?;
63    serde_json::from_slice(&decompressed_data).map_err(serde::de::Error::custom)
64}
65
66/// build_version of program
67pub fn build_version() -> String {
68    let mut infos = vec![];
69    if let Some(version) = option_env!("CARGO_PKG_VERSION") {
70        infos.push(version);
71    };
72    if let Some(git_hash) = option_env!("GIT_SHORT_HASH") {
73        infos.push(git_hash);
74    }
75    infos.join("-")
76}
77
78/// Expand path with "~" to absolute path.
79#[cfg(feature = "node")]
80pub fn expand_home<P>(path: P) -> Result<std::path::PathBuf, Error>
81where P: AsRef<std::path::Path> {
82    let Ok(stripped) = path.as_ref().strip_prefix("~") else {
83        return Ok(path.as_ref().to_path_buf());
84    };
85
86    let Some(mut p) = home::home_dir() else {
87        return Err(Error::HomeDirError);
88    };
89
90    p.push(stripped);
91
92    Ok(p)
93}
94
95/// Create parent directory of a path if not exists.
96#[cfg(feature = "node")]
97pub fn ensure_parent_dir<P>(path: P) -> Result<(), Error>
98where P: AsRef<std::path::Path> {
99    let path = expand_home(path)?;
100    let parent = path.parent().ok_or(Error::ParentDirError)?;
101    if !parent.is_dir() {
102        std::fs::create_dir_all(parent).map_err(|e| Error::CreateFileError(e.to_string()))?;
103    };
104    Ok(())
105}
106
107#[cfg(feature = "node")]
108pub mod loader {
109    //! A module to help user load config from local file or remote url.
110
111    use async_trait::async_trait;
112    use reqwest::Url;
113    use serde::de::DeserializeOwned;
114
115    use crate::seed::Seed;
116
117    /// Load config from local file or remote url.
118    /// To use this trait, derive DeserializeOwned then implement this trait.
119    #[async_trait]
120    pub trait ResourceLoader {
121        /// Load config from local file or remote url.
122        async fn load(source: &str) -> anyhow::Result<Self>
123        where Self: Sized + DeserializeOwned {
124            let url = Url::parse(source).map_err(|e| anyhow::anyhow!("{}", e))?;
125
126            if let Ok(path) = url.to_file_path() {
127                let data = std::fs::read_to_string(path)
128                    .map_err(|_| anyhow::anyhow!("Unable to read resource file"))?;
129
130                serde_json::from_str(&data).map_err(|e| anyhow::anyhow!("{}", e))
131            } else {
132                let resp = reqwest::get(source)
133                    .await
134                    .map_err(|_| anyhow::anyhow!("failed to get resource from {}", source))?;
135                resp.json()
136                    .await
137                    .map_err(|_| anyhow::anyhow!("failed to load resource from {}", source))
138            }
139        }
140    }
141
142    impl ResourceLoader for Seed {}
143}
144
145#[cfg(test)]
146#[cfg(feature = "node")]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_expand_home_with_tilde() {
152        let input = "~";
153        let mut expected = std::env::var("HOME").unwrap();
154        expected.push('/');
155        let result = expand_home(input).unwrap();
156        assert_eq!(result.to_str(), Some(expected.as_str()));
157    }
158
159    #[test]
160    fn test_expand_home_with_relative_path() {
161        let input = "~/path/to/file.txt";
162        let mut expected = std::env::var("HOME").unwrap();
163        expected.push_str("/path/to/file.txt");
164        let result = expand_home(input).unwrap();
165        assert_eq!(result.to_str(), Some(expected.as_str()));
166    }
167
168    #[test]
169    fn test_expand_home_with_absolute_path() {
170        let input = "/absolute/path/to/file.txt";
171        let expected = std::path::PathBuf::from(input);
172        let result = expand_home(input).unwrap();
173        assert_eq!(result, expected);
174    }
175
176    #[test]
177    fn test_expand_home_with_invalid_path() {
178        let input = "path/does/not/exist.txt";
179        let expected = std::path::PathBuf::from(input);
180        let result = expand_home(input).unwrap();
181        assert_eq!(result, expected);
182    }
183
184    #[test]
185    fn test_expand_home_with_empty_path() {
186        let input = "";
187        let expected = std::path::PathBuf::from("");
188        let result = expand_home(input).unwrap();
189        assert_eq!(result, expected);
190    }
191}