diff --git a/app/Docs/Attributes/Limit.php b/app/Docs/Attributes/Limit.php
new file mode 100644
index 00000000000..62c42906fbe
--- /dev/null
+++ b/app/Docs/Attributes/Limit.php
@@ -0,0 +1,29 @@
+. Licensed under the GNU Affero General Public License v3.0.
+// See the LICENCE file in the repository root for full licence text.
+
+declare(strict_types=1);
+
+namespace App\Docs\Attributes;
+
+use App\Models\Model;
+use Attribute;
+use Knuckles\Scribe\Attributes\GenericParam;
+
+#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
+class Limit extends GenericParam
+{
+ public function __construct(
+ int $default = Model::PER_PAGE,
+ int $minimum = 1,
+ int $maximum = 50,
+ string $description = 'Maximum number of results',
+ bool $required = false,
+ string|int $example = 'No-example',
+ ) {
+ $description .= " (Default: {$default}, Minimum: {$minimum}, Maximum: {$maximum})";
+
+ parent::__construct('limit', 'integer', $description, $required, $example);
+ }
+}
diff --git a/app/Docs/Attributes/Offset.php b/app/Docs/Attributes/Offset.php
new file mode 100644
index 00000000000..2fd162ead95
--- /dev/null
+++ b/app/Docs/Attributes/Offset.php
@@ -0,0 +1,23 @@
+. Licensed under the GNU Affero General Public License v3.0.
+// See the LICENCE file in the repository root for full licence text.
+
+declare(strict_types=1);
+
+namespace App\Docs\Attributes;
+
+use Attribute;
+use Knuckles\Scribe\Attributes\GenericParam;
+
+#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
+class Offset extends GenericParam
+{
+ public function __construct(
+ string $description = 'Result offset for pagination.',
+ bool $required = false,
+ ?int $example = 1,
+ ) {
+ parent::__construct('offset', 'integer', $description, $required, $example ?? 'No-example');
+ }
+}
diff --git a/app/Docs/Attributes/Page.php b/app/Docs/Attributes/Page.php
new file mode 100644
index 00000000000..64f9b869044
--- /dev/null
+++ b/app/Docs/Attributes/Page.php
@@ -0,0 +1,23 @@
+. Licensed under the GNU Affero General Public License v3.0.
+// See the LICENCE file in the repository root for full licence text.
+
+declare(strict_types=1);
+
+namespace App\Docs\Attributes;
+
+use Attribute;
+use Knuckles\Scribe\Attributes\GenericParam;
+
+#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
+class Page extends GenericParam
+{
+ public function __construct(
+ string $description = 'Search result page',
+ bool $required = false,
+ ?int $example = 1,
+ ) {
+ parent::__construct('page', 'integer', $description, $required, $example ?? 'No-example');
+ }
+}
diff --git a/app/Docs/Attributes/Sort.php b/app/Docs/Attributes/Sort.php
new file mode 100644
index 00000000000..c925922a278
--- /dev/null
+++ b/app/Docs/Attributes/Sort.php
@@ -0,0 +1,34 @@
+. Licensed under the GNU Affero General Public License v3.0.
+// See the LICENCE file in the repository root for full licence text.
+
+declare(strict_types=1);
+
+namespace App\Docs\Attributes;
+
+use Attribute;
+use Knuckles\Scribe\Attributes\GenericParam;
+
+#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
+class Sort extends GenericParam
+{
+ public function __construct(
+ array|string $sorts,
+ ?string $defaultText = null,
+ ?string $example = null,
+ ) {
+ if (is_string($sorts)) {
+ $hash = mb_strtolower($sorts);
+ $description = "Sort option as defined in [{$sorts}](#{$hash}).";
+ } else {
+ $description = 'Sort order; '.implode(', ', array_map(fn ($sort) => "`{$sort}`", $sorts));
+ }
+
+ if ($defaultText !== null) {
+ $description .= " {$defaultText}";
+ }
+
+ parent::__construct('sort', 'string', $description, false, $example ?? 'No-example');
+ }
+}
diff --git a/app/Docs/Strategies/GetFromQueryParamAttribute.php b/app/Docs/Strategies/GetFromQueryParamAttribute.php
new file mode 100644
index 00000000000..83d7fb3e73e
--- /dev/null
+++ b/app/Docs/Strategies/GetFromQueryParamAttribute.php
@@ -0,0 +1,26 @@
+. Licensed under the GNU Affero General Public License v3.0.
+// See the LICENCE file in the repository root for full licence text.
+
+declare(strict_types=1);
+
+namespace App\Docs\Strategies;
+
+use App\Docs\Attributes\Limit;
+use App\Docs\Attributes\Offset;
+use App\Docs\Attributes\Page;
+use App\Docs\Attributes\Sort;
+use Http\Message\Authentication\QueryParam;
+use Knuckles\Scribe\Extracting\Strategies\GetParamsFromAttributeStrategy;
+
+class GetFromQueryParamAttribute extends GetParamsFromAttributeStrategy
+{
+ protected static array $attributeNames = [
+ Limit::class,
+ Offset::class,
+ Page::class,
+ QueryParam::class,
+ Sort::class,
+ ];
+}
diff --git a/app/Http/Controllers/BeatmapDiscussionPostsController.php b/app/Http/Controllers/BeatmapDiscussionPostsController.php
index f02f81f125c..541b9e8c70d 100644
--- a/app/Http/Controllers/BeatmapDiscussionPostsController.php
+++ b/app/Http/Controllers/BeatmapDiscussionPostsController.php
@@ -5,6 +5,9 @@
namespace App\Http\Controllers;
+use App\Docs\Attributes\Limit;
+use App\Docs\Attributes\Page;
+use App\Docs\Attributes\Sort;
use App\Exceptions\ModelNotSavedException;
use App\Libraries\BeatmapsetDiscussion\Discussion;
use App\Libraries\BeatmapsetDiscussion\Reply;
@@ -15,6 +18,7 @@
use App\Models\Beatmapset;
use App\Models\BeatmapsetWatch;
use App\Models\User;
+use Knuckles\Scribe\Attributes\QueryParam;
/**
* @group Beatmapset Discussions
@@ -63,14 +67,13 @@ public function destroy($id)
* posts | [BeatmapsetDiscussionPost](#beatmapsetdiscussionpost)[] | |
* users | [User](#user) | |
*
- * @queryParam beatmapset_discussion_id `id` of the [BeatmapsetDiscussion](#beatmapsetdiscussion).
- * @queryParam limit Maximum number of results.
- * @queryParam page Search result page.
- * @queryParam sort `id_desc` for newest first; `id_asc` for oldest first. Defaults to `id_desc`.
- * @queryParam types[] `first`, `reply`, `system` are the valid values. Defaults to `reply`.
- * @queryParam user The `id` of the [User](#user).
- * @queryParam with_deleted This param has no effect as api calls do not currently receive group permissions.
+ * @usesCursor
+ * @queryParam beatmapset_discussion_id integer `id` of the [BeatmapsetDiscussion](#beatmapsetdiscussion).
+ * @queryParam types string[] `first`, `reply`, `system` are the valid values. Defaults to `reply`.
+ * @queryParam user integer The `id` of the [User](#user).
+ * @queryParam with_deleted boolean This param has no effect as api calls do not currently receive group permissions. No-example
*/
+ #[Limit(BeatmapDiscussionPost::PER_PAGE, 5), Page, Sort('IdSort')]
public function index()
{
$bundle = new BeatmapsetDiscussionPostsBundle(request()->all());
diff --git a/app/Http/Controllers/BeatmapDiscussionsController.php b/app/Http/Controllers/BeatmapDiscussionsController.php
index ecc1e30e2fe..0474c068eb3 100644
--- a/app/Http/Controllers/BeatmapDiscussionsController.php
+++ b/app/Http/Controllers/BeatmapDiscussionsController.php
@@ -5,6 +5,9 @@
namespace App\Http\Controllers;
+use App\Docs\Attributes\Limit;
+use App\Docs\Attributes\Page;
+use App\Docs\Attributes\Sort;
use App\Exceptions\ModelNotSavedException;
use App\Libraries\BeatmapsetDiscussion\Review;
use App\Libraries\BeatmapsetDiscussionsBundle;
@@ -90,17 +93,15 @@ public function destroy($id)
* users | [User](#user)[] | List of users associated with the discussions returned.
*
* @usesCursor
- * @queryParam beatmap_id `id` of the [Beatmap](#beatmap).
- * @queryParam beatmapset_id `id` of the [Beatmapset](#beatmapset).
- * @queryParam beatmapset_status One of `all`, `ranked`, `qualified`, `disqualified`, `never_qualified`. Defaults to `all`. TODO: better descriptions.
- * @queryParam limit Maximum number of results.
- * @queryParam message_types[] `suggestion`, `problem`, `mapper_note`, `praise`, `hype`, `review`. Blank defaults to all types. TODO: better descriptions.
- * @queryParam only_unresolved `true` to show only unresolved issues; `false`, otherwise. Defaults to `false`.
- * @queryParam page Search result page.
- * @queryParam sort `id_desc` for newest first; `id_asc` for oldest first. Defaults to `id_desc`.
- * @queryParam user The `id` of the [User](#user).
- * @queryParam with_deleted This param has no effect as api calls do not currently receive group permissions.
+ * @queryParam beatmap_id integer `id` of the [Beatmap](#beatmap).
+ * @queryParam beatmapset_id integer `id` of the [Beatmapset](#beatmapset).
+ * @queryParam beatmapset_status string One of `all`, `ranked`, `qualified`, `disqualified`, `never_qualified`. Defaults to `all`. TODO: better descriptions. No-example
+ * @queryParam message_types string[] `suggestion`, `problem`, `mapper_note`, `praise`, `hype`, `review`. Blank defaults to all types. TODO: better descriptions. No-example
+ * @queryParam only_unresolved boolean `true` to show only unresolved issues; `false`, otherwise. Defaults to `false`.
+ * @queryParam user integer The `id` of the [User](#user).
+ * @queryParam with_deleted boolean This param has no effect as api calls do not currently receive group permissions. No-example
*/
+ #[Limit(BeatmapDiscussion::PER_PAGE, 5), Page, Sort('IdSort')]
public function index()
{
$bundle = new BeatmapsetDiscussionsBundle(request()->all());
diff --git a/app/Http/Controllers/BeatmapsController.php b/app/Http/Controllers/BeatmapsController.php
index 33b76791953..9394f5b0c58 100644
--- a/app/Http/Controllers/BeatmapsController.php
+++ b/app/Http/Controllers/BeatmapsController.php
@@ -197,7 +197,7 @@ public function attributes($id)
* -------- | ------------------------------------- | -----------
* beatmaps | [BeatmapExtended](#beatmapextended)[] | Includes `beatmapset` (with `ratings`), `failtimes`, and `max_combo`.
*
- * @queryParam ids[] integer Beatmap IDs to be returned. Specify once for each beatmap ID requested. Up to 50 beatmaps can be requested at once. Example: 1
+ * @queryParam ids integer[] Beatmap IDs to be returned. Specify once for each beatmap ID requested. Up to 50 beatmaps can be requested at once. Example: [1]
*
* @response {
* "beatmaps": [
@@ -241,9 +241,9 @@ public function index()
*
* See [Get Beatmap](#get-beatmap)
*
- * @queryParam checksum A beatmap checksum.
- * @queryParam filename A filename to lookup.
- * @queryParam id A beatmap ID to lookup.
+ * @queryParam checksum string A beatmap checksum. No-example
+ * @queryParam filename string A filename to lookup. No-example
+ * @queryParam id integer A beatmap ID to lookup. No-example
*
* @response "See Beatmap object section"
*/
@@ -342,9 +342,9 @@ public function show($id)
* @urlParam beatmap integer required Id of the [Beatmap](#beatmap).
*
* @queryParam legacy_only integer Whether or not to exclude lazer scores. Defaults to 0. Example: 0
- * @queryParam mode The [Ruleset](#ruleset) to get scores for.
- * @queryParam mods An array of matching Mods, or none // TODO.
- * @queryParam type Beatmap score ranking type // TODO.
+ * @queryParam mode The [Ruleset](#ruleset) to get scores for. Example: osu
+ * @queryParam mods string[] An array of matching Mods, or none // TODO. No-example
+ * @queryParam type string Beatmap score ranking type // TODO. No-example
*/
public function scores($id)
{
@@ -369,9 +369,9 @@ public function scores($id)
*
* @urlParam beatmap integer required Id of the [Beatmap](#beatmap).
*
- * @queryParam mode The [Ruleset](#ruleset) to get scores for.
- * @queryParam mods An array of matching Mods, or none // TODO.
- * @queryParam type Beatmap score ranking type // TODO.
+ * @queryParam mode string The [Ruleset](#ruleset) to get scores for. Example: osu
+ * @queryParam mods string[] An array of matching Mods, or none // TODO.
+ * @queryParam type string Beatmap score ranking type // TODO. No-example
*/
public function soloScores($id)
{
@@ -422,8 +422,8 @@ public function updateOwner($id)
* @urlParam user integer required Id of the [User](#user).
*
* @queryParam legacy_only integer Whether or not to exclude lazer scores. Defaults to 0. Example: 0
- * @queryParam mode The [Ruleset](#ruleset) to get scores for.
- * @queryParam mods An array of matching Mods, or none // TODO.
+ * @queryParam mode string The [Ruleset](#ruleset) to get scores for. Example: osu
+ * @queryParam mods string[] An array of matching Mods, or none // TODO. No-example
*/
public function userScore($beatmapId, $userId)
{
diff --git a/app/Http/Controllers/BeatmapsetDiscussionVotesController.php b/app/Http/Controllers/BeatmapsetDiscussionVotesController.php
index 7181060db3a..e1a9df09312 100644
--- a/app/Http/Controllers/BeatmapsetDiscussionVotesController.php
+++ b/app/Http/Controllers/BeatmapsetDiscussionVotesController.php
@@ -5,7 +5,11 @@
namespace App\Http\Controllers;
+use App\Docs\Attributes\Limit;
+use App\Docs\Attributes\Page;
+use App\Docs\Attributes\Sort;
use App\Libraries\BeatmapsetDiscussionVotesBundle;
+use App\Models\BeatmapDiscussionVote;
/**
* @group Beatmapset Discussions
@@ -39,15 +43,14 @@ public function __construct()
* users | [User](#user) | |
* votes | [BeatmapsetDiscussionVote](#beatmapsetdiscussionvote)[] | |
*
- * @queryParam beatmapset_discussion_id `id` of the [BeatmapsetDiscussion](#beatmapsetdiscussion).
- * @queryParam limit Maximum number of results.
- * @queryParam page Search result page.
- * @queryParam receiver The `id` of the [User](#user) receiving the votes.
- * @queryParam score `1` for up vote, `-1` for down vote.
- * @queryParam sort `id_desc` for newest first; `id_asc` for oldest first. Defaults to `id_desc`.
- * @queryParam user The `id` of the [User](#user) giving the votes.
- * @queryParam with_deleted This param has no effect as api calls do not currently receive group permissions.
+ * @usesCursor
+ * @queryParam beatmapset_discussion_id integer `id` of the [BeatmapsetDiscussion](#beatmapsetdiscussion).
+ * @queryParam receiver integer The `id` of the [User](#user) receiving the votes.
+ * @queryParam score integer `1` for up vote, `-1` for down vote. Example: 1
+ * @queryParam user integer The `id` of the [User](#user) giving the votes.
+ * @queryParam with_deleted boolean This param has no effect as api calls do not currently receive group permissions. No-example
*/
+ #[Limit(BeatmapDiscussionVote::PER_PAGE, 5), Page, Sort('IdSort')]
public function index()
{
$bundle = new BeatmapsetDiscussionVotesBundle(request()->all());
diff --git a/app/Http/Controllers/ChangelogController.php b/app/Http/Controllers/ChangelogController.php
index 058e09501cd..38e0897edf3 100644
--- a/app/Http/Controllers/ChangelogController.php
+++ b/app/Http/Controllers/ChangelogController.php
@@ -65,7 +65,7 @@ private static function changelogEntryMessageIncludes(?array $formats): array
* @queryParam max_id integer Maximum build ID. No-example
* @queryParam stream string Stream name to return builds from. No-example
* @queryParam to string Maximum build version. No-example
- * @queryParam message_formats[] string `html`, `markdown`. Default to both.
+ * @queryParam message_formats[] string `html`, `markdown`. Default to both. No-example
* @response {
* "streams": [
* {
@@ -244,7 +244,7 @@ public function github()
*
* @urlParam changelog string required Build version, update stream name, or build ID. Example: 20210520.2
* @queryParam key string Unset to query by build version or stream name, or `id` to query by build ID. No-example
- * @queryParam message_formats[] string `html`, `markdown`. Default to both.
+ * @queryParam message_formats string[] `html`, `markdown`. Default to both.
* @response See "Get Changelog Build" response.
*/
public function show($version)
diff --git a/app/Http/Controllers/Chat/Channels/MessagesController.php b/app/Http/Controllers/Chat/Channels/MessagesController.php
index 23af90f72fd..9ba4574a0e5 100644
--- a/app/Http/Controllers/Chat/Channels/MessagesController.php
+++ b/app/Http/Controllers/Chat/Channels/MessagesController.php
@@ -5,6 +5,7 @@
namespace App\Http\Controllers\Chat\Channels;
+use App\Docs\Attributes\Limit;
use App\Http\Controllers\Chat\Controller as BaseController;
use App\Libraries\Chat;
use App\Models\Chat\Channel;
@@ -37,7 +38,6 @@ public function __construct()
* Returns an array of [ChatMessage](#chatmessage)
*
* @urlParam channel integer required The ID of the channel to retrieve messages for
- * @queryParam limit integer number of messages to return (max of 50)
* @queryParam since integer messages after the specified message id will be returned
* @queryParam until integer messages up to but not including the specified message id will be returned
*
@@ -82,6 +82,7 @@ public function __construct()
* }
* ]
*/
+ #[Limit]
public function index($channelId)
{
[
diff --git a/app/Http/Controllers/Chat/ChannelsController.php b/app/Http/Controllers/Chat/ChannelsController.php
index 372acccd2f9..321ff8bf01e 100644
--- a/app/Http/Controllers/Chat/ChannelsController.php
+++ b/app/Http/Controllers/Chat/ChannelsController.php
@@ -113,6 +113,9 @@ public function join($channelId, $userId)
* This endpoint will only allow the leaving of public channels initially.
*
*
+ * @urlParam channel integer required `channel_id` of the [ChatChannel](#chatchannel) to leave.
+ * @urlParam user integer required `id` of the [User](#user) to leave the channel.
+ *
* @response 204
*/
public function part($channelId, $userId)
@@ -146,6 +149,8 @@ public function part($channelId, $userId)
* channel | [ChatChannel](#chatchannel) | |
* users | [User](#user) | Users are only visible for PM channels.
*
+ * @urlParam channel integer required `channel_id` of the [ChatChannel](#chatchannel) to get.
+ *
* @response {
* "channel": {
* "channel_id": 1337,
@@ -191,6 +196,7 @@ public function part($channelId, $userId)
* }
* ]
* }
+ *
*/
public function show($channelId)
{
@@ -314,8 +320,8 @@ public function store()
* Note that the read marker cannot be moved backwards - i.e. if a channel has been marked as read up to message_id = 12, you cannot then set it backwards to message_id = 10. It will be rejected.
*
*
- * @queryParam channel_id required The `channel_id` of the channel to mark as read
- * @queryParam message_id required The `message_id` of the message to mark as read up to
+ * @urlParam channel integer required The `channel_id` of the [ChatChannel](#chatchannel) to mark as read.
+ * @urlParam message integer required The `message_id` of the [ChatMessage](#chatmessage) to mark as read up to.
*
* @response 204
*/
diff --git a/app/Http/Controllers/CommentsController.php b/app/Http/Controllers/CommentsController.php
index a85024632c9..cf6b05188ae 100644
--- a/app/Http/Controllers/CommentsController.php
+++ b/app/Http/Controllers/CommentsController.php
@@ -5,6 +5,7 @@
namespace App\Http\Controllers;
+use App\Docs\Attributes\Sort;
use App\Exceptions\ModelNotSavedException;
use App\Jobs\Notifications\CommentNew;
use App\Libraries\CommentBundle;
@@ -15,6 +16,7 @@
use Carbon\Carbon;
use Exception;
use Illuminate\Pagination\LengthAwarePaginator;
+use Knuckles\Scribe\Attributes\QueryParam;
/**
* @group Comments
@@ -38,6 +40,8 @@ public function __construct()
* ### Response Format
*
* Returns [CommentBundle](#commentbundle)
+ *
+ * @urlParam comment integer required The `id` of the [Comment](#comment)
*/
public function destroy($id)
{
@@ -67,13 +71,13 @@ public function destroy($id)
*
* `pinned_comments` is only included when `commentable_type` and `commentable_id` are specified.
*
- * @queryParam after Return comments which come after the specified comment id as per sort option. No-example
- * @queryParam commentable_type The type of resource to get comments for. Example: beatmapset
- * @queryParam commentable_id The id of the resource to get comments for. Example: 1
+ * @queryParam after integer Return comments which come after the specified comment id as per sort option. No-example
+ * @queryParam commentable_type string The type of resource to get comments for. Example: beatmapset
+ * @queryParam commentable_id integer The id of the resource to get comments for. Example: 1
* @queryParam cursor Pagination option. See [CommentSort](#commentsort) for detail. The format follows [Cursor](#cursor) except it's not currently included in the response. No-example
- * @queryParam parent_id Limit to comments which are reply to the specified id. Specify 0 to get top level comments. Example: 1
- * @queryParam sort Sort option as defined in [CommentSort](#commentsort). Defaults to `new` for guests and user-specified default when authenticated. Example: new
+ * @queryParam parent_id integer Limit to comments which are reply to the specified id. Specify 0 to get top level comments. Example: 1
*/
+ #[Sort('CommentSort', 'Defaults to `new` for guests and user-specified default when authenticated.', 'new')]
public function index()
{
$params = request()->all();
@@ -150,6 +154,8 @@ public function restore($id)
* ### Response Format
*
* Returns [CommentBundle](#commentbundle)
+ *
+ * @urlParam comment integer required The `id` of the [Comment](#comment)
*/
public function show($id)
{
@@ -177,10 +183,10 @@ public function show($id)
*
* Returns [CommentBundle](#commentbundle)
*
- * @queryParam comment.commentable_id Resource ID the comment thread is attached to
- * @queryParam comment.commentable_type Resource type the comment thread is attached to
- * @queryParam comment.message Text of the comment
- * @queryParam comment.parent_id The id of the comment to reply to, null if not a reply
+ * @queryParam comment.commentable_id integer required Resource ID the comment thread is attached to. Example: 1
+ * @queryParam comment.commentable_type string required Resource type the comment thread is attached to. Example: beatmapset
+ * @queryParam comment.message string required Text of the comment
+ * @queryParam comment.parent_id integer The id of the comment to reply to, null if not a reply. No-example
*/
public function store()
{
@@ -225,7 +231,8 @@ public function store()
*
* Returns [CommentBundle](#commentbundle)
*
- * @queryParam comment.message New text of the comment
+ * @urlParam comment integer required The `id` of the [Comment](#comment)
+ * @queryParam comment.message string required New text of the comment
*/
public function update($id)
{
@@ -277,6 +284,8 @@ public function pinStore($id)
* ### Response Format
*
* Returns [CommentBundle](#commentbundle)
+ *
+ * @urlParam comment integer required The `id` of the [Comment](#comment)
*/
public function voteDestroy($id)
{
@@ -303,6 +312,8 @@ public function voteDestroy($id)
* ### Response Format
*
* Returns [CommentBundle](#commentbundle)
+ *
+ * @urlParam comment integer required The `id` of the [Comment](#comment)
*/
public function voteStore($id)
{
diff --git a/app/Http/Controllers/EventsController.php b/app/Http/Controllers/EventsController.php
index 14a1a5d4ebf..fd22cb0bf76 100644
--- a/app/Http/Controllers/EventsController.php
+++ b/app/Http/Controllers/EventsController.php
@@ -7,6 +7,7 @@
namespace App\Http\Controllers;
+use App\Docs\Attributes\Sort;
use App\Models\Event;
/**
@@ -36,7 +37,6 @@ public function __construct()
* events | [Event](#event)[]
*
* @usesCursor
- * @queryParam sort Sorting option. Valid values are `id_desc` (default) and `id_asc`. No-example
*
* @response {
* events: [
@@ -52,6 +52,7 @@ public function __construct()
* cursor_string: "eyJldmVudF9pZCI6OH0"
* }
*/
+ #[Sort('IdSort')]
public function index()
{
$params = request()->all();
diff --git a/app/Http/Controllers/Forum/TopicsController.php b/app/Http/Controllers/Forum/TopicsController.php
index d8c4e34b628..c9d7f872136 100644
--- a/app/Http/Controllers/Forum/TopicsController.php
+++ b/app/Http/Controllers/Forum/TopicsController.php
@@ -5,6 +5,8 @@
namespace App\Http\Controllers\Forum;
+use App\Docs\Attributes\Limit;
+use App\Docs\Attributes\Sort;
use App\Exceptions\ModelNotSavedException;
use App\Jobs\Notifications\ForumTopicReply;
use App\Libraries\NewForumTopic;
@@ -298,7 +300,6 @@ public function reply($id)
* @urlParam topic integer required Id of the topic. Example: 1
*
* @usesCursor
- * @queryParam sort Post sorting option. Valid values are `id_asc` (default) and `id_desc`. No-example
* @queryParam limit Maximum number of posts to be returned (20 default, 50 at most). No-example
* @queryParam start First post id to be returned with `sort` set to `id_asc`. This parameter is ignored if `cursor_string` is specified. No-example
* @queryParam end First post id to be returned with `sort` set to `id_desc`. This parameter is ignored if `cursor_string` is specified. No-example
@@ -313,6 +314,7 @@ public function reply($id)
* "sort": "id_asc"
* }
*/
+ #[Limit(description: 'Maximum number of posts to be returned'), Sort('IdSort')]
public function show($id)
{
$topic = Topic::with(['forum'])->withTrashed()->findOrFail($id);
@@ -558,7 +560,7 @@ public function store()
* The edited [ForumTopic](#forum-topic).
*
* @urlParam topic integer required Id of the topic. Example: 1
- * @bodyParam forum_topic[topic_title] string New topic title. Example: titled
+ * @bodyParam forum_topic[topic_title] string required New topic title. Example: titled
*/
public function update($id)
{
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index 0cd6ff043d9..66fb11aab1d 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -6,6 +6,7 @@
namespace App\Http\Controllers;
use App;
+use App\Docs\Attributes\Page;
use App\Libraries\CurrentStats;
use App\Libraries\MenuContent;
use App\Libraries\Search\AllSearch;
@@ -177,8 +178,8 @@ public function quickSearch()
*
* @queryParam mode string Either `all`, `user`, or `wiki_page`. Default is `all`. Example: all
* @queryParam query Search keyword. Example: hello
- * @queryParam page Search result page. Ignored for mode `all`. Example: 1
*/
+ #[Page('Search result page. Ignored for mode `all`')]
public function search()
{
$currentUser = Auth::user();
diff --git a/app/Http/Controllers/MatchesController.php b/app/Http/Controllers/MatchesController.php
index f471caf0fa8..6c16c69e9f5 100644
--- a/app/Http/Controllers/MatchesController.php
+++ b/app/Http/Controllers/MatchesController.php
@@ -5,6 +5,8 @@
namespace App\Http\Controllers;
+use App\Docs\Attributes\Limit;
+use App\Docs\Attributes\Sort;
use App\Models\LegacyMatch\LegacyMatch;
use App\Models\User;
use App\Transformers\LegacyMatch\EventTransformer;
@@ -38,8 +40,6 @@ public function __construct()
* params.sort | string | |
*
* @usesCursor
- * @queryParam limit integer Maximum number of matches (50 default, 1 minimum, 50 maximum). No-example
- * @queryParam sort string `id_desc` for newest first; `id_asc` for oldest first. Defaults to `id_desc`. No-example
* @response {
* "matches": [
* {
@@ -60,6 +60,7 @@ public function __construct()
* "cursor_string": "eyJtYXRjaF9pZCI6MTE0NDI4Njg1fQ"
* }
*/
+ #[Limit, Sort('IdSort')]
public function index()
{
$params = request()->all();
@@ -99,7 +100,6 @@ public function index()
* @urlParam match integer required Match ID. No-example
* @queryParam before integer Filter for match events before the specified [MatchEvent.id](#matchevent). No-example
* @queryParam after integer Filter for match events after the specified [MatchEvent.id](#matchevent). No-example
- * @queryParam limit integer Maximum number of match events (100 default, 1 minimum, 101 maximum). No-example
* @response {
* "match": {
* "id": 16155689,
@@ -124,6 +124,7 @@ public function index()
* "current_game_id": null
* }
*/
+ #[Limit(100, 1, 101)]
public function show($id)
{
$match = LegacyMatch::findOrFail($id);
diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php
index ea1f7321d1f..fbfeb0fb4d3 100644
--- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php
+++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php
@@ -5,6 +5,8 @@
namespace App\Http\Controllers\Multiplayer\Rooms\Playlist;
+use App\Docs\Attributes\Limit;
+use App\Docs\Attributes\Sort;
use App\Exceptions\InvariantException;
use App\Http\Controllers\Controller as BaseController;
use App\Libraries\ClientCheck;
@@ -43,9 +45,9 @@ public function __construct()
* @urlParam playlist integer required Id of the playlist item.
*
* @usesCursor
- * @queryParam limit Number of scores to be returned.
- * @queryParam sort [MultiplayerScoresSort](#multiplayerscoressort) parameter.
*/
+ #[Limit]
+ #[Sort('MultiplayerScoresSort')]
public function index($roomId, $playlistId)
{
$playlist = PlaylistItem::where('room_id', $roomId)->findOrFail($playlistId);
diff --git a/app/Http/Controllers/Multiplayer/RoomsController.php b/app/Http/Controllers/Multiplayer/RoomsController.php
index 33e7e7c13c4..0072a766c29 100644
--- a/app/Http/Controllers/Multiplayer/RoomsController.php
+++ b/app/Http/Controllers/Multiplayer/RoomsController.php
@@ -5,6 +5,8 @@
namespace App\Http\Controllers\Multiplayer;
+use App\Docs\Attributes\Limit;
+use App\Docs\Attributes\Sort;
use App\Exceptions\InvariantException;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Ranking\DailyChallengeController;
@@ -34,12 +36,11 @@ public function destroy($id)
*
* @group Multiplayer
*
- * @queryParam limit Maximum number of results. No-example
* @queryParam mode Filter mode; `active` (default), `all`, `ended`, `participated`, `owned`. No-example
* @queryParam season_id Season ID to return Rooms from. No-example
- * @queryParam sort Sort order; `ended`, `created`. No-example
* @queryParam type_group `playlists` (default) or `realtime`. No-example
*/
+ #[Limit(250, 1, 250), Sort(['ended', 'created'])]
public function index()
{
$apiVersion = api_version();
diff --git a/app/Http/Controllers/NewsController.php b/app/Http/Controllers/NewsController.php
index 8fa04aeb12a..f77fa4945a9 100644
--- a/app/Http/Controllers/NewsController.php
+++ b/app/Http/Controllers/NewsController.php
@@ -5,6 +5,7 @@
namespace App\Http\Controllers;
+use App\Docs\Attributes\Limit;
use App\Libraries\CommentBundle;
use App\Models\NewsPost;
@@ -37,7 +38,6 @@ class NewsController extends Controller
*
*
* @usesCursor
- * @queryParam limit integer Maximum number of posts (12 default, 1 minimum, 21 maximum). No-example
* @queryParam year integer Year to return posts from. No-example
* @response {
* "news_posts": [
@@ -78,6 +78,7 @@ class NewsController extends Controller
* "cursor_string": "WyJodHRwczpcL1wvd3d3LnlvdXR1YmUuY29tXC93YXRjaD92PWRRdzR3OVdnWGNRIl0"
* }
*/
+ #[Limit(12, 1, 21)]
public function index()
{
$params = request()->all();
diff --git a/app/Http/Controllers/NotificationsController.php b/app/Http/Controllers/NotificationsController.php
index b65192d69e0..b0b79b5cb91 100644
--- a/app/Http/Controllers/NotificationsController.php
+++ b/app/Http/Controllers/NotificationsController.php
@@ -56,7 +56,7 @@ public function endpoint()
* unread_count | total unread notifications
* notification_endpoint | url to connect to websocket server
*
- * @queryParam max_id Maximum `id` fetched. Can be used to load earlier notifications. Defaults to no limit (fetch latest notifications)
+ * @queryParam max_id integer Maximum `id` fetched. Can be used to load earlier notifications. Defaults to no limit (fetch latest notifications). No-example
*
* @response {
* "has_more": true,
diff --git a/app/Http/Controllers/RankingController.php b/app/Http/Controllers/RankingController.php
index 51980dc82d8..da357464c8d 100644
--- a/app/Http/Controllers/RankingController.php
+++ b/app/Http/Controllers/RankingController.php
@@ -5,6 +5,7 @@
namespace App\Http\Controllers;
+use App\Docs\Attributes\Page;
use App\Models\Beatmap;
use App\Models\Country;
use App\Models\CountryStatistics;
@@ -151,10 +152,10 @@ public static function url(
* @urlParam type string required [RankingType](#rankingtype). Example: performance
*
* @queryParam country string Filter ranking by country code. Only available for `type` of `performance`. Example: JP
- * @queryParam cursor [Cursor](#cursor). No-example
- * @queryParam filter Either `all` (default) or `friends`. Example: all
- * @queryParam spotlight The id of the spotlight if `type` is `charts`. Ranking for latest spotlight will be returned if not specified. No-example
- * @queryParam variant Filter ranking to specified mode variant. For `mode` of `mania`, it's either `4k` or `7k`. Only available for `type` of `performance`. Example: 4k
+ * @queryParam cursor.page integer See [Cursor](#cursor). No-example
+ * @queryParam filter string Either `all` (default) or `friends`. Example: all
+ * @queryParam spotlight integer The id of the spotlight if `type` is `charts`. Ranking for latest spotlight will be returned if not specified. No-example
+ * @queryParam variant string Filter ranking to specified mode variant. For `mode` of `mania`, it's either `4k` or `7k`. Only available for `type` of `performance`. Example: 4k
*/
public function index($mode, $type)
{
@@ -296,9 +297,8 @@ public function index($mode, $type)
* Field | Type | Description
* ------- | --------------- | -----------
* ranking | [User](#user)[] | Includes `kudosu`.
- *
- * @queryParam page Ranking page. Example: 1
*/
+ #[Page('Ranking page')]
public function kudosu()
{
static $maxResults = 1000;
diff --git a/app/Http/Controllers/UsersController.php b/app/Http/Controllers/UsersController.php
index cd72c10642a..73ea2a0abfc 100644
--- a/app/Http/Controllers/UsersController.php
+++ b/app/Http/Controllers/UsersController.php
@@ -5,6 +5,8 @@
namespace App\Http\Controllers;
+use App\Docs\Attributes\Limit;
+use App\Docs\Attributes\Offset;
use App\Exceptions\ModelNotSavedException;
use App\Exceptions\ValidationException;
use App\Http\Middleware\RequestCost;
@@ -280,9 +282,6 @@ public function storeWeb()
* @urlParam user integer required Id of the user. Example: 1
* @urlParam type string required Beatmap type. Example: favourite
*
- * @queryParam limit Maximum number of results.
- * @queryParam offset Result offset for pagination. Example: 1
- *
* @response [
* {
* "id": 1,
@@ -294,6 +293,7 @@ public function storeWeb()
* }
* ]
*/
+ #[Limit(5, 1, 100), Offset]
public function beatmapsets($_userId, $type)
{
static $mapping = [
@@ -332,7 +332,7 @@ public function beatmapsets($_userId, $type)
* ----- | --------------- | -----------
* users | [User](#user)[] | Includes `country`, `cover`, `groups`, and `statistics_rulesets`.
*
- * @queryParam ids[] User id to be returned. Specify once for each user id requested. Up to 50 users can be requested at once. Example: 1
+ * @queryParam ids integer[] `id`s of users to be returned. Specify once for each user id requested. Up to 50 users can be requested at once. Example: [1,2]
* @queryParam include_variant_statistics boolean Whether to additionally include `statistics_rulesets.variants` (default: `false`). No-example
*
* @response {
@@ -430,9 +430,6 @@ public function posts($id)
*
* @urlParam user integer required Id of the user. Example: 1
*
- * @queryParam limit Maximum number of results.
- * @queryParam offset Result offset for pagination. Example: 1
- *
* @response [
* {
* "id": 1,
@@ -444,6 +441,7 @@ public function posts($id)
* }
* ]
*/
+ #[Limit(5, 1, 100), Offset]
public function kudosu($_userId)
{
return $this->getExtra('recentlyReceivedKudosu', [], $this->perPage, $this->offset);
@@ -461,10 +459,7 @@ public function kudosu($_userId)
* Array of [Event](#event).
*
* @urlParam user integer required Id of the user. Example: 1
- *
- * @queryParam limit Maximum number of results.
- * @queryParam offset Result offset for pagination. Example: 1
- *
+ * *
* @response [
* {
* "id": 1,
@@ -476,6 +471,7 @@ public function kudosu($_userId)
* }
* ]
*/
+ #[Limit(5, 1, 100), Offset]
public function recentActivity($_userId)
{
return $this->getExtra('recentActivity', [], $this->perPage, $this->offset);
@@ -503,10 +499,8 @@ public function recentActivity($_userId)
* @urlParam type string required Score type. Must be one of these: `best`, `firsts`, `recent`. Example: best
*
* @queryParam legacy_only integer Whether or not to exclude lazer scores. Defaults to 0. Example: 0
- * @queryParam include_fails Only for recent scores, include scores of failed plays. Set to 1 to include them. Defaults to 0. Example: 0
- * @queryParam mode [Ruleset](#ruleset) of the scores to be returned. Defaults to the specified `user`'s mode. Example: osu
- * @queryParam limit Maximum number of results.
- * @queryParam offset Result offset for pagination. Example: 1
+ * @queryParam include_fails integer Only for recent scores, include scores of failed plays. Set to 1 to include them. Defaults to 0. Example: 0
+ * @queryParam mode string [Ruleset](#ruleset) of the scores to be returned. Defaults to the specified `user`'s mode. Example: osu
*
* @response [
* {
@@ -519,6 +513,7 @@ public function recentActivity($_userId)
* }
* ]
*/
+ #[Limit(5, 1, 100), Offset]
public function scores($_userId, $type)
{
static $mapping = [
@@ -594,10 +589,6 @@ public function me($mode = null)
*
* This endpoint returns the detail of specified user.
*
- *
- *
* ---
*
* ### Response format
@@ -636,7 +627,7 @@ public function me($mode = null)
* @urlParam user integer required Id or `@`-prefixed username of the user. Previous usernames are also checked in some cases. Example: 1
* @urlParam mode string [Ruleset](#ruleset). User default mode will be used if not specified. Example: osu
*
- * @queryParam key Type of `user` passed in url parameter. Can be either `id` or `username` to limit lookup by their respective type. Passing empty or invalid value will result in id lookup followed by username lookup if not found. This parameter has been deprecated. Prefix `user` parameter with `@` instead to lookup by username.
+ * @queryParam key string Type of `user` passed in url parameter. Can be either `id` or `username` to limit lookup by their respective type. Passing empty or invalid value will result in id lookup followed by username lookup if not found. This parameter has been deprecated. Prefix `user` parameter with `@` instead to lookup by username. No-example
*
* @response "See User object section"
*/
diff --git a/config/scribe.php b/config/scribe.php
index 38ea83d10ad..815758cf403 100644
--- a/config/scribe.php
+++ b/config/scribe.php
@@ -300,17 +300,20 @@
'strategies' => [
'metadata' => [
Strategies\Metadata\GetFromDocBlocks::class,
+ Strategies\Metadata\GetFromMetadataAttributes::class,
],
'urlParameters' => [
Strategies\UrlParameters\GetFromLaravelAPI::class,
Strategies\UrlParameters\GetFromLumenAPI::class,
+ Strategies\UrlParameters\GetFromUrlParamAttribute::class,
Strategies\UrlParameters\GetFromUrlParamTag::class,
],
'queryParameters' => [
+ App\Docs\Strategies\GetFromQueryParamAttribute::class,
+ App\Docs\Strategies\UsesCursor::class,
Strategies\QueryParameters\GetFromQueryParamTag::class,
Strategies\QueryParameters\GetFromFormRequest::class,
Strategies\QueryParameters\GetFromInlineValidator::class,
- App\Docs\Strategies\UsesCursor::class,
],
'headers' => [
Strategies\Headers\GetFromRouteRules::class,
diff --git a/resources/views/docs/_structures/id_sort.md b/resources/views/docs/_structures/id_sort.md
new file mode 100644
index 00000000000..4760b96f4b3
--- /dev/null
+++ b/resources/views/docs/_structures/id_sort.md
@@ -0,0 +1,9 @@
+## IdSort
+
+Available sort types are `id_asc`, `id_desc`.
+
+Name | Description
+------- | ----------------------------
+id_asc | Sort by oldest first.
+id_desc | Sort by newest first.
+
diff --git a/resources/views/docs/_websocket_events.md b/resources/views/docs/_websocket_events.md
index c0edb63d259..9b1aaa24c0a 100644
--- a/resources/views/docs/_websocket_events.md
+++ b/resources/views/docs/_websocket_events.md
@@ -48,7 +48,7 @@ Broadcast to the user when the user joins a chat channel.
### Payload Format
-[ChatChannel](#chat-channel) with `current_user_attributes`, `last_message_id`, `users` additional attributes.
+[ChatChannel](#chatchannel) with `current_user_attributes`, `last_message_id`, `users` additional attributes.
## chat.channel.part
@@ -56,7 +56,7 @@ Broadcast to the user when the user leaves a chat channel.
### Payload Format
-[ChatChannel](#chat-channel) with `current_user_attributes`, `last_message_id`, `users` additional attributes.
+[ChatChannel](#chatchannel) with `current_user_attributes`, `last_message_id`, `users` additional attributes.
## chat.message.new