Skip to content

Commit

Permalink
feat: API 명세를 정의, 문서를 제작 (#7) (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
seokjin8678 authored Dec 12, 2023
1 parent 76666c7 commit a003a20
Show file tree
Hide file tree
Showing 36 changed files with 1,265 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ out/

### VS Code ###
.vscode/

### Docs ###
/src/main/resources/static/docs/*
38 changes: 31 additions & 7 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ repositories {
mavenCentral()
}

val snippetsDir by extra { file("build/generated-snippets") }
val asciidoctorExt = "asciidoctorExt"
configurations.create(asciidoctorExt) {
extendsFrom(configurations.testImplementation.get())
}

val snippetsDir = file("build/generated-snippets")

dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
Expand All @@ -51,6 +56,9 @@ dependencies {

// https://mvnrepository.com/artifact/com.github.f4b6a3/ulid-creator
implementation("com.github.f4b6a3:ulid-creator:5.2.2")
asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor")

implementation("io.github.oshai:kotlin-logging-jvm:5.1.1")
}

tasks.withType<KotlinCompile> {
Expand All @@ -60,21 +68,37 @@ tasks.withType<KotlinCompile> {
}
}

tasks.withType<Test> {
useJUnitPlatform()
}

tasks.test {
outputs.dir(snippetsDir)
useJUnitPlatform()
}

tasks.asciidoctor {
inputs.dir(snippetsDir)
dependsOn(tasks.test)
configurations(asciidoctorExt)
baseDirFollowsSourceFile()
}

val copyDocument = tasks.register<Copy>("copyDocument") {
dependsOn(tasks.asciidoctor)
doFirst {
delete(file("src/main/resources/static/docs"))
}
from(file("build/docs/asciidoc"))
into(file("src/main/resources/static/docs"))
}

tasks.build {
dependsOn(copyDocument)
}

tasks.bootJar {
dependsOn(copyDocument)
}

allOpen {
annotation("jakarta.persistence.Entity")
annotation ("jakarta.persistence.Embeddable")
annotation ("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
annotation("jakarta.persistence.MappedSuperclass")
}
7 changes: 7 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
= Star Citizen Korean Translation Project - API Docs
:doctype: book
:toc: left
:source-highlighter: highlightjs
:sectlinks:

include::news-api.adoc[]
29 changes: 29 additions & 0 deletions src/docs/asciidoc/news-api.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[[news-api]]
== 뉴스

[[news-create]]
=== 뉴스 생성
*요청*
include::{snippets}/news/create/http-request.adoc[]
include::{snippets}/news/create/request-fields.adoc[]
*응답*
include::{snippets}/news/create/http-response.adoc[]
include::{snippets}/news/create/response-fields.adoc[]

[[news-find-all]]
=== 뉴스 전체 조회
*요청*
include::{snippets}/news/find-all/http-request.adoc[]
*응답*
include::{snippets}/news/find-all/http-response.adoc[]
include::{snippets}/news/find-all/response-fields.adoc[]

[[news-find-detail]]
=== 뉴스 상세 조회
*요청*
include::{snippets}/news/find-detail/http-request.adoc[]
include::{snippets}/news/find-detail/path-parameters.adoc[]
include::{snippets}/news/find-detail/query-parameters.adoc[]
*응답*
include::{snippets}/news/find-detail/http-response.adoc[]
include::{snippets}/news/find-detail/response-fields.adoc[]
22 changes: 22 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/api/common/ApiResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kr.galaxyhub.sc.api.common

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonInclude.Include

@JsonInclude(Include.NON_NULL)
class ApiResponse<T>(
val message: String? = null,
val data: T,
) {

companion object {

fun error(message: String): ApiResponse<Unit> {
return ApiResponse(message, Unit)
}

fun <T> success(data: T): ApiResponse<T> {
return ApiResponse(data = data)
}
}
}
63 changes: 63 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/api/common/ExceptionHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package kr.galaxyhub.sc.api.common

import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletRequest
import kr.galaxyhub.sc.common.exception.GalaxyhubException
import kr.galaxyhub.sc.common.support.LogLevel
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MissingServletRequestParameterException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler

private val log = KotlinLogging.logger {}

@RestControllerAdvice
class ExceptionHandler : ResponseEntityExceptionHandler() {

override fun handleMissingServletRequestParameter(
ex: MissingServletRequestParameterException,
headers: HttpHeaders,
status: HttpStatusCode,
request: WebRequest,
): ResponseEntity<Any> {
return ResponseEntity(
ApiResponse("쿼리 파라미터에 누락된 값이 있습니다.", ex.missingParameters()),
HttpStatus.BAD_REQUEST
)
}

private fun MissingServletRequestParameterException.missingParameters(): String {
return this.detailMessageArguments.contentToString()
}

@ExceptionHandler(GalaxyhubException::class)
fun handleGalaxyhubException(
e: GalaxyhubException,
request: HttpServletRequest,
): ResponseEntity<ApiResponse<Unit>> {
when (e.logLevel) {
LogLevel.ERROR -> log.error(e) { "[🔴ERROR] - (${request.method} ${request.requestURI})" }
LogLevel.WARN -> log.warn(e) { "[🟠WARN] - (${request.method} ${request.requestURI})" }
LogLevel.INFO -> log.warn(e) { "[🔵INFO] - (${request.method} ${request.requestURI})" }
LogLevel.DEBUG -> log.debug(e) { "[🟢DEBUG] - (${request.method} ${request.requestURI})" }
}
return ResponseEntity(ApiResponse.error(e.message!!), e.httpStatus)
}

@ExceptionHandler(Exception::class)
fun handleException(e: Exception, request: HttpServletRequest): ResponseEntity<ApiResponse<Unit>> {
log.error(e) { "[🔴ERROR] - (${request.method} ${request.requestURI})" }
return ResponseEntity(DEFAULT_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
}

companion object {

private val DEFAULT_ERROR = ApiResponse.error("서버 내부에 알 수 없는 문제가 발생했습니다.")
}
}

53 changes: 53 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/api/v1/news/NewsControllerV1.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package kr.galaxyhub.sc.api.v1.news

import java.util.UUID
import kr.galaxyhub.sc.api.common.ApiResponse
import kr.galaxyhub.sc.api.v1.news.dto.NewsCreateRequest
import kr.galaxyhub.sc.common.support.toUri
import kr.galaxyhub.sc.news.application.NewsCommandService
import kr.galaxyhub.sc.news.application.NewsQueryService
import kr.galaxyhub.sc.news.application.dto.NewsDetailResponse
import kr.galaxyhub.sc.news.application.dto.NewsResponse
import kr.galaxyhub.sc.news.domain.Language
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/v1/news")
class NewsControllerV1(
private val newsCommandService: NewsCommandService,
private val newsQueryService: NewsQueryService,
) {

@GetMapping
fun findAll(): ResponseEntity<ApiResponse<List<NewsResponse>>> {
val response = newsQueryService.findAll()
return ResponseEntity.ok()
.body(ApiResponse.success(response))
}

@GetMapping("/{id}")
fun findDetailById(
@PathVariable id: UUID,
@RequestParam language: Language,
): ResponseEntity<ApiResponse<NewsDetailResponse>> {
val response = newsQueryService.getDetailByIdAndLanguage(id, language)
return ResponseEntity.ok()
.body(ApiResponse.success(response))
}

@PostMapping
fun create(
@RequestBody request: NewsCreateRequest,
): ResponseEntity<ApiResponse<UUID>> {
val id = newsCommandService.create(request.toCommand())
return ResponseEntity.created("/api/v1/news/${id}".toUri())
.body(ApiResponse.success(id))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package kr.galaxyhub.sc.api.v1.news.dto

import java.time.ZonedDateTime
import kr.galaxyhub.sc.news.application.NewsCreateCommand
import kr.galaxyhub.sc.news.domain.Language
import kr.galaxyhub.sc.news.domain.NewsType

data class NewsCreateRequest(
val newsType: NewsType,
val title: String,
val excerpt: String?,
val publishedAt: ZonedDateTime,
val originId: Long,
val originUrl: String,
val language: Language,
val content: String,
) {

fun toCommand() = NewsCreateCommand(
newsType = newsType,
title = title,
excerpt = excerpt,
publishedAt = publishedAt,
originId = originId,
originUrl = originUrl,
language = language,
content = content
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.galaxyhub.sc.common.exception

import kr.galaxyhub.sc.common.support.LogLevel
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode

class BadRequestException(
message: String,
httpStatus: HttpStatusCode = HttpStatus.BAD_REQUEST,
logLevel: LogLevel = LogLevel.INFO,
) : GalaxyhubException(message, httpStatus, logLevel)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.galaxyhub.sc.common.exception

import kr.galaxyhub.sc.common.support.LogLevel
import org.springframework.http.HttpStatusCode

open class GalaxyhubException(
message: String,
val httpStatus: HttpStatusCode,
val logLevel: LogLevel,
) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kr.galaxyhub.sc.common.exception

import kr.galaxyhub.sc.common.support.LogLevel
import org.springframework.http.HttpStatus
import org.springframework.http.HttpStatusCode

class NotFoundException(
message: String,
httpStatus: HttpStatusCode = HttpStatus.NOT_FOUND,
logLevel: LogLevel = LogLevel.DEBUG,
) : GalaxyhubException(message, httpStatus, logLevel)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.galaxyhub.sc.common.support

import kr.galaxyhub.sc.common.exception.BadRequestException

inline fun validate(value: Boolean, lazyMessage: () -> Any) {
if (value) {
val message = lazyMessage()
throw BadRequestException(message.toString())
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/common/support/LogLevel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kr.galaxyhub.sc.common.support

enum class LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
;
}
5 changes: 5 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/common/support/StringToUri.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kr.galaxyhub.sc.common.support

import java.net.URI

fun String.toUri(): URI = URI.create(this)
Loading

0 comments on commit a003a20

Please sign in to comment.