diff --git a/build.gradle b/build.gradle index 38edf1e7..af4d5bdd 100644 --- a/build.gradle +++ b/build.gradle @@ -163,6 +163,10 @@ samples { dependOn 'spring-restdocs-mockmvc:install' dependOn 'spring-restdocs-restassured:install' + restNotesGrails { + workingDir "$projectDir/samples/rest-notes-grails" + } + restNotesSpringHateoas { workingDir "$projectDir/samples/rest-notes-spring-hateoas" } @@ -183,6 +187,7 @@ samples { workingDir "$projectDir/samples/rest-notes-slate" build false } + } task api (type: Javadoc) { diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 1be930cf..69b6f1fd 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,6 +61,9 @@ public class SampleBuildConfigurer { replaceVersion(new File(this.workingDir, 'build.gradle'), "ext\\['spring-restdocs.version'\\] = '.*'", "ext['spring-restdocs.version'] = '${project.version}'") + replaceVersion(new File(this.workingDir, 'build.gradle'), + "restDocsVersion = \".*\"", + "restDocsVersion = \"${project.version}\"") } } else if (new File(sampleDir, 'pom.xml').isFile()) { diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 094c64c2..2a1d161c 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -18,6 +18,10 @@ If you want to jump straight in, a number of sample applications are available: | Gradle | Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. +| {samples}/rest-notes-grails[Grails] +| Gradle +| Demonstrates the use of Spring REST docs with https://grails.org[Grails] and https://github.com/spockframework/spock[Spock] + | {samples}/rest-notes-slate[Slate] | Gradle | Demonstrates the use of Spring REST Docs with Markdown and diff --git a/samples/rest-notes-grails/.gitignore b/samples/rest-notes-grails/.gitignore new file mode 100644 index 00000000..a521ca89 --- /dev/null +++ b/samples/rest-notes-grails/.gitignore @@ -0,0 +1,17 @@ +Thumbs.db +.DS_Store +.gradle +build/ +classes/ +.idea +*.iml +*.ipr +*.iws +.project +.settings +.classpath +gradlew* +gradle/wrapper + + +src/docs/generated-snippets diff --git a/samples/rest-notes-grails/README.md b/samples/rest-notes-grails/README.md new file mode 100644 index 00000000..386292af --- /dev/null +++ b/samples/rest-notes-grails/README.md @@ -0,0 +1,2 @@ +# grails-spring-restdocs-example +Example of using Spring REST Docs with Grails diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle new file mode 100644 index 00000000..85ac7c4e --- /dev/null +++ b/samples/rest-notes-grails/build.gradle @@ -0,0 +1,107 @@ +buildscript { + ext { + grailsVersion = project.grailsVersion + } + repositories { + mavenLocal() + maven { url "https://repo.grails.org/grails/core" } + maven { url 'https://repo.spring.io/libs-snapshot' } + } + dependencies { + classpath "org.grails:grails-gradle-plugin:$grailsVersion" + classpath "org.grails.plugins:hibernate:4.3.10.5" + classpath 'org.ajoberstar:gradle-git:1.1.0' + } +} + +plugins { + id "io.spring.dependency-management" version "0.5.4.RELEASE" + id 'org.asciidoctor.convert' version '1.5.3' +} + +version "0.1" +group "com.example" + +apply plugin: "spring-boot" +apply plugin: "war" +apply plugin: 'eclipse' +apply plugin: 'idea' +apply plugin: "org.grails.grails-web" + +ext { + grailsVersion = project.grailsVersion + gradleWrapperVersion = project.gradleWrapperVersion + restDocsVersion = "1.1.0.BUILD-SNAPSHOT" + snippetsDir = file('src/docs/generated-snippets') +} + +repositories { + mavenLocal() + maven { url 'https://repo.spring.io/libs-snapshot' } + maven { url "https://repo.grails.org/grails/core" } +} + +dependencyManagement { + dependencies { + dependency "org.springframework.restdocs:spring-restdocs-restassured:$restDocsVersion" + } + imports { + mavenBom "org.grails:grails-bom:$grailsVersion" + } + applyMavenExclusions false +} + +dependencies { + compile "org.springframework.boot:spring-boot-starter-logging" + compile "org.springframework.boot:spring-boot-starter-actuator" + compile "org.springframework.boot:spring-boot-autoconfigure" + compile "org.springframework.boot:spring-boot-starter-tomcat" + compile "org.grails:grails-plugin-url-mappings" + compile "org.grails:grails-plugin-rest" + compile "org.grails:grails-plugin-interceptors" + compile "org.grails:grails-plugin-services" + compile "org.grails:grails-plugin-datasource" + compile "org.grails:grails-plugin-databinding" + compile "org.grails:grails-plugin-async" + compile "org.grails:grails-web-boot" + compile "org.grails:grails-logging" + + compile "org.grails.plugins:hibernate" + compile "org.grails.plugins:cache" + compile "org.hibernate:hibernate-ehcache" + + runtime "com.h2database:h2" + + testCompile "org.grails:grails-plugin-testing" + testCompile "org.grails.plugins:geb" + testCompile 'org.springframework.restdocs:spring-restdocs-restassured' + + console "org.grails:grails-console" +} + +task wrapper(type: Wrapper) { + gradleVersion = gradleWrapperVersion +} + +ext { + snippetsDir = file('src/docs/generated-snippets') +} + +task cleanTempDirs(type: Delete) { + delete fileTree(dir: 'src/docs/generated-snippets') +} + +test { + dependsOn cleanTempDirs + outputs.dir snippetsDir +} + +asciidoctor { + dependsOn integrationTest + inputs.dir snippetsDir + sourceDir = file('src/docs') + separateOutputDirs = false + attributes 'snippets': snippetsDir +} + +build.dependsOn asciidoctor diff --git a/samples/rest-notes-grails/gradle.properties b/samples/rest-notes-grails/gradle.properties new file mode 100644 index 00000000..1b1c50f8 --- /dev/null +++ b/samples/rest-notes-grails/gradle.properties @@ -0,0 +1,2 @@ +grailsVersion=3.0.15 +gradleWrapperVersion=2.3 diff --git a/samples/rest-notes-grails/grails-app/conf/application.yml b/samples/rest-notes-grails/grails-app/conf/application.yml new file mode 100644 index 00000000..6d7d00e8 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/conf/application.yml @@ -0,0 +1,98 @@ +--- +grails: + profile: web-api + codegen: + defaultPackage: com.example +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +spring: + groovy: + template: + check-template-location: false + +--- +grails: + mime: + disable: + accept: + header: + userAgents: + - Gecko + - WebKit + - Presto + - Trident + types: + all: '*/*' + atom: application/atom+xml + css: text/css + csv: text/csv + form: application/x-www-form-urlencoded + html: + - text/html + - application/xhtml+xml + js: text/javascript + json: + - application/json + - text/json + multipartForm: multipart/form-data + rss: application/rss+xml + text: text/plain + hal: + - application/hal+json + - application/hal+xml + xml: + - text/xml + - application/xml + urlmapping: + cache: + maxsize: 1000 + controllers: + defaultScope: singleton + converters: + encoding: UTF-8 + hibernate: + cache: + queries: false + +--- +dataSource: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: + +environments: + development: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + test: + dataSource: + dbCreate: update + url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + production: + dataSource: + dbCreate: update + url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + properties: + jmxEnabled: true + initialSize: 5 + maxActive: 50 + minIdle: 5 + maxIdle: 25 + maxWait: 10000 + maxAge: 600000 + timeBetweenEvictionRunsMillis: 5000 + minEvictableIdleTimeMillis: 60000 + validationQuery: SELECT 1 + validationQueryTimeout: 3 + validationInterval: 15000 + testOnBorrow: true + testWhileIdle: true + testOnReturn: false + jdbcInterceptors: ConnectionState + defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED diff --git a/samples/rest-notes-grails/grails-app/conf/logback.groovy b/samples/rest-notes-grails/grails-app/conf/logback.groovy new file mode 100644 index 00000000..c8bc3a40 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/conf/logback.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * 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. + */ + +import grails.util.BuildSettings +import grails.util.Environment + +// See http://logback.qos.ch/manual/groovy.html for details on configuration +appender('STDOUT', ConsoleAppender) { + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } +} + +root(ERROR, ['STDOUT']) + +def targetDir = BuildSettings.TARGET_DIR +if (Environment.isDevelopmentMode() && targetDir) { + appender("FULL_STACKTRACE", FileAppender) { + file = "${targetDir}/stacktrace.log" + append = true + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } + } + logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) +} diff --git a/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy new file mode 100644 index 00000000..4907ee43 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy @@ -0,0 +1,2 @@ +// Place your Spring DSL code here +beans = {} diff --git a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy b/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy new file mode 100644 index 00000000..7d8cffe5 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy @@ -0,0 +1,25 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * 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. + */ + +class UrlMappings { + + static mappings = { + "/"(controller: 'index') + "500"(controller: 'InternalServerError') + "404"(controller: 'NotFound') + } + +} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy new file mode 100644 index 00000000..01f58518 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * 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 com.example + +import grails.core.GrailsApplication +import grails.util.Environment + +class IndexController { + + GrailsApplication grailsApplication + + def index() { + render(contentType: 'application/json') { + message = "Welcome to Grails!" + environment = Environment.current.name + appversion = grailsApplication.metadata['info.app.version'] + grailsversion = grailsApplication.metadata['info.app.grailsVersion'] + appprofile = grailsApplication.config.grails?.profile + groovyversion = GroovySystem.getVersion() + jvmversion = System.getProperty('java.version') + controllers = array { + for (c in grailsApplication.controllerClasses) { + controller([name: c.fullName]) + } + } + plugins = array { + for (p in grailsApplication.mainContext.pluginManager.allPlugins) { + plugin([name: p.fullName]) + } + } + } + } + +} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy new file mode 100644 index 00000000..c0e80312 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * 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 com.example + +class InternalServerErrorController { + + def index() { + render(contentType: 'application/json') { + error = 500 + message = "Internal server error" + } + } + +} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy new file mode 100644 index 00000000..039968a9 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * 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 com.example + +class NotFoundController { + + def index() { + render(contentType: 'application/json') { + error = 404 + message = "Not Found" + } + } + +} diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy new file mode 100644 index 00000000..140b839c --- /dev/null +++ b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * 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 com.example + +import grails.rest.Resource + +@Resource(uri='/notes', formats = ['json', 'xml']) +class Note { + + Long id + + String title + + String body + + static hasMany = [tags: Tag] + + static mapping = { + tags joinTable: [name: "mm_notes_tags", key: 'mm_note_id' ] + } + +} + diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy new file mode 100644 index 00000000..b0c712c6 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy @@ -0,0 +1,36 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * 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 com.example + +import grails.rest.Resource + +@Resource(uri='/tags', formats = ['json', 'xml']) +class Tag { + + Long id + + String name + + static hasMany = [notes: Note] + + static belongsTo = Note + + static mapping = { + notes joinTable: [name: "mm_notes_tags", key: 'mm_tag_id'] + } + +} diff --git a/samples/rest-notes-grails/grails-app/i18n/messages.properties b/samples/rest-notes-grails/grails-app/i18n/messages.properties new file mode 100644 index 00000000..b0451362 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/i18n/messages.properties @@ -0,0 +1,56 @@ +default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] +default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL +default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number +default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address +default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}] +default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}] +default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}] +default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}] +default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}] +default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}] +default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation +default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}] +default.blank.message=Property [{0}] of class [{1}] cannot be blank +default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] +default.null.message=Property [{0}] of class [{1}] cannot be null +default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique + +default.paginate.prev=Previous +default.paginate.next=Next +default.boolean.true=True +default.boolean.false=False +default.date.format=yyyy-MM-dd HH:mm:ss z +default.number.format=0 + +default.created.message={0} {1} created +default.updated.message={0} {1} updated +default.deleted.message={0} {1} deleted +default.not.deleted.message={0} {1} could not be deleted +default.not.found.message={0} not found with id {1} +default.optimistic.locking.failure=Another user has updated this {0} while you were editing + +default.home.label=Home +default.list.label={0} List +default.add.label=Add {0} +default.new.label=New {0} +default.create.label=Create {0} +default.show.label=Show {0} +default.edit.label=Edit {0} + +default.button.create.label=Create +default.button.edit.label=Edit +default.button.update.label=Update +default.button.delete.label=Delete +default.button.delete.confirm.message=Are you sure? + +# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) +typeMismatch.java.net.URL=Property {0} must be a valid URL +typeMismatch.java.net.URI=Property {0} must be a valid URI +typeMismatch.java.util.Date=Property {0} must be a valid Date +typeMismatch.java.lang.Double=Property {0} must be a valid number +typeMismatch.java.lang.Integer=Property {0} must be a valid number +typeMismatch.java.lang.Long=Property {0} must be a valid number +typeMismatch.java.lang.Short=Property {0} must be a valid number +typeMismatch.java.math.BigDecimal=Property {0} must be a valid number +typeMismatch.java.math.BigInteger=Property {0} must be a valid number +typeMismatch=Property {0} is type-mismatched diff --git a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy new file mode 100644 index 00000000..0efbc954 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy @@ -0,0 +1,31 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * 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. + */ + +import com.example.Note + +class BootStrap { + + def init = { servletContext -> + environments { + test { + new Note(title: 'Hello, World!', body: 'Hello from the Integration Test').save() + } + } + } + + def destroy = {} + +} diff --git a/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy new file mode 100644 index 00000000..6da2f620 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * 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 com.example + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration + +class Application extends GrailsAutoConfiguration { + + static void main(String[] args) { + GrailsApp.run(Application, args) + } + +} diff --git a/samples/rest-notes-grails/src/docs/index.adoc b/samples/rest-notes-grails/src/docs/index.adoc new file mode 100644 index 00000000..231af2ec --- /dev/null +++ b/samples/rest-notes-grails/src/docs/index.adoc @@ -0,0 +1,148 @@ += Grails RESTful Notes API Guide +Andy Wilkinson; Jenn Strater +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 4 +:sectlinks: + +[[overview]] += Overview + +[[overview-http-verbs]] +== HTTP verbs + +Grails RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +use of HTTP verbs. + +|=== +| Verb | Usage + +| `GET` +| Used to retrieve a resource + +| `POST` +| Used to create a new resource + +| `PATCH` +| Used to update an existing resource, including partial updates + +| `DELETE` +| Used to delete an existing resource +|=== + +[[overview-http-status-codes]] +== HTTP status codes + +Grails RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +use of HTTP status codes. + +|=== +| Status code | Usage + +| `200 OK` +| The request completed successfully + +| `201 Created` +| A new resource has been created successfully. The resource's URI is available from the response's +`Location` header + +| `204 No Content` +| An update to an existing resource has been applied successfully + +| `400 Bad Request` +| The request was malformed. The response body will include an error providing further information + +| `404 Not Found` +| The requested resource did not exist +|=== + +[[resources]] += Resources + + +[[resources-index]] +== Index + +The index provides the entry point into the service. + + + +[[resources-index-access]] +=== Accessing the index + +A `GET` request is used to access the index + +==== Example request + +include::{snippets}/index-example/curl-request.adoc[] + +==== Response structure + +include::{snippets}/index-example/response-fields.adoc[] + +==== Example response + +include::{snippets}/index-example/http-response.adoc[] + + +[[resources-notes]] +== Notes + +The Notes resources is used to create and list notes + + + +[[resources-notes-list]] +=== Listing notes + +A `GET` request will list all of the service's notes. + +==== Response structure + +include::{snippets}/notes-list-example/response-fields.adoc[] + +==== Example request + +include::{snippets}/notes-list-example/curl-request.adoc[] + +==== Example response + +include::{snippets}/notes-list-example/http-response.adoc[] + + + +[[resources-notes-create]] +=== Creating a note + +A `POST` request is used to create a note + +==== Request structure + +include::{snippets}/notes-create-example/request-fields.adoc[] + +==== Example request + +include::{snippets}/notes-create-example/curl-request.adoc[] + +==== Example response + +include::{snippets}/notes-create-example/http-response.adoc[] + +[[resources-note-retrieve]] +=== Retrieve a note + +A `GET` request will retrieve the details of a note + +==== Response structure + +include::{snippets}/note-get-example/response-fields.adoc[] + +==== Example request + +include::{snippets}/note-get-example/curl-request.adoc[] + +==== Example response + +include::{snippets}/note-get-example/http-response.adoc[] diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy new file mode 100644 index 00000000..ae594901 --- /dev/null +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -0,0 +1,164 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * 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 com.example + +import org.springframework.restdocs.payload.JsonFieldType + +import static com.jayway.restassured.RestAssured.given +import static org.hamcrest.CoreMatchers.is +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields +import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration + +import com.jayway.restassured.builder.RequestSpecBuilder +import com.jayway.restassured.specification.RequestSpecification +import grails.test.mixin.integration.Integration +import grails.transaction.Rollback +import org.junit.Rule +import org.springframework.restdocs.JUnitRestDocumentation +import org.springframework.http.MediaType +import spock.lang.Specification + +@Integration +@Rollback +class ApiDocumentationSpec extends Specification { + + @Rule + JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') + + protected RequestSpecification documentationSpec + + void setup() { + this.documentationSpec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(restDocumentation)) + .build() + } + + void 'test and document get request for /index'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('index-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('message').description('Welcome to Grails!'), + fieldWithPath('environment').description("The running environment"), + fieldWithPath('appversion').description('version of the app that is running'), + fieldWithPath('grailsversion').description('the version of grails used in this project'), + fieldWithPath('appprofile').description('the profile of grails used in this project'), + fieldWithPath('groovyversion').description('the version of groovy used in this project'), + fieldWithPath('jvmversion').description('the version of the jvm used in this project'), + fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), + fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), + ))) + .when() + .port(8080) + .get('/') + .then() + .assertThat() + .statusCode(is(200)) + } + + void 'test and document notes list request'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('notes-list-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('[].class').description('the class of the resource'), + fieldWithPath('[].id').description('the id of the note'), + fieldWithPath('[].title').description('the title of the note'), + fieldWithPath('[].body').description('the body of the note'), + fieldWithPath('[].tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .when() + .port(8080) + .get('/notes') + .then() + .assertThat() + .statusCode(is(200)) + } + + void 'test and document create new note'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .contentType(MediaType.APPLICATION_JSON.toString()) + .filter(document('notes-create-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + requestFields( + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') + ), + responseFields( + fieldWithPath('class').description('the class of the resource'), + fieldWithPath('id').description('the id of the note'), + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') + .when() + .port(8080) + .post('/notes') + .then() + .assertThat() + .statusCode(is(201)) + } + + void 'test and document getting specific note'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('note-get-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('class').description('the class of the resource'), + fieldWithPath('id').description('the id of the note'), + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .when() + .port(8080) + .get('/notes/1') + .then() + .assertThat() + .statusCode(is(200)) + } + +}