From 259905bb279996e3ca2355a328ce21051142770b Mon Sep 17 00:00:00 2001 From: Sanjana Date: Sat, 27 Jun 2026 02:31:22 +0530 Subject: [PATCH] Add Grails 8 HTTP client guide (v8 prose, snippets, guides.yml) Guide under guides/grails-http-client/v8 with vendored snippets. Registers grails-http-client in conf/guides.yml. Sample app: grails-guides/grails-http-client (grails8). --- conf/guides.yml | 45 +++++++++++++++ .../v8/guide/domainAndControllers.adoc | 40 +++++++++++++ .../v8/guide/gettingStarted.adoc | 22 ++++++++ .../v8/guide/helpWithGrails.adoc | 1 + .../v8/guide/httpClientSetup.adoc | 39 +++++++++++++ .../v8/guide/introduction.adoc | 5 ++ .../v8/guide/requirements.adoc | 5 ++ .../v8/guide/runningTheApp.adoc | 15 +++++ .../v8/guide/searchService.adoc | 17 ++++++ .../grails-http-client/v8/guide/summary.adoc | 8 +++ .../grails-http-client/v8/guide/testing.adoc | 52 +++++++++++++++++ .../example/RecordLabelController.groovy | 56 +++++++++++++++++++ .../example/SearchController.groovy | 21 +++++++ .../controllers/example/UrlMappings.groovy | 19 +++++++ .../domain/example/RecordLabel.groovy | 19 +++++++ .../init/example/Application.groovy | 14 +++++ .../grails-app/init/example/BootStrap.groovy | 21 +++++++ .../example/ItunesSearchService.groovy | 19 +++++++ .../ItunesClientIntegrationSpec.groovy | 21 +++++++ .../example/RecordLabelIntegrationSpec.groovy | 21 +++++++ .../src/main/groovy/example/Album.groovy | 10 ++++ .../main/groovy/example/ItunesClient.groovy | 13 +++++ .../main/groovy/example/SearchResult.groovy | 9 +++ .../example/ItunesSearchServiceSpec.groovy | 25 +++++++++ .../groovy/example/RecordLabelSpec.groovy | 32 +++++++++++ 25 files changed, 549 insertions(+) create mode 100644 guides/grails-http-client/v8/guide/domainAndControllers.adoc create mode 100644 guides/grails-http-client/v8/guide/gettingStarted.adoc create mode 100644 guides/grails-http-client/v8/guide/helpWithGrails.adoc create mode 100644 guides/grails-http-client/v8/guide/httpClientSetup.adoc create mode 100644 guides/grails-http-client/v8/guide/introduction.adoc create mode 100644 guides/grails-http-client/v8/guide/requirements.adoc create mode 100644 guides/grails-http-client/v8/guide/runningTheApp.adoc create mode 100644 guides/grails-http-client/v8/guide/searchService.adoc create mode 100644 guides/grails-http-client/v8/guide/summary.adoc create mode 100644 guides/grails-http-client/v8/guide/testing.adoc create mode 100644 guides/grails-http-client/v8/snippets/grails-app/controllers/example/RecordLabelController.groovy create mode 100644 guides/grails-http-client/v8/snippets/grails-app/controllers/example/SearchController.groovy create mode 100644 guides/grails-http-client/v8/snippets/grails-app/controllers/example/UrlMappings.groovy create mode 100644 guides/grails-http-client/v8/snippets/grails-app/domain/example/RecordLabel.groovy create mode 100644 guides/grails-http-client/v8/snippets/grails-app/init/example/Application.groovy create mode 100644 guides/grails-http-client/v8/snippets/grails-app/init/example/BootStrap.groovy create mode 100644 guides/grails-http-client/v8/snippets/grails-app/services/example/ItunesSearchService.groovy create mode 100644 guides/grails-http-client/v8/snippets/src/integration-test/groovy/example/ItunesClientIntegrationSpec.groovy create mode 100644 guides/grails-http-client/v8/snippets/src/integration-test/groovy/example/RecordLabelIntegrationSpec.groovy create mode 100644 guides/grails-http-client/v8/snippets/src/main/groovy/example/Album.groovy create mode 100644 guides/grails-http-client/v8/snippets/src/main/groovy/example/ItunesClient.groovy create mode 100644 guides/grails-http-client/v8/snippets/src/main/groovy/example/SearchResult.groovy create mode 100644 guides/grails-http-client/v8/snippets/src/test/groovy/example/ItunesSearchServiceSpec.groovy create mode 100644 guides/grails-http-client/v8/snippets/src/test/groovy/example/RecordLabelSpec.groovy diff --git a/conf/guides.yml b/conf/guides.yml index 902a3eeffff..d7dc9924ef9 100644 --- a/conf/guides.yml +++ b/conf/guides.yml @@ -2823,6 +2823,51 @@ guides: helpWithGrails: title: Help with Grails + - name: 'grails-http-client' + title: 'Calling REST APIs with Spring HTTP Services in Grails 8' + subtitle: 'Add a declarative @HttpExchange client to a Grails 8 REST API, combine GORM data with external API results, and test with Spock.' + authors: + - 'Sanjana' + category: 'Grails REST APIs' + publicationDate: '2026-06-26' + versions: + '8': + sourcePath: guides/grails-http-client/v8 + publicationDate: '2026-06-26' + tags: + - 'rest-api' + - 'http-client' + - 'http-exchange' + - 'spring-boot' + - 'gorm' + - 'postgresql' + - 'spock' + - 'testcontainers' + - 'integration-testing' + sampleRef: + repo: 'grails-guides/grails-http-client' + branch: 'grails8' + toc: + introduction: + title: Introduction + requirements: What you will need + gettingStarted: + title: Getting Started + httpClientSetup: + title: HTTP client setup + domainAndControllers: + title: Domain and local REST API + searchService: + title: Search service and controller + testing: + title: Testing + runningTheApp: + title: Running the application + summary: + title: Summary + helpWithGrails: + title: Do you need help with Grails? + - name: 'grails-htmx' title: 'HTMX with Grails 8' subtitle: 'Build a small task tracker with server-rendered Grails 8 GSP and HTMX-driven inline editing, live search, optimistic delete, and toggle - no SPA, no JSON.' diff --git a/guides/grails-http-client/v8/guide/domainAndControllers.adoc b/guides/grails-http-client/v8/guide/domainAndControllers.adoc new file mode 100644 index 00000000000..3a9110889c8 --- /dev/null +++ b/guides/grails-http-client/v8/guide/domainAndControllers.adoc @@ -0,0 +1,40 @@ +Local record label data stays in GORM while album search results come from the external API. + +[[recordLabel]] +== RecordLabel domain + +[source,groovy] +.grails-app/domain/example/RecordLabel.groovy +---- +include::../snippets/grails-app/domain/example/RecordLabel.groovy[] +---- + +Seed development data in `BootStrap`: + +[source,groovy] +.grails-app/init/example/BootStrap.groovy +---- +include::../snippets/grails-app/init/example/BootStrap.groovy[] +---- + +[[urlMappings]] +== URL mappings + +[source,groovy] +.grails-app/controllers/example/UrlMappings.groovy +---- +include::../snippets/grails-app/controllers/example/UrlMappings.groovy[] +---- + +[[recordLabelController]] +== RecordLabelController + +The REST controller delegates persistence to GORM and returns JSON views: + +[source,groovy] +.grails-app/controllers/example/RecordLabelController.groovy +---- +include::../snippets/grails-app/controllers/example/RecordLabelController.groovy[] +---- + +Call `validate()` after binding request JSON and before `save`. That returns structured `422` responses for constraint violations. diff --git a/guides/grails-http-client/v8/guide/gettingStarted.adoc b/guides/grails-http-client/v8/guide/gettingStarted.adoc new file mode 100644 index 00000000000..acbada958d5 --- /dev/null +++ b/guides/grails-http-client/v8/guide/gettingStarted.adoc @@ -0,0 +1,22 @@ +Clone the repository and run the starting application: + +[source,bash] +---- +git clone -b grails8 https://github.com/grails-guides/grails-http-client.git +cd grails-http-client/initial +./gradlew bootRun +---- + +Open http://localhost:8080/[http://localhost:8080/] for the welcome JSON payload. The `initial/` project is a vanilla Grails 8 REST API starter with no HTTP client yet. + +To skip ahead, `cd complete` and run the same commands — that tree contains the finished `@HttpExchange` client, services, and tests. + +[[cloneAndRun]] +== Verify tests + +[source,bash] +---- +./gradlew test +---- + +Both `initial` and `complete` must pass in CI (see `.github/workflows/grails8.yml`). diff --git a/guides/grails-http-client/v8/guide/helpWithGrails.adoc b/guides/grails-http-client/v8/guide/helpWithGrails.adoc new file mode 100644 index 00000000000..e062f614b1a --- /dev/null +++ b/guides/grails-http-client/v8/guide/helpWithGrails.adoc @@ -0,0 +1 @@ +include::{commondir}/common-helpWithGrails.adoc[] diff --git a/guides/grails-http-client/v8/guide/httpClientSetup.adoc b/guides/grails-http-client/v8/guide/httpClientSetup.adoc new file mode 100644 index 00000000000..3b4baa75201 --- /dev/null +++ b/guides/grails-http-client/v8/guide/httpClientSetup.adoc @@ -0,0 +1,39 @@ +Register HTTP service interfaces on the application class with `@ImportHttpServices`: + +[source,groovy] +.grails-app/init/example/Application.groovy +---- +include::../snippets/grails-app/init/example/Application.groovy[] +---- + +Spring Boot scans `example` for `@HttpExchange` interfaces and auto-configures a `RestClient` proxy for each one. + +[[itunesClient]] +== ItunesClient + +Declare the iTunes Search API client as a Spring HTTP service interface: + +[source,groovy] +.src/main/groovy/example/ItunesClient.groovy +---- +include::../snippets/src/main/groovy/example/ItunesClient.groovy[] +---- + +[[dtos]] +== Response DTOs + +Map the JSON response with simple POGOs: + +[source,groovy] +.src/main/groovy/example/Album.groovy +---- +include::../snippets/src/main/groovy/example/Album.groovy[] +---- + +[source,groovy] +.src/main/groovy/example/SearchResult.groovy +---- +include::../snippets/src/main/groovy/example/SearchResult.groovy[] +---- + +Spring deserializes the iTunes JSON into these types automatically. diff --git a/guides/grails-http-client/v8/guide/introduction.adoc b/guides/grails-http-client/v8/guide/introduction.adoc new file mode 100644 index 00000000000..9b656913c69 --- /dev/null +++ b/guides/grails-http-client/v8/guide/introduction.adoc @@ -0,0 +1,5 @@ +Learn how to call external REST APIs from a Grails 8 application using Spring Boot's built-in HTTP Services support: define a `@HttpExchange` interface, register it with `@ImportHttpServices`, inject the generated client into a Grails service, and expose results through JSON views. Local `RecordLabel` data stays in GORM/PostgreSQL; album metadata comes from the iTunes Search API. + +No Micronaut plugin or extra HTTP client dependency is required — this uses the same Spring stack Grails 8 already runs on. + +This guide follows the standard Grails guides layout: work in the `initial/` project and compare your progress with `complete/`. diff --git a/guides/grails-http-client/v8/guide/requirements.adoc b/guides/grails-http-client/v8/guide/requirements.adoc new file mode 100644 index 00000000000..238e8d2b921 --- /dev/null +++ b/guides/grails-http-client/v8/guide/requirements.adoc @@ -0,0 +1,5 @@ +* Approximately 45 minutes +* JDK 21 (Apache Grails 8 requires Java 21) +* A https://www.grails.org/[Grails] installation or the bundled Gradle wrapper in `initial/` and `complete/` +* **Docker** — required for integration tests (Testcontainers PostgreSQL) +* **PostgreSQL** on `localhost:5432` — required for `./gradlew bootRun` in both `initial/` and `complete/` (default database `devDb`; see `grails-app/conf/application.yml`) diff --git a/guides/grails-http-client/v8/guide/runningTheApp.adoc b/guides/grails-http-client/v8/guide/runningTheApp.adoc new file mode 100644 index 00000000000..5ff4bcd86b3 --- /dev/null +++ b/guides/grails-http-client/v8/guide/runningTheApp.adoc @@ -0,0 +1,15 @@ +[source,bash] +---- +cd complete +./gradlew bootRun +---- + +Example endpoints: + +[source,bash] +---- +curl -s http://localhost:8080/api/recordLabels +curl -s 'http://localhost:8080/api/search?q=U2' +---- + +The search endpoint returns album metadata from the iTunes Search API. Record labels are served from your local PostgreSQL database. diff --git a/guides/grails-http-client/v8/guide/searchService.adoc b/guides/grails-http-client/v8/guide/searchService.adoc new file mode 100644 index 00000000000..58e441b4a13 --- /dev/null +++ b/guides/grails-http-client/v8/guide/searchService.adoc @@ -0,0 +1,17 @@ +Inject the `@HttpExchange` client into a Grails service: + +[source,groovy] +.grails-app/services/example/ItunesSearchService.groovy +---- +include::../snippets/grails-app/services/example/ItunesSearchService.groovy[] +---- + +Expose search results through a thin controller: + +[source,groovy] +.grails-app/controllers/example/SearchController.groovy +---- +include::../snippets/grails-app/controllers/example/SearchController.groovy[] +---- + +The service trims blank search terms and returns an empty list rather than calling the remote API with invalid input. diff --git a/guides/grails-http-client/v8/guide/summary.adoc b/guides/grails-http-client/v8/guide/summary.adoc new file mode 100644 index 00000000000..74ff4bfe5d5 --- /dev/null +++ b/guides/grails-http-client/v8/guide/summary.adoc @@ -0,0 +1,8 @@ +You added a declarative HTTP client to a Grails 8 REST API: + +* `@ImportHttpServices` and a `@HttpExchange` interface for the iTunes Search API +* A Grails service that injects the generated client +* Local GORM data alongside external API results +* Spock unit and integration tests + +Next steps: add error handling for remote API failures, cache search results, or secure outbound calls with API keys. diff --git a/guides/grails-http-client/v8/guide/testing.adoc b/guides/grails-http-client/v8/guide/testing.adoc new file mode 100644 index 00000000000..b979338ed30 --- /dev/null +++ b/guides/grails-http-client/v8/guide/testing.adoc @@ -0,0 +1,52 @@ +[[unitTests]] +== Unit tests + +Domain constraints: + +[source,groovy] +.src/test/groovy/example/RecordLabelSpec.groovy +---- +include::../snippets/src/test/groovy/example/RecordLabelSpec.groovy[] +---- + +Service delegation to the HTTP client with a mock: + +[source,groovy] +.src/test/groovy/example/ItunesSearchServiceSpec.groovy +---- +include::../snippets/src/test/groovy/example/ItunesSearchServiceSpec.groovy[] +---- + +[[integrationTests]] +== Integration tests + +Verify the HTTP client is registered as a Spring bean: + +[source,groovy] +.src/integration-test/groovy/example/ItunesClientIntegrationSpec.groovy +---- +include::../snippets/src/integration-test/groovy/example/ItunesClientIntegrationSpec.groovy[] +---- + +Assert GORM persistence against real PostgreSQL (Testcontainers): + +[source,groovy] +.src/integration-test/groovy/example/RecordLabelIntegrationSpec.groovy +---- +include::../snippets/src/integration-test/groovy/example/RecordLabelIntegrationSpec.groovy[] +---- + +Run unit tests: + +[source,bash] +---- +cd complete +./gradlew test +---- + +Run integration tests (requires Docker): + +[source,bash] +---- +./gradlew integrationTest +---- diff --git a/guides/grails-http-client/v8/snippets/grails-app/controllers/example/RecordLabelController.groovy b/guides/grails-http-client/v8/snippets/grails-app/controllers/example/RecordLabelController.groovy new file mode 100644 index 00000000000..a065bed48e0 --- /dev/null +++ b/guides/grails-http-client/v8/snippets/grails-app/controllers/example/RecordLabelController.groovy @@ -0,0 +1,56 @@ +package example + +import grails.gorm.transactions.Transactional + +class RecordLabelController { + + static responseFormats = ['json'] + static allowedMethods = [index: 'GET', show: 'GET', save: 'POST', update: 'PUT', delete: 'DELETE'] + + def index(Integer max) { + params.max = Math.min(max ?: 10, 100) + respond RecordLabel.list(params), model: [recordLabelCount: RecordLabel.count()] + } + + def show(Long id) { + respond RecordLabel.get(id) + } + + @Transactional + def save() { + def recordLabel = new RecordLabel(request.JSON as Map) + if (!recordLabel.validate()) { + respond recordLabel.errors, status: 422 + return + } + recordLabel.save(failOnError: true, flush: true) + respond recordLabel, status: 201 + } + + @Transactional + def update(Long id) { + def recordLabel = RecordLabel.get(id) + if (!recordLabel) { + render status: 404 + return + } + recordLabel.properties = request.JSON + if (!recordLabel.validate()) { + respond recordLabel.errors, status: 422 + return + } + recordLabel.save(failOnError: true, flush: true) + respond recordLabel + } + + @Transactional + def delete(Long id) { + def recordLabel = RecordLabel.get(id) + if (!recordLabel) { + render status: 404 + return + } + recordLabel.delete(flush: true) + render status: 204 + } +} diff --git a/guides/grails-http-client/v8/snippets/grails-app/controllers/example/SearchController.groovy b/guides/grails-http-client/v8/snippets/grails-app/controllers/example/SearchController.groovy new file mode 100644 index 00000000000..cfc7d72cf04 --- /dev/null +++ b/guides/grails-http-client/v8/snippets/grails-app/controllers/example/SearchController.groovy @@ -0,0 +1,21 @@ +package example + +import groovy.transform.CompileStatic + +@CompileStatic +class SearchController { + + static responseFormats = ['json'] + static allowedMethods = [index: 'GET'] + + ItunesSearchService itunesSearchService + + def index(String q) { + if (!q?.trim()) { + respond([searchTerm: q, albums: []]) + return + } + List albums = itunesSearchService.searchAlbums(q) + respond([searchTerm: q.trim(), albums: albums]) + } +} diff --git a/guides/grails-http-client/v8/snippets/grails-app/controllers/example/UrlMappings.groovy b/guides/grails-http-client/v8/snippets/grails-app/controllers/example/UrlMappings.groovy new file mode 100644 index 00000000000..991686cdec9 --- /dev/null +++ b/guides/grails-http-client/v8/snippets/grails-app/controllers/example/UrlMappings.groovy @@ -0,0 +1,19 @@ +package example + +class UrlMappings { + + static mappings = { + "/$controller/$action?/$id?(.$format)?"{ + constraints { + // apply constraints here + } + } + + "/api/search"(controller: 'search', action: 'index') + "/api/recordLabels"(resources: 'recordLabel') + + "/"(controller: 'application', action: 'index') + "500"(view: '/error') + "404"(view: '/notFound') + } +} diff --git a/guides/grails-http-client/v8/snippets/grails-app/domain/example/RecordLabel.groovy b/guides/grails-http-client/v8/snippets/grails-app/domain/example/RecordLabel.groovy new file mode 100644 index 00000000000..ca0e5956846 --- /dev/null +++ b/guides/grails-http-client/v8/snippets/grails-app/domain/example/RecordLabel.groovy @@ -0,0 +1,19 @@ +package example + +import grails.persistence.Entity +import groovy.util.logging.Slf4j + +@Slf4j +@Entity +class RecordLabel { + + String name + + static constraints = { + name blank: false, nullable: false, maxSize: 100, unique: true + } + + String toString() { + name + } +} diff --git a/guides/grails-http-client/v8/snippets/grails-app/init/example/Application.groovy b/guides/grails-http-client/v8/snippets/grails-app/init/example/Application.groovy new file mode 100644 index 00000000000..c47e709e4fd --- /dev/null +++ b/guides/grails-http-client/v8/snippets/grails-app/init/example/Application.groovy @@ -0,0 +1,14 @@ +package example + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration +import groovy.transform.CompileStatic +import org.springframework.web.service.registry.ImportHttpServices + +@CompileStatic +@ImportHttpServices(basePackages = 'example') +class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application, args) + } +} diff --git a/guides/grails-http-client/v8/snippets/grails-app/init/example/BootStrap.groovy b/guides/grails-http-client/v8/snippets/grails-app/init/example/BootStrap.groovy new file mode 100644 index 00000000000..31657b3a135 --- /dev/null +++ b/guides/grails-http-client/v8/snippets/grails-app/init/example/BootStrap.groovy @@ -0,0 +1,21 @@ +package example + +class BootStrap { + + def init = { servletContext -> + environments { + development { + RecordLabel.withTransaction { + if (RecordLabel.count() == 0) { + ['Island Records', 'Motown', 'Blue Note'].each { labelName -> + new RecordLabel(name: labelName).save(failOnError: true) + } + } + } + } + } + } + + def destroy = { + } +} diff --git a/guides/grails-http-client/v8/snippets/grails-app/services/example/ItunesSearchService.groovy b/guides/grails-http-client/v8/snippets/grails-app/services/example/ItunesSearchService.groovy new file mode 100644 index 00000000000..b6073def3fe --- /dev/null +++ b/guides/grails-http-client/v8/snippets/grails-app/services/example/ItunesSearchService.groovy @@ -0,0 +1,19 @@ +package example + +import groovy.transform.CompileStatic +import org.springframework.beans.factory.annotation.Autowired + +@CompileStatic +class ItunesSearchService { + + @Autowired + ItunesClient itunesClient + + List searchAlbums(String searchTerm) { + if (!searchTerm?.trim()) { + return [] + } + SearchResult searchResult = itunesClient.search(searchTerm.trim()) + searchResult?.results ?: [] + } +} diff --git a/guides/grails-http-client/v8/snippets/src/integration-test/groovy/example/ItunesClientIntegrationSpec.groovy b/guides/grails-http-client/v8/snippets/src/integration-test/groovy/example/ItunesClientIntegrationSpec.groovy new file mode 100644 index 00000000000..83cb506617a --- /dev/null +++ b/guides/grails-http-client/v8/snippets/src/integration-test/groovy/example/ItunesClientIntegrationSpec.groovy @@ -0,0 +1,21 @@ +package example + +import grails.gorm.transactions.Rollback +import grails.testing.mixin.integration.Integration +import spock.lang.Specification + +import org.springframework.beans.factory.annotation.Autowired + +@Integration +@Rollback +class ItunesClientIntegrationSpec extends Specification { + + @Autowired + ItunesClient itunesClient + + void 'declarative ItunesClient HTTP service is registered as a Spring bean'() { + expect: + itunesClient != null + itunesClient instanceof ItunesClient + } +} diff --git a/guides/grails-http-client/v8/snippets/src/integration-test/groovy/example/RecordLabelIntegrationSpec.groovy b/guides/grails-http-client/v8/snippets/src/integration-test/groovy/example/RecordLabelIntegrationSpec.groovy new file mode 100644 index 00000000000..e62adae202a --- /dev/null +++ b/guides/grails-http-client/v8/snippets/src/integration-test/groovy/example/RecordLabelIntegrationSpec.groovy @@ -0,0 +1,21 @@ +package example + +import grails.testing.mixin.integration.Integration +import grails.gorm.transactions.Rollback +import spock.lang.Specification + +@Integration +@Rollback +class RecordLabelIntegrationSpec extends Specification { + + void 'record labels persist in PostgreSQL'() { + when: + RecordLabel.withTransaction { + new RecordLabel(name: 'Integration Label').save(flush: true, failOnError: true) + } + + then: + RecordLabel.count() >= 1 + RecordLabel.findByName('Integration Label') != null + } +} diff --git a/guides/grails-http-client/v8/snippets/src/main/groovy/example/Album.groovy b/guides/grails-http-client/v8/snippets/src/main/groovy/example/Album.groovy new file mode 100644 index 00000000000..c9a792f3072 --- /dev/null +++ b/guides/grails-http-client/v8/snippets/src/main/groovy/example/Album.groovy @@ -0,0 +1,10 @@ +package example + +import groovy.transform.CompileStatic + +@CompileStatic +class Album { + String artistName + String collectionName + String collectionViewUrl +} diff --git a/guides/grails-http-client/v8/snippets/src/main/groovy/example/ItunesClient.groovy b/guides/grails-http-client/v8/snippets/src/main/groovy/example/ItunesClient.groovy new file mode 100644 index 00000000000..2231e664079 --- /dev/null +++ b/guides/grails-http-client/v8/snippets/src/main/groovy/example/ItunesClient.groovy @@ -0,0 +1,13 @@ +package example + +import groovy.transform.CompileStatic +import org.springframework.web.service.annotation.GetExchange +import org.springframework.web.service.annotation.HttpExchange + +@CompileStatic +@HttpExchange(url = 'https://itunes.apple.com') +interface ItunesClient { + + @GetExchange('/search?limit=25&media=music&entity=album&term={term}') + SearchResult search(String term) +} diff --git a/guides/grails-http-client/v8/snippets/src/main/groovy/example/SearchResult.groovy b/guides/grails-http-client/v8/snippets/src/main/groovy/example/SearchResult.groovy new file mode 100644 index 00000000000..65e8cc159be --- /dev/null +++ b/guides/grails-http-client/v8/snippets/src/main/groovy/example/SearchResult.groovy @@ -0,0 +1,9 @@ +package example + +import groovy.transform.CompileStatic + +@CompileStatic +class SearchResult { + int resultCount + List results = [] +} diff --git a/guides/grails-http-client/v8/snippets/src/test/groovy/example/ItunesSearchServiceSpec.groovy b/guides/grails-http-client/v8/snippets/src/test/groovy/example/ItunesSearchServiceSpec.groovy new file mode 100644 index 00000000000..abe9b6d793a --- /dev/null +++ b/guides/grails-http-client/v8/snippets/src/test/groovy/example/ItunesSearchServiceSpec.groovy @@ -0,0 +1,25 @@ +package example + +import spock.lang.Specification + +class ItunesSearchServiceSpec extends Specification { + + ItunesSearchService service = new ItunesSearchService() + + void 'searchAlbums delegates to the ItunesClient HTTP service'() { + given: + def client = Mock(ItunesClient) + service.itunesClient = client + def albums = [new Album(artistName: 'U2', collectionName: 'The Joshua Tree')] + client.search('U2') >> new SearchResult(resultCount: 1, results: albums) + + expect: + service.searchAlbums('U2') == albums + } + + void 'searchAlbums returns an empty list for blank terms'() { + expect: + service.searchAlbums(null).isEmpty() + service.searchAlbums(' ').isEmpty() + } +} diff --git a/guides/grails-http-client/v8/snippets/src/test/groovy/example/RecordLabelSpec.groovy b/guides/grails-http-client/v8/snippets/src/test/groovy/example/RecordLabelSpec.groovy new file mode 100644 index 00000000000..5cd56a6a183 --- /dev/null +++ b/guides/grails-http-client/v8/snippets/src/test/groovy/example/RecordLabelSpec.groovy @@ -0,0 +1,32 @@ +package example + +import grails.testing.gorm.DataTest +import spock.lang.Specification + +class RecordLabelSpec extends Specification implements DataTest { + + Class[] getDomainClassesToMock() { + [RecordLabel] as Class[] + } + + void 'name cannot be blank'() { + when: + RecordLabel recordLabel = new RecordLabel(name: '') + + then: + !recordLabel.validate() + recordLabel.errors['name'].code in ['blank', 'nullable'] + } + + void 'name must be unique'() { + given: + new RecordLabel(name: 'Motown').save(flush: true) + + when: + RecordLabel duplicate = new RecordLabel(name: 'Motown') + + then: + !duplicate.validate() + duplicate.errors['name'].code == 'unique' + } +}