cargo_compete/
oj_api.rs

1use crate::shell::Shell;
2use anyhow::{bail, Context as _};
3use camino::Utf8Path;
4use itertools::Itertools as _;
5use serde::{
6    de::{DeserializeOwned, Error as _},
7    Deserialize, Deserializer,
8};
9use std::{env, ffi::OsStr, path::Path};
10use url::Url;
11
12pub(crate) fn get_problem(
13    url: &Url,
14    system: bool,
15    cwd: &Utf8Path,
16    shell: &mut Shell,
17) -> anyhow::Result<Problem> {
18    let args = &mut vec!["get-problem", url.as_ref()];
19    if system {
20        args.push("--system".as_ref());
21    }
22    call(args, cwd, shell)
23}
24
25pub(crate) fn get_contest(
26    url: &Url,
27    cwd: &Utf8Path,
28    shell: &mut Shell,
29) -> anyhow::Result<Vec<(Url, Option<String>)>> {
30    let Contest { problems } = call(&["get-contest", url.as_ref()], cwd, shell)?;
31    return Ok(problems
32        .into_iter()
33        .map(|ContestProblem { url, context }| (url, context.alphabet))
34        .collect());
35
36    #[derive(Deserialize)]
37    struct Contest {
38        problems: Vec<ContestProblem>,
39    }
40
41    #[derive(Deserialize)]
42    struct ContestProblem {
43        url: Url,
44        context: ContestProblemContext,
45    }
46
47    #[derive(Deserialize)]
48    struct ContestProblemContext {
49        alphabet: Option<String>,
50    }
51}
52
53pub(crate) fn guess_language_id(
54    url: &Url,
55    file: &Path,
56    cwd: &Utf8Path,
57    shell: &mut Shell,
58) -> anyhow::Result<String> {
59    return call(
60        &[
61            OsStr::new("guess-language-id"),
62            url.as_str().as_ref(),
63            "--file".as_ref(),
64            file.as_ref(),
65        ],
66        cwd,
67        shell,
68    )
69    .map(|GuessLanguageId { id }| id);
70
71    #[derive(Deserialize)]
72    struct GuessLanguageId {
73        id: String,
74    }
75}
76
77pub(crate) fn submit_code(
78    url: &Url,
79    file: &Path,
80    language: &str,
81    cwd: &Utf8Path,
82    shell: &mut Shell,
83) -> anyhow::Result<Url> {
84    return call(
85        &[
86            "submit-code".as_ref(),
87            url.as_str().as_ref(),
88            "--file".as_ref(),
89            file.as_os_str(),
90            "--language".as_ref(),
91            language.as_ref(),
92        ],
93        cwd,
94        shell,
95    )
96    .map(|SubmitCode { url }| url);
97
98    #[derive(Deserialize)]
99    struct SubmitCode {
100        url: Url,
101    }
102}
103
104fn call<T: DeserializeOwned, S: AsRef<OsStr>>(
105    args: &[S],
106    cwd: &Utf8Path,
107    shell: &mut Shell,
108) -> anyhow::Result<T> {
109    let oj_api_exe = which::which_in("oj-api", env::var_os("PATH"), cwd)
110        .with_context(|| "`oj-api` not found")?;
111
112    let output = crate::process::process(oj_api_exe)
113        .args(args)
114        .cwd(cwd)
115        .display_cwd()
116        .read_with_shell_status(shell)?;
117
118    let Outcome { result, messages } = serde_json::from_str(&output)
119        .with_context(|| "could not parse the output from `oj-api`")?;
120
121    return if let Ok(result) = result {
122        for message in messages {
123            shell.warn(format!("oj-api: {message}"))?;
124        }
125        Ok(result)
126    } else {
127        bail!(
128            "`oj-api` returned error:\n{}",
129            messages.iter().map(|s| format!("- {s}\n")).join(""),
130        );
131    };
132
133    struct Outcome<T> {
134        result: Result<T, ()>,
135        messages: Vec<String>,
136    }
137
138    impl<'de, T: DeserializeOwned> Deserialize<'de> for Outcome<T> {
139        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
140        where
141            D: Deserializer<'de>,
142        {
143            let Repr {
144                status,
145                messages,
146                result,
147            } = Repr::deserialize(deserializer)?;
148
149            return match &*status {
150                "ok" => Ok(Self {
151                    result: Ok(result),
152                    messages,
153                }),
154                "error" => Ok(Self {
155                    result: Err(()),
156                    messages,
157                }),
158                status => Err(D::Error::custom(format!(
159                    "expected \"ok\" or \"error\", got {status:?}",
160                ))),
161            };
162
163            #[derive(Deserialize)]
164            struct Repr<T> {
165                status: String,
166                messages: Vec<String>,
167                result: T,
168            }
169        }
170    }
171}
172
173#[derive(Debug, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub(crate) struct Problem {
176    /// > ```text
177    /// > "url": {
178    /// >   "type": "string",
179    /// >   "format": "uri"
180    /// > },
181    /// > ```
182    pub(crate) url: Url,
183
184    ///// > ```text
185    ///// > "name": {
186    ///// >   "type": "string",
187    ///// >   "description": "the title of the problem without alphabets, i.e. \"Xor Sum\" is used instead of \"D - Xor Sum\"; because in many contest sites, the alphabets are attributes belonging to the relation between problems and contests, rather than only the problem",
188    ///// >   "examples": [
189    ///// >     "Xor Sum",
190    ///// >     "K-th Beautiful String"
191    ///// >   ]
192    ///// > },
193    ///// > ```
194    //pub(crate) name: Option<String>,
195    /// > ```text
196    /// > "context": {
197    /// >   "type": "object",
198    /// >   "properties": {
199    /// >     "contest": {
200    /// >       "type": "object",
201    /// >       "properties": {
202    /// >         "url": {
203    /// >           "type": "string",
204    /// >           "format": "uri"
205    /// >         },
206    /// >         "name": {
207    /// >           "type": "string"
208    /// >         }
209    /// >       }
210    /// >     },
211    /// >     "alphabet": {
212    /// >       "type": "string"
213    /// >     }
214    /// >   }
215    /// > },
216    /// > ```
217    pub(crate) context: ProblemContext,
218
219    /// > ```text
220    /// > "timeLimit": {
221    /// >   "type": "integer",
222    /// >   "description": "in milliseconds (msec)"
223    /// > },
224    /// > ```
225    pub(crate) time_limit: Option<u64>,
226
227    /// > ```text
228    /// > "tests": {
229    /// >   "type": "array",
230    /// >   "items": {
231    /// >     "type": "object",
232    /// >     "properties": {
233    /// >       "name": {
234    /// >         "type": "string"
235    /// >       },
236    /// >       "input": {
237    /// >         "type": "string"
238    /// >       },
239    /// >       "output": {
240    /// >         "type": "string"
241    /// >       }
242    /// >     },
243    /// >     "required": [
244    /// >       "input",
245    /// >       "output"
246    /// >     ]
247    /// >   },
248    /// >   "examples": [
249    /// >     [
250    /// >       {
251    /// >         "input": "35\n",
252    /// >         "output": "57\n"
253    /// >       },
254    /// >       {
255    /// >         "input": "57\n",
256    /// >         "output": "319\n"
257    /// >       }
258    /// >     ]
259    /// >   ]
260    /// > },
261    /// > ```
262    pub(crate) tests: Vec<ProblemTest>,
263}
264
265#[derive(Debug, Deserialize)]
266pub(crate) struct ProblemContext {
267    pub(crate) contest: Option<ProblemContextContest>,
268    pub(crate) alphabet: Option<String>,
269}
270
271#[derive(Debug, Deserialize)]
272pub(crate) struct ProblemContextContest {
273    pub(crate) url: Option<Url>,
274    //pub(crate) name: Option<String>,
275}
276
277#[derive(Debug, Deserialize)]
278pub(crate) struct ProblemTest {
279    pub(crate) name: Option<String>,
280    pub(crate) input: String,
281    pub(crate) output: String,
282}