plantuml_server_client_rs/
locate.rs

1use crate::Format;
2use anyhow::{Context as _, Result};
3use bytes::Bytes;
4use futures::{Stream, StreamExt};
5use std::path::{Path, PathBuf};
6use tokio::io::{AsyncWrite, BufWriter};
7
8/// An wrapper for representing file or stdin / stdout
9#[derive(Debug)]
10pub enum Locate {
11    File(PathBuf),
12    StdInOut,
13}
14
15impl Locate {
16    /// Creates a new [`Locate`].
17    /// - Some(x) -> [`Locate::File`]
18    /// - None -> [`Locate::StdInOut`] (stdin / stdout)
19    pub fn new(locate: Option<PathBuf>) -> Self {
20        match locate {
21            Some(locate) if locate.to_string_lossy() == "-" => Self::StdInOut,
22            Some(locate) => Self::File(locate),
23            None => Self::StdInOut,
24        }
25    }
26
27    /// Converts a [`Locate`] to an owned [`PathBuf`].
28    pub fn to_path_buf(&self) -> PathBuf {
29        match self {
30            Self::File(input) => input.clone(),
31            Self::StdInOut => PathBuf::from("."),
32        }
33    }
34
35    /// Creates image's output path.
36    ///
37    /// * `input` - An input [`Locate`].
38    /// * `id` - Identifiers in the PlantUML diagram.
39    /// * `format` - A format of output.
40    ///
41    /// # Examples
42    ///
43    /// ```
44    /// # use plantuml_server_client_rs as pscr;
45    /// #
46    /// # use pscr::{Locate, Format};
47    /// # use std::path::PathBuf;
48    /// #
49    /// # fn main() {
50    /// let locate = Locate::from("./output/dir");
51    /// let output_path = locate.output_path(&"./foo.puml".into(), "id_a", &Format::Svg);
52    ///
53    /// let expected = Locate::from(PathBuf::from("./output/dir/foo/id_a.svg"));
54    /// assert_eq!(output_path, expected);
55    /// # }
56    /// ```
57    ///
58    /// ```
59    /// # use plantuml_server_client_rs as pscr;
60    /// #
61    /// # use pscr::{Locate, Format};
62    /// # use std::path::PathBuf;
63    /// #
64    /// # fn main() {
65    /// let locate = Locate::from(None); // `--output` is not specified
66    /// let output_path = locate.output_path(&"./foo.puml".into(), "id_a", &Format::Svg);
67    ///
68    /// let expected = Locate::from(PathBuf::from("foo/id_a.svg"));
69    /// assert_eq!(output_path, expected);
70    /// # }
71    /// ```
72    ///
73    /// ```
74    /// # use plantuml_server_client_rs as pscr;
75    /// #
76    /// # use pscr::{Locate, Format};
77    /// # use std::path::PathBuf;
78    /// #
79    /// # fn main() {
80    /// let locate = Locate::from("./output/dir");
81    /// let input = Locate::from(None); // `--input` is not specified (read from stdin)
82    /// let output_path = locate.output_path(&input, "id_a", &Format::Svg);
83    ///
84    /// let expected = Locate::from(PathBuf::from("./output/dir/id_a.svg"));
85    /// assert_eq!(output_path, expected);
86    /// # }
87    /// ```
88    ///
89    /// ```
90    /// # use plantuml_server_client_rs as pscr;
91    /// #
92    /// # use pscr::{Locate, Format};
93    /// # use std::path::PathBuf;
94    /// #
95    /// # fn main() {
96    /// let locate = Locate::from(None);
97    /// let input = Locate::from(None);
98    /// let output_path = locate.output_path(&input, "N/A", &Format::Svg);
99    ///
100    /// let expected = Locate::from(None);
101    /// assert_eq!(output_path, expected);
102    /// # }
103    /// ```
104    pub fn output_path(&self, input: &Locate, id: &str, format: &Format) -> Self {
105        let extension: &str = format.into();
106        self.output_path_inner(input, id, extension)
107    }
108
109    /// Creates "combined" PlantUML content's path.
110    /// The "combined PlantUML content" is output when the `--combined` option is specified.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// # use plantuml_server_client_rs as pscr;
116    /// #
117    /// # use pscr::{Locate, Format};
118    /// # use std::path::PathBuf;
119    /// #
120    /// # fn main() {
121    /// let locate = Locate::from("./output/dir");
122    /// let output_path = locate.combined_output_path(&"./foo.puml".into(), "id_a");
123    ///
124    /// let expected_path = "./output/dir/foo/id_a.puml"; // different extension from `output_path()`.
125    /// let expected = Some(Locate::from(PathBuf::from(expected_path)));
126    /// assert_eq!(output_path, expected);
127    /// # }
128    /// ```
129    pub fn combined_output_path(&self, input: &Locate, id: &str) -> Option<Self> {
130        let path = self.output_path_inner(input, id, "puml");
131
132        match path {
133            Self::StdInOut => None,
134            Self::File(file) => Some(Self::File(file)),
135        }
136    }
137
138    fn output_path_inner(&self, input: &Locate, id: &str, extension: &str) -> Self {
139        if self.is_std_inout() && input.is_std_inout() {
140            return Self::StdInOut;
141        }
142
143        let input = match input {
144            Self::File(x) => {
145                let x = x.clone();
146                match x.clone().file_name().map(PathBuf::from) {
147                    Some(mut x) => {
148                        x.set_extension("");
149                        x
150                    }
151                    None => PathBuf::new(),
152                }
153            }
154            Self::StdInOut => PathBuf::new(),
155        };
156
157        let mut base = match self {
158            Self::File(x) => x.clone(),
159            Self::StdInOut => PathBuf::new(),
160        };
161
162        base.push(input);
163        base.push(id);
164        base.set_extension(extension);
165
166        Self::File(base)
167    }
168
169    /// Read from the [`Locate`].
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// # use plantuml_server_client_rs as pscr;
175    /// #
176    /// # use pscr::{Locate };
177    /// # use std::path::PathBuf;
178    /// #
179    /// # fn main() -> anyhow::Result<()> {
180    /// # let _ = std::fs::create_dir_all("./target/tests_docs/");
181    /// let locate = Locate::from("./target/tests_docs/locate_read");
182    /// let data = "foo";
183    ///
184    /// std::fs::write(locate.to_path_buf(), data)?;
185    /// let read_data = locate.read()?;
186    /// assert_eq!(read_data, data);
187    /// # Ok(())
188    /// # }
189    /// ```
190    pub fn read(&self) -> Result<String> {
191        use std::fs::read_to_string;
192
193        match self {
194            Self::File(input) => read_to_string(input)
195                .with_context(|| format!("failed to read file: input = ({input:?})")),
196
197            Self::StdInOut => Self::read_from_stdin().context("failed to read stdin"),
198        }
199    }
200
201    fn read_from_stdin() -> Result<String> {
202        let mut delimiter = "";
203
204        std::io::stdin()
205            .lines()
206            .try_fold(String::new(), |mut acc, item| {
207                acc += delimiter;
208                acc += &item?;
209                delimiter = "\n";
210                Ok(acc)
211            })
212    }
213
214    /// Write a stream to [`Locate`].
215    ///
216    /// * `stream` - Data to write
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// # use plantuml_server_client_rs as pscr;
222    /// #
223    /// # use pscr::{Locate };
224    /// # use std::path::PathBuf;
225    /// #
226    /// # #[tokio::main]
227    /// # async fn main() -> anyhow::Result<()> {
228    /// # let _ = std::fs::create_dir_all("./target/tests_docs/");
229    /// let locate = Locate::from("./target/tests_docs/locate_write");
230    /// let data = "foo2";
231    /// let stream = futures::stream::iter(vec![Ok(data.into())]);
232    ///
233    /// locate.write(stream).await?;
234    /// let read_data = std::fs::read_to_string(locate.to_path_buf())?;
235    /// assert_eq!(read_data, data);
236    /// # Ok(())
237    /// # }
238    /// ```
239    pub async fn write(
240        &self,
241        stream: impl Stream<Item = Result<Bytes, reqwest::Error>> + Unpin,
242    ) -> Result<()> {
243        use tokio::fs::File;
244
245        match self {
246            Self::File(output) => {
247                tracing::info!("output = {:?}", output);
248
249                create_base_dir(&output).await?;
250
251                let file = File::create(output)
252                    .await
253                    .with_context(|| format!("failed to open write file: output = {output:?}"))?;
254
255                let writer = BufWriter::new(file);
256                write_inner(writer, stream).await
257            }
258
259            Self::StdInOut => {
260                let writer = BufWriter::new(tokio::io::stdout());
261                write_inner(writer, stream).await
262            }
263        }
264    }
265
266    fn is_std_inout(&self) -> bool {
267        matches!(self, Self::StdInOut)
268    }
269}
270
271impl From<Option<PathBuf>> for Locate {
272    fn from(inner: Option<PathBuf>) -> Self {
273        Self::new(inner)
274    }
275}
276
277impl From<PathBuf> for Locate {
278    fn from(inner: PathBuf) -> Self {
279        Self::new(Some(inner))
280    }
281}
282
283impl From<&str> for Locate {
284    fn from(inner: &str) -> Self {
285        Self::new(Some(PathBuf::from(inner)))
286    }
287}
288
289impl PartialEq<Locate> for Locate {
290    fn eq(&self, rhs: &Locate) -> bool {
291        match (self, rhs) {
292            (Self::File(lhs), Self::File(rhs)) => lhs.eq(rhs),
293            (Self::StdInOut, Self::StdInOut) => true,
294            _ => false,
295        }
296    }
297}
298
299async fn write_inner<W>(
300    mut writer: BufWriter<W>,
301    mut stream: impl Stream<Item = Result<Bytes, reqwest::Error>> + Unpin,
302) -> Result<()>
303where
304    W: AsyncWrite + Unpin,
305{
306    use tokio::io::AsyncWriteExt;
307    while let Some(chunk) = stream.next().await {
308        let chunk = chunk.context("failed to read chunk")?;
309        writer.write_all(&chunk).await?;
310    }
311    writer.flush().await?;
312    Ok(())
313}
314
315async fn create_base_dir<P: AsRef<Path>>(output: P) -> Result<()> {
316    use tokio::fs::create_dir_all;
317
318    let mut base = output.as_ref().to_path_buf();
319    base.pop();
320    create_dir_all(base)
321        .await
322        .with_context(|| "failed to create directory: directory = {base:?}")
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[tokio::test]
330    async fn test_output_path() {
331        let format = Format::Svg;
332
333        // in: Some(_), out: Some(_)
334        let input = Locate::new(Some(PathBuf::from("input_dir/input_1.puml")));
335        let output = Locate::new(Some(PathBuf::from("output_dir")));
336
337        let path = output.output_path(&input, "id_1", &format);
338        assert_eq!(
339            path.to_path_buf(),
340            PathBuf::from("output_dir/input_1/id_1.svg")
341        );
342        let path = output.output_path(&input, "id_2", &format);
343        assert_eq!(
344            path.to_path_buf(),
345            PathBuf::from("output_dir/input_1/id_2.svg")
346        );
347
348        // in: Some(_), out: None
349        let input = Locate::new(Some(PathBuf::from("input_dir/input_2.puml")));
350        let output = Locate::new(None);
351
352        let path = output.output_path(&input, "id_1", &format);
353        assert_eq!(path.to_path_buf(), PathBuf::from("input_2/id_1.svg"));
354
355        // in: None, out: Some(_)
356        let input = Locate::new(None);
357        let output = Locate::new(Some(PathBuf::from("output_dir")));
358
359        let path = output.output_path(&input, "id_1", &format);
360        assert_eq!(path.to_path_buf(), PathBuf::from("output_dir/id_1.svg"));
361
362        // in: None, out: None
363        let input = Locate::new(None);
364        let output = Locate::new(None);
365
366        let path = output.output_path(&input, "id_1", &format);
367        assert_eq!(path.to_path_buf(), PathBuf::from("."));
368    }
369}