acick_config/
lib.rs

1#![warn(clippy::all)]
2
3//! Config for acick.
4//!
5//! ## Templates
6//!
7//! In some fields, you can use [Tera](https://tera.netlify.com/) template.
8//! Tera template is similar to Jinja2 and Django templates.
9//! See [Tera documentation](https://tera.netlify.com/docs/) for details.
10//!
11//! Following filters are available in addition to built-in filters of Tera.
12//! - `camel` : converts string to `camelCase`
13//! - `pascal` : converts string to `PascalCase`
14//! - `snake` : converts string to `snake_case`
15//! - `kebab` : converts string to `kebab-case`
16//!
17//! Available variables depend on fields.
18//! See [Field features](#field-features) section for details.
19//!
20//! ## Field features
21//!
22//! Fields have following features.
23//!
24//! ### `[c]` Command template field
25//!
26//! The field is recognized as an array of Tera templates
27//! with the following variables available:
28//! - `command` (str): command to be executed on shell
29//!
30//! ### `[t]` Target template field
31//!
32//! The field is recognized as a Tera template
33//! with the following variables available:
34//! - `service` (str): id of service (e.g.: `atcoder`)
35//! - `contest` (str): id of contest (e.g.: `arc100`)
36//! - `problem` (str): id of problem (e.g.: `C`)
37//!
38//! ### `[p]` Problem template field
39//!
40//! The field is recognized as a Tera template
41//! with the following variables available:
42//! - `service` (object): object that describes service
43//! - `contest` (object): object that describes contest
44//! - `problem` (object): object that describes problem
45//!
46//! ### `[s]` Shell-expanded field
47//!
48//! The field is processed with shell-like expansions.
49//! - Tilde `~` is expanded to the home directory.
50//! - Environment variables are expanded into their values.
51//!
52//! When combined with Tera template,
53//! the field is first processed as a template and then expanded.
54
55use std::fmt;
56use std::io::{Read as _, Write};
57
58use anyhow::{anyhow, Context as _};
59use lazy_static::lazy_static;
60use semver::{Version, VersionReq};
61use serde::{Deserialize, Serialize};
62use tokio::process::Command;
63
64use acick_util::{abs_path, console, model, DATA_LOCAL_DIR};
65
66mod session_config;
67mod template;
68
69use crate::abs_path::AbsPathBuf;
70use crate::console::Console;
71use crate::model::{Contest, ContestId, LangName, Problem, ProblemId, Service, ServiceKind};
72pub use session_config::SessionConfig;
73use template::{Expand, ProblemTempl, Shell, TargetContext, TargetTempl};
74
75pub type Error = anyhow::Error;
76pub type Result<T> = anyhow::Result<T>;
77
78lazy_static! {
79    static ref VERSION: Version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
80}
81
82#[derive(Serialize, Debug, Clone, PartialEq, Eq, Hash)]
83pub struct Config {
84    pub service_id: ServiceKind,
85    pub contest_id: ContestId,
86    pub base_dir: AbsPathBuf,
87    body: ConfigBody,
88}
89
90impl Config {
91    pub fn load(
92        service_id: ServiceKind,
93        contest_id: ContestId,
94        base_dir: Option<AbsPathBuf>,
95        cnsl: &mut Console,
96    ) -> Result<Self> {
97        let base_dir = match base_dir {
98            Some(base_dir) => base_dir,
99            None => ConfigBody::search(cnsl)?,
100        };
101        let body = ConfigBody::load(&base_dir, cnsl)?;
102        Ok(Self {
103            service_id,
104            contest_id,
105            base_dir,
106            body,
107        })
108    }
109
110    pub fn session(&self) -> &SessionConfig {
111        &self.body.session
112    }
113
114    pub fn service(&self) -> &ServiceConfig {
115        self.body.services.get(self.service_id)
116    }
117
118    pub fn move_testcases_dir(
119        &self,
120        problem: &Problem,
121        from: &AbsPathBuf,
122        cnsl: &mut Console,
123    ) -> Result<bool> {
124        let testcases_abs_dir = self.testcases_abs_dir(problem.id())?;
125        if testcases_abs_dir.as_ref().exists() {
126            let message = format!(
127                "remove existing testcases dir {}?",
128                testcases_abs_dir.strip_prefix(&self.base_dir).display()
129            );
130            if !cnsl.confirm(&message, false)? {
131                return Ok(false);
132            }
133            testcases_abs_dir.remove_dir_all_pretty(Some(&self.base_dir), cnsl)?;
134        } else if let Some(parent) = testcases_abs_dir.parent() {
135            parent.create_dir_all()?;
136        }
137
138        testcases_abs_dir.move_from_pretty(from, Some(&self.base_dir), cnsl)?;
139
140        Ok(true)
141    }
142
143    pub fn save_problem(
144        &self,
145        problem: &Problem,
146        overwrite: bool,
147        cnsl: &mut Console,
148    ) -> Result<Option<bool>> {
149        let problem_abs_path = self.problem_abs_path(problem.id())?;
150        problem_abs_path.save_pretty(
151            |file| serde_yaml::to_writer(file, &problem).context("Could not save problem as yaml"),
152            overwrite,
153            Some(&self.base_dir),
154            cnsl,
155        )
156    }
157
158    pub fn load_problem(&self, problem_id: &ProblemId, cnsl: &mut Console) -> Result<Problem> {
159        let problem_abs_path = self.problem_abs_path(problem_id)?;
160        let problem: Problem = problem_abs_path
161            .load_pretty(
162                |file| serde_yaml::from_reader(file).context("Could not read problem as yaml"),
163                Some(&self.base_dir),
164                cnsl,
165            )
166            .context(
167                "Could not load problem file. \
168                 Fetch problem data first by `acick fetch` command.",
169            )?;
170        if problem.id() != problem_id {
171            Err(anyhow!(
172                "Found mismatching problem id in problem file : {}",
173                problem.id()
174            ))
175        } else {
176            Ok(problem)
177        }
178    }
179
180    pub fn expand_and_save_source(
181        &self,
182        service: &Service,
183        contest: &Contest,
184        problem: &Problem,
185        overwrite: bool,
186        cnsl: &mut Console,
187    ) -> Result<Option<bool>> {
188        if service.id() != self.service_id || contest.id() != &self.contest_id {
189            return Err(anyhow!("Found mismatching service id or contest id"));
190        }
191        let source_abs_path = self.source_abs_path(problem.id())?;
192        let template = match &self.service().template {
193            Some(template) => template,
194            None => return Ok(None), // skip if template is empty
195        };
196        let template_expanded = template.expand_with(service, contest, problem)?;
197        source_abs_path.save_pretty(
198            |mut file| Ok(file.write_all(template_expanded.as_bytes())?),
199            overwrite,
200            Some(&self.base_dir),
201            cnsl,
202        )
203    }
204
205    pub fn load_source(&self, problem_id: &ProblemId, cnsl: &mut Console) -> Result<String> {
206        let source_abs_path = self.source_abs_path(problem_id)?;
207        source_abs_path.load_pretty(
208            |mut file| {
209                let mut buf = String::new();
210                file.read_to_string(&mut buf)?;
211                Ok(buf)
212            },
213            Some(&self.base_dir),
214            cnsl,
215        )
216    }
217
218    pub fn exec_compile(&self, problem_id: &ProblemId) -> Result<Command> {
219        let compile = &self.service().compile;
220        self.exec_templ(compile, problem_id)
221    }
222
223    pub fn exec_run(&self, problem_id: &ProblemId) -> Result<Command> {
224        let run = &self.service().run;
225        self.exec_templ(run, problem_id)
226    }
227
228    fn problem_abs_path(&self, problem_id: &ProblemId) -> Result<AbsPathBuf> {
229        let problem_path = &self.body.problem_path;
230        self.expand_to_abs(problem_path, problem_id)
231    }
232
233    pub fn testcases_abs_dir(&self, problem_id: &ProblemId) -> Result<AbsPathBuf> {
234        let testcases_dir = &self.body.testcases_dir;
235        self.expand_to_abs(testcases_dir, problem_id)
236    }
237
238    fn working_abs_dir(&self, problem_id: &ProblemId) -> Result<AbsPathBuf> {
239        let working_dir = &self.service().working_dir;
240        self.expand_to_abs(working_dir, problem_id)
241    }
242
243    fn source_abs_path(&self, problem_id: &ProblemId) -> Result<AbsPathBuf> {
244        let source_path = &self.service().source_path;
245        self.expand_to_abs(source_path, problem_id)
246    }
247
248    fn expand_to_abs(&self, path: &TargetTempl, problem_id: &ProblemId) -> Result<AbsPathBuf> {
249        path.expand_with(self.service_id, &self.contest_id, problem_id)
250            .and_then(|path_expanded| self.base_dir.join_expand(path_expanded))
251    }
252
253    fn exec_templ<'a, T: Expand<'a>>(
254        &'a self,
255        templ: &T,
256        problem_id: &'a ProblemId,
257    ) -> Result<Command>
258    where
259        T: Expand<'a, Context = TargetContext<'a>>,
260    {
261        let target_context = TargetContext::new(self.service_id, &self.contest_id, problem_id);
262        let working_abs_dir = self.working_abs_dir(problem_id)?;
263        let mut command = self.body.shell.exec_templ(templ, &target_context)?;
264        command.current_dir(working_abs_dir.as_ref());
265        Ok(command)
266    }
267
268    pub fn default_in_dir(base_dir: AbsPathBuf) -> Self {
269        let body = ConfigBody::default_in_dir(&base_dir);
270
271        Self {
272            service_id: ServiceKind::default(),
273            contest_id: Contest::default().id().clone(),
274            base_dir,
275            body,
276        }
277    }
278}
279
280impl fmt::Display for Config {
281    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
282        let yaml_str = serde_yaml::to_string(self).map_err(|_| fmt::Error)?;
283        write!(f, "{}", yaml_str)
284    }
285}
286
287#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
288pub struct ConfigBody {
289    #[serde(with = "string_serde")]
290    version: Version,
291    #[serde(default)]
292    shell: Shell,
293    #[serde(default = "ConfigBody::default_problem_path")]
294    problem_path: TargetTempl,
295    #[serde(default = "ConfigBody::default_testcases_dir")]
296    testcases_dir: TargetTempl,
297    #[serde(default)]
298    session: SessionConfig,
299    #[serde(default)]
300    services: ServicesConfig,
301}
302
303impl ConfigBody {
304    pub const FILE_NAME: &'static str = ".acick.yaml";
305
306    const DEFAULT_PROBLEM_PATH: &'static str =
307        "{{ service }}/{{ contest }}/{{ problem | lower }}/problem.yaml";
308
309    const DEFAULT_TESTCASES_DIR: &'static str =
310        "{{ service }}/{{ contest }}/{{ problem | lower }}/testcases";
311
312    pub fn generate_to(writer: &mut dyn Write) -> Result<()> {
313        writeln!(
314            writer,
315            include_str!("../resources/.acick.yaml.txt"),
316            version = &*VERSION,
317            bash = Shell::find_bash().display()
318        )
319        .context("Could not write config")
320    }
321
322    fn default_in_dir(base_dir: &AbsPathBuf) -> Self {
323        Self {
324            version: VERSION.clone(),
325            shell: Shell::default(),
326            problem_path: Self::default_problem_path(),
327            testcases_dir: Self::default_testcases_dir(),
328            session: SessionConfig::default_in_dir(base_dir),
329            services: ServicesConfig::default(),
330        }
331    }
332
333    fn default_problem_path() -> TargetTempl {
334        Self::DEFAULT_PROBLEM_PATH.into()
335    }
336
337    fn default_testcases_dir() -> TargetTempl {
338        Self::DEFAULT_TESTCASES_DIR.into()
339    }
340
341    fn search(cnsl: &mut Console) -> Result<AbsPathBuf> {
342        let cwd = AbsPathBuf::cwd()?;
343        let base_dir = cwd.search_dir_contains(Self::FILE_NAME).with_context(|| {
344            format!(
345                "Could not find config file ({}) in {} or any of the parent directories. \
346                 Create config file first by `acick init` command.",
347                Self::FILE_NAME,
348                cwd
349            )
350        })?;
351        writeln!(cnsl, "Found config file in base_dir: {}", base_dir)?;
352        Ok(base_dir)
353    }
354
355    fn load(base_dir: &AbsPathBuf, cnsl: &mut Console) -> Result<Self> {
356        let body: Self = base_dir.join(Self::FILE_NAME).load_pretty(
357            |file| serde_yaml::from_reader(file).context("Could not read config file as yaml"),
358            Some(base_dir),
359            cnsl,
360        )?;
361        body.validate()?;
362        Ok(body)
363    }
364
365    fn validate(&self) -> Result<()> {
366        // check version
367        let version_req = VersionReq::parse(&self.version.to_string())
368            .context("Could not parse version requirement")?;
369        if !version_req.matches(&VERSION) {
370            return Err(anyhow!(
371                r#"Found mismatched version in config file.
372    config version: {}
373    acick version : {}
374Fix the config file so that it is compatible with the current version of acick."#,
375                self.version,
376                &*VERSION
377            ));
378        }
379
380        Ok(())
381    }
382}
383
384impl Default for ConfigBody {
385    fn default() -> Self {
386        Self {
387            version: VERSION.clone(),
388            shell: Shell::default(),
389            problem_path: Self::default_problem_path(),
390            testcases_dir: Self::default_testcases_dir(),
391            session: SessionConfig::default(),
392            services: ServicesConfig::default(),
393        }
394    }
395}
396
397#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
398#[serde(default)]
399pub struct ServicesConfig {
400    atcoder: ServiceConfig,
401}
402
403impl ServicesConfig {
404    fn get(&self, service_id: ServiceKind) -> &ServiceConfig {
405        match service_id {
406            ServiceKind::Atcoder => &self.atcoder,
407        }
408    }
409}
410
411impl Default for ServicesConfig {
412    fn default() -> Self {
413        Self {
414            atcoder: ServiceConfig::default_for(ServiceKind::Atcoder),
415        }
416    }
417}
418
419#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
420pub struct ServiceConfig {
421    lang_names: Vec<LangName>,
422    working_dir: TargetTempl,
423    source_path: TargetTempl,
424    compile: TargetTempl,
425    run: TargetTempl,
426    #[serde(default)]
427    template: Option<ProblemTempl>,
428}
429
430impl ServiceConfig {
431    const DEFAULT_TEMPLATE: &'static str = r#"/*
432[{{ contest.id }}] {{ problem.id }} - {{ problem.name }}
433*/
434
435#include <iostream>
436using namespace std;
437
438int main() {
439    return 0;
440}
441"#;
442
443    fn default_for(service_id: ServiceKind) -> Self {
444        match service_id {
445            ServiceKind::Atcoder => Self {
446                lang_names: vec!["C++ (GCC 9.2.1)".into(), "C++14 (GCC 5.4.1)".into()],
447                working_dir: "{{ service }}/{{ contest }}/{{ problem | lower }}".into(),
448                source_path: "{{ service }}/{{ contest }}/{{ problem | lower }}/Main.cpp".into(),
449                compile: "set -x && g++ -std=gnu++17 -Wall -Wextra -O2 -o ./a.out ./Main.cpp"
450                    .into(),
451                // compile: "set -x && g++ -std=gnu++17 -Wall -Wextra -O2 -DONLINE_JUDGE -I/opt/boost/gcc/include -L/opt/boost/gcc/lib -o ./a.out ./Main.cpp".into(),
452                run: "./a.out".into(),
453                template: Some(Self::DEFAULT_TEMPLATE.into()),
454            },
455        }
456    }
457
458    pub fn lang_names(&self) -> &[LangName] {
459        &self.lang_names
460    }
461}
462
463mod string_serde {
464    use std::fmt::Display;
465    use std::str::FromStr;
466
467    use serde::{de, Deserialize, Deserializer, Serializer};
468
469    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
470    where
471        T: Display,
472        S: Serializer,
473    {
474        serializer.collect_str(value)
475    }
476
477    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
478    where
479        T: FromStr,
480        T::Err: Display,
481        D: Deserializer<'de>,
482    {
483        String::deserialize(deserializer)?
484            .parse()
485            .map_err(de::Error::custom)
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use std::fs::OpenOptions;
492
493    use tempfile::tempdir;
494
495    use super::*;
496    use crate::template::TargetContext;
497
498    #[test]
499    fn generate_and_deserialize() -> anyhow::Result<()> {
500        let mut buf = Vec::new();
501        ConfigBody::generate_to(&mut buf)?;
502        let body_yaml_str = String::from_utf8(buf)?;
503        let body_generated: ConfigBody = serde_yaml::from_str(&body_yaml_str)?;
504
505        let body_default = ConfigBody::default();
506
507        assert_eq!(body_generated, body_default);
508
509        Ok(())
510    }
511
512    #[tokio::test]
513    async fn exec_default_atcoder_compile() -> anyhow::Result<()> {
514        let test_dir = tempdir()?;
515
516        // prepare source file
517        let mut file = OpenOptions::new()
518            .write(true)
519            .create(true)
520            .open(format!("{}/Main.cpp", test_dir.path().display()))?;
521        file.write_all(
522            r#"
523#include <iostream>
524using namespace std;
525
526int main() {{
527    return 0;
528}}
529        "#
530            .as_bytes(),
531        )?;
532
533        // exec compile command
534        let contest = Contest::default();
535        let problem = Problem::default();
536        let shell = Shell::default();
537        let compile = ServiceConfig::default_for(ServiceKind::Atcoder).compile;
538        let context = TargetContext::new(ServiceKind::default(), contest.id(), problem.id());
539        let output = shell
540            .exec_templ(&compile, &context)?
541            .current_dir(test_dir.path())
542            .output()
543            .await?;
544        eprintln!("{:?}", output);
545        assert!(output.status.success());
546
547        Ok(())
548    }
549}