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}