From a0e877f86107efec43a885f48ce2fe5b94b21381 Mon Sep 17 00:00:00 2001 From: jackjohn7 Date: Sun, 31 Aug 2025 17:41:08 -0500 Subject: [PATCH 1/2] WIP: fixed one route, don't ignore openapi.yaml, rename some stuff --- .gitignore | 1 - Cargo.toml | 2 + basalt-server-lib/src/services/testing.rs | 7 +- basalt-server/Cargo.toml | 1 + openapi.yaml | 686 ++++++++++++++++++++++ 5 files changed, 693 insertions(+), 4 deletions(-) create mode 100644 openapi.yaml diff --git a/.gitignore b/.gitignore index 8108eca..652cc9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ target/ redoc-static.html doc/ -openapi.yaml diff --git a/Cargo.toml b/Cargo.toml index ad52ea8..c0b7b81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ resolver = "2" edition = "2021" version = "0.1.0" rust-version = "1.81" +description = "Basalt competition server runtime" +license = "GPL-3.0-only" [workspace.dependencies] anyhow = "1.0.95" diff --git a/basalt-server-lib/src/services/testing.rs b/basalt-server-lib/src/services/testing.rs index 234bcfd..0ee760b 100644 --- a/basalt-server-lib/src/services/testing.rs +++ b/basalt-server-lib/src/services/testing.rs @@ -125,7 +125,8 @@ pub async fn get_submissions_state( } #[derive(Deserialize, IntoParams)] -pub struct SubmissionsParams { +#[into_params(parameter_in = Query)] +pub struct SubmissionsQueryParams { user_id: Option, question_index: usize, } @@ -133,7 +134,7 @@ pub struct SubmissionsParams { #[axum::debug_handler] #[utoipa::path( get, path = "/submissions", tag = "testing", - params(SubmissionsParams), + params(SubmissionsQueryParams), responses( (status = OK, body = Vec, content_type = "application/json"), (status = 403, description = "User does not have permission to view the submissions for this user"), @@ -141,7 +142,7 @@ pub struct SubmissionsParams { )] pub async fn get_submissions( user: User, - params: Query, + params: Query, State(state): State>, ) -> Result>, StatusCode> { let user_id = params.user_id.as_ref().unwrap_or(&user.id); diff --git a/basalt-server/Cargo.toml b/basalt-server/Cargo.toml index 23c75e1..667112d 100644 --- a/basalt-server/Cargo.toml +++ b/basalt-server/Cargo.toml @@ -3,6 +3,7 @@ name = "basalt-server" edition.workspace = true version.workspace = true rust-version.workspace = true +description = "Basalt server competition runtime" [features] doc-gen = [] diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..8463ebe --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,686 @@ +openapi: 3.1.0 +info: + title: basalt-server + description: Basalt server competition runtime + license: + name: '' + version: 0.1.0 +paths: + /announcements: + get: + tags: + - announcements + operationId: get_all + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Announcement' + post: + tags: + - announcements + operationId: new + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewAnnouncement' + required: true + responses: + '201': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Announcement' + '401': + description: User may not create announcements + /announcements/{id}: + delete: + tags: + - announcements + operationId: delete + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Announcement' + '401': + description: User may not delete announcements + '404': + description: Announcement with provided id does not exists + /auth/login: + post: + tags: + - auth + operationId: login + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + required: true + responses: + '200': + description: Session cookie has been set + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + '401': + description: Incorrect credentials provided + /auth/logout: + post: + tags: + - auth + operationId: logout + responses: + '200': + description: User has been logged out + '401': + description: User was not logged in + /auth/me: + get: + tags: + - auth + description: Get information about the current user + operationId: me + responses: + '200': + description: User is signed in + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '401': + description: Auth token is expired + /clock: + get: + tags: + - clock + operationId: get_clock + responses: + '200': + description: Information about the clock + content: + application/json: + schema: + $ref: '#/components/schemas/ClockStatusResponse' + patch: + tags: + - clock + operationId: patch_clock + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateClockRequest' + required: true + responses: + '200': + description: Game is paused + content: + application/json: + schema: + $ref: '#/components/schemas/ClockStatusResponse' + '401': + description: Insufficient permissions + /competition/packet: + get: + tags: + - competition + operationId: download_packet + responses: + '200': + description: '' + content: + application/pdf: + schema: + type: array + items: + type: integer + format: int32 + minimum: 0 + /leaderboard: + get: + tags: + - leaderboard + description: Gets all team's submission states and total number of points + operationId: get_leaderboard_info + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TeamProgression' + '403': + description: User does not have permission to view the leaderboard + /questions: + get: + tags: + - questions + operationId: get_all + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/QuestionResponse' + /questions/{id}: + get: + tags: + - questions + operationId: get_specific_question + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/QuestionResponse' + '404': + description: Question Not Found + /teams: + get: + tags: + - teams + operationId: get_teams + responses: + '200': + description: Information about teams + content: + application/json: + schema: + $ref: '#/components/schemas/TeamsListResponse' + '500': + description: '' + post: + tags: + - teams + operationId: add_team + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OneOrMany_NewTeam' + required: true + responses: + '200': + description: Team(s) were created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/OneOrMany_User' + '409': + description: Team(s) with returned usernames already exist + content: + application/json: + schema: + type: array + items: + type: string + '500': + description: '' + /teams/{id}: + patch: + tags: + - teams + operationId: patch_team + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchTeam' + required: true + responses: + '200': + description: Team was succesfully updated + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: User with ID not found + '409': + description: Team with provided username already exists + '500': + description: '' + /testing/state: + get: + tags: + - testing + description: Get the current state of the current user's submissions + operationId: get_submissions_state + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/QuestionSubmissionState' + '403': + description: User does not have permission to view the submissions for this user + /testing/submissions: + get: + tags: + - testing + operationId: get_submissions + parameters: + - name: user_id + in: query + required: false + schema: + $ref: '#/components/schemas/UserId' + - name: question_index + in: query + required: true + schema: + type: integer + minimum: 0 + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SubmissionHistory' + '403': + description: User does not have permission to view the submissions for this user + /ws: + get: + tags: + - ws + operationId: connect_websocket + responses: + '200': + description: connected to websocket +components: + schemas: + Announcement: + type: object + required: + - id + - sender + - time + - message + properties: + id: + $ref: '#/components/schemas/AnnouncementId' + message: + type: string + sender: + $ref: '#/components/schemas/UserId' + time: + type: string + format: date + AnnouncementId: + type: string + ClockStatusResponse: + type: object + required: + - is_paused + - time_left_in_seconds + properties: + is_paused: + type: boolean + time_left_in_seconds: + type: integer + format: int64 + minimum: 0 + DisplayNamePatch: + oneOf: + - type: string + enum: + - remove + - type: object + required: + - set + properties: + set: + type: string + LanguageSyntax: + type: object + required: + - name + - syntax + properties: + name: + type: string + syntax: + type: string + LoginRequest: + type: object + required: + - username + - password + properties: + password: + type: string + username: + type: string + LoginResponse: + type: object + required: + - token + - role + properties: + role: + $ref: '#/components/schemas/Role' + token: + $ref: '#/components/schemas/SessionId' + NewAnnouncement: + type: object + required: + - message + properties: + message: + type: string + NewTeam: + type: object + required: + - username + - password + properties: + displayName: + type: + - string + - 'null' + password: + type: string + username: + type: string + OneOrMany_NewTeam: + oneOf: + - type: object + required: + - username + - password + properties: + displayName: + type: + - string + - 'null' + password: + type: string + username: + type: string + - type: array + items: + type: object + required: + - username + - password + properties: + displayName: + type: + - string + - 'null' + password: + type: string + username: + type: string + OneOrMany_User: + oneOf: + - type: object + required: + - id + - username + - role + properties: + displayName: + type: + - string + - 'null' + id: + $ref: '#/components/schemas/UserId' + role: + $ref: '#/components/schemas/Role' + username: + type: string + - type: array + items: + type: object + required: + - id + - username + - role + properties: + displayName: + type: + - string + - 'null' + id: + $ref: '#/components/schemas/UserId' + role: + $ref: '#/components/schemas/Role' + username: + type: string + PatchTeam: + type: object + properties: + displayName: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/DisplayNamePatch' + password: + type: + - string + - 'null' + username: + type: + - string + - 'null' + QuestionResponse: + type: object + required: + - languages + - title + - tests + properties: + description: + type: + - string + - 'null' + languages: + type: array + items: + $ref: '#/components/schemas/LanguageSyntax' + points: + type: + - integer + - 'null' + format: int32 + tests: + type: array + items: + $ref: '#/components/schemas/TestResponse' + title: + type: string + QuestionState: + type: string + enum: + - pass + - fail + - in-progress + - not-attempted + QuestionSubmissionState: + type: object + required: + - state + properties: + remainingAttempts: + type: + - integer + - 'null' + format: int32 + minimum: 0 + state: + $ref: '#/components/schemas/QuestionState' + Role: + type: string + enum: + - competitor + - host + SessionId: + type: string + SubmissionHistory: + type: object + required: + - id + - submitter + - time + - compile_fail + - code + - question_index + - score + - success + - language + properties: + code: + type: string + compile_fail: + type: boolean + id: + $ref: '#/components/schemas/SubmissionId' + language: + type: string + question_index: + type: integer + format: int64 + score: + type: number + format: double + submitter: + $ref: '#/components/schemas/UserId' + success: + type: boolean + time: + type: string + format: date + SubmissionId: + type: string + TeamFull: + allOf: + - $ref: '#/components/schemas/TeamInfo' + description: Contains full information about team + - type: object + required: + - id + properties: + id: + $ref: '#/components/schemas/UserId' + description: Username of team/player + TeamInfo: + type: object + required: + - checkedIn + - disconnected + properties: + checkedIn: + type: boolean + description: Whether or not the team has checked into the competition by logging in + disconnected: + type: boolean + description: Just a flag stating whether or not the team has deliberately disconnected + lastSeen: + type: + - string + - 'null' + format: date-time + description: When the team last contacted the server + TeamProgression: + type: object + required: + - user + - score + - submissionStates + properties: + score: + type: number + format: double + submissionStates: + type: array + items: + $ref: '#/components/schemas/QuestionState' + user: + $ref: '#/components/schemas/User' + TeamWithScore: + allOf: + - $ref: '#/components/schemas/TeamFull' + - type: object + required: + - score + - id + - name + properties: + displayName: + type: + - string + - 'null' + id: + $ref: '#/components/schemas/UserId' + name: + type: string + score: + type: number + format: double + TeamsListResponse: + type: array + items: + $ref: '#/components/schemas/TeamWithScore' + TestResponse: + type: object + required: + - input + - output + - visible + properties: + input: + type: string + output: + type: string + visible: + type: boolean + UpdateClockRequest: + oneOf: + - type: object + required: + - is_paused + properties: + is_paused: + type: boolean + User: + type: object + required: + - id + - username + - role + properties: + displayName: + type: + - string + - 'null' + id: + $ref: '#/components/schemas/UserId' + role: + $ref: '#/components/schemas/Role' + username: + type: string + UserId: + type: string From f906b69486e816d2e4a55af6a90722478385f2e5 Mon Sep 17 00:00:00 2001 From: jackjohn7 Date: Sun, 31 Aug 2025 18:47:51 -0500 Subject: [PATCH 2/2] add axum_extras feature from utoipa, expose announcement_id param in spec --- Cargo.lock | 30 +++++++++++++++---- Cargo.toml | 7 ++++- .../src/services/announcements.rs | 4 +-- basalt-server-lib/src/services/clock.rs | 2 +- openapi.yaml | 13 ++++++++ 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da17d09..aea3f83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5516,6 +5516,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_norway" +version = "0.9.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e408f29489b5fd500fab51ff1484fc859bb655f32c671f307dcd733b72e8168c" +dependencies = [ + "indexmap 2.9.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml-norway", +] + [[package]] name = "serde_path_to_error" version = "0.1.16" @@ -7684,6 +7697,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unsafe-libyaml-norway" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39abd59bf32521c7f2301b52d05a6a2c975b6003521cbd0c6dc1582f0a22104" + [[package]] name = "unscanny" version = "0.1.0" @@ -7789,14 +7808,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ "indexmap 2.9.0", "serde", "serde_json", - "serde_yaml", + "serde_norway", "utoipa-gen", ] @@ -7815,12 +7834,13 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" dependencies = [ "proc-macro2", "quote", + "regex", "syn 2.0.98", ] diff --git a/Cargo.toml b/Cargo.toml index c0b7b81..3314c83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,4 +50,9 @@ tower-http = { version = "0.6.2", features = ["cors", "trace", "fs"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing = { version = "0.1.41", features = ["release_max_level_debug"] } utoipa-axum = { version = "0.2.0", features = ["debug"] } -utoipa = { version = "5.3.1", features = ["chrono", "debug", "yaml"] } +utoipa = { version = "5.4.0", features = [ + "chrono", + "debug", + "yaml", + "axum_extras", +] } diff --git a/basalt-server-lib/src/services/announcements.rs b/basalt-server-lib/src/services/announcements.rs index fc99977..b50d894 100644 --- a/basalt-server-lib/src/services/announcements.rs +++ b/basalt-server-lib/src/services/announcements.rs @@ -99,8 +99,8 @@ pub async fn new( )] pub async fn delete( State(state): State>, - Path(id): Path, - HostUser(_): HostUser, + HostUser(_u): HostUser, + Path((id,)): Path<(AnnouncementId,)>, ) -> Result, StatusCode> { let sql = state.db.read().await; diff --git a/basalt-server-lib/src/services/clock.rs b/basalt-server-lib/src/services/clock.rs index 9df29d7..fe70565 100644 --- a/basalt-server-lib/src/services/clock.rs +++ b/basalt-server-lib/src/services/clock.rs @@ -122,7 +122,7 @@ async fn patch_clock( ) )] async fn get_clock( - OptionalUser(_): OptionalUser, + OptionalUser(_u): OptionalUser, State(state): State>, ) -> Result, StatusCode> { trace!("user getting clock"); diff --git a/openapi.yaml b/openapi.yaml index 8463ebe..9fd23a2 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -44,6 +44,12 @@ paths: tags: - announcements operationId: delete + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/AnnouncementId' responses: '200': description: '' @@ -183,6 +189,13 @@ paths: tags: - questions operationId: get_specific_question + parameters: + - name: id + in: path + required: true + schema: + type: integer + minimum: 0 responses: '200': description: ''