aboutsummaryrefslogtreecommitdiffhomepage
path: root/services/app/src/Models
diff options
context:
space:
mode:
Diffstat (limited to 'services/app/src/Models')
-rw-r--r--services/app/src/Models/AggregatedExecutionStatus.php54
-rw-r--r--services/app/src/Models/Answer.php69
-rw-r--r--services/app/src/Models/ExecutionStatus.php83
-rw-r--r--services/app/src/Models/Quiz.php133
-rw-r--r--services/app/src/Models/Testcase.php29
-rw-r--r--services/app/src/Models/TestcaseExecution.php32
-rw-r--r--services/app/src/Models/User.php26
7 files changed, 426 insertions, 0 deletions
diff --git a/services/app/src/Models/AggregatedExecutionStatus.php b/services/app/src/Models/AggregatedExecutionStatus.php
new file mode 100644
index 0000000..82b19e4
--- /dev/null
+++ b/services/app/src/Models/AggregatedExecutionStatus.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Albatross\Models;
+
+enum AggregatedExecutionStatus: string
+{
+ case UpdateNeeded = 'UpdateNeeded';
+ case Pending = 'Pending';
+ case Failed = 'Failed';
+ case OK = 'OK';
+
+ public function label(): string
+ {
+ return match ($this) {
+ self::UpdateNeeded => '実行待機中',
+ self::Pending => '実行待機中',
+ self::Failed => '失敗',
+ self::OK => 'OK',
+ };
+ }
+
+ public function showLoadingIndicator(): bool
+ {
+ return match ($this) {
+ self::UpdateNeeded => true,
+ self::Pending => true,
+ self::Failed => false,
+ self::OK => false,
+ };
+ }
+
+ public function toInt(): int
+ {
+ return match ($this) {
+ self::UpdateNeeded => 0,
+ self::Pending => 1,
+ self::Failed => 2,
+ self::OK => 3,
+ };
+ }
+
+ public static function fromInt(int $n): self
+ {
+ // @phpstan-ignore-next-line
+ return match ($n) {
+ 0 => self::UpdateNeeded,
+ 1 => self::Pending,
+ 2 => self::Failed,
+ 3 => self::OK,
+ };
+ }
+}
diff --git a/services/app/src/Models/Answer.php b/services/app/src/Models/Answer.php
new file mode 100644
index 0000000..6cb8006
--- /dev/null
+++ b/services/app/src/Models/Answer.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Albatross\Models;
+
+use DateTimeImmutable;
+use Nsfisis\Albatross\Exceptions\EntityValidationException;
+
+final class Answer
+{
+ public function __construct(
+ public readonly int $answer_id,
+ public readonly int $quiz_id,
+ public readonly int $answer_number,
+ public readonly DateTimeImmutable $submitted_at,
+ public readonly int $author_id,
+ public readonly string $code,
+ public readonly int $code_size,
+ public readonly AggregatedExecutionStatus $execution_status,
+ public readonly ?string $author_name, // joined
+ public readonly ?bool $author_is_admin, // joined
+ ) {
+ }
+
+ public static function create(
+ int $quiz_id,
+ int $author_id,
+ string $code,
+ ): self {
+ self::validate($quiz_id, $author_id, $code);
+ $answer = new self(
+ answer_id: 0,
+ quiz_id: $quiz_id,
+ answer_number: 0,
+ submitted_at: new DateTimeImmutable(), // dummy
+ author_id: $author_id,
+ code: $code,
+ code_size: strlen(self::normalizeCode($code)), // not mb_strlen
+ execution_status: AggregatedExecutionStatus::Pending,
+ author_name: null,
+ author_is_admin: null,
+ );
+ return $answer;
+ }
+
+ private static function validate(
+ int $quiz_id,
+ int $author_id,
+ string $code,
+ ): void {
+ $errors = [];
+ if (strlen($code) <= 0) {
+ $errors['code'] = 'コードを入力してください';
+ }
+ if (10 * 1024 <= strlen($code)) {
+ $errors['code'] = 'コードが長すぎます。10 KiB 未満まで縮めてください';
+ }
+
+ if (0 < count($errors)) {
+ throw new EntityValidationException(errors: $errors);
+ }
+ }
+
+ public static function normalizeCode(string $code): string
+ {
+ return preg_replace('/^\s*<\?(?:php\b)?\s*/', '', str_replace(["\r\n", "\r"], "\n", $code)) ?? $code;
+ }
+}
diff --git a/services/app/src/Models/ExecutionStatus.php b/services/app/src/Models/ExecutionStatus.php
new file mode 100644
index 0000000..5ff1c9d
--- /dev/null
+++ b/services/app/src/Models/ExecutionStatus.php
@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Albatross\Models;
+
+enum ExecutionStatus: string
+{
+ case Pending = 'Pending';
+ case Running = 'Running';
+ case IE = 'IE';
+ case RE = 'RE';
+ case WA = 'WA';
+ case TLE = 'TLE';
+ case AC = 'AC';
+
+ public function label(): string
+ {
+ return match ($this) {
+ self::Pending => '実行待機中',
+ self::Running => '実行中',
+ self::IE => '内部エラー',
+ self::RE => '実行時エラー',
+ self::WA => '不正解',
+ self::TLE => '時間制限超過',
+ self::AC => 'OK',
+ };
+ }
+
+ public function showLoadingIndicator(): bool
+ {
+ return match ($this) {
+ self::Pending => true,
+ self::Running => true,
+ self::IE => false,
+ self::RE => false,
+ self::WA => false,
+ self::TLE => false,
+ self::AC => false,
+ };
+ }
+
+ public function toInt(): int
+ {
+ return match ($this) {
+ self::Pending => 0,
+ self::Running => 1,
+ self::IE => 2,
+ self::RE => 3,
+ self::WA => 4,
+ self::TLE => 5,
+ self::AC => 6,
+ };
+ }
+
+ public static function fromInt(int $n): self
+ {
+ // @phpstan-ignore-next-line
+ return match ($n) {
+ 0 => self::Pending,
+ 1 => self::Running,
+ 2 => self::IE,
+ 3 => self::RE,
+ 4 => self::WA,
+ 5 => self::TLE,
+ 6 => self::AC,
+ };
+ }
+
+ public static function fromString(string $s): self
+ {
+ // @phpstan-ignore-next-line
+ return match ($s) {
+ 'Pending' => self::Pending,
+ 'Running' => self::Running,
+ 'IE' => self::IE,
+ 'RE' => self::RE,
+ 'WA' => self::WA,
+ 'TLE' => self::TLE,
+ 'AC' => self::AC,
+ };
+ }
+}
diff --git a/services/app/src/Models/Quiz.php b/services/app/src/Models/Quiz.php
new file mode 100644
index 0000000..5546646
--- /dev/null
+++ b/services/app/src/Models/Quiz.php
@@ -0,0 +1,133 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Albatross\Models;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use Nsfisis\Albatross\Exceptions\EntityValidationException;
+
+final class Quiz
+{
+ public function __construct(
+ public readonly int $quiz_id,
+ public readonly DateTimeImmutable $created_at,
+ public readonly DateTimeImmutable $started_at,
+ public readonly DateTimeImmutable $ranking_hidden_at,
+ public readonly DateTimeImmutable $finished_at,
+ public readonly string $title,
+ public readonly string $slug,
+ public readonly string $description,
+ public readonly string $example_code,
+ public readonly ?int $birdie_code_size,
+ ) {
+ }
+
+ public function isStarted(?DateTimeImmutable $now = null): bool
+ {
+ if ($now === null) {
+ $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
+ }
+ return $this->started_at <= $now;
+ }
+
+ public function isFinished(?DateTimeImmutable $now = null): bool
+ {
+ if ($now === null) {
+ $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
+ }
+ return $this->finished_at <= $now;
+ }
+
+ public function isOpenToAnswer(?DateTimeImmutable $now = null): bool
+ {
+ return $this->isStarted($now) && !$this->isFinished($now);
+ }
+
+ public function isClosedToAnswer(?DateTimeImmutable $now = null): bool
+ {
+ return !$this->isOpenToAnswer($now);
+ }
+
+ public function isRankingHidden(?DateTimeImmutable $now = null): bool
+ {
+ if ($now === null) {
+ $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
+ }
+ return $this->ranking_hidden_at <= $now && !$this->isFinished($now);
+ }
+
+ public static function create(
+ string $title,
+ string $slug,
+ string $description,
+ string $example_code,
+ ?int $birdie_code_size,
+ DateTimeImmutable $started_at,
+ DateTimeImmutable $ranking_hidden_at,
+ DateTimeImmutable $finished_at,
+ ): self {
+ self::validate(
+ $title,
+ $slug,
+ $description,
+ $example_code,
+ $birdie_code_size,
+ $started_at,
+ $ranking_hidden_at,
+ $finished_at,
+ );
+ $quiz = new self(
+ quiz_id: 0,
+ created_at: new DateTimeImmutable(), // dummy
+ started_at: $started_at,
+ ranking_hidden_at: $ranking_hidden_at,
+ finished_at: $finished_at,
+ title: $title,
+ slug: $slug,
+ description: $description,
+ example_code: $example_code,
+ birdie_code_size: $birdie_code_size,
+ );
+ return $quiz;
+ }
+
+ public static function validate(
+ string $title,
+ string $slug,
+ string $description,
+ string $example_code,
+ ?int $birdie_code_size,
+ DateTimeImmutable $started_at,
+ DateTimeImmutable $ranking_hidden_at,
+ DateTimeImmutable $finished_at,
+ ): void {
+ $errors = [];
+ if (strlen($slug) < 1) {
+ $errors['slug'] = 'スラグは必須です';
+ }
+ if (32 < strlen($slug)) {
+ $errors['slug'] = 'スラグは32文字以下である必要があります';
+ }
+ if (strlen($description) < 1) {
+ $errors['description'] = '説明は必須です';
+ }
+ if (strlen($example_code) < 1) {
+ $errors['example_code'] = '実装例は必須です';
+ }
+ if ($birdie_code_size !== null && $birdie_code_size < 1) {
+ $errors['birdie_code_size'] = 'バーディになるコードサイズは 1 byte 以上である必要があります';
+ }
+ if ($ranking_hidden_at < $started_at) {
+ $errors['ranking_hidden_at'] = 'ランキングが非表示になる日時は開始日時より後である必要があります';
+ }
+ if ($finished_at < $ranking_hidden_at) {
+ $errors['finished_at'] = '終了日時はランキングが非表示になる日時より後である必要があります';
+ }
+
+ if (0 < count($errors)) {
+ throw new EntityValidationException(errors: $errors);
+ }
+ }
+}
diff --git a/services/app/src/Models/Testcase.php b/services/app/src/Models/Testcase.php
new file mode 100644
index 0000000..68ee891
--- /dev/null
+++ b/services/app/src/Models/Testcase.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Albatross\Models;
+
+final class Testcase
+{
+ public function __construct(
+ public readonly int $testcase_id,
+ public readonly int $quiz_id,
+ public readonly string $input,
+ public readonly string $expected_result,
+ ) {
+ }
+
+ public static function create(
+ int $quiz_id,
+ string $input,
+ string $expected_result,
+ ): self {
+ return new self(
+ testcase_id: 0,
+ quiz_id: $quiz_id,
+ input: $input,
+ expected_result: $expected_result,
+ );
+ }
+}
diff --git a/services/app/src/Models/TestcaseExecution.php b/services/app/src/Models/TestcaseExecution.php
new file mode 100644
index 0000000..63d2d6a
--- /dev/null
+++ b/services/app/src/Models/TestcaseExecution.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Albatross\Models;
+
+final class TestcaseExecution
+{
+ public function __construct(
+ public readonly int $testcase_execution_id,
+ public readonly int $testcase_id,
+ public readonly int $answer_id,
+ public readonly ExecutionStatus $status,
+ public readonly ?string $stdout,
+ public readonly ?string $stderr,
+ ) {
+ }
+
+ public static function create(
+ int $testcase_id,
+ int $answer_id,
+ ): self {
+ return new self(
+ testcase_execution_id: 0,
+ testcase_id: $testcase_id,
+ answer_id: $answer_id,
+ status: ExecutionStatus::Pending,
+ stdout: null,
+ stderr: null,
+ );
+ }
+}
diff --git a/services/app/src/Models/User.php b/services/app/src/Models/User.php
new file mode 100644
index 0000000..c80e661
--- /dev/null
+++ b/services/app/src/Models/User.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Albatross\Models;
+
+final class User
+{
+ public function __construct(
+ public readonly int $user_id,
+ public readonly string $username,
+ public readonly bool $is_admin,
+ ) {
+ }
+
+ public static function create(
+ string $username,
+ bool $is_admin,
+ ): self {
+ return new self(
+ user_id: 0,
+ username: $username,
+ is_admin: $is_admin,
+ );
+ }
+}