Skip to main content

rings_node/
util.rs

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