├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── docs └── asciidoc │ ├── index.adoc │ ├── user-create.adoc │ ├── user-delete.adoc │ ├── user-grant.adoc │ ├── user-search.adoc │ └── user-update.adoc ├── main ├── kotlin │ └── com │ │ └── example │ │ └── restdocs │ │ ├── RestdocsApplication.kt │ │ ├── configuration │ │ └── WebMvcConfig.kt │ │ └── user │ │ ├── controller │ │ ├── ResponseCode.kt │ │ └── UserController.kt │ │ ├── dto │ │ ├── Response.kt │ │ └── UserDto.kt │ │ ├── repository │ │ ├── Role.kt │ │ └── User.kt │ │ └── service │ │ ├── EmptyUserService.kt │ │ └── UserService.kt └── resources │ ├── application.yml │ └── static │ └── docs │ ├── index.html │ ├── user-create.html │ ├── user-delete.html │ ├── user-grant.html │ ├── user-search.html │ └── user-update.html └── test ├── kotlin └── com │ └── example │ └── restdocs │ ├── docs │ ├── ResponseCodeController.kt │ ├── ResponseCodeDocs.kt │ ├── ResponseCodeSnippet.kt │ ├── RestApiDocumentUtils.kt │ └── RestDocsExtensions.kt │ └── user │ └── UserControllerTest.kt └── resources └── org └── springframework └── restdocs └── templates ├── path-parameters.snippet ├── request-fields.snippet └── response-code-fields.snippet /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle/ 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | *.idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | *.vscode/ 33 | 34 | */out/ 35 | */build/ 36 | */bin/ 37 | */src/main/resources/static/docs 38 | /src/main/resources/static/docs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-rest-docs-example 2 | Spring Rest Docs 해보기 (아래 블로그를 참고 바랍니다.) 3 | https://jaehun2841.github.io/2019/08/04/2019-08-04-spring-rest-docs/#more 4 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | kotlinVersion = '1.3.41' 4 | springBootVersion = "2.1.6.RELEASE" 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 13 | } 14 | } 15 | 16 | plugins { 17 | id 'org.asciidoctor.convert' version '1.5.9.2' 18 | id 'org.springframework.boot' version '2.1.6.RELEASE' 19 | id "io.spring.dependency-management" version "1.0.5.RELEASE" 20 | } 21 | 22 | apply plugin: 'io.spring.dependency-management' 23 | apply plugin: 'kotlin' 24 | apply plugin: "groovy" 25 | apply plugin: "org.springframework.boot" 26 | apply plugin: "io.spring.dependency-management" 27 | apply plugin: "kotlin-kapt" 28 | 29 | group = 'com.example' 30 | version = '0.0.1-SNAPSHOT' 31 | sourceCompatibility = '1.8' 32 | 33 | repositories { 34 | mavenCentral() 35 | } 36 | 37 | ext { 38 | set('snippetsDir', file("build/generated-snippets")) 39 | } 40 | 41 | dependencies { 42 | implementation 'org.springframework.boot:spring-boot-starter-web' 43 | testImplementation("org.springframework.boot:spring-boot-starter-test") { 44 | exclude group: "junit", module: "junit" 45 | } 46 | testImplementation "org.junit.jupiter:junit-jupiter-api" 47 | testImplementation "org.junit.jupiter:junit-jupiter-params" 48 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" 49 | testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' 50 | testImplementation "com.nhaarman:mockito-kotlin:1.6.0" 51 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" 52 | } 53 | 54 | kapt { 55 | useBuildCache = true 56 | correctErrorTypes = true 57 | } 58 | 59 | test { 60 | useJUnitPlatform() 61 | outputs.dir snippetsDir 62 | } 63 | 64 | asciidoctor { 65 | inputs.dir snippetsDir 66 | dependsOn test 67 | } 68 | 69 | compileKotlin { 70 | kotlinOptions { 71 | freeCompilerArgs = ["-Xjsr305=strict"] 72 | jvmTarget = "1.8" 73 | } 74 | } 75 | 76 | compileTestKotlin { 77 | kotlinOptions { 78 | freeCompilerArgs = ["-Xjsr305=strict"] 79 | jvmTarget = "1.8" 80 | } 81 | } 82 | 83 | jar.enabled = false 84 | bootJar.enabled = true 85 | bootJar.mainClassName = 'com.example.restdocs.RestdocsApplication' 86 | 87 | asciidoctor.doFirst { 88 | println "=====start asciidoctor" 89 | delete file('src/main/resources/static/docs') 90 | } 91 | asciidoctor.doLast { 92 | println "=====finish asciidoctor" 93 | } 94 | 95 | task copyDocument(type: Copy) { 96 | dependsOn asciidoctor 97 | from file("build/asciidoc/html5") 98 | into file("src/main/resources/static/docs") // resources/static/docs 로 복사하여 서버가 돌아가고 있을때 /docs/index.html 로 접속하면 볼수 있음 99 | } 100 | 101 | build { 102 | dependsOn copyDocument 103 | } 104 | 105 | bootJar { 106 | archiveName = 'app.jar' 107 | dependsOn asciidoctor 108 | from ("${asciidoctor.outputDir}/html5") { 109 | into "BOOT-INF/classes/static/docs" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaehun2841/spring-rest-docs-example/e5f1c71e385824a8c20f304425b7f6b0b4003cfd/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Aug 03 18:44:39 KST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'restdocs' 2 | -------------------------------------------------------------------------------- /src/docs/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | ifndef::snippets[] 2 | :snippet: ../../../build/generated-snippets 3 | :root: ./ 4 | endif::[] 5 | :doctype: book 6 | :icons: font 7 | :source-highlighter: highlightjs 8 | :toc: left 9 | :toclevels: 4 10 | :sectlinks: 11 | :site-url: /build/asciidoc/html5/ 12 | = User API 13 | 14 | = 공통 Response Code 15 | include::{snippet}/common/response-code-fields.adoc[] 16 | = 사용자 조회 API 17 | include::{root}/user-search.adoc[] 18 | = 사용자 생성 API 19 | include::{root}/user-create.adoc[] 20 | = 사용자 수정 API 21 | include::{root}/user-update.adoc[] 22 | = 사용자 삭제 API 23 | include::{root}/user-delete.adoc[] 24 | = 사용자 권한 추가 API 25 | include::{root}/user-grant.adoc[] 26 | -------------------------------------------------------------------------------- /src/docs/asciidoc/user-create.adoc: -------------------------------------------------------------------------------- 1 | ifndef::snippets[] 2 | :snippets: ../../../build/generated-snippets 3 | endif::[] 4 | :doctype: book 5 | :icons: font 6 | :source-highlighter: highlightjs 7 | :toc: left 8 | :toclevels: 4 9 | :sectlinks: 10 | :site-url: /build/asciidoc/html5/ 11 | 12 | 13 | == Request 14 | 15 | === [Request URL] 16 | .... 17 | POST /user 18 | Content-Type: application/json;charset=UTF-8 19 | .... 20 | 21 | === [Request Headers] 22 | include::{snippets}/user-create/request-headers.adoc[] 23 | 24 | === [Request body] 25 | include::{snippets}/user-create/request-body.adoc[] 26 | 27 | === [Request HTTP Example] 28 | 29 | include::{snippets}/user-create/http-request.adoc[] 30 | 31 | == Response 32 | 33 | 34 | === [Response Fields] 35 | 36 | include::{snippets}/user-create/response-fields.adoc[] 37 | 38 | === [Response HTTP Example] 39 | 40 | include::{snippets}/user-create/http-response.adoc[] -------------------------------------------------------------------------------- /src/docs/asciidoc/user-delete.adoc: -------------------------------------------------------------------------------- 1 | ifndef::snippets[] 2 | :snippets: ../../../build/generated-snippets 3 | endif::[] 4 | :doctype: book 5 | :icons: font 6 | :source-highlighter: highlightjs 7 | :toc: left 8 | :toclevels: 4 9 | :sectlinks: 10 | :site-url: /build/asciidoc/html5/ 11 | 12 | 13 | == Request 14 | 15 | === [Request URL] 16 | .... 17 | DELETE /user/{userId} 18 | Content-Type: application/json;charset=UTF-8 19 | .... 20 | 21 | === [Request Headers] 22 | include::{snippets}/user-delete/request-headers.adoc[] 23 | 24 | === [Request Path Parameters] 25 | 26 | include::{snippets}/user-delete/path-parameters.adoc[] 27 | 28 | === [Request HTTP Example] 29 | 30 | include::{snippets}/user-delete/http-request.adoc[] 31 | 32 | == Response 33 | 34 | 35 | === [Response Fields] 36 | 37 | include::{snippets}/user-delete/response-fields.adoc[] 38 | 39 | === [Response HTTP Example] 40 | 41 | include::{snippets}/user-delete/http-response.adoc[] -------------------------------------------------------------------------------- /src/docs/asciidoc/user-grant.adoc: -------------------------------------------------------------------------------- 1 | ifndef::snippets[] 2 | :snippets: ../../../build/generated-snippets 3 | endif::[] 4 | :doctype: book 5 | :icons: font 6 | :source-highlighter: highlightjs 7 | :toc: left 8 | :toclevels: 4 9 | :sectlinks: 10 | :site-url: /build/asciidoc/html5/ 11 | 12 | 13 | == Request 14 | 15 | === [Request URL] 16 | .... 17 | POST /user/{userId}/role/{roleId} 18 | Content-Type: application/json;charset=UTF-8 19 | .... 20 | 21 | === [Request Headers] 22 | include::{snippets}/user-grant-role/request-headers.adoc[] 23 | 24 | === [Request Path Parameters] 25 | 26 | include::{snippets}/user-grant-role/path-parameters.adoc[] 27 | 28 | === [Request HTTP Example] 29 | 30 | include::{snippets}/user-grant-role/http-request.adoc[] 31 | 32 | == Response 33 | 34 | 35 | === [Response Fields] 36 | 37 | include::{snippets}/user-grant-role/response-fields.adoc[] 38 | 39 | === [Response HTTP Example] 40 | 41 | include::{snippets}/user-grant-role/http-response.adoc[] -------------------------------------------------------------------------------- /src/docs/asciidoc/user-search.adoc: -------------------------------------------------------------------------------- 1 | ifndef::snippets[] 2 | :snippets: ../../../build/generated-snippets 3 | endif::[] 4 | :doctype: book 5 | :icons: font 6 | :source-highlighter: highlightjs 7 | :toc: left 8 | :toclevels: 4 9 | :sectlinks: 10 | :site-url: /build/asciidoc/html5/ 11 | 12 | 13 | == Request 14 | 15 | === [Request URL] 16 | .... 17 | GET /user/{userId} 18 | Content-Type: application/json;charset=UTF-8 19 | .... 20 | 21 | === [Request Headers] 22 | include::{snippets}/user-search/request-headers.adoc[] 23 | 24 | === [Request Path Parameters] 25 | 26 | include::{snippets}/user-search/path-parameters.adoc[] 27 | 28 | === [Request HTTP Example] 29 | 30 | include::{snippets}/user-search/http-request.adoc[] 31 | 32 | == Response 33 | 34 | 35 | === [Response Fields] 36 | 37 | include::{snippets}/user-search/response-fields.adoc[] 38 | 39 | === [Response HTTP Example] 40 | 41 | include::{snippets}/user-search/http-response.adoc[] -------------------------------------------------------------------------------- /src/docs/asciidoc/user-update.adoc: -------------------------------------------------------------------------------- 1 | ifndef::snippets[] 2 | :snippets: ../../../build/generated-snippets 3 | endif::[] 4 | :doctype: book 5 | :icons: font 6 | :source-highlighter: highlightjs 7 | :toc: left 8 | :toclevels: 4 9 | :sectlinks: 10 | :site-url: /build/asciidoc/html5/ 11 | 12 | 13 | == Request 14 | 15 | === [Request URL] 16 | .... 17 | PUT /user/{userId} 18 | Content-Type: application/json;charset=UTF-8 19 | .... 20 | 21 | === [Request Headers] 22 | include::{snippets}/user-update/request-headers.adoc[] 23 | 24 | === [Request Path Parameters] 25 | include::{snippets}/user-update/path-parameters.adoc[] 26 | 27 | === [Request Parameters] 28 | include::{snippets}/user-update/request-body.adoc[] 29 | 30 | === [Request HTTP Example] 31 | 32 | include::{snippets}/user-update/http-request.adoc[] 33 | 34 | == Response 35 | 36 | 37 | === [Response Fields] 38 | 39 | include::{snippets}/user-update/response-fields.adoc[] 40 | 41 | === [Response HTTP Example] 42 | 43 | include::{snippets}/user-update/http-response.adoc[] -------------------------------------------------------------------------------- /src/main/kotlin/com/example/restdocs/RestdocsApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | open class RestdocsApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/example/restdocs/configuration/WebMvcConfig.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.configuration 2 | 3 | import org.springframework.context.annotation.Configuration 4 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer 6 | 7 | @Configuration 8 | open class WebMvcConfig: WebMvcConfigurer { 9 | 10 | override fun addResourceHandlers(registry: ResourceHandlerRegistry) { 11 | registry.addResourceHandler("/docs/**").addResourceLocations("classpath:/static/docs/") 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/restdocs/user/controller/ResponseCode.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.user.controller 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat 4 | 5 | @JsonFormat(shape = JsonFormat.Shape.OBJECT) 6 | enum class ResponseCode( 7 | val code: Int, 8 | val message: String 9 | ) { 10 | OK(200, "OK"), 11 | INVALID_PARAMETER(400, "Parameter validation error"), 12 | UNAUTHORIZED_USER(401, "Unauthorized api key."), 13 | INTERNAL_SERVER_ERROR(500, "Internal server error") 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/restdocs/user/controller/UserController.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.user.controller 2 | 3 | import com.example.restdocs.user.dto.Response 4 | import com.example.restdocs.user.dto.UserDto 5 | import com.example.restdocs.user.repository.User 6 | import com.example.restdocs.user.service.UserService 7 | import org.springframework.web.bind.annotation.DeleteMapping 8 | import org.springframework.web.bind.annotation.GetMapping 9 | import org.springframework.web.bind.annotation.PathVariable 10 | import org.springframework.web.bind.annotation.PostMapping 11 | import org.springframework.web.bind.annotation.PutMapping 12 | import org.springframework.web.bind.annotation.RequestBody 13 | import org.springframework.web.bind.annotation.RequestMapping 14 | import org.springframework.web.bind.annotation.RestController 15 | 16 | @Suppress("SpringJavaInjectionPointsAutowiringInspection") 17 | @RestController 18 | @RequestMapping("/user") 19 | open class UserController( 20 | val userService: UserService 21 | ) { 22 | 23 | @GetMapping("/{userId}") 24 | fun getUser(@PathVariable(value = "userId") userId: Long): Response { 25 | val searchUser = userService.search(userId) 26 | return Response.success(searchUser) 27 | } 28 | 29 | @PostMapping 30 | fun createUser(@RequestBody userDto: UserDto): Response { 31 | val createUser = userService.create(User(name = userDto.name, address = userDto.address, age = userDto.age)) 32 | return Response.success(createUser) 33 | } 34 | 35 | 36 | @PutMapping("/{userId}") 37 | fun updateUser(@PathVariable("userId") userId: Long, 38 | @RequestBody userDto: UserDto): Response { 39 | val updateUser = userService.update(User(id = userId, name = userDto.name, address = userDto.address, age = userDto.age)) 40 | return Response.success(updateUser) 41 | } 42 | 43 | @DeleteMapping("/{userId}") 44 | fun deleteUser(@PathVariable(value = "userId") userId: Long): Response { 45 | userService.delete(userId) 46 | return Response.success() 47 | } 48 | 49 | @PostMapping("/{userId}/role/{roleId}") 50 | fun grantRole(@PathVariable(value = "userId") userId: Long, 51 | @PathVariable(value = "roleId") roleId: Long): Response { 52 | userService.grantRole(userId = userId, roleId = roleId) 53 | return Response.success() 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/restdocs/user/dto/Response.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.user.dto 2 | 3 | data class Response( 4 | val code: Int, 5 | val message: String, 6 | val data: T?, 7 | val error: T? 8 | ) { 9 | 10 | companion object { 11 | fun success(): Response = success(null) 12 | fun success(data: T?): Response = Response(200, "OK", data, null) 13 | fun error(error: T?): Response = Response(500, "Server Error", null, error) 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/restdocs/user/dto/UserDto.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.user.dto 2 | 3 | data class UserDto( 4 | val name: String, 5 | val age: Int, 6 | val address: String 7 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/restdocs/user/repository/Role.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.user.repository 2 | 3 | data class Role( 4 | val id: Long, 5 | val name: String 6 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/restdocs/user/repository/User.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.user.repository 2 | 3 | data class User( 4 | val id: Long? = null, 5 | val name: String, 6 | val age: Int, 7 | val address: String, 8 | var roles: MutableList = mutableListOf() 9 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/example/restdocs/user/service/EmptyUserService.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.user.service 2 | 3 | import com.example.restdocs.user.repository.User 4 | import org.springframework.stereotype.Service 5 | 6 | @Service 7 | class EmptyUserService: UserService { 8 | override fun search(userId: Long): User? = null 9 | 10 | override fun create(user: User): User? = null 11 | 12 | override fun update(user: User): User? = null 13 | 14 | override fun delete(userId: Long){ 15 | } 16 | 17 | override fun grantRole(userId: Long, roleId: Long): User? = null 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/example/restdocs/user/service/UserService.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.user.service 2 | 3 | import com.example.restdocs.user.repository.User 4 | 5 | interface UserService { 6 | 7 | fun search(userId: Long): User? 8 | fun create(user: User): User? 9 | fun update(user: User): User? 10 | fun delete(userId: Long) 11 | fun grantRole(userId: Long, roleId: Long): User? 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaehun2841/spring-rest-docs-example/e5f1c71e385824a8c20f304425b7f6b0b4003cfd/src/main/resources/application.yml -------------------------------------------------------------------------------- /src/main/resources/static/docs/user-create.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Request 9 | 10 | 432 | 433 | 434 | 435 | 456 |
457 |
458 |

Request

459 |
460 |
461 |

[Request URL]

462 |
463 |
464 |
POST  /user
465 | Content-Type: application/json;charset=UTF-8
466 |
467 |
468 |
469 |
470 |

[Request Headers]

471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 |
NameDescription

x-api-key

Api Key

489 |
490 |
491 |

[Request body]

492 |
493 |
494 |
{
495 |   "name" : "배달이",
496 |   "age" : 30,
497 |   "address" : "서울특별시 송파구 올림픽로 295"
498 | }
499 |
500 |
501 |
502 |
503 |

[Request HTTP Example]

504 |
505 |
506 |
POST /user HTTP/1.1
507 | Accept: application/json;charset=UTF-8
508 | Content-Type: application/json;charset=UTF-8
509 | Content-Length: 100
510 | Host: user.api.com
511 | x-api-key: API-KEY
512 | 
513 | {
514 |   "name" : "배달이",
515 |   "age" : 30,
516 |   "address" : "서울특별시 송파구 올림픽로 295"
517 | }
518 |
519 |
520 |
521 |
522 |
523 |
524 |

Response

525 |
526 |
527 |

[Response Fields]

528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 |
PathTypeDescription

code

Number

응답 코드

message

String

응답 메세지

error

Object

에러 Data

data

Object

응답 Data

data.id

Number

User ID

data.name

String

이름

data.age

Number

나이

data.address

String

주소

data.roles[].id

Number

Role ID

data.roles[].name

String

Role명

594 |
595 |
596 |

[Response HTTP Example]

597 |
598 |
599 |
HTTP/1.1 200 OK
600 | Content-Type: application/json;charset=UTF-8
601 | Content-Length: 210
602 | 
603 | {
604 |   "code" : 200,
605 |   "message" : "OK",
606 |   "data" : {
607 |     "id" : 1,
608 |     "name" : "배달이",
609 |     "age" : 30,
610 |     "address" : "서울특별시 송파구 올림픽로 295",
611 |     "roles" : [ ]
612 |   },
613 |   "error" : null
614 | }
615 |
616 |
617 |
618 |
619 |
620 |
621 | 626 | 627 | 628 | 629 | 630 | -------------------------------------------------------------------------------- /src/main/resources/static/docs/user-delete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Request 9 | 10 | 432 | 433 | 434 | 435 | 456 |
457 |
458 |

Request

459 |
460 |
461 |

[Request URL]

462 |
463 |
464 |
DELETE  /user/{userId}
465 | Content-Type: application/json;charset=UTF-8
466 |
467 |
468 |
469 |
470 |

[Request Headers]

471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 |
NameDescription

x-api-key

Api Key

489 |
490 |
491 |

[Request Path Parameters]

492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 |
Table 1. /user/{userId}

Parameter

Description

최대길이

비고

userId

USER ID

10

User ID가 없는 경우는 먼저 생성하세

515 |
516 |
517 |

[Request HTTP Example]

518 |
519 |
520 |
DELETE /user/1 HTTP/1.1
521 | Accept: application/json;charset=UTF-8
522 | Host: user.api.com
523 | x-api-key: API-KEY
524 |
525 |
526 |
527 |
528 |
529 |
530 |

Response

531 |
532 |
533 |

[Response Fields]

534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 |
PathTypeDescription

code

Number

응답 코드

message

String

응답 메세지

error

Object

에러 Data

data

Object

응답 Data

570 |
571 |
572 |

[Response HTTP Example]

573 |
574 |
575 |
HTTP/1.1 200 OK
576 | Content-Length: 73
577 | Content-Type: application/json;charset=UTF-8
578 | 
579 | {
580 |   "code" : 200,
581 |   "message" : "OK",
582 |   "data" : null,
583 |   "error" : null
584 | }
585 |
586 |
587 |
588 |
589 |
590 |
591 | 596 | 597 | 598 | 599 | 600 | -------------------------------------------------------------------------------- /src/main/resources/static/docs/user-grant.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Request 9 | 10 | 432 | 433 | 434 | 435 | 456 |
457 |
458 |

Request

459 |
460 |
461 |

[Request URL]

462 |
463 |
464 |
POST /user/{userId}/role/{roleId}
465 | Content-Type: application/json;charset=UTF-8
466 |
467 |
468 |
469 |
470 |

[Request Headers]

471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 |
NameDescription

x-api-key

Api Key

489 |
490 |
491 |

[Request Path Parameters]

492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 |
Table 1. /user/{userId}/role/{roleId}

Parameter

Description

최대길이

비고

userId

USER ID

10

User ID가 없는 경우는 먼저 생성하세

roleId

ROLE ID

521 |
522 |
523 |

[Request HTTP Example]

524 |
525 |
526 |
POST /user/1/role/1 HTTP/1.1
527 | Accept: application/json;charset=UTF-8
528 | Host: user.api.com
529 | x-api-key: API-KEY
530 |
531 |
532 |
533 |
534 |
535 |
536 |

Response

537 |
538 |
539 |

[Response Fields]

540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 |
PathTypeDescription

code

Number

응답 코드

message

String

응답 메세지

error

Object

에러 Data

data

Object

응답 Data

data.id

Number

User ID

data.name

String

이름

data.age

Number

나이

data.address

String

주소

data.roles[].id

Number

Role ID

data.roles[].name

String

Role명

606 |
607 |
608 |

[Response HTTP Example]

609 |
610 |
611 |
HTTP/1.1 200 OK
612 | Content-Length: 73
613 | Content-Type: application/json;charset=UTF-8
614 | 
615 | {
616 |   "code" : 200,
617 |   "message" : "OK",
618 |   "data" : null,
619 |   "error" : null
620 | }
621 |
622 |
623 |
624 |
625 |
626 |
627 | 632 | 633 | 634 | 635 | 636 | -------------------------------------------------------------------------------- /src/main/resources/static/docs/user-search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Request 9 | 10 | 432 | 433 | 434 | 435 | 456 |
457 |
458 |

Request

459 |
460 |
461 |

[Request URL]

462 |
463 |
464 |
GET /user/{userId}
465 | Content-Type: application/json;charset=UTF-8
466 |
467 |
468 |
469 |
470 |

[Request Headers]

471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 |
NameDescription

x-api-key

Api Key

489 |
490 |
491 |

[Request Path Parameters]

492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 |
Table 1. /user/{userId}

Parameter

Description

최대길이

비고

userId

USER ID

10

User ID가 없는 경우는 먼저 생성하세

515 |
516 |
517 |

[Request HTTP Example]

518 |
519 |
520 |
GET /user/1 HTTP/1.1
521 | Accept: application/json;charset=UTF-8
522 | Host: user.api.com
523 | x-api-key: API-KEY
524 |
525 |
526 |
527 |
528 |
529 |
530 |

Response

531 |
532 |
533 |

[Response Fields]

534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 |
PathTypeDescription

code

Number

응답 코드

message

String

응답 메세지

error

Object

에러 Data

data

Object

응답 Data

data.id

Number

User ID

data.name

String

이름

data.age

Number

나이

data.address

String

주소

data.roles[].id

Number

Role ID

data.roles[].name

String

Role명

600 |
601 |
602 |

[Response HTTP Example]

603 |
604 |
605 |
HTTP/1.1 200 OK
606 | Content-Type: application/json;charset=UTF-8
607 | Content-Length: 210
608 | 
609 | {
610 |   "code" : 200,
611 |   "message" : "OK",
612 |   "data" : {
613 |     "id" : 1,
614 |     "name" : "배달이",
615 |     "age" : 30,
616 |     "address" : "서울특별시 송파구 올림픽로 295",
617 |     "roles" : [ ]
618 |   },
619 |   "error" : null
620 | }
621 |
622 |
623 |
624 |
625 |
626 |
627 | 632 | 633 | 634 | 635 | 636 | -------------------------------------------------------------------------------- /src/test/kotlin/com/example/restdocs/docs/ResponseCodeController.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.docs 2 | 3 | import com.example.restdocs.user.dto.Response 4 | import org.springframework.web.bind.annotation.GetMapping 5 | import org.springframework.web.bind.annotation.RestController 6 | 7 | @RestController 8 | class ResponseCodeController { 9 | 10 | @GetMapping("/response-code") 11 | fun getResponseCode(): Response { 12 | return Response.success() 13 | } 14 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/example/restdocs/docs/ResponseCodeDocs.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.docs 2 | 3 | import com.example.restdocs.user.controller.ResponseCode 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.extension.ExtendWith 6 | import org.springframework.beans.factory.annotation.Autowired 7 | import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs 8 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest 9 | import org.springframework.http.MediaType 10 | import org.springframework.restdocs.RestDocumentationExtension 11 | import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document 12 | import org.springframework.restdocs.payload.FieldDescriptor 13 | import org.springframework.restdocs.payload.JsonFieldType 14 | import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath 15 | import org.springframework.restdocs.snippet.Attributes 16 | import org.springframework.test.context.junit.jupiter.SpringExtension 17 | import org.springframework.test.web.servlet.MockMvc 18 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 19 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers 20 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 21 | import java.util.* 22 | import java.util.stream.Collectors 23 | 24 | @ExtendWith(RestDocumentationExtension::class, SpringExtension::class) 25 | @WebMvcTest(ResponseCodeController::class, secure = false) 26 | @AutoConfigureRestDocs 27 | class ResponseCodeDocs { 28 | 29 | @Autowired 30 | lateinit var mockMvc: MockMvc 31 | 32 | @Test 33 | fun `build response code snippet`() { 34 | 35 | mockMvc.perform(get("/response-code") 36 | .accept(MediaType.APPLICATION_JSON) 37 | ).andDo(MockMvcResultHandlers.print()) 38 | .andExpect(status().isOk) 39 | .andDo( 40 | document( 41 | "common", 42 | responseCodeFields( 43 | "response-code", //{name}-fields.snippet 이라는 파일명으로 생성 44 | Attributes.attributes(Attributes.key("title").value("공통 응답 코드")), 45 | *convertResponseCodeToFieldDescriptor(ResponseCode.values()) 46 | ) 47 | ) 48 | ) 49 | } 50 | 51 | fun responseCodeFields( 52 | name: String, 53 | attributes: Map, 54 | vararg descriptors: FieldDescriptor 55 | ): ResponseCodeSnippet { 56 | return ResponseCodeSnippet(name, mutableListOf(*descriptors), attributes, true) 57 | } 58 | 59 | private fun convertResponseCodeToFieldDescriptor(enumTypes: Array): Array { 60 | return Arrays.stream(enumTypes) 61 | .map { 62 | fieldWithPath(it.code.toString()).type(JsonFieldType.NUMBER).description(it.message).optional() 63 | } 64 | .collect(Collectors.toList()) 65 | .toTypedArray() 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /src/test/kotlin/com/example/restdocs/docs/ResponseCodeSnippet.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.docs 2 | 3 | import org.springframework.http.MediaType 4 | import org.springframework.restdocs.operation.Operation 5 | import org.springframework.restdocs.payload.AbstractFieldsSnippet 6 | import org.springframework.restdocs.payload.FieldDescriptor 7 | 8 | class ResponseCodeSnippet( 9 | name: String, 10 | descriptors: MutableList, 11 | attributes: Map, 12 | ignoreUndocumentedFields: Boolean 13 | ) : AbstractFieldsSnippet(name, descriptors, attributes, ignoreUndocumentedFields) { 14 | 15 | override fun getContentType(operation: Operation): MediaType? { 16 | return operation.response.headers.contentType 17 | } 18 | 19 | override fun getContent(operation: Operation): ByteArray { 20 | return operation.response.content 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/kotlin/com/example/restdocs/docs/RestApiDocumentUtils.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.docs 2 | 3 | import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor 4 | import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor 5 | import org.springframework.restdocs.operation.preprocess.Preprocessors 6 | import org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris 7 | 8 | object RestApiDocumentUtils { 9 | 10 | fun getDocumentRequest(): OperationRequestPreprocessor { 11 | return Preprocessors.preprocessRequest( 12 | modifyUris() 13 | .scheme("http") 14 | .host("user.api.com") 15 | .removePort(), 16 | Preprocessors.prettyPrint() 17 | ) 18 | } 19 | 20 | fun getDocumentResponse(): OperationResponsePreprocessor { 21 | return Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/example/restdocs/docs/RestDocsExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.docs 2 | 3 | import org.springframework.restdocs.snippet.AbstractDescriptor 4 | import org.springframework.restdocs.snippet.Attributes.key 5 | 6 | fun > AbstractDescriptor.remarks(remarks: String): T { 7 | return this.attributes(key("remarks").value(remarks)) 8 | } 9 | 10 | fun > AbstractDescriptor.maxLength(length: Int): T { 11 | return this.attributes(key("maxLength").value(length)) 12 | } 13 | -------------------------------------------------------------------------------- /src/test/kotlin/com/example/restdocs/user/UserControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.restdocs.user 2 | 3 | import com.example.restdocs.docs.RestApiDocumentUtils.getDocumentRequest 4 | import com.example.restdocs.docs.RestApiDocumentUtils.getDocumentResponse 5 | import com.example.restdocs.docs.maxLength 6 | import com.example.restdocs.docs.remarks 7 | import com.example.restdocs.user.controller.UserController 8 | import com.example.restdocs.user.repository.Role 9 | import com.example.restdocs.user.repository.User 10 | import com.example.restdocs.user.service.UserService 11 | import com.nhaarman.mockito_kotlin.any 12 | import org.junit.jupiter.api.Test 13 | import org.junit.jupiter.api.extension.ExtendWith 14 | import org.mockito.ArgumentMatchers.anyLong 15 | import org.mockito.ArgumentMatchers.eq 16 | import org.mockito.BDDMockito.given 17 | import org.springframework.beans.factory.annotation.Autowired 18 | import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs 19 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest 20 | import org.springframework.boot.test.mock.mockito.MockBean 21 | import org.springframework.http.MediaType 22 | import org.springframework.restdocs.RestDocumentationExtension 23 | import org.springframework.restdocs.headers.HeaderDescriptor 24 | import org.springframework.restdocs.headers.HeaderDocumentation.headerWithName 25 | import org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders 26 | import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document 27 | import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders 28 | import org.springframework.restdocs.payload.FieldDescriptor 29 | import org.springframework.restdocs.payload.JsonFieldType 30 | import org.springframework.restdocs.payload.PayloadDocumentation 31 | import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath 32 | import org.springframework.restdocs.payload.PayloadDocumentation.responseFields 33 | import org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath 34 | import org.springframework.restdocs.request.ParameterDescriptor 35 | import org.springframework.restdocs.request.RequestDocumentation.parameterWithName 36 | import org.springframework.restdocs.request.RequestDocumentation.pathParameters 37 | import org.springframework.test.context.junit.jupiter.SpringExtension 38 | import org.springframework.test.web.servlet.MockMvc 39 | import org.springframework.test.web.servlet.result.MockMvcResultHandlers 40 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status 41 | 42 | @ExtendWith(RestDocumentationExtension::class, SpringExtension::class) 43 | @WebMvcTest(UserController::class, secure = false) 44 | @AutoConfigureRestDocs 45 | class UserControllerTest { 46 | 47 | @Autowired 48 | lateinit var mockMvc: MockMvc 49 | 50 | @MockBean 51 | lateinit var userService: UserService 52 | 53 | @Test 54 | fun `user search api docs`() { 55 | 56 | //given 57 | given(userService.search((eq(1L)))) 58 | .willReturn(User(1, "배달이", 30, "서울특별시 송파구 올림픽로 295")) 59 | 60 | //when 61 | val resultActions = mockMvc.perform( 62 | RestDocumentationRequestBuilders.get("/user/{userId}", 1) 63 | .header("x-api-key", "API-KEY") 64 | .accept(MediaType.APPLICATION_JSON_UTF8) 65 | ).andDo(MockMvcResultHandlers.print()) 66 | 67 | //then 68 | val userIdPathParameter = userIdPathParameter() 69 | resultActions 70 | .andExpect(status().isOk) 71 | .andDo( 72 | document( 73 | "user-search", 74 | getDocumentRequest(), 75 | getDocumentResponse(), 76 | requestHeaders(*header()), 77 | pathParameters(userIdPathParameter()), 78 | responseFields(*common()) 79 | .andWithPrefix("data.", *user()) 80 | .andWithPrefix("data.roles[].", *role()) 81 | // responseFields( 82 | // beneathPath("data").withSubsectionId("user"), 83 | // *user(), 84 | // subsectionWithPath("roles").description("User Role")) 85 | ) 86 | ) 87 | } 88 | 89 | @Test 90 | fun `user create api docs`() { 91 | 92 | //given 93 | given(userService.create(any())) 94 | .willReturn(User(1, "배달이", 30, "서울특별시 송파구 올림픽로 295")) 95 | 96 | //when 97 | val resultActions = mockMvc.perform( 98 | RestDocumentationRequestBuilders.post("/user") 99 | .header("x-api-key", "API-KEY") 100 | .accept(MediaType.APPLICATION_JSON_UTF8) 101 | .contentType(MediaType.APPLICATION_JSON_UTF8) 102 | .content(getUserDto()) 103 | ).andDo(MockMvcResultHandlers.print()) 104 | 105 | //then 106 | resultActions 107 | .andExpect(status().isOk) 108 | .andDo( 109 | document( 110 | "user-create", 111 | getDocumentRequest(), 112 | getDocumentResponse(), 113 | requestHeaders(*header()), 114 | responseFields(*common()) 115 | .andWithPrefix("data.", *user()) 116 | .andWithPrefix("data.roles[].", *role()) 117 | ) 118 | ) 119 | } 120 | 121 | @Test 122 | fun `user update api docs`() { 123 | //given 124 | given(userService.update(any())) 125 | .willReturn(User(1, "배달이", 30, "서울특별시 송파구 올림픽로 295")) 126 | 127 | //when 128 | val resultActions = mockMvc.perform( 129 | RestDocumentationRequestBuilders.put("/user/{userId}", 1) 130 | .header("x-api-key", "API-KEY") 131 | .accept(MediaType.APPLICATION_JSON_UTF8) 132 | .contentType(MediaType.APPLICATION_JSON_UTF8) 133 | .content(getUserDto()) 134 | ).andDo(MockMvcResultHandlers.print()) 135 | 136 | //then 137 | resultActions 138 | .andExpect(status().isOk) 139 | .andDo( 140 | document( 141 | "user-update", 142 | getDocumentRequest(), 143 | getDocumentResponse(), 144 | requestHeaders(*header()), 145 | pathParameters(userIdPathParameter()), 146 | responseFields(*common()) 147 | .andWithPrefix("data.", *user()) 148 | .andWithPrefix("data.roles[].", *role()) 149 | ) 150 | ) 151 | } 152 | 153 | @Test 154 | fun `user delete api docs`() { 155 | //given 156 | 157 | //when 158 | val resultActions = mockMvc.perform( 159 | RestDocumentationRequestBuilders.delete("/user/{userId}", 1) 160 | .header("x-api-key", "API-KEY") 161 | .accept(MediaType.APPLICATION_JSON_UTF8) 162 | ).andDo(MockMvcResultHandlers.print()) 163 | 164 | //then 165 | resultActions 166 | .andExpect(status().isOk) 167 | .andDo( 168 | document( 169 | "user-delete", 170 | getDocumentRequest(), 171 | getDocumentResponse(), 172 | requestHeaders(*header()), 173 | pathParameters(userIdPathParameter()), 174 | responseFields(*common()) 175 | ) 176 | ) 177 | } 178 | 179 | @Test 180 | fun `grant role to user api docs`() { 181 | //given 182 | given(userService.grantRole(anyLong(), anyLong())) 183 | .willReturn( 184 | User(1, "배달이", 30, "서울특별시 송파구 올림픽로 295", mutableListOf(Role(1, "ROLE_DEFAULT")))) 185 | 186 | //when 187 | val resultActions = mockMvc.perform( 188 | RestDocumentationRequestBuilders.post("/user/{userId}/role/{roleId}", 1, 1) 189 | .header("x-api-key", "API-KEY") 190 | .accept(MediaType.APPLICATION_JSON_UTF8) 191 | ).andDo(MockMvcResultHandlers.print()) 192 | 193 | //then 194 | resultActions 195 | .andExpect(status().isOk) 196 | .andDo( 197 | document( 198 | "user-grant-role", 199 | getDocumentRequest(), 200 | getDocumentResponse(), 201 | requestHeaders(*header()), 202 | pathParameters(userIdPathParameter(), roleIdPathParameter()), 203 | responseFields(*common()) 204 | .andWithPrefix("data.", *user()) 205 | .andWithPrefix("data.roles[].", *role()) 206 | ) 207 | ) 208 | } 209 | 210 | private fun getUserDto(): String { 211 | return """ 212 | { 213 | "name": "배달이", 214 | "age": 30, 215 | "address": "서울특별시 송파구 올림픽로 295" 216 | } 217 | """.trimIndent() 218 | } 219 | 220 | private fun header(): Array { 221 | return arrayOf(headerWithName("x-api-key").description("Api Key")) 222 | } 223 | 224 | private fun userIdPathParameter(): ParameterDescriptor { 225 | return parameterWithName("userId").description("USER ID").maxLength(10).remarks("User ID가 없는 경우는 먼저 생성하세") 226 | } 227 | 228 | private fun roleIdPathParameter(): ParameterDescriptor { 229 | return parameterWithName("roleId").description("ROLE ID") 230 | } 231 | 232 | private fun common(): Array { 233 | return arrayOf( 234 | fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"), 235 | fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메세지"), 236 | subsectionWithPath("error").type(JsonFieldType.OBJECT).description("에러 Data").optional(), 237 | subsectionWithPath("data").type(JsonFieldType.OBJECT).description("응답 Data").optional() 238 | ) 239 | } 240 | 241 | private fun user(): Array { 242 | return arrayOf( 243 | fieldWithPath("id").type(JsonFieldType.NUMBER).description("User ID"), 244 | fieldWithPath("name").type(JsonFieldType.STRING).description("이름"), 245 | fieldWithPath("age").type(JsonFieldType.NUMBER).description("나이"), 246 | fieldWithPath("address").type(JsonFieldType.STRING).description("주소") 247 | ) 248 | } 249 | 250 | private fun role(): Array { 251 | return arrayOf( 252 | fieldWithPath("id").type(JsonFieldType.NUMBER).description("Role ID").optional(), 253 | fieldWithPath("name").type(JsonFieldType.STRING).description("Role명").optional() 254 | ) 255 | } 256 | } -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/path-parameters.snippet: -------------------------------------------------------------------------------- 1 | .+{{path}}+ 2 | |=== 3 | |Parameter|Description|최대길이|비고 4 | {{#parameters}} 5 | |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} 6 | |{{#tableCellContent}}{{description}}{{/tableCellContent}} 7 | |{{#tableCellContent}}{{#maxLength}}{{maxLength}}{{/maxLength}}{{/tableCellContent}} 8 | |{{#tableCellContent}}{{#remarks}}{{remarks}}{{/remarks}}{{/tableCellContent}} 9 | {{/parameters}} 10 | |=== -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/request-fields.snippet: -------------------------------------------------------------------------------- 1 | |=== 2 | |필드명|타입|최대길이|필수여부|설명|비고 3 | 4 | {{#fields}} 5 | |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} 6 | |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} 7 | |{{#tableCellContent}}{{#maxLength}}{{maxLength}}{{/maxLength}}{{/tableCellContent}} 8 | |{{#tableCellContent}}{{^optional}}O{{/optional}}{{#optional}}X{{/optional}}{{/tableCellContent}} 9 | |{{#tableCellContent}}{{description}}{{/tableCellContent}} 10 | |{{#tableCellContent}}{{#remarks}}{{remarks}}{{/remarks}}{{/tableCellContent}} 11 | {{/fields}} 12 | |=== -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/response-code-fields.snippet: -------------------------------------------------------------------------------- 1 | {{title}} 2 | |=== 3 | |Code|Message 4 | {{#fields}} 5 | |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} 6 | |{{#tableCellContent}}{{description}}{{/tableCellContent}} 7 | {{/fields}} 8 | |=== --------------------------------------------------------------------------------