From 44ecfa241304a5374c132740839ce80cf813fe53 Mon Sep 17 00:00:00 2001 From: comavius Date: Fri, 4 Oct 2024 17:52:57 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20generated=20openapi=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend-api-schema/.gitignore | 3 + .../.openapi-generator-ignore | 23 + .../.openapi-generator/FILES | 18 + .../.openapi-generator/VERSION | 1 + .../backend-api-schema/.travis.yml | 1 + .../backend-api-schema/Cargo.toml | 15 + .../backend-api-schema/README.md | 48 ++ .../backend-api-schema/backend-api.yaml | 99 +++ .../backend-api-schema/docs/JudgeStatus.md | 19 + .../backend-api-schema/docs/SubmitResult.md | 13 + .../docs/SubmitResultApi.md | 39 + .../backend-api-schema/docs/TestResult.md | 15 + .../backend-api-schema/git_push.sh | 57 ++ .../src/apis/configuration.rs | 53 ++ .../backend-api-schema/src/apis/mod.rs | 95 +++ .../src/apis/submit_result_api.rs | 54 ++ .../backend-api-schema/src/lib.rs | 11 + .../src/models/judge_status.rs | 57 ++ .../backend-api-schema/src/models/mod.rs | 6 + .../src/models/submit_result.rs | 37 + .../src/models/test_result.rs | 45 ++ judge-control-app/judge-api-server/.gitignore | 2 + .../.openapi-generator-ignore | 23 + .../judge-api-server/.openapi-generator/FILES | 11 + .../.openapi-generator/VERSION | 1 + judge-control-app/judge-api-server/Cargo.toml | 47 ++ judge-control-app/judge-api-server/README.md | 92 +++ .../judge-api-server/judge-api.yaml | 142 ++++ .../judge-api-server/src/apis/judge.rs | 33 + .../judge-api-server/src/apis/mod.rs | 2 + .../judge-api-server/src/header.rs | 181 +++++ judge-control-app/judge-api-server/src/lib.rs | 28 + .../judge-api-server/src/models.rs | 690 ++++++++++++++++++ .../judge-api-server/src/server/mod.rs | 108 +++ .../judge-api-server/src/types.rs | 665 +++++++++++++++++ 35 files changed, 2734 insertions(+) create mode 100644 judge-control-app/backend-api-schema/.gitignore create mode 100644 judge-control-app/backend-api-schema/.openapi-generator-ignore create mode 100644 judge-control-app/backend-api-schema/.openapi-generator/FILES create mode 100644 judge-control-app/backend-api-schema/.openapi-generator/VERSION create mode 100644 judge-control-app/backend-api-schema/.travis.yml create mode 100644 judge-control-app/backend-api-schema/Cargo.toml create mode 100644 judge-control-app/backend-api-schema/README.md create mode 100644 judge-control-app/backend-api-schema/backend-api.yaml create mode 100644 judge-control-app/backend-api-schema/docs/JudgeStatus.md create mode 100644 judge-control-app/backend-api-schema/docs/SubmitResult.md create mode 100644 judge-control-app/backend-api-schema/docs/SubmitResultApi.md create mode 100644 judge-control-app/backend-api-schema/docs/TestResult.md create mode 100644 judge-control-app/backend-api-schema/git_push.sh create mode 100644 judge-control-app/backend-api-schema/src/apis/configuration.rs create mode 100644 judge-control-app/backend-api-schema/src/apis/mod.rs create mode 100644 judge-control-app/backend-api-schema/src/apis/submit_result_api.rs create mode 100644 judge-control-app/backend-api-schema/src/lib.rs create mode 100644 judge-control-app/backend-api-schema/src/models/judge_status.rs create mode 100644 judge-control-app/backend-api-schema/src/models/mod.rs create mode 100644 judge-control-app/backend-api-schema/src/models/submit_result.rs create mode 100644 judge-control-app/backend-api-schema/src/models/test_result.rs create mode 100644 judge-control-app/judge-api-server/.gitignore create mode 100644 judge-control-app/judge-api-server/.openapi-generator-ignore create mode 100644 judge-control-app/judge-api-server/.openapi-generator/FILES create mode 100644 judge-control-app/judge-api-server/.openapi-generator/VERSION create mode 100644 judge-control-app/judge-api-server/Cargo.toml create mode 100644 judge-control-app/judge-api-server/README.md create mode 100644 judge-control-app/judge-api-server/judge-api.yaml create mode 100644 judge-control-app/judge-api-server/src/apis/judge.rs create mode 100644 judge-control-app/judge-api-server/src/apis/mod.rs create mode 100644 judge-control-app/judge-api-server/src/header.rs create mode 100644 judge-control-app/judge-api-server/src/lib.rs create mode 100644 judge-control-app/judge-api-server/src/models.rs create mode 100644 judge-control-app/judge-api-server/src/server/mod.rs create mode 100644 judge-control-app/judge-api-server/src/types.rs diff --git a/judge-control-app/backend-api-schema/.gitignore b/judge-control-app/backend-api-schema/.gitignore new file mode 100644 index 0000000..6aa1064 --- /dev/null +++ b/judge-control-app/backend-api-schema/.gitignore @@ -0,0 +1,3 @@ +/target/ +**/*.rs.bk +Cargo.lock diff --git a/judge-control-app/backend-api-schema/.openapi-generator-ignore b/judge-control-app/backend-api-schema/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/judge-control-app/backend-api-schema/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/judge-control-app/backend-api-schema/.openapi-generator/FILES b/judge-control-app/backend-api-schema/.openapi-generator/FILES new file mode 100644 index 0000000..52860e0 --- /dev/null +++ b/judge-control-app/backend-api-schema/.openapi-generator/FILES @@ -0,0 +1,18 @@ +.gitignore +.openapi-generator-ignore +.travis.yml +Cargo.toml +README.md +docs/JudgeStatus.md +docs/SubmitResult.md +docs/SubmitResultApi.md +docs/TestResult.md +git_push.sh +src/apis/configuration.rs +src/apis/mod.rs +src/apis/submit_result_api.rs +src/lib.rs +src/models/judge_status.rs +src/models/mod.rs +src/models/submit_result.rs +src/models/test_result.rs diff --git a/judge-control-app/backend-api-schema/.openapi-generator/VERSION b/judge-control-app/backend-api-schema/.openapi-generator/VERSION new file mode 100644 index 0000000..17f2442 --- /dev/null +++ b/judge-control-app/backend-api-schema/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.9.0-SNAPSHOT diff --git a/judge-control-app/backend-api-schema/.travis.yml b/judge-control-app/backend-api-schema/.travis.yml new file mode 100644 index 0000000..22761ba --- /dev/null +++ b/judge-control-app/backend-api-schema/.travis.yml @@ -0,0 +1 @@ +language: rust diff --git a/judge-control-app/backend-api-schema/Cargo.toml b/judge-control-app/backend-api-schema/Cargo.toml new file mode 100644 index 0000000..9a35f53 --- /dev/null +++ b/judge-control-app/backend-api-schema/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "backend_api_schema" +version = "0.1.0" +authors = ["OpenAPI Generator team and contributors"] +description = "No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)" +license = "MIT" +edition = "2021" + +[dependencies] +serde = { version = "^1.0", features = ["derive"] } +serde_json = "^1.0" +serde_repr = "^0.1" +url = "^2.5" +uuid = { version = "^1.8", features = ["serde", "v4"] } +reqwest = { version = "^0.12", features = ["json", "multipart"] } diff --git a/judge-control-app/backend-api-schema/README.md b/judge-control-app/backend-api-schema/README.md new file mode 100644 index 0000000..61b206e --- /dev/null +++ b/judge-control-app/backend-api-schema/README.md @@ -0,0 +1,48 @@ +# Rust API client for backend_api_schema + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + +## Overview + +This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://openapis.org) from a remote server, you can easily generate an API client. + +- API version: 0.1 +- Package version: 0.1 +- Generator version: 7.9.0-SNAPSHOT +- Build package: `org.openapitools.codegen.languages.RustClientCodegen` + +## Installation + +Put the package under your project folder in a directory named `backend_api_schema` and add the following to `Cargo.toml` under `[dependencies]`: + +``` +backend_api_schema = { path = "./backend_api_schema" } +``` + +## Documentation for API Endpoints + +All URIs are relative to *https://api.server.test/v1* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +*SubmitResultApi* | [**submit_result**](docs/SubmitResultApi.md#submit_result) | **POST** /submit-result | 通常のテストケースのジャッジ + + +## Documentation For Models + + - [JudgeStatus](docs/JudgeStatus.md) + - [SubmitResult](docs/SubmitResult.md) + - [TestResult](docs/TestResult.md) + + +To get access to the crate's generated documentation, use: + +``` +cargo doc --open +``` + +## Author + + + diff --git a/judge-control-app/backend-api-schema/backend-api.yaml b/judge-control-app/backend-api-schema/backend-api.yaml new file mode 100644 index 0000000..1aa5926 --- /dev/null +++ b/judge-control-app/backend-api-schema/backend-api.yaml @@ -0,0 +1,99 @@ +openapi: '3.0.3' +info: + title: Backend to Judge API + version: '0.1' + license: + name: MIT + url: 'https://github.com/traP-jp/traO-Judge-docs/blob/main/LICENSE' +servers: + - url: https://api.server.test/v1 +paths: + '/submit-result': + post: + operationId: submitResult + description: ジャッジ結果 + tags: + - submit result + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SubmitResult' + required: true + summary: 通常のテストケースのジャッジ + responses: + '200': + description: OK + +components: + schemas: + JudgeStatus: + title: JudgeStatus + type: string + description: ジャッジの結果 + enum: + - AC + - WA + - TLE + - MLE + - OLE + - RE + - CE + - IE + TestResult: + title: TestResult + type: object + description: テストケースの結果 + properties: + status: + title: status + $ref: '#/components/schemas/JudgeStatus' + text: + title: text + type: string + description: 追加情報 + example: 'Alice win' + score: + title: score + type: number + description: スコア + example: 100 + execTime: + title: execTime + type: number + description: 実行時間 + example: 1.23 + memorySize: + title: memorySize + type: number + description: メモリ使用量(byte) + example: 1234567 + required: + - status + - score + - execTime + - memorySize + SubmitResult: + type: object + title: JudgeResult + description: テストケースのジャッジ結果 + properties: + judgeId: + title: judgeId + type: string + description: ジャッジID + format: uuid + example: 01234567-89ab-cdef-0123-456789abcdef + testResults: + title: testResults + type: array + description: テストケースのリスト + items: + $ref: '#/components/schemas/TestResult' + totalResult: + title: totalResult + $ref: '#/components/schemas/TestResult' + required: + - judgeId + - testResults + - totalResult \ No newline at end of file diff --git a/judge-control-app/backend-api-schema/docs/JudgeStatus.md b/judge-control-app/backend-api-schema/docs/JudgeStatus.md new file mode 100644 index 0000000..de530a2 --- /dev/null +++ b/judge-control-app/backend-api-schema/docs/JudgeStatus.md @@ -0,0 +1,19 @@ +# JudgeStatus + +## Enum Variants + +| Name | Value | +|---- | -----| +| Ac | AC | +| Wa | WA | +| Tle | TLE | +| Mle | MLE | +| Ole | OLE | +| Re | RE | +| Ce | CE | +| Ie | IE | + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/judge-control-app/backend-api-schema/docs/SubmitResult.md b/judge-control-app/backend-api-schema/docs/SubmitResult.md new file mode 100644 index 0000000..6d33497 --- /dev/null +++ b/judge-control-app/backend-api-schema/docs/SubmitResult.md @@ -0,0 +1,13 @@ +# SubmitResult + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**judge_id** | [**uuid::Uuid**](uuid::Uuid.md) | ジャッジID | +**test_results** | [**Vec**](TestResult.md) | テストケースのリスト | +**total_result** | [**models::TestResult**](TestResult.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/judge-control-app/backend-api-schema/docs/SubmitResultApi.md b/judge-control-app/backend-api-schema/docs/SubmitResultApi.md new file mode 100644 index 0000000..38f685c --- /dev/null +++ b/judge-control-app/backend-api-schema/docs/SubmitResultApi.md @@ -0,0 +1,39 @@ +# \SubmitResultApi + +All URIs are relative to *https://api.server.test/v1* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**submit_result**](SubmitResultApi.md#submit_result) | **POST** /submit-result | 通常のテストケースのジャッジ + + + +## submit_result + +> submit_result(submit_result) +通常のテストケースのジャッジ + +ジャッジ結果 + +### Parameters + + +Name | Type | Description | Required | Notes +------------- | ------------- | ------------- | ------------- | ------------- +**submit_result** | [**SubmitResult**](SubmitResult.md) | | [required] | + +### Return type + + (empty response body) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/judge-control-app/backend-api-schema/docs/TestResult.md b/judge-control-app/backend-api-schema/docs/TestResult.md new file mode 100644 index 0000000..f8f6461 --- /dev/null +++ b/judge-control-app/backend-api-schema/docs/TestResult.md @@ -0,0 +1,15 @@ +# TestResult + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**status** | [**models::JudgeStatus**](JudgeStatus.md) | | +**text** | Option<**String**> | 追加情報 | [optional] +**score** | **f64** | スコア | +**exec_time** | **f64** | 実行時間 | +**memory_size** | **f64** | メモリ使用量(byte) | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/judge-control-app/backend-api-schema/git_push.sh b/judge-control-app/backend-api-schema/git_push.sh new file mode 100644 index 0000000..f53a75d --- /dev/null +++ b/judge-control-app/backend-api-schema/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/judge-control-app/backend-api-schema/src/apis/configuration.rs b/judge-control-app/backend-api-schema/src/apis/configuration.rs new file mode 100644 index 0000000..882a98a --- /dev/null +++ b/judge-control-app/backend-api-schema/src/apis/configuration.rs @@ -0,0 +1,53 @@ +/* + * Backend to Judge API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1 + * + * Generated by: https://openapi-generator.tech + */ + + + +#[derive(Debug, Clone)] +pub struct Configuration { + pub base_path: String, + pub user_agent: Option, + pub client: reqwest::Client, + pub basic_auth: Option, + pub oauth_access_token: Option, + pub bearer_access_token: Option, + pub api_key: Option, + // TODO: take an oauth2 token source, similar to the go one +} + +pub type BasicAuth = (String, Option); + +#[derive(Debug, Clone)] +pub struct ApiKey { + pub prefix: Option, + pub key: String, +} + + +impl Configuration { + pub fn new() -> Configuration { + Configuration::default() + } +} + +impl Default for Configuration { + fn default() -> Self { + Configuration { + base_path: "https://api.server.test/v1".to_owned(), + user_agent: Some("OpenAPI-Generator/0.1/rust".to_owned()), + client: reqwest::Client::new(), + basic_auth: None, + oauth_access_token: None, + bearer_access_token: None, + api_key: None, + + } + } +} diff --git a/judge-control-app/backend-api-schema/src/apis/mod.rs b/judge-control-app/backend-api-schema/src/apis/mod.rs new file mode 100644 index 0000000..15a7ad0 --- /dev/null +++ b/judge-control-app/backend-api-schema/src/apis/mod.rs @@ -0,0 +1,95 @@ +use std::error; +use std::fmt; + +#[derive(Debug, Clone)] +pub struct ResponseContent { + pub status: reqwest::StatusCode, + pub content: String, + pub entity: Option, +} + +#[derive(Debug)] +pub enum Error { + Reqwest(reqwest::Error), + Serde(serde_json::Error), + Io(std::io::Error), + ResponseError(ResponseContent), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (module, e) = match self { + Error::Reqwest(e) => ("reqwest", e.to_string()), + Error::Serde(e) => ("serde", e.to_string()), + Error::Io(e) => ("IO", e.to_string()), + Error::ResponseError(e) => ("response", format!("status code {}", e.status)), + }; + write!(f, "error in {}: {}", module, e) + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + Some(match self { + Error::Reqwest(e) => e, + Error::Serde(e) => e, + Error::Io(e) => e, + Error::ResponseError(_) => return None, + }) + } +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Error::Reqwest(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Serde(e) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +pub fn urlencode>(s: T) -> String { + ::url::form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() +} + +pub fn parse_deep_object(prefix: &str, value: &serde_json::Value) -> Vec<(String, String)> { + if let serde_json::Value::Object(object) = value { + let mut params = vec![]; + + for (key, value) in object { + match value { + serde_json::Value::Object(_) => params.append(&mut parse_deep_object( + &format!("{}[{}]", prefix, key), + value, + )), + serde_json::Value::Array(array) => { + for (i, value) in array.iter().enumerate() { + params.append(&mut parse_deep_object( + &format!("{}[{}][{}]", prefix, key, i), + value, + )); + } + }, + serde_json::Value::String(s) => params.push((format!("{}[{}]", prefix, key), s.clone())), + _ => params.push((format!("{}[{}]", prefix, key), value.to_string())), + } + } + + return params; + } + + unimplemented!("Only objects are supported with style=deepObject") +} + +pub mod submit_result_api; + +pub mod configuration; diff --git a/judge-control-app/backend-api-schema/src/apis/submit_result_api.rs b/judge-control-app/backend-api-schema/src/apis/submit_result_api.rs new file mode 100644 index 0000000..7e7b247 --- /dev/null +++ b/judge-control-app/backend-api-schema/src/apis/submit_result_api.rs @@ -0,0 +1,54 @@ +/* + * Backend to Judge API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1 + * + * Generated by: https://openapi-generator.tech + */ + + +use reqwest; +use serde::{Deserialize, Serialize}; +use crate::{apis::ResponseContent, models}; +use super::{Error, configuration}; + + +/// struct for typed errors of method [`submit_result`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SubmitResultError { + UnknownValue(serde_json::Value), +} + + +/// ジャッジ結果 +pub async fn submit_result(configuration: &configuration::Configuration, submit_result: models::SubmitResult) -> Result<(), Error> { + let local_var_configuration = configuration; + + let local_var_client = &local_var_configuration.client; + + let local_var_uri_str = format!("{}/submit-result", local_var_configuration.base_path); + let mut local_var_req_builder = local_var_client.request(reqwest::Method::POST, local_var_uri_str.as_str()); + + if let Some(ref local_var_user_agent) = local_var_configuration.user_agent { + local_var_req_builder = local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone()); + } + local_var_req_builder = local_var_req_builder.json(&submit_result); + + let local_var_req = local_var_req_builder.build()?; + let local_var_resp = local_var_client.execute(local_var_req).await?; + + let local_var_status = local_var_resp.status(); + let local_var_content = local_var_resp.text().await?; + + if !local_var_status.is_client_error() && !local_var_status.is_server_error() { + Ok(()) + } else { + let local_var_entity: Option = serde_json::from_str(&local_var_content).ok(); + let local_var_error = ResponseContent { status: local_var_status, content: local_var_content, entity: local_var_entity }; + Err(Error::ResponseError(local_var_error)) + } +} + diff --git a/judge-control-app/backend-api-schema/src/lib.rs b/judge-control-app/backend-api-schema/src/lib.rs new file mode 100644 index 0000000..e152062 --- /dev/null +++ b/judge-control-app/backend-api-schema/src/lib.rs @@ -0,0 +1,11 @@ +#![allow(unused_imports)] +#![allow(clippy::too_many_arguments)] + +extern crate serde_repr; +extern crate serde; +extern crate serde_json; +extern crate url; +extern crate reqwest; + +pub mod apis; +pub mod models; diff --git a/judge-control-app/backend-api-schema/src/models/judge_status.rs b/judge-control-app/backend-api-schema/src/models/judge_status.rs new file mode 100644 index 0000000..af0f58a --- /dev/null +++ b/judge-control-app/backend-api-schema/src/models/judge_status.rs @@ -0,0 +1,57 @@ +/* + * Backend to Judge API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// JudgeStatus : ジャッジの結果 +/// ジャッジの結果 +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum JudgeStatus { + #[serde(rename = "AC")] + Ac, + #[serde(rename = "WA")] + Wa, + #[serde(rename = "TLE")] + Tle, + #[serde(rename = "MLE")] + Mle, + #[serde(rename = "OLE")] + Ole, + #[serde(rename = "RE")] + Re, + #[serde(rename = "CE")] + Ce, + #[serde(rename = "IE")] + Ie, + +} + +impl std::fmt::Display for JudgeStatus { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Ac => write!(f, "AC"), + Self::Wa => write!(f, "WA"), + Self::Tle => write!(f, "TLE"), + Self::Mle => write!(f, "MLE"), + Self::Ole => write!(f, "OLE"), + Self::Re => write!(f, "RE"), + Self::Ce => write!(f, "CE"), + Self::Ie => write!(f, "IE"), + } + } +} + +impl Default for JudgeStatus { + fn default() -> JudgeStatus { + Self::Ac + } +} + diff --git a/judge-control-app/backend-api-schema/src/models/mod.rs b/judge-control-app/backend-api-schema/src/models/mod.rs new file mode 100644 index 0000000..bb9bbbe --- /dev/null +++ b/judge-control-app/backend-api-schema/src/models/mod.rs @@ -0,0 +1,6 @@ +pub mod judge_status; +pub use self::judge_status::JudgeStatus; +pub mod submit_result; +pub use self::submit_result::SubmitResult; +pub mod test_result; +pub use self::test_result::TestResult; diff --git a/judge-control-app/backend-api-schema/src/models/submit_result.rs b/judge-control-app/backend-api-schema/src/models/submit_result.rs new file mode 100644 index 0000000..a2b0f12 --- /dev/null +++ b/judge-control-app/backend-api-schema/src/models/submit_result.rs @@ -0,0 +1,37 @@ +/* + * Backend to Judge API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// SubmitResult : テストケースのジャッジ結果 +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct SubmitResult { + /// ジャッジID + #[serde(rename = "judgeId")] + pub judge_id: uuid::Uuid, + /// テストケースのリスト + #[serde(rename = "testResults")] + pub test_results: Vec, + #[serde(rename = "totalResult")] + pub total_result: Box, +} + +impl SubmitResult { + /// テストケースのジャッジ結果 + pub fn new(judge_id: uuid::Uuid, test_results: Vec, total_result: models::TestResult) -> SubmitResult { + SubmitResult { + judge_id, + test_results, + total_result: Box::new(total_result), + } + } +} + diff --git a/judge-control-app/backend-api-schema/src/models/test_result.rs b/judge-control-app/backend-api-schema/src/models/test_result.rs new file mode 100644 index 0000000..941c9dc --- /dev/null +++ b/judge-control-app/backend-api-schema/src/models/test_result.rs @@ -0,0 +1,45 @@ +/* + * Backend to Judge API + * + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1 + * + * Generated by: https://openapi-generator.tech + */ + +use crate::models; +use serde::{Deserialize, Serialize}; + +/// TestResult : テストケースの結果 +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct TestResult { + #[serde(rename = "status")] + pub status: models::JudgeStatus, + /// 追加情報 + #[serde(rename = "text", skip_serializing_if = "Option::is_none")] + pub text: Option, + /// スコア + #[serde(rename = "score")] + pub score: f64, + /// 実行時間 + #[serde(rename = "execTime")] + pub exec_time: f64, + /// メモリ使用量(byte) + #[serde(rename = "memorySize")] + pub memory_size: f64, +} + +impl TestResult { + /// テストケースの結果 + pub fn new(status: models::JudgeStatus, score: f64, exec_time: f64, memory_size: f64) -> TestResult { + TestResult { + status, + text: None, + score, + exec_time, + memory_size, + } + } +} + diff --git a/judge-control-app/judge-api-server/.gitignore b/judge-control-app/judge-api-server/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/judge-control-app/judge-api-server/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/judge-control-app/judge-api-server/.openapi-generator-ignore b/judge-control-app/judge-api-server/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/judge-control-app/judge-api-server/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/judge-control-app/judge-api-server/.openapi-generator/FILES b/judge-control-app/judge-api-server/.openapi-generator/FILES new file mode 100644 index 0000000..8f29035 --- /dev/null +++ b/judge-control-app/judge-api-server/.openapi-generator/FILES @@ -0,0 +1,11 @@ +.gitignore +.openapi-generator-ignore +Cargo.toml +README.md +src/apis/judge.rs +src/apis/mod.rs +src/header.rs +src/lib.rs +src/models.rs +src/server/mod.rs +src/types.rs diff --git a/judge-control-app/judge-api-server/.openapi-generator/VERSION b/judge-control-app/judge-api-server/.openapi-generator/VERSION new file mode 100644 index 0000000..17f2442 --- /dev/null +++ b/judge-control-app/judge-api-server/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.9.0-SNAPSHOT diff --git a/judge-control-app/judge-api-server/Cargo.toml b/judge-control-app/judge-api-server/Cargo.toml new file mode 100644 index 0000000..87d40b5 --- /dev/null +++ b/judge-control-app/judge-api-server/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "judge_api_server" +version = "0.1.0" +authors = ["OpenAPI Generator team and contributors"] +description = "No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)" +license = "MIT" +edition = "2021" + +[features] +default = ["server"] +server = [] +conversion = [ + "frunk", + "frunk_derives", + "frunk_core", + "frunk-enum-core", + "frunk-enum-derive", +] + +[dependencies] +async-trait = "0.1" +axum = { version = "0.7" } +axum-extra = { version = "0.9", features = ["cookie", "multipart"] } +base64 = "0.22" +bytes = "1" +chrono = { version = "0.4", features = ["serde"] } +frunk = { version = "0.4", optional = true } +frunk-enum-core = { version = "0.3", optional = true } +frunk-enum-derive = { version = "0.3", optional = true } +frunk_core = { version = "0.4", optional = true } +frunk_derives = { version = "0.4", optional = true } +http = "1" +lazy_static = "1" +regex = "1" +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", features = ["raw_value"] } +serde_urlencoded = "0.7" +tokio = { version = "1", default-features = false, features = [ + "signal", + "rt-multi-thread", +] } +tracing = { version = "0.1", features = ["attributes"] } +uuid = { version = "1", features = ["serde"] } +validator = { version = "0.18", features = ["derive"] } + +[dev-dependencies] +tracing-subscriber = "0.3" diff --git a/judge-control-app/judge-api-server/README.md b/judge-control-app/judge-api-server/README.md new file mode 100644 index 0000000..d7cd7c9 --- /dev/null +++ b/judge-control-app/judge-api-server/README.md @@ -0,0 +1,92 @@ +# Rust API for judge_api_server + +No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + +## Overview + +This server was generated by the [openapi-generator] +(https://openapi-generator.tech) project. By using the +[OpenAPI-Spec](https://github.com/OAI/OpenAPI-Specification) from a remote +server, you can easily generate a server stub. + +To see how to make this your own, look here: [README]((https://openapi-generator.tech)) + +- API version: 0.1 +- Build date: 2024-10-04T08:51:19.896289611Z[Etc/UTC] +- Generator version: 7.9.0-SNAPSHOT + + + +This autogenerated project defines an API crate `judge_api_server` which contains: +* An `Api` trait defining the API in Rust. +* Data types representing the underlying data model. +* Axum router which accepts HTTP requests and invokes the appropriate `Api` method for each operation. + * Request validations (path, query, body params) are included. + +## Using the generated library + +The generated library has a few optional features that can be activated through Cargo. + +* `server` + * This defaults to enabled and creates the basic skeleton of a server implementation based on Axum. + * To create the server stack you'll need to provide an implementation of the API trait to provide the server function. +* `conversions` + * This defaults to disabled and creates extra derives on models to allow "transmogrification" between objects of structurally similar types. + +See https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section for how to use features in your `Cargo.toml`. + +### Example + +```rust +struct ServerImpl { + // database: sea_orm::DbConn, +} + +#[allow(unused_variables)] +#[async_trait] +impl judge_api_server::Api for ServerImpl { + // API implementation goes here +} + +pub async fn start_server(addr: &str) { + // initialize tracing + tracing_subscriber::fmt::init(); + + // Init Axum router + let app = judge_api_server::server::new(Arc::new(ServerImpl)); + + // Add layers to the router + let app = app.layer(...); + + // Run the server with graceful shutdown + let listener = TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} +``` diff --git a/judge-control-app/judge-api-server/judge-api.yaml b/judge-control-app/judge-api-server/judge-api.yaml new file mode 100644 index 0000000..0e39464 --- /dev/null +++ b/judge-control-app/judge-api-server/judge-api.yaml @@ -0,0 +1,142 @@ +openapi: '3.0.3' +info: + title: Judge to Backend API + version: '0.1' + license: + name: MIT + url: 'https://github.com/traP-jp/traO-Judge-docs/blob/main/LICENSE' +servers: + - url: https://api.server.test/v1 +paths: + '/judge': + post: + operationId: judge + description: 全てのジャッジ + tags: + - judge + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Judge' + required: true + summary: 全てのジャッジ + responses: + '200': + description: OK +components: + schemas: + Execution: + title: Execution + type: object + description: テストケース実行時の実行時間など + properties: + optionalInfo: + title: optionalInfo + type: object + description: 任意の情報 + properties: + execTime: + title: execTime + type: number + description: 実行時間 + example: 1.23 + memorySize: + title: memorySize + type: number + description: メモリ使用量(byte) + example: 123456789 + language: + title: language + type: string + description: 使用言語 + example: python + shellScriptId: + title: shellScriptId + type: string + format: uuid + description: シェルスクリプトのID + example: 68ffddc4-adc6-4f93-9ff2-d862ee6cce7a + directoryCount: + title: directoryCount + type: number + description: ディレクトリの数 + example: 3 + textResourceCount: + title: textResourceCount + type: number + description: テキストリソースの数 + example: 3 + oneTimeTextCount: + title: oneTimeTextCount + type: number + description: テキストの数 + example: 3 + required: + - shellScriptId + - directoryCount + - textResourceCount + - oneTimeTextCount + ExecutionConfigMap: + title: ExecutionConfigMap + type: object + description: ジャッジの設定 + properties: + textResourceIds: + title: textResourceIds + type: array + description: 静的なテキストリソースのID + items: + type: string + format: uuid + example: 68ffddc4-adc6-4f93-9ff2-d862ee6cce7a + oneTimeTextContents: + title: oneTimeTextContents + type: array + description: 動的に変化するテキストデータ + items: + type: string + example: | + user-1: 100 + user-2: 200 + Judge: + title: JudgeConfig + type: object + description: ジャッジの設定 + properties: + judgeId: + title: JudgeId + type: string + format: uuid + description: ジャッジのID + example: 68ffddc4-adc6-4f93-9ff2-d862ee6cce7a + testCount: + title: testCount + type: number + description: テストケースの数 + example: 3 + beforeTestExecs: + $ref: '#/components/schemas/Execution' + onTestExecs: + $ref: '#/components/schemas/Execution' + afterTestExecs: + $ref: '#/components/schemas/Execution' + beforeTestConfigMap: + $ref: '#/components/schemas/ExecutionConfigMap' + onTestConfigMaps: + title: onTestConfigMaps + type: array + description: テストケース実行時に実行されるコマンドの設定 + items: + $ref: '#/components/schemas/ExecutionConfigMap' + afterTestConfigMap: + $ref: '#/components/schemas/ExecutionConfigMap' + required: + - judgeId + - testCount + - beforeTestExecs + - onTestExecs + - afterTestExecs + - beforeTestConfigMap + - onTestConfigMaps + - afterTestConfigMap \ No newline at end of file diff --git a/judge-control-app/judge-api-server/src/apis/judge.rs b/judge-control-app/judge-api-server/src/apis/judge.rs new file mode 100644 index 0000000..a4d39f7 --- /dev/null +++ b/judge-control-app/judge-api-server/src/apis/judge.rs @@ -0,0 +1,33 @@ +use async_trait::async_trait; +use axum::extract::*; +use axum_extra::extract::{CookieJar, Multipart}; +use bytes::Bytes; +use http::Method; +use serde::{Deserialize, Serialize}; + +use crate::{models, types::*}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[must_use] +#[allow(clippy::large_enum_variant)] +pub enum JudgeResponse { + /// OK + Status200_OK +} + + +/// Judge +#[async_trait] +#[allow(clippy::ptr_arg)] +pub trait Judge { + /// 全てのジャッジ. + /// + /// Judge - POST /v1/judge + async fn judge( + &self, + method: Method, + host: Host, + cookies: CookieJar, + body: models::Judge, + ) -> Result; +} diff --git a/judge-control-app/judge-api-server/src/apis/mod.rs b/judge-control-app/judge-api-server/src/apis/mod.rs new file mode 100644 index 0000000..cd5a1b1 --- /dev/null +++ b/judge-control-app/judge-api-server/src/apis/mod.rs @@ -0,0 +1,2 @@ +pub mod judge; + diff --git a/judge-control-app/judge-api-server/src/header.rs b/judge-control-app/judge-api-server/src/header.rs new file mode 100644 index 0000000..5c714f8 --- /dev/null +++ b/judge-control-app/judge-api-server/src/header.rs @@ -0,0 +1,181 @@ +use std::{convert::TryFrom, fmt, ops::Deref}; + +use chrono::{DateTime, Utc}; +use http::HeaderValue; + +/// A struct to allow homogeneous conversion into a HeaderValue. We can't +/// implement the From/Into trait on HeaderValue because we don't own +/// either of the types. +#[derive(Debug, Clone)] +pub(crate) struct IntoHeaderValue(pub T); + +// Generic implementations + +impl Deref for IntoHeaderValue { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +// Derive for each TryFrom in http::HeaderValue + +macro_rules! ihv_generate { + ($t:ident) => { + impl TryFrom for IntoHeaderValue<$t> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match hdr_value.parse::<$t>() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value)), + Err(e) => Err(format!("Unable to parse {} as a string: {}", + stringify!($t), e)), + }, + Err(e) => Err(format!("Unable to parse header {:?} as a string - {}", + hdr_value, e)), + } + } + } + + impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue<$t>) -> Result { + Ok(hdr_value.0.into()) + } + } + }; +} + +ihv_generate!(u64); +ihv_generate!(i64); +ihv_generate!(i16); +ihv_generate!(u16); +ihv_generate!(u32); +ihv_generate!(usize); +ihv_generate!(isize); +ihv_generate!(i32); + +// Custom derivations + +// Vec + +impl TryFrom for IntoHeaderValue> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => Ok(IntoHeaderValue( + hdr_value + .split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y.to_string()), + }) + .collect())), + Err(e) => Err(format!("Unable to parse header: {:?} as a string - {}", + hdr_value, e)), + } + } +} + +impl TryFrom>> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue>) -> Result { + match HeaderValue::from_str(&hdr_value.0.join(", ")) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!("Unable to convert {:?} into a header - {}", + hdr_value, e)) + } + } +} + +// String + +impl TryFrom for IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value.to_string())), + Err(e) => Err(format!("Unable to convert header {:?} to {}", + hdr_value, e)), + } + } +} + +impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue) -> Result { + match HeaderValue::from_str(&hdr_value.0) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!("Unable to convert {:?} from a header {}", + hdr_value, e)) + } + } +} + +// Bool + +impl TryFrom for IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match hdr_value.parse() { + Ok(hdr_value) => Ok(IntoHeaderValue(hdr_value)), + Err(e) => Err(format!("Unable to parse bool from {} - {}", + hdr_value, e)), + }, + Err(e) => Err(format!("Unable to convert {:?} from a header {}", + hdr_value, e)), + } + } +} + +impl TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue) -> Result { + match HeaderValue::from_str(&hdr_value.0.to_string()) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!("Unable to convert: {:?} into a header: {}", + hdr_value, e)) + } + } +} + +// DateTime + +impl TryFrom for IntoHeaderValue> { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> Result { + match hdr_value.to_str() { + Ok(hdr_value) => match DateTime::parse_from_rfc3339(hdr_value) { + Ok(date) => Ok(IntoHeaderValue(date.with_timezone(&Utc))), + Err(e) => Err(format!("Unable to parse: {} as date - {}", + hdr_value, e)), + }, + Err(e) => Err(format!("Unable to convert header {:?} to string {}", + hdr_value, e)), + } + } +} + +impl TryFrom>> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: IntoHeaderValue>) -> Result { + match HeaderValue::from_str(hdr_value.0.to_rfc3339().as_str()) { + Ok(hdr_value) => Ok(hdr_value), + Err(e) => Err(format!("Unable to convert {:?} to a header: {}", + hdr_value, e)), + } + } +} + diff --git a/judge-control-app/judge-api-server/src/lib.rs b/judge-control-app/judge-api-server/src/lib.rs new file mode 100644 index 0000000..ce06698 --- /dev/null +++ b/judge-control-app/judge-api-server/src/lib.rs @@ -0,0 +1,28 @@ +#![allow( + missing_docs, + trivial_casts, + unused_variables, + unused_mut, + unused_extern_crates, + non_camel_case_types, + unused_imports, + unused_attributes, +)] +#![allow( + clippy::derive_partial_eq_without_eq, + clippy::disallowed_names, + clippy::too_many_arguments +)] + +pub const BASE_PATH: &str = "/v1"; +pub const API_VERSION: &str = "0.1"; + +#[cfg(feature = "server")] +pub mod server; + +pub mod models; +pub mod types; +pub mod apis; + +#[cfg(feature = "server")] +pub(crate) mod header; diff --git a/judge-control-app/judge-api-server/src/models.rs b/judge-control-app/judge-api-server/src/models.rs new file mode 100644 index 0000000..660040d --- /dev/null +++ b/judge-control-app/judge-api-server/src/models.rs @@ -0,0 +1,690 @@ +#![allow(unused_qualifications)] + +use http::HeaderValue; +use validator::Validate; + +#[cfg(feature = "server")] +use crate::header; +use crate::{models, types::*}; + + + + +/// テストケース実行時の実行時間など + + + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Execution { + #[serde(rename = "optionalInfo")] + #[serde(skip_serializing_if="Option::is_none")] + pub optional_info: Option, + +/// シェルスクリプトのID + #[serde(rename = "shellScriptId")] + pub shell_script_id: uuid::Uuid, + +/// ディレクトリの数 + #[serde(rename = "directoryCount")] + pub directory_count: f64, + +/// テキストリソースの数 + #[serde(rename = "textResourceCount")] + pub text_resource_count: f64, + +/// テキストの数 + #[serde(rename = "oneTimeTextCount")] + pub one_time_text_count: f64, + +} + + +impl Execution { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new(shell_script_id: uuid::Uuid, directory_count: f64, text_resource_count: f64, one_time_text_count: f64, ) -> Execution { + Execution { + optional_info: None, + shell_script_id, + directory_count, + text_resource_count, + one_time_text_count, + } + } +} + +/// Converts the Execution value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Execution { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + // Skipping optionalInfo in query parameter serialization + + // Skipping shellScriptId in query parameter serialization + + + Some("directoryCount".to_string()), + Some(self.directory_count.to_string()), + + + Some("textResourceCount".to_string()), + Some(self.text_resource_count.to_string()), + + + Some("oneTimeTextCount".to_string()), + Some(self.one_time_text_count.to_string()), + + ]; + + write!(f, "{}", params.into_iter().flatten().collect::>().join(",")) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Execution value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Execution { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub optional_info: Vec, + pub shell_script_id: Vec, + pub directory_count: Vec, + pub text_resource_count: Vec, + pub one_time_text_count: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => return std::result::Result::Err("Missing value while parsing Execution".to_string()) + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "optionalInfo" => intermediate_rep.optional_info.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "shellScriptId" => intermediate_rep.shell_script_id.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "directoryCount" => intermediate_rep.directory_count.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "textResourceCount" => intermediate_rep.text_resource_count.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "oneTimeTextCount" => intermediate_rep.one_time_text_count.push(::from_str(val).map_err(|x| x.to_string())?), + _ => return std::result::Result::Err("Unexpected key while parsing Execution".to_string()) + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Execution { + optional_info: intermediate_rep.optional_info.into_iter().next(), + shell_script_id: intermediate_rep.shell_script_id.into_iter().next().ok_or_else(|| "shellScriptId missing in Execution".to_string())?, + directory_count: intermediate_rep.directory_count.into_iter().next().ok_or_else(|| "directoryCount missing in Execution".to_string())?, + text_resource_count: intermediate_rep.text_resource_count.into_iter().next().ok_or_else(|| "textResourceCount missing in Execution".to_string())?, + one_time_text_count: intermediate_rep.one_time_text_count.into_iter().next().ok_or_else(|| "oneTimeTextCount missing in Execution".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: header::IntoHeaderValue) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err( + format!("Invalid header value for Execution - value: {} is invalid {}", + hdr_value, e)) + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => std::result::Result::Ok(header::IntoHeaderValue(value)), + std::result::Result::Err(err) => std::result::Result::Err( + format!("Unable to convert header value '{}' into Execution - {}", + value, err)) + } + }, + std::result::Result::Err(e) => std::result::Result::Err( + format!("Unable to convert header: {:?} to string: {}", + hdr_value, e)) + } + } +} + + + + +/// ジャッジの設定 + + + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct ExecutionConfigMap { +/// 静的なテキストリソースのID + #[serde(rename = "textResourceIds")] + #[serde(skip_serializing_if="Option::is_none")] + pub text_resource_ids: Option>, + +/// 動的に変化するテキストデータ + #[serde(rename = "oneTimeTextContents")] + #[serde(skip_serializing_if="Option::is_none")] + pub one_time_text_contents: Option>, + +} + + +impl ExecutionConfigMap { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> ExecutionConfigMap { + ExecutionConfigMap { + text_resource_ids: None, + one_time_text_contents: None, + } + } +} + +/// Converts the ExecutionConfigMap value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for ExecutionConfigMap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + // Skipping textResourceIds in query parameter serialization + + + self.one_time_text_contents.as_ref().map(|one_time_text_contents| { + [ + "oneTimeTextContents".to_string(), + one_time_text_contents.iter().map(|x| x.to_string()).collect::>().join(","), + ].join(",") + }), + + ]; + + write!(f, "{}", params.into_iter().flatten().collect::>().join(",")) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a ExecutionConfigMap value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for ExecutionConfigMap { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub text_resource_ids: Vec>, + pub one_time_text_contents: Vec>, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => return std::result::Result::Err("Missing value while parsing ExecutionConfigMap".to_string()) + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + "textResourceIds" => return std::result::Result::Err("Parsing a container in this style is not supported in ExecutionConfigMap".to_string()), + "oneTimeTextContents" => return std::result::Result::Err("Parsing a container in this style is not supported in ExecutionConfigMap".to_string()), + _ => return std::result::Result::Err("Unexpected key while parsing ExecutionConfigMap".to_string()) + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(ExecutionConfigMap { + text_resource_ids: intermediate_rep.text_resource_ids.into_iter().next(), + one_time_text_contents: intermediate_rep.one_time_text_contents.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: header::IntoHeaderValue) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err( + format!("Invalid header value for ExecutionConfigMap - value: {} is invalid {}", + hdr_value, e)) + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => std::result::Result::Ok(header::IntoHeaderValue(value)), + std::result::Result::Err(err) => std::result::Result::Err( + format!("Unable to convert header value '{}' into ExecutionConfigMap - {}", + value, err)) + } + }, + std::result::Result::Err(e) => std::result::Result::Err( + format!("Unable to convert header: {:?} to string: {}", + hdr_value, e)) + } + } +} + + + + +/// ジャッジの設定 + + + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct Judge { +/// ジャッジのID + #[serde(rename = "judgeId")] + pub judge_id: uuid::Uuid, + +/// テストケースの数 + #[serde(rename = "testCount")] + pub test_count: f64, + + #[serde(rename = "beforeTestExecs")] + pub before_test_execs: models::Execution, + + #[serde(rename = "onTestExecs")] + pub on_test_execs: models::Execution, + + #[serde(rename = "afterTestExecs")] + pub after_test_execs: models::Execution, + + #[serde(rename = "beforeTestConfigMap")] + pub before_test_config_map: models::ExecutionConfigMap, + +/// テストケース実行時に実行されるコマンドの設定 + #[serde(rename = "onTestConfigMaps")] + pub on_test_config_maps: Vec, + + #[serde(rename = "afterTestConfigMap")] + pub after_test_config_map: models::ExecutionConfigMap, + +} + + +impl Judge { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new(judge_id: uuid::Uuid, test_count: f64, before_test_execs: models::Execution, on_test_execs: models::Execution, after_test_execs: models::Execution, before_test_config_map: models::ExecutionConfigMap, on_test_config_maps: Vec, after_test_config_map: models::ExecutionConfigMap, ) -> Judge { + Judge { + judge_id, + test_count, + before_test_execs, + on_test_execs, + after_test_execs, + before_test_config_map, + on_test_config_maps, + after_test_config_map, + } + } +} + +/// Converts the Judge value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for Judge { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + // Skipping judgeId in query parameter serialization + + + Some("testCount".to_string()), + Some(self.test_count.to_string()), + + // Skipping beforeTestExecs in query parameter serialization + + // Skipping onTestExecs in query parameter serialization + + // Skipping afterTestExecs in query parameter serialization + + // Skipping beforeTestConfigMap in query parameter serialization + + // Skipping onTestConfigMaps in query parameter serialization + + // Skipping afterTestConfigMap in query parameter serialization + + ]; + + write!(f, "{}", params.into_iter().flatten().collect::>().join(",")) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a Judge value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for Judge { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub judge_id: Vec, + pub test_count: Vec, + pub before_test_execs: Vec, + pub on_test_execs: Vec, + pub after_test_execs: Vec, + pub before_test_config_map: Vec, + pub on_test_config_maps: Vec>, + pub after_test_config_map: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => return std::result::Result::Err("Missing value while parsing Judge".to_string()) + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "judgeId" => intermediate_rep.judge_id.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "testCount" => intermediate_rep.test_count.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "beforeTestExecs" => intermediate_rep.before_test_execs.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "onTestExecs" => intermediate_rep.on_test_execs.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "afterTestExecs" => intermediate_rep.after_test_execs.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "beforeTestConfigMap" => intermediate_rep.before_test_config_map.push(::from_str(val).map_err(|x| x.to_string())?), + "onTestConfigMaps" => return std::result::Result::Err("Parsing a container in this style is not supported in Judge".to_string()), + #[allow(clippy::redundant_clone)] + "afterTestConfigMap" => intermediate_rep.after_test_config_map.push(::from_str(val).map_err(|x| x.to_string())?), + _ => return std::result::Result::Err("Unexpected key while parsing Judge".to_string()) + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(Judge { + judge_id: intermediate_rep.judge_id.into_iter().next().ok_or_else(|| "judgeId missing in Judge".to_string())?, + test_count: intermediate_rep.test_count.into_iter().next().ok_or_else(|| "testCount missing in Judge".to_string())?, + before_test_execs: intermediate_rep.before_test_execs.into_iter().next().ok_or_else(|| "beforeTestExecs missing in Judge".to_string())?, + on_test_execs: intermediate_rep.on_test_execs.into_iter().next().ok_or_else(|| "onTestExecs missing in Judge".to_string())?, + after_test_execs: intermediate_rep.after_test_execs.into_iter().next().ok_or_else(|| "afterTestExecs missing in Judge".to_string())?, + before_test_config_map: intermediate_rep.before_test_config_map.into_iter().next().ok_or_else(|| "beforeTestConfigMap missing in Judge".to_string())?, + on_test_config_maps: intermediate_rep.on_test_config_maps.into_iter().next().ok_or_else(|| "onTestConfigMaps missing in Judge".to_string())?, + after_test_config_map: intermediate_rep.after_test_config_map.into_iter().next().ok_or_else(|| "afterTestConfigMap missing in Judge".to_string())?, + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: header::IntoHeaderValue) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err( + format!("Invalid header value for Judge - value: {} is invalid {}", + hdr_value, e)) + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => std::result::Result::Ok(header::IntoHeaderValue(value)), + std::result::Result::Err(err) => std::result::Result::Err( + format!("Unable to convert header value '{}' into Judge - {}", + value, err)) + } + }, + std::result::Result::Err(e) => std::result::Result::Err( + format!("Unable to convert header: {:?} to string: {}", + hdr_value, e)) + } + } +} + + + + +/// 任意の情報 + + + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, validator::Validate)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub struct OptionalInfo { +/// 実行時間 + #[serde(rename = "execTime")] + #[serde(skip_serializing_if="Option::is_none")] + pub exec_time: Option, + +/// メモリ使用量(byte) + #[serde(rename = "memorySize")] + #[serde(skip_serializing_if="Option::is_none")] + pub memory_size: Option, + +/// 使用言語 + #[serde(rename = "language")] + #[serde(skip_serializing_if="Option::is_none")] + pub language: Option, + +} + + +impl OptionalInfo { + #[allow(clippy::new_without_default, clippy::too_many_arguments)] + pub fn new() -> OptionalInfo { + OptionalInfo { + exec_time: None, + memory_size: None, + language: None, + } + } +} + +/// Converts the OptionalInfo value to the Query Parameters representation (style=form, explode=false) +/// specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde serializer +impl std::fmt::Display for OptionalInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params: Vec> = vec![ + + self.exec_time.as_ref().map(|exec_time| { + [ + "execTime".to_string(), + exec_time.to_string(), + ].join(",") + }), + + + self.memory_size.as_ref().map(|memory_size| { + [ + "memorySize".to_string(), + memory_size.to_string(), + ].join(",") + }), + + + self.language.as_ref().map(|language| { + [ + "language".to_string(), + language.to_string(), + ].join(",") + }), + + ]; + + write!(f, "{}", params.into_iter().flatten().collect::>().join(",")) + } +} + +/// Converts Query Parameters representation (style=form, explode=false) to a OptionalInfo value +/// as specified in https://swagger.io/docs/specification/serialization/ +/// Should be implemented in a serde deserializer +impl std::str::FromStr for OptionalInfo { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + /// An intermediate representation of the struct to use for parsing. + #[derive(Default)] + #[allow(dead_code)] + struct IntermediateRep { + pub exec_time: Vec, + pub memory_size: Vec, + pub language: Vec, + } + + let mut intermediate_rep = IntermediateRep::default(); + + // Parse into intermediate representation + let mut string_iter = s.split(','); + let mut key_result = string_iter.next(); + + while key_result.is_some() { + let val = match string_iter.next() { + Some(x) => x, + None => return std::result::Result::Err("Missing value while parsing OptionalInfo".to_string()) + }; + + if let Some(key) = key_result { + #[allow(clippy::match_single_binding)] + match key { + #[allow(clippy::redundant_clone)] + "execTime" => intermediate_rep.exec_time.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "memorySize" => intermediate_rep.memory_size.push(::from_str(val).map_err(|x| x.to_string())?), + #[allow(clippy::redundant_clone)] + "language" => intermediate_rep.language.push(::from_str(val).map_err(|x| x.to_string())?), + _ => return std::result::Result::Err("Unexpected key while parsing OptionalInfo".to_string()) + } + } + + // Get the next key + key_result = string_iter.next(); + } + + // Use the intermediate representation to return the struct + std::result::Result::Ok(OptionalInfo { + exec_time: intermediate_rep.exec_time.into_iter().next(), + memory_size: intermediate_rep.memory_size.into_iter().next(), + language: intermediate_rep.language.into_iter().next(), + }) + } +} + +// Methods for converting between header::IntoHeaderValue and HeaderValue + +#[cfg(feature = "server")] +impl std::convert::TryFrom> for HeaderValue { + type Error = String; + + fn try_from(hdr_value: header::IntoHeaderValue) -> std::result::Result { + let hdr_value = hdr_value.to_string(); + match HeaderValue::from_str(&hdr_value) { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(e) => std::result::Result::Err( + format!("Invalid header value for OptionalInfo - value: {} is invalid {}", + hdr_value, e)) + } + } +} + +#[cfg(feature = "server")] +impl std::convert::TryFrom for header::IntoHeaderValue { + type Error = String; + + fn try_from(hdr_value: HeaderValue) -> std::result::Result { + match hdr_value.to_str() { + std::result::Result::Ok(value) => { + match ::from_str(value) { + std::result::Result::Ok(value) => std::result::Result::Ok(header::IntoHeaderValue(value)), + std::result::Result::Err(err) => std::result::Result::Err( + format!("Unable to convert header value '{}' into OptionalInfo - {}", + value, err)) + } + }, + std::result::Result::Err(e) => std::result::Result::Err( + format!("Unable to convert header: {:?} to string: {}", + hdr_value, e)) + } + } +} + + + diff --git a/judge-control-app/judge-api-server/src/server/mod.rs b/judge-control-app/judge-api-server/src/server/mod.rs new file mode 100644 index 0000000..9d5286b --- /dev/null +++ b/judge-control-app/judge-api-server/src/server/mod.rs @@ -0,0 +1,108 @@ +use std::collections::HashMap; + +use axum::{body::Body, extract::*, response::Response, routing::*}; +use axum_extra::extract::{CookieJar, Multipart}; +use bytes::Bytes; +use http::{header::CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, Method, StatusCode}; +use tracing::error; +use validator::{Validate, ValidationErrors}; + +use crate::{header, types::*}; + +#[allow(unused_imports)] +use crate::{apis, models}; + + +/// Setup API Server. +pub fn new(api_impl: I) -> Router +where + I: AsRef + Clone + Send + Sync + 'static, + A: apis::judge::Judge + 'static, +{ + // build our application with a route + Router::new() + .route("/v1/judge", + post(judge::) + ) + .with_state(api_impl) +} + + #[derive(validator::Validate)] + #[allow(dead_code)] + struct JudgeBodyValidator<'a> { + #[validate(nested)] + body: &'a models::Judge, + } + + +#[tracing::instrument(skip_all)] +fn judge_validation( + body: models::Judge, +) -> std::result::Result<( + models::Judge, +), ValidationErrors> +{ + let b = JudgeBodyValidator { body: &body }; + b.validate()?; + +Ok(( + body, +)) +} +/// Judge - POST /v1/judge +#[tracing::instrument(skip_all)] +async fn judge( + method: Method, + host: Host, + cookies: CookieJar, + State(api_impl): State, + Json(body): Json, +) -> Result +where + I: AsRef + Send + Sync, + A: apis::judge::Judge, +{ + + #[allow(clippy::redundant_closure)] + let validation = tokio::task::spawn_blocking(move || + judge_validation( + body, + ) + ).await.unwrap(); + + let Ok(( + body, + )) = validation else { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(validation.unwrap_err().to_string())) + .map_err(|_| StatusCode::BAD_REQUEST); + }; + + let result = api_impl.as_ref().judge( + method, + host, + cookies, + body, + ).await; + + let mut response = Response::builder(); + + let resp = match result { + Ok(rsp) => match rsp { + apis::judge::JudgeResponse::Status200_OK + => { + let mut response = response.status(200); + response.body(Body::empty()) + }, + }, + Err(_) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + response.status(500).body(Body::empty()) + }, + }; + + resp.map_err(|e| { error!(error = ?e); StatusCode::INTERNAL_SERVER_ERROR }) +} + diff --git a/judge-control-app/judge-api-server/src/types.rs b/judge-control-app/judge-api-server/src/types.rs new file mode 100644 index 0000000..38cbd1c --- /dev/null +++ b/judge-control-app/judge-api-server/src/types.rs @@ -0,0 +1,665 @@ +use std::{mem, str::FromStr}; + +use base64::{engine::general_purpose, Engine}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[allow(dead_code)] +pub struct Object(serde_json::Value); + +impl validator::Validate for Object { + fn validate(&self) -> Result<(), validator::ValidationErrors> { + Ok(()) + } +} + +impl FromStr for Object { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(Self(serde_json::Value::String(s.to_owned()))) + } +} + +/// Serde helper function to create a default `Option>` while +/// deserializing +pub fn default_optional_nullable() -> Option> { + None +} + +/// Serde helper function to deserialize into an `Option>` +pub fn deserialize_optional_nullable<'de, D, T>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + Option::::deserialize(deserializer).map(|val| match val { + Some(inner) => Some(Nullable::Present(inner)), + None => Some(Nullable::Null), + }) +} + +/// The Nullable type. Represents a value which may be specified as null on an API. +/// Note that this is distinct from a value that is optional and not present! +/// +/// Nullable implements many of the same methods as the Option type (map, unwrap, etc). +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub enum Nullable { + /// Null value + Null, + /// Value is present + Present(T), +} + +impl Nullable { + ///////////////////////////////////////////////////////////////////////// + // Querying the contained values + ///////////////////////////////////////////////////////////////////////// + + /// Returns `true` if the Nullable is a `Present` value. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x: Nullable = Nullable::Present(2); + /// assert_eq!(x.is_present(), true); + /// + /// let x: Nullable = Nullable::Null; + /// assert_eq!(x.is_present(), false); + /// ``` + #[inline] + pub fn is_present(&self) -> bool { + match *self { + Nullable::Present(_) => true, + Nullable::Null => false, + } + } + + /// Returns `true` if the Nullable is a `Null` value. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x: Nullable = Nullable::Present(2); + /// assert_eq!(x.is_null(), false); + /// + /// let x: Nullable = Nullable::Null; + /// assert_eq!(x.is_null(), true); + /// ``` + #[inline] + pub fn is_null(&self) -> bool { + !self.is_present() + } + + ///////////////////////////////////////////////////////////////////////// + // Adapter for working with references + ///////////////////////////////////////////////////////////////////////// + + /// Converts from `Nullable` to `Nullable<&T>`. + /// + /// # Examples + /// + /// Convert an `Nullable<`[`String`]`>` into a `Nullable<`[`usize`]`>`, preserving the original. + /// The [`map`] method takes the `self` argument by value, consuming the original, + /// so this technique uses `as_ref` to first take a `Nullable` to a reference + /// to the value inside the original. + /// + /// [`map`]: enum.Nullable.html#method.map + /// [`String`]: ../../std/string/struct.String.html + /// [`usize`]: ../../std/primitive.usize.html + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let num_as_str: Nullable = Nullable::Present("10".to_string()); + /// // First, cast `Nullable` to `Nullable<&String>` with `as_ref`, + /// // then consume *that* with `map`, leaving `num_as_str` on the stack. + /// let num_as_int: Nullable = num_as_str.as_ref().map(|n| n.len()); + /// println!("still can print num_as_str: {:?}", num_as_str); + /// ``` + #[inline] + pub fn as_ref(&self) -> Nullable<&T> { + match *self { + Nullable::Present(ref x) => Nullable::Present(x), + Nullable::Null => Nullable::Null, + } + } + + /// Converts from `Nullable` to `Nullable<&mut T>`. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let mut x = Nullable::Present(2); + /// match x.as_mut() { + /// Nullable::Present(v) => *v = 42, + /// Nullable::Null => {}, + /// } + /// assert_eq!(x, Nullable::Present(42)); + /// ``` + #[inline] + pub fn as_mut(&mut self) -> Nullable<&mut T> { + match *self { + Nullable::Present(ref mut x) => Nullable::Present(x), + Nullable::Null => Nullable::Null, + } + } + + ///////////////////////////////////////////////////////////////////////// + // Getting to contained values + ///////////////////////////////////////////////////////////////////////// + + /// Unwraps a Nullable, yielding the content of a `Nullable::Present`. + /// + /// # Panics + /// + /// Panics if the value is a [`Nullable::Null`] with a custom panic message provided by + /// `msg`. + /// + /// [`Nullable::Null`]: #variant.Null + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x = Nullable::Present("value"); + /// assert_eq!(x.expect("the world is ending"), "value"); + /// ``` + /// + /// ```{.should_panic} + /// # use judge_api_server::types::Nullable; + /// + /// let x: Nullable<&str> = Nullable::Null; + /// x.expect("the world is ending"); // panics with `the world is ending` + /// ``` + #[inline] + pub fn expect(self, msg: &str) -> T { + match self { + Nullable::Present(val) => val, + Nullable::Null => expect_failed(msg), + } + } + + /// Moves the value `v` out of the `Nullable` if it is `Nullable::Present(v)`. + /// + /// In general, because this function may panic, its use is discouraged. + /// Instead, prefer to use pattern matching and handle the `Nullable::Null` + /// case explicitly. + /// + /// # Panics + /// + /// Panics if the self value equals [`Nullable::Null`]. + /// + /// [`Nullable::Null`]: #variant.Null + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x = Nullable::Present("air"); + /// assert_eq!(x.unwrap(), "air"); + /// ``` + /// + /// ```{.should_panic} + /// # use judge_api_server::types::Nullable; + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.unwrap(), "air"); // fails + /// ``` + #[inline] + pub fn unwrap(self) -> T { + match self { + Nullable::Present(val) => val, + Nullable::Null => panic!("called `Nullable::unwrap()` on a `Nullable::Null` value"), + } + } + + /// Returns the contained value or a default. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// assert_eq!(Nullable::Present("car").unwrap_or("bike"), "car"); + /// assert_eq!(Nullable::Null.unwrap_or("bike"), "bike"); + /// ``` + #[inline] + pub fn unwrap_or(self, def: T) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => def, + } + } + + /// Returns the contained value or computes it from a closure. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let k = 10; + /// assert_eq!(Nullable::Present(4).unwrap_or_else(|| 2 * k), 4); + /// assert_eq!(Nullable::Null.unwrap_or_else(|| 2 * k), 20); + /// ``` + #[inline] + pub fn unwrap_or_else T>(self, f: F) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => f(), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Transforming contained values + ///////////////////////////////////////////////////////////////////////// + + /// Maps a `Nullable` to `Nullable` by applying a function to a contained value. + /// + /// # Examples + /// + /// Convert a `Nullable<`[`String`]`>` into a `Nullable<`[`usize`]`>`, consuming the original: + /// + /// [`String`]: ../../std/string/struct.String.html + /// [`usize`]: ../../std/primitive.usize.html + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let maybe_some_string = Nullable::Present(String::from("Hello, World!")); + /// // `Nullable::map` takes self *by value*, consuming `maybe_some_string` + /// let maybe_some_len = maybe_some_string.map(|s| s.len()); + /// + /// assert_eq!(maybe_some_len, Nullable::Present(13)); + /// ``` + #[inline] + pub fn map U>(self, f: F) -> Nullable { + match self { + Nullable::Present(x) => Nullable::Present(f(x)), + Nullable::Null => Nullable::Null, + } + } + + /// Applies a function to the contained value (if any), + /// or returns a `default` (if not). + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.map_or(42, |v| v.len()), 3); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.map_or(42, |v| v.len()), 42); + /// ``` + #[inline] + pub fn map_or U>(self, default: U, f: F) -> U { + match self { + Nullable::Present(t) => f(t), + Nullable::Null => default, + } + } + + /// Applies a function to the contained value (if any), + /// or computes a `default` (if not). + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let k = 21; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.map_or_else(|| 2 * k, |v| v.len()), 3); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.map_or_else(|| 2 * k, |v| v.len()), 42); + /// ``` + #[inline] + pub fn map_or_else U, F: FnOnce(T) -> U>(self, default: D, f: F) -> U { + match self { + Nullable::Present(t) => f(t), + Nullable::Null => default(), + } + } + + /// Transforms the `Nullable` into a [`Result`], mapping `Nullable::Present(v)` to + /// [`Ok(v)`] and `Nullable::Null` to [`Err(err)`][Err]. + /// + /// [`Result`]: ../../std/result/enum.Result.html + /// [`Ok(v)`]: ../../std/result/enum.Result.html#variant.Ok + /// [Err]: ../../std/result/enum.Result.html#variant.Err + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.ok_or(0), Ok("foo")); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.ok_or(0), Err(0)); + /// ``` + #[inline] + pub fn ok_or(self, err: E) -> Result { + match self { + Nullable::Present(v) => Ok(v), + Nullable::Null => Err(err), + } + } + + /// Transforms the `Nullable` into a [`Result`], mapping `Nullable::Present(v)` to + /// [`Ok(v)`] and `Nullable::Null` to [`Err(err())`][Err]. + /// + /// [`Result`]: ../../std/result/enum.Result.html + /// [`Ok(v)`]: ../../std/result/enum.Result.html#variant.Ok + /// [Err]: ../../std/result/enum.Result.html#variant.Err + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x = Nullable::Present("foo"); + /// assert_eq!(x.ok_or_else(|| 0), Ok("foo")); + /// + /// let x: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.ok_or_else(|| 0), Err(0)); + /// ``` + #[inline] + pub fn ok_or_else E>(self, err: F) -> Result { + match self { + Nullable::Present(v) => Ok(v), + Nullable::Null => Err(err()), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Boolean operations on the values, eager and lazy + ///////////////////////////////////////////////////////////////////////// + + /// Returns `Nullable::Null` if the Nullable is `Nullable::Null`, otherwise returns `optb`. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x = Nullable::Present(2); + /// let y: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.and(y), Nullable::Null); + /// + /// let x: Nullable = Nullable::Null; + /// let y = Nullable::Present("foo"); + /// assert_eq!(x.and(y), Nullable::Null); + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Present("foo"); + /// assert_eq!(x.and(y), Nullable::Present("foo")); + /// + /// let x: Nullable = Nullable::Null; + /// let y: Nullable<&str> = Nullable::Null; + /// assert_eq!(x.and(y), Nullable::Null); + /// ``` + #[inline] + pub fn and(self, optb: Nullable) -> Nullable { + match self { + Nullable::Present(_) => optb, + Nullable::Null => Nullable::Null, + } + } + + /// Returns `Nullable::Null` if the Nullable is `Nullable::Null`, otherwise calls `f` with the + /// wrapped value and returns the result. + /// + /// Some languages call this operation flatmap. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// fn sq(x: u32) -> Nullable { Nullable::Present(x * x) } + /// fn nope(_: u32) -> Nullable { Nullable::Null } + /// + /// assert_eq!(Nullable::Present(2).and_then(sq).and_then(sq), Nullable::Present(16)); + /// assert_eq!(Nullable::Present(2).and_then(sq).and_then(nope), Nullable::Null); + /// assert_eq!(Nullable::Present(2).and_then(nope).and_then(sq), Nullable::Null); + /// assert_eq!(Nullable::Null.and_then(sq).and_then(sq), Nullable::Null); + /// ``` + #[inline] + pub fn and_then Nullable>(self, f: F) -> Nullable { + match self { + Nullable::Present(x) => f(x), + Nullable::Null => Nullable::Null, + } + } + + /// Returns the Nullable if it contains a value, otherwise returns `optb`. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Null; + /// assert_eq!(x.or(y), Nullable::Present(2)); + /// + /// let x = Nullable::Null; + /// let y = Nullable::Present(100); + /// assert_eq!(x.or(y), Nullable::Present(100)); + /// + /// let x = Nullable::Present(2); + /// let y = Nullable::Present(100); + /// assert_eq!(x.or(y), Nullable::Present(2)); + /// + /// let x: Nullable = Nullable::Null; + /// let y = Nullable::Null; + /// assert_eq!(x.or(y), Nullable::Null); + /// ``` + #[inline] + pub fn or(self, optb: Nullable) -> Nullable { + match self { + Nullable::Present(_) => self, + Nullable::Null => optb, + } + } + + /// Returns the Nullable if it contains a value, otherwise calls `f` and + /// returns the result. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// fn nobody() -> Nullable<&'static str> { Nullable::Null } + /// fn vikings() -> Nullable<&'static str> { Nullable::Present("vikings") } + /// + /// assert_eq!(Nullable::Present("barbarians").or_else(vikings), + /// Nullable::Present("barbarians")); + /// assert_eq!(Nullable::Null.or_else(vikings), Nullable::Present("vikings")); + /// assert_eq!(Nullable::Null.or_else(nobody), Nullable::Null); + /// ``` + #[inline] + pub fn or_else Nullable>(self, f: F) -> Nullable { + match self { + Nullable::Present(_) => self, + Nullable::Null => f(), + } + } + + ///////////////////////////////////////////////////////////////////////// + // Misc + ///////////////////////////////////////////////////////////////////////// + + /// Takes the value out of the Nullable, leaving a `Nullable::Null` in its place. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let mut x = Nullable::Present(2); + /// x.take(); + /// assert_eq!(x, Nullable::Null); + /// + /// let mut x: Nullable = Nullable::Null; + /// x.take(); + /// assert_eq!(x, Nullable::Null); + /// ``` + #[inline] + pub fn take(&mut self) -> Nullable { + mem::replace(self, Nullable::Null) + } +} + +impl<'a, T: Clone> Nullable<&'a T> { + /// Maps an `Nullable<&T>` to an `Nullable` by cloning the contents of the + /// Nullable. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x = 12; + /// let opt_x = Nullable::Present(&x); + /// assert_eq!(opt_x, Nullable::Present(&12)); + /// let cloned = opt_x.cloned(); + /// assert_eq!(cloned, Nullable::Present(12)); + /// ``` + pub fn cloned(self) -> Nullable { + self.map(Clone::clone) + } +} + +impl Nullable { + /// Returns the contained value or a default + /// + /// Consumes the `self` argument then, if `Nullable::Present`, returns the contained + /// value, otherwise if `Nullable::Null`, returns the default value for that + /// type. + /// + /// # Examples + /// + /// ``` + /// # use judge_api_server::types::Nullable; + /// + /// let x = Nullable::Present(42); + /// assert_eq!(42, x.unwrap_or_default()); + /// + /// let y: Nullable = Nullable::Null; + /// assert_eq!(0, y.unwrap_or_default()); + /// ``` + #[inline] + pub fn unwrap_or_default(self) -> T { + match self { + Nullable::Present(x) => x, + Nullable::Null => Default::default(), + } + } +} + +impl Default for Nullable { + /// Returns None. + #[inline] + fn default() -> Nullable { + Nullable::Null + } +} + +impl From for Nullable { + fn from(val: T) -> Nullable { + Nullable::Present(val) + } +} + +impl Serialize for Nullable +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + Nullable::Present(ref inner) => serializer.serialize_some(&inner), + Nullable::Null => serializer.serialize_none(), + } + } +} + +impl<'de, T> Deserialize<'de> for Nullable +where + T: serde::de::DeserializeOwned, +{ + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + // In order to deserialize a required, but nullable, value, we first have to check whether + // the value is present at all. To do this, we deserialize to a serde_json::Value, which + // fails if the value is missing, or gives serde_json::Value::Null if the value is present. + // If that succeeds as null, we can easily return a Null. + // If that succeeds as some value, we deserialize that value and return a Present. + // If that errors, we return the error. + let presence: Result<::serde_json::Value, _> = + serde::Deserialize::deserialize(deserializer); + match presence { + Ok(serde_json::Value::Null) => Ok(Nullable::Null), + Ok(some_value) => serde_json::from_value(some_value) + .map(Nullable::Present) + .map_err(serde::de::Error::custom), + Err(x) => Err(x), + } + } +} + +#[inline(never)] +#[cold] +fn expect_failed(msg: &str) -> ! { + panic!("{}", msg) +} + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +/// Base64-encoded byte array +pub struct ByteArray(pub Vec); + +impl Serialize for ByteArray { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&general_purpose::STANDARD.encode(&self.0)) + } +} + +impl<'de> Deserialize<'de> for ByteArray { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match general_purpose::STANDARD.decode(s) { + Ok(bin) => Ok(ByteArray(bin)), + _ => Err(serde::de::Error::custom("invalid base64")), + } + } +}