leetcode_api/dao/
save_info.rs

1use std::{
2    fmt::Write as _,
3    ops::Not,
4    path::{Path, PathBuf},
5};
6
7use lcode_config::global::G_USER_CONFIG;
8use miette::{IntoDiagnostic, Result};
9use tokio::{
10    fs::{File, OpenOptions, create_dir_all},
11    io::{AsyncReadExt, AsyncWriteExt},
12};
13use tracing::{instrument, trace};
14
15use crate::{
16    entities::*,
17    leetcode::{IdSlug, question::qs_detail::Question},
18    render::Render,
19};
20
21/// Contains file's info,
22/// Useful for write some content to question's files.
23#[derive(Clone)]
24#[derive(Debug)]
25#[derive(Default)]
26#[derive(PartialEq, Eq)]
27pub struct FileInfo {
28    pub code_path: PathBuf,
29    pub test_case_path: PathBuf,
30    pub content_path: PathBuf,
31}
32
33impl FileInfo {
34    async fn rest_file<A: AsRef<Path> + Send>(path: A) -> Result<File> {
35        OpenOptions::new()
36            .create(true)
37            .truncate(true)
38            .write(true)
39            .open(path)
40            .await
41            .into_diagnostic()
42    }
43    async fn append_file<A: AsRef<Path> + Send>(path: A) -> Result<File> {
44        OpenOptions::new()
45            .create(true)
46            .append(true)
47            .open(path)
48            .await
49            .into_diagnostic()
50    }
51
52    /// When submit have testcase failed, can call it.
53    pub async fn append_test_case(&self, case: &str) -> Result<()> {
54        if case.is_empty() {
55            return Ok(());
56        }
57
58        let mut f = Self::append_file(&self.test_case_path).await?;
59
60        f.write_all(b"\n")
61            .await
62            .into_diagnostic()?;
63        f.write_all(case.as_bytes())
64            .await
65            .into_diagnostic()?;
66
67        Ok(())
68    }
69
70    pub async fn reset_test_case(&self, case: &str) -> Result<()> {
71        if case.is_empty() {
72            return Ok(());
73        }
74
75        let mut f = Self::rest_file(&self.test_case_path).await?;
76        f.write_all(case.as_bytes())
77            .await
78            .into_diagnostic()?;
79
80        Ok(())
81    }
82}
83
84impl FileInfo {
85    /// Get code, test, content dir
86    #[instrument]
87    pub async fn build(pb: &index::Model) -> Result<Self> {
88        let mut cache_path = G_USER_CONFIG.config.code_dir.clone();
89
90        // shit `format_args!` has Lifetime limitation
91        let sub_dir = if G_USER_CONFIG
92            .config
93            .dir_with_frontend_id
94        {
95            format!("{}_{}", pb.frontend_question_id, pb.question_title_slug)
96        }
97        else {
98            format!("{}_{}", pb.question_id, pb.question_title_slug)
99        };
100        cache_path.push(sub_dir);
101
102        create_dir_all(&cache_path)
103            .await
104            .into_diagnostic()?;
105
106        let mut code_path = cache_path.clone();
107        let code_file_name = format!("{}{}", pb.question_id, G_USER_CONFIG.get_suffix());
108        code_path.push(code_file_name);
109        trace!("code path: {:?}", code_path);
110
111        let mut test_case_path = cache_path.clone();
112        let test_file_name = format!("{}_test_case.txt", pb.question_id);
113        test_case_path.push(test_file_name);
114        trace!("test case path: {:?}", test_case_path);
115
116        let mut content_path = cache_path;
117        let temp = if G_USER_CONFIG.config.translate {
118            "cn"
119        }
120        else {
121            "en"
122        };
123        let detail_file_name = format!("{}_detail_{}.md", pb.question_id, temp);
124        content_path.push(detail_file_name);
125        trace!("content case path: {:?}", content_path);
126        Ok(Self {
127            code_path,
128            test_case_path,
129            content_path,
130        })
131    }
132
133    /// Refresh a question's `content`, `code` and `test_case` to file
134    pub async fn write_to_file(&self, detail: &Question) -> Result<()> {
135        let content = detail.to_md_str(true);
136
137        let (r1, r2) = tokio::join!(
138            Self::write_file(&self.test_case_path, &detail.example_testcases),
139            Self::write_file(&self.content_path, &content)
140        );
141        r1?;
142        r2?;
143
144        if let Some(snippets) = &detail.code_snippets {
145            for snippet in snippets {
146                if snippet.lang_slug == G_USER_CONFIG.config.lang {
147                    let (start, end, mut inject_start, inject_end) = G_USER_CONFIG.get_lang_info();
148
149                    if !inject_start.is_empty() {
150                        inject_start += "\n";
151                    }
152                    let code_str = format!(
153                        "{}{}\n{}\n{}\n{}",
154                        inject_start, start, snippet.code, end, inject_end
155                    );
156                    Self::write_file(&self.code_path, &code_str).await?;
157                }
158            }
159        }
160
161        // if this question not support this lang, or is paid only
162        if !self.code_path.exists() {
163            let temp = if detail.is_paid_only {
164                "this question is paid only".to_owned()
165            }
166            else {
167                let mut temp = format!(
168                    "this question not support {} \n\nsupport below:\n",
169                    G_USER_CONFIG.config.lang
170                );
171                if let Some(snippets) = &detail.code_snippets {
172                    for snippet in snippets {
173                        writeln!(&mut temp, "{}", snippet.lang_slug).into_diagnostic()?;
174                    }
175                }
176                temp
177            };
178
179            Self::write_file(&self.code_path, &temp).await?;
180        }
181
182        Ok(())
183    }
184    pub async fn get_user_code(&self, idslug: &IdSlug) -> Result<(String, String)> {
185        let (code_file, test_case_file) = tokio::join!(
186            File::open(&self.code_path),
187            File::open(&self.test_case_path)
188        );
189
190        let (mut code_file, mut test_case_file) = (
191            code_file.map_err(|err| {
192                miette::miette!(
193                    "Error: {}. There is no code file, maybe you changed the name, please get \
194                     **{}** question detail again",
195                    err,
196                    idslug
197                )
198            })?,
199            test_case_file.map_err(|err| {
200                miette::miette!(
201                    "Error: {}. There is no test case file, maybe you changed the name, please \
202                     remove relate file and get **{}** question detail again, or manual create a \
203                     same name blank file",
204                    err,
205                    idslug
206                )
207            })?,
208        );
209
210        let mut code = String::new();
211        let mut test_case = String::new();
212
213        let (code_res, test_case_res) = tokio::join!(
214            code_file.read_to_string(&mut code),
215            test_case_file.read_to_string(&mut test_case)
216        );
217        code_res.into_diagnostic()?;
218        test_case_res.into_diagnostic()?;
219
220        Ok((code, test_case))
221    }
222
223    /// if file not exists, create file and write something
224    async fn write_file(path: &PathBuf, val: &str) -> Result<()> {
225        if path.exists().not() {
226            create_dir_all(
227                &path
228                    .parent()
229                    .expect("get path parent failed"),
230            )
231            .await
232            .into_diagnostic()
233            .expect("create_dir_all failed");
234
235            let mut f = Self::rest_file(&path).await?;
236            f.write_all(val.as_bytes())
237                .await
238                .into_diagnostic()?;
239
240            f.sync_all().await.into_diagnostic()?;
241        }
242        Ok(())
243    }
244}