diff --git a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt index a70400fb..77f785c9 100644 --- a/src/intTest/kotlin/mastodon/account/AccountApiTest.kt +++ b/src/intTest/kotlin/mastodon/account/AccountApiTest.kt @@ -141,10 +141,11 @@ class AccountApiTest { mockMvc .post("/api/v1/accounts") { contentType = MediaType.APPLICATION_FORM_URLENCODED - param("username", "api-test-user-3") + param("password", "api-test-user-3") with(csrf()) } - .andExpect { status { isBadRequest() } } + .andDo { print() } + .andExpect { status { isUnprocessableEntity() } } } @Test @@ -156,7 +157,7 @@ class AccountApiTest { param("username", "api-test-user-4") with(csrf()) } - .andExpect { status { isBadRequest() } } + .andExpect { status { isUnprocessableEntity() } } } @Test diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt index 249c3ab4..f53c9cf8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SecurityConfig.kt @@ -239,6 +239,7 @@ class SecurityConfig { authorize(POST, "/api/v1/media", rf.hasScope("write:media")) authorize(POST, "/api/v1/statuses", rf.hasScope("write:statuses")) + authorize(GET, "/api/v1/statuses/*", permitAll) authorize(GET, "/api/v1/timelines/public", permitAll) authorize(GET, "/api/v1/timelines/home", rf.hasScope("read:statuses")) diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/ClientException.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/ClientException.kt new file mode 100644 index 00000000..3414889a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/ClientException.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.domain.exception + +import dev.usbharu.hideout.mastodon.domain.model.MastodonApiErrorResponse + +open class ClientException : MastodonApiException { + constructor(response: MastodonApiErrorResponse<*>) : super(response) + constructor(message: String?, response: MastodonApiErrorResponse<*>) : super(message, response) + constructor(message: String?, cause: Throwable?, response: MastodonApiErrorResponse<*>) : super( + message, + cause, + response + ) + + constructor(cause: Throwable?, response: MastodonApiErrorResponse<*>) : super(cause, response) + constructor( + message: String?, + cause: Throwable?, + enableSuppression: Boolean, + writableStackTrace: Boolean, + response: MastodonApiErrorResponse<*>, + ) : super(message, cause, enableSuppression, writableStackTrace, response) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/MastodonApiException.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/MastodonApiException.kt new file mode 100644 index 00000000..4a13854a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/MastodonApiException.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.domain.exception + +import dev.usbharu.hideout.mastodon.domain.model.MastodonApiErrorResponse + +abstract class MastodonApiException : RuntimeException { + + val response: MastodonApiErrorResponse<*> + + constructor(response: MastodonApiErrorResponse<*>) : super() { + this.response = response + } + + constructor(message: String?, response: MastodonApiErrorResponse<*>) : super(message) { + this.response = response + } + + constructor(message: String?, cause: Throwable?, response: MastodonApiErrorResponse<*>) : super(message, cause) { + this.response = response + } + + constructor(cause: Throwable?, response: MastodonApiErrorResponse<*>) : super(cause) { + this.response = response + } + + constructor( + message: String?, + cause: Throwable?, + enableSuppression: Boolean, + writableStackTrace: Boolean, + response: MastodonApiErrorResponse<*>, + ) : super( + message, + cause, + enableSuppression, + writableStackTrace + ) { + this.response = response + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/ServerException.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/ServerException.kt new file mode 100644 index 00000000..d2d6b187 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/ServerException.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.domain.exception + +import dev.usbharu.hideout.mastodon.domain.model.MastodonApiErrorResponse + +open class ServerException(response: MastodonApiErrorResponse<*>) : MastodonApiException(response) diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/StatusNotFoundException.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/StatusNotFoundException.kt new file mode 100644 index 00000000..934403ec --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/exception/StatusNotFoundException.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.domain.exception + +import dev.usbharu.hideout.domain.mastodon.model.generated.NotFoundResponse +import dev.usbharu.hideout.mastodon.domain.model.MastodonApiErrorResponse + +class StatusNotFoundException : ClientException { + + constructor(response: MastodonApiErrorResponse) : super(response) + + constructor(message: String?, response: MastodonApiErrorResponse) : super(message, response) + constructor(message: String?, cause: Throwable?, response: MastodonApiErrorResponse) : super( + message, + cause, + response + ) + constructor(cause: Throwable?, response: MastodonApiErrorResponse) : super(cause, response) + + constructor( + message: String?, + cause: Throwable?, + enableSuppression: Boolean, + writableStackTrace: Boolean, + response: MastodonApiErrorResponse, + ) : super(message, cause, enableSuppression, writableStackTrace, response) + + fun getTypedResponse(): MastodonApiErrorResponse = + response as MastodonApiErrorResponse + + companion object { + fun ofId(id: Long): StatusNotFoundException = StatusNotFoundException( + "id: $id was not found.", + MastodonApiErrorResponse( + NotFoundResponse( + "Record not found" + ), + 404 + ), + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonApiErrorResponse.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonApiErrorResponse.kt new file mode 100644 index 00000000..ee19ebcf --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/domain/model/MastodonApiErrorResponse.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.domain.model + +data class MastodonApiErrorResponse(val response: R, val statusCode: Int) diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt index 007eb561..3194f368 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/exposedquery/StatusQueryServiceImpl.kt @@ -122,12 +122,13 @@ class StatusQueryServiceImpl : StatusQueryService { ) } - override suspend fun findByPostId(id: Long): Status { + override suspend fun findByPostId(id: Long): Status? { val map = Posts .leftJoin(PostsMedia) .leftJoin(Actors) .leftJoin(Media) - .selectAll().where { Posts.id eq id } + .selectAll() + .where { Posts.id eq id } .groupBy { it[Posts.id] } .map { it.value } .map { @@ -138,7 +139,7 @@ class StatusQueryServiceImpl : StatusQueryService { emojis = it.mapNotNull { resultRow -> resultRow.toCustomEmojiOrNull()?.toMastodonEmoji() } ) to it.first()[Posts.repostId] } - return resolveReplyAndRepost(map).single() + return resolveReplyAndRepost(map).singleOrNull() } private fun resolveReplyAndRepost(pairs: List>): List { diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt new file mode 100644 index 00000000..003b071f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/infrastructure/springweb/MastodonApiControllerAdvice.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 usbharu + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.usbharu.hideout.mastodon.infrastructure.springweb + +import dev.usbharu.hideout.domain.mastodon.model.generated.NotFoundResponse +import dev.usbharu.hideout.domain.mastodon.model.generated.UnprocessableEntityResponse +import dev.usbharu.hideout.domain.mastodon.model.generated.UnprocessableEntityResponseDetails +import dev.usbharu.hideout.mastodon.domain.exception.StatusNotFoundException +import dev.usbharu.hideout.mastodon.interfaces.api.account.MastodonAccountApiController +import dev.usbharu.hideout.mastodon.interfaces.api.apps.MastodonAppsApiController +import dev.usbharu.hideout.mastodon.interfaces.api.filter.MastodonFilterApiController +import dev.usbharu.hideout.mastodon.interfaces.api.instance.MastodonInstanceApiController +import dev.usbharu.hideout.mastodon.interfaces.api.media.MastodonMediaApiController +import dev.usbharu.hideout.mastodon.interfaces.api.notification.MastodonNotificationApiController +import dev.usbharu.hideout.mastodon.interfaces.api.status.MastodonStatusesApiContoller +import dev.usbharu.hideout.mastodon.interfaces.api.timeline.MastodonTimelineApiController +import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.validation.BindException +import org.springframework.validation.FieldError +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler + +@ControllerAdvice( + assignableTypes = [ + MastodonAccountApiController::class, + MastodonAppsApiController::class, + MastodonFilterApiController::class, + MastodonInstanceApiController::class, + MastodonMediaApiController::class, + MastodonNotificationApiController::class, + MastodonStatusesApiContoller::class, + MastodonTimelineApiController::class + ] +) +class MastodonApiControllerAdvice { + + @ExceptionHandler(BindException::class) + fun handleException(ex: BindException): ResponseEntity { + logger.debug("Failed bind entity.", ex) + + val details = mutableMapOf>() + + ex.allErrors.forEach { + val defaultMessage = it.defaultMessage + when { + it is FieldError -> { + val code = when (it.code) { + "Email" -> "ERR_INVALID" + "Pattern" -> "ERR_INVALID" + else -> "ERR_INVALID" + } + details.getOrPut(it.field) { + mutableListOf() + }.add(UnprocessableEntityResponseDetails(code, defaultMessage.orEmpty())) + } + + defaultMessage?.startsWith("Parameter specified as non-null is null:") == true -> { + val parameter = defaultMessage.substringAfterLast("parameter ") + + details.getOrPut(parameter) { + mutableListOf() + }.add(UnprocessableEntityResponseDetails("ERR_BLANK", "can't be blank")) + } + + else -> { + logger.warn("Unknown validation error", ex) + } + } + } + + val message = details.map { + it.key + " " + it.value.joinToString { responseDetails -> responseDetails.description } + }.joinToString() + + return ResponseEntity.unprocessableEntity() + .body(UnprocessableEntityResponse(message, details)) + } + + @ExceptionHandler(StatusNotFoundException::class) + fun handleException(ex: StatusNotFoundException): ResponseEntity { + logger.warn("Status not found.", ex) + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getTypedResponse().response) + } + + companion object { + private val logger = LoggerFactory.getLogger(MastodonApiControllerAdvice::class.java) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiContoller.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiContoller.kt index 0ee0d263..20858ec6 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiContoller.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/interfaces/api/status/MastodonStatusesApiContoller.kt @@ -57,5 +57,9 @@ class MastodonStatusesApiContoller( return ResponseEntity.ok(statusesApiService.emojiReactions(id.toLong(), uid, emoji)) } - override suspend fun apiV1StatusesIdGet(id: String): ResponseEntity = super.apiV1StatusesIdGet(id) + override suspend fun apiV1StatusesIdGet(id: String): ResponseEntity { + val uid = loginUserContextHolder.getLoginUserIdOrNull() + + return ResponseEntity.ok(statusesApiService.findById(id.toLong(), uid)) + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt index b67a3308..e5640509 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/query/StatusQueryService.kt @@ -37,5 +37,5 @@ interface StatusQueryService { page: Page ): PaginationList - suspend fun findByPostId(id: Long): Status + suspend fun findByPostId(id: Long): Status? } diff --git a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/status/StatusesApiService.kt b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/status/StatusesApiService.kt index a6e4598e..07ae96c8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/mastodon/service/status/StatusesApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/mastodon/service/status/StatusesApiService.kt @@ -29,6 +29,7 @@ import dev.usbharu.hideout.core.service.post.PostService import dev.usbharu.hideout.core.service.reaction.ReactionService import dev.usbharu.hideout.domain.mastodon.model.generated.Status import dev.usbharu.hideout.domain.mastodon.model.generated.Status.Visibility.* +import dev.usbharu.hideout.mastodon.domain.exception.StatusNotFoundException import dev.usbharu.hideout.mastodon.interfaces.api.status.StatusesRequest import dev.usbharu.hideout.mastodon.interfaces.api.status.toPostVisibility import dev.usbharu.hideout.mastodon.interfaces.api.status.toStatusVisibility @@ -43,25 +44,25 @@ import java.time.Instant interface StatusesApiService { suspend fun postStatus( statusesRequest: StatusesRequest, - userId: Long + userId: Long, ): Status suspend fun findById( id: Long, - userId: Long? - ): Status? + userId: Long?, + ): Status suspend fun emojiReactions( postId: Long, userId: Long, - emojiName: String - ): Status? + emojiName: String, + ): Status suspend fun removeEmojiReactions( postId: Long, userId: Long, - emojiName: String - ): Status? + emojiName: String, + ): Status } @Service @@ -76,12 +77,12 @@ class StatsesApiServiceImpl( private val statusQueryService: StatusQueryService, private val relationshipRepository: RelationshipRepository, private val reactionService: ReactionService, - private val emojiService: EmojiService + private val emojiService: EmojiService, ) : StatusesApiService { override suspend fun postStatus( statusesRequest: StatusesRequest, - userId: Long + userId: Long, ): Status = transaction.transaction { logger.debug("START create post by mastodon api. {}", statusesRequest) @@ -140,39 +141,49 @@ class StatsesApiServiceImpl( ) } - override suspend fun findById(id: Long, userId: Long?): Status? { - val status = statusQueryService.findByPostId(id) + override suspend fun findById(id: Long, userId: Long?): Status = transaction.transaction { + val status = statusQueryService.findByPostId(id) ?: statusNotFound(id) - return status(status, userId) + return@transaction status(status, userId) + } + + private fun accessDenied(id: String): Nothing { + logger.debug("Access Denied $id") + throw StatusNotFoundException.ofId(id.toLong()) + } + + private fun statusNotFound(id: Long): Nothing { + logger.debug("Status Not Found $id") + throw StatusNotFoundException.ofId(id) } private suspend fun status( status: Status, - userId: Long? - ): Status? { + userId: Long?, + ): Status { return when (status.visibility) { public -> status unlisted -> status private -> { if (userId == null) { - return null + accessDenied(status.id) } val relationship = relationshipRepository.findByUserIdAndTargetUserId(userId, status.account.id.toLong()) - ?: return null + ?: accessDenied(status.id) if (relationship.following) { return status } - return null + accessDenied(status.id) } - direct -> null + direct -> accessDenied(status.id) } } - override suspend fun emojiReactions(postId: Long, userId: Long, emojiName: String): Status? { - status(statusQueryService.findByPostId(postId), userId) ?: return null + override suspend fun emojiReactions(postId: Long, userId: Long, emojiName: String): Status { + status(statusQueryService.findByPostId(postId) ?: statusNotFound(postId), userId) val emoji = try { if (EmojiUtil.isEmoji(emojiName)) { @@ -186,13 +197,13 @@ class StatsesApiServiceImpl( UnicodeEmoji("❤") } reactionService.sendReaction(emoji, userId, postId) - return statusQueryService.findByPostId(postId) + return statusQueryService.findByPostId(postId) ?: statusNotFound(postId) } - override suspend fun removeEmojiReactions(postId: Long, userId: Long, emojiName: String): Status? { + override suspend fun removeEmojiReactions(postId: Long, userId: Long, emojiName: String): Status { reactionService.removeReaction(userId, postId) - return status(statusQueryService.findByPostId(postId), userId) + return status(statusQueryService.findByPostId(postId) ?: statusNotFound(postId), userId) } companion object { diff --git a/src/main/resources/openapi/mastodon.yaml b/src/main/resources/openapi/mastodon.yaml index 6e5478d0..e104eff7 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -250,6 +250,8 @@ paths: application/json: schema: $ref: "#/components/schemas/Application" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/accounts/verify_credentials: get: @@ -265,6 +267,12 @@ paths: application/json: schema: $ref: "#/components/schemas/CredentialAccount" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/accounts: post: @@ -285,6 +293,10 @@ paths: responses: 200: description: 成功 + 401: + $ref: "#/components/responses/unauthorized" + 429: + $ref: "#/components/responses/rateLimited" /api/v1/accounts/relationships: get: @@ -316,6 +328,10 @@ paths: type: array items: $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/accounts/update_credentials: patch: @@ -337,6 +353,10 @@ paths: application/json: schema: $ref: "#/components/schemas/Account" + 401: + $ref: "#/components/responses/unauthorized" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/accounts/{id}: get: @@ -357,6 +377,10 @@ paths: application/json: schema: $ref: "#/components/schemas/Account" + 401: + $ref: "#/components/responses/unauthorized" + 404: + $ref: "#/components/responses/notfound" /api/v1/accounts/{id}/follow: post: @@ -387,6 +411,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/accounts/{id}/block: post: @@ -408,6 +438,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/accounts/{id}/unfollow: post: @@ -429,6 +465,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/accounts/{id}/unblock: post: @@ -450,6 +492,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" + /api/v1/accounts/{id}/remove_from_followers: post: tags: @@ -470,6 +519,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/accounts/{id}/mute: post: @@ -491,6 +546,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/accounts/{id}/unmute: post: @@ -512,6 +573,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Relationship" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/mutes: get: @@ -544,6 +611,12 @@ paths: schema: items: $ref: "#/components/schemas/Account" + 401: + $ref: "#/components/responses/unauthorized" + 403: + $ref: "#/components/responses/forbidden" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/accounts/{id}/statuses: get: @@ -620,6 +693,11 @@ paths: items: $ref: "#/components/schemas/Status" + 401: + $ref: "#/components/responses/unauthorized" + 404: + $ref: "#/components/responses/notfound" + /api/v1/timelines/public: get: tags: @@ -707,6 +785,11 @@ paths: type: array items: $ref: "#/components/schemas/Status" + 206: + $ref: "#/components/responses/partialContent" + 401: + $ref: "#/components/responses/unauthorized" + /api/v1/media: post: tags: @@ -730,6 +813,10 @@ paths: application/json: schema: $ref: "#/components/schemas/MediaAttachment" + 401: + $ref: "#/components/responses/unauthorized" + 422: + $ref: "#/components/responses/unprocessableEntity" /api/v1/follow_requests: get: @@ -1368,6 +1455,8 @@ components: type: object properties: username: + + nullable: false type: string minLength: 1 maxLength: 300 @@ -2748,6 +2837,110 @@ components: - created_at - account + UnprocessableEntityResponse: + type: object + properties: + error: + type: string + details: + type: array + additionalProperties: + type: array + items: + $ref: "#/components/schemas/UnprocessableEntityResponseDetails" + + UnprocessableEntityResponseDetails: + type: object + properties: + error: + type: string + description: + type: string + nullable: false + required: + - error + - description + + UnauthorizedResponse: + type: object + properties: + error: + type: string + default: "The access token is invalid" + required: + - error + + RateLimitedResponse: + type: object + properties: + error: + type: string + default: "Too many requests" + required: + - error + + ForbiddenResponse: + type: object + properties: + error: + type: string + required: + - error + + NotFoundResponse: + type: object + properties: + error: + type: string + required: + - error + + PartialContentResponse: + type: object + + + responses: + forbidden: + description: forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/ForbiddenResponse" + unprocessableEntity: + description: Unprocessable entity + content: + application/json: + schema: + $ref: "#/components/schemas/UnprocessableEntityResponse" + + unauthorized: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/UnauthorizedResponse" + + rateLimited: + description: Too many requests + content: + application/json: + schema: + $ref: "#/components/schemas/RateLimitedResponse" + + notfound: + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/NotFoundResponse" + + partialContent: + description: Partial content + content: + application/json: + schema: + $ref: "#/components/schemas/PartialContentResponse" + securitySchemes: OAuth2: type: oauth2 diff --git a/templates/beanValidationModel.mustache b/templates/beanValidationModel.mustache index 99635314..0d4b99fa 100644 --- a/templates/beanValidationModel.mustache +++ b/templates/beanValidationModel.mustache @@ -35,4 +35,5 @@ isLong set Not Integer, not Long => we have a decimal value! }}{{^isInteger}}{{^isLong}}{{#minimum}} @get:DecimalMin("{{.}}"){{/minimum}}{{#maximum}} - @get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}} \ No newline at end of file + @get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}} + diff --git a/templates/dataClassReqVar.mustache b/templates/dataClassReqVar.mustache index 1812c1cf..9ae7d3a9 100644 --- a/templates/dataClassReqVar.mustache +++ b/templates/dataClassReqVar.mustache @@ -1,4 +1,4 @@ {{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}} @Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} - @ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}} + @ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{^isNullable}}@get:NotNull{{/isNullable}} @get:JsonProperty("{{{baseName}}}", required = true){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}