Skip to content

Commit 228f9a5

Browse files
committed
refactor(deps): Remove Micronaut HTTP client from grails-data-graphql tests
The `GraphQLSpec` test trait in `grails-data-graphql/plugin` shipped a Micronaut RxJava2 HTTP client (`io.micronaut.rxjava2:micronaut-rxjava2-http-client`) purely to exercise the GraphQL controller from integration tests. Spring Boot 4 already provides everything that trait needs via Spring's `RestClient` (org.springframework.web.client.RestClient) - there is no reason for a Grails GraphQL plugin to depend on the Micronaut HTTP stack, and `io.micronaut.rxjava2` has not been ported to Micronaut 5 anyway. Replace the Micronaut HTTP client with Spring `RestClient`: - `GraphQLSpec.groovy` is rewritten to use `RestClient.builder()` with a single `StringHttpMessageConverter` (configured to accept all media types). JSON encoding/decoding is handled by Groovy's `JsonOutput` and `JsonSlurper` so the trait does not pull a Jackson or Gson runtime onto the consuming apps - the graphql test apps use `grails-views-gson` but do not actually ship the Gson library, and Grails 8 does not ship Jackson on the default classpath either. - Helper return type changes from `io.micronaut.http.HttpResponse<Map>` to `org.springframework.http.ResponseEntity<Map>`. Spring's `ResponseEntity.getBody()` returns `T` directly (not `Optional<T>`), so the 109 `resp.body()` call sites collapse to `resp.body` (Groovy property access) across 17 integration test specs. - The two-arg `graphql(String, Class<T>)` overload survives but is now String-only (the one caller, `InheritanceIntegrationSpec`, asserts on the raw JSON string body). The single remaining `resp.getBody().get()` call site (from Micronaut's `Optional`-returning getter) becomes `resp.getBody()`. The unused-after-removal version pins are dropped from `gradle.properties`: - `micronautRxjava2Version=2.9.0` - was the rxjava2 client pin. - `micronautSerdeJacksonVersion=2.16.2` - was only there as the JSON mapper for the rxjava2 client. No production code uses `@Serdeable` or any `io.micronaut.serde.*` API; confirmed via grep across `grails-data-graphql/` and `grails-test-examples/graphql/`. Each of the 4 graphql test apps loses both `implementation` lines (rxjava2 + serde-jackson) - they brought no Spring Boot-side value once the Mn HTTP client is gone. Verified locally on JDK 21: ``` ./gradlew validateDependencyVersions \ :grails-test-examples-graphql-grails-test-app:integrationTest \ :grails-test-examples-graphql-grails-docs-app:integrationTest \ :grails-test-examples-graphql-grails-multi-datastore-app:integrationTest \ :grails-test-examples-graphql-grails-tenant-app:integrationTest \ -PskipMicronautProjects # BUILD SUCCESSFUL - all integration tests pass across all 4 apps ``` Side note: this closes the long-standing "out-of-band Micronaut pin" comment in `gradle.properties`: nothing in grails-core now depends on Micronaut artifacts outside the Grails-Micronaut "island" managed by `grails-micronaut-bom`. Assisted-by: claude-code:claude-opus-4-7
1 parent a2ac447 commit 228f9a5

27 files changed

Lines changed: 190 additions & 168 deletions

gradle.properties

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,7 @@ apacheRatVersion=0.8.1
5252
gradleChecksumPluginVersion=1.4.0
5353
gradleCycloneDxPluginVersion=3.0.0
5454

55-
# micronaut libraries not in the bom due to the potential for spring mismatches
5655
micronautPlatformVersion=5.0.0
57-
micronautRxjava2Version=2.9.0
58-
micronautSerdeJacksonVersion=2.16.2
5956

6057
# Pass -PskipMicronautProjects (presence-based, like skipFunctionalTests / skipCodeStyle)
6158
# to drop the Grails-Micronaut "island" (grails-micronaut, grails-micronaut-bom, and

grails-data-graphql/plugin/build.gradle

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,13 @@ dependencies {
7272
// api: HttpServletRequest/Response in GraphqlController
7373
}
7474

75-
// GraphQLSpec test trait imports types from io.micronaut.http.* and
76-
// io.micronaut.rxjava2.http.client.* so the rxjava2 client (which transitively
77-
// pulls micronaut-http-client and micronaut-http) is required to compile the
78-
// trait. The trait is only useful from integration tests; the runtime
79-
// dependency is therefore deferred to consumers (the example apps already
80-
// declare it as `implementation`). Keeping it `compileOnly` here avoids
81-
// shipping an unused micronaut HTTP client on every Grails app's runtime
82-
// classpath - test dependencies must not leak onto the production classpath
83-
// post Grails 7.
84-
compileOnly "io.micronaut.rxjava2:micronaut-rxjava2-http-client:$micronautRxjava2Version"
75+
// GraphQLSpec test trait talks to the running app over HTTP via
76+
// org.springframework.web.client.RestClient (Spring Boot 4 / Spring 7). The
77+
// trait is only useful from integration tests, and every Grails app already
78+
// has spring-web on its runtime classpath via grails-web-boot, so the
79+
// dependency stays `compileOnly` to avoid shipping anything new on the
80+
// production classpath.
81+
compileOnly 'org.springframework:spring-web'
8582

8683
testImplementation project(':grails-testing-support-web')
8784
testImplementation 'net.bytebuddy:byte-buddy'

grails-data-graphql/plugin/src/main/groovy/org/grails/gorm/graphql/plugin/testing/GraphQLSpec.groovy

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919

2020
package org.grails.gorm.graphql.plugin.testing
2121

22-
import groovy.json.StreamingJsonBuilder
22+
import groovy.json.JsonOutput
23+
import groovy.json.JsonSlurper
2324
import groovy.transform.TupleConstructor
24-
import io.micronaut.http.HttpRequest
25-
import io.micronaut.http.HttpResponse
26-
import io.micronaut.http.uri.UriBuilder
27-
import io.micronaut.rxjava2.http.client.RxHttpClient
2825
import org.springframework.beans.factory.annotation.Value
26+
import org.springframework.http.MediaType
27+
import org.springframework.http.ResponseEntity
28+
import org.springframework.http.converter.StringHttpMessageConverter
29+
import org.springframework.web.client.RestClient
2930

3031
trait GraphQLSpec {
3132

@@ -37,7 +38,15 @@ trait GraphQLSpec {
3738

3839
GraphQLRequestHelper getGraphQL() {
3940
if (_graphql == null) {
40-
_graphql = new GraphQLRequestHelper(rest: RxHttpClient.create(new URL(getServerUrl())))
41+
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter()
42+
stringConverter.supportedMediaTypes = [MediaType.ALL]
43+
_graphql = new GraphQLRequestHelper(rest: RestClient.builder()
44+
.baseUrl(getServerUrl())
45+
.messageConverters({ converters ->
46+
converters.clear()
47+
converters.add(stringConverter)
48+
})
49+
.build())
4150
}
4251
_graphql
4352
}
@@ -56,59 +65,90 @@ trait GraphQLSpec {
5665
@TupleConstructor
5766
static class GraphQLRequestHelper {
5867

59-
RxHttpClient rest
68+
private static final MediaType APPLICATION_GRAPHQL = MediaType.parseMediaType('application/graphql')
69+
private static final JsonSlurper SLURPER = new JsonSlurper()
6070

61-
HttpResponse<Map> graphql(String requestBody) {
62-
rest.exchange(HttpRequest.POST('/graphql', requestBody).contentType('application/graphql'), Map)
63-
.firstOrError().blockingGet()
71+
RestClient rest
72+
73+
ResponseEntity<Map> graphql(String requestBody) {
74+
wrapJson(exchangeGraphql(requestBody))
75+
}
76+
77+
// Overload that returns the raw body for callers asserting on the
78+
// unparsed JSON payload (only String is supported - tests asserting on
79+
// a structured body should use the no-class overload above which parses
80+
// into a Map).
81+
@SuppressWarnings('unchecked')
82+
def <T> ResponseEntity<T> graphql(String requestBody, Class<T> bodyType) {
83+
if (bodyType != String) {
84+
throw new IllegalArgumentException(
85+
"graphql(String, Class) only supports String.class; got ${bodyType.name}")
86+
}
87+
(ResponseEntity<T>) exchangeGraphql(requestBody)
6488
}
6589

66-
def <T> HttpResponse<T> graphql(String requestBody, Class<T> bodyType) {
67-
rest.exchange(HttpRequest.POST('/graphql', requestBody).contentType('application/graphql'), bodyType)
68-
.firstOrError().blockingGet()
90+
private ResponseEntity<String> exchangeGraphql(String requestBody) {
91+
rest.post()
92+
.uri('/graphql')
93+
.contentType(APPLICATION_GRAPHQL)
94+
.body(requestBody)
95+
.retrieve()
96+
.toEntity(String)
6997
}
7098

71-
private HttpResponse<Map> buildJsonRequest(Map<String, Object> data) {
72-
rest.exchange(HttpRequest.POST('/graphql', data), Map).firstOrError().blockingGet()
99+
private ResponseEntity<Map> buildJsonRequest(Map<String, Object> data) {
100+
wrapJson(rest.post()
101+
.uri('/graphql')
102+
.contentType(MediaType.APPLICATION_JSON)
103+
.body(JsonOutput.toJson(data))
104+
.retrieve()
105+
.toEntity(String))
73106
}
74-
private HttpResponse<Map> buildGetRequest(Map<String, Object> data) {
107+
108+
private ResponseEntity<Map> buildGetRequest(Map<String, Object> data) {
75109
if (data.containsKey('variables')) {
76-
StringWriter sw = new StringWriter()
77-
new StreamingJsonBuilder(sw).call(data.variables)
78-
data.put('variables', sw.toString())
110+
data.put('variables', JsonOutput.toJson(data.variables))
79111
}
112+
wrapJson(rest.get()
113+
.uri('/', { uriBuilder ->
114+
data.each { key, value ->
115+
uriBuilder.queryParam(key, value)
116+
}
117+
uriBuilder.build()
118+
})
119+
.retrieve()
120+
.toEntity(String))
121+
}
80122

81-
UriBuilder uriBuilder = UriBuilder.of('/')
82-
data.forEach({ key, value ->
83-
uriBuilder.queryParam(key, value)
84-
})
85-
86-
rest.exchange(HttpRequest.GET(uriBuilder.build()), Map).firstOrError().blockingGet()
123+
private static ResponseEntity<Map> wrapJson(ResponseEntity<String> raw) {
124+
String body = raw.body
125+
Map parsed = (body == null || body.isEmpty()) ? null : (Map) SLURPER.parseText(body)
126+
new ResponseEntity<Map>(parsed, raw.headers, raw.statusCode)
87127
}
88128

89-
HttpResponse<Map> json(String query) {
129+
ResponseEntity<Map> json(String query) {
90130
buildJsonRequest([query: query])
91131
}
92-
HttpResponse<Map> json(String query, String operationName) {
132+
ResponseEntity<Map> json(String query, String operationName) {
93133
buildJsonRequest([query: query, operationName: operationName])
94134
}
95-
HttpResponse<Map> json(String query, Map variables) {
135+
ResponseEntity<Map> json(String query, Map variables) {
96136
buildJsonRequest([query: query, variables: variables])
97137
}
98-
HttpResponse<Map> json(String query, Map variables, String operationName) {
138+
ResponseEntity<Map> json(String query, Map variables, String operationName) {
99139
buildJsonRequest([query: query, operationName: operationName, variables: variables])
100140
}
101141

102-
HttpResponse<Map> get(String query) {
142+
ResponseEntity<Map> get(String query) {
103143
buildGetRequest([query: query])
104144
}
105-
HttpResponse<Map> get(String query, String operationName) {
145+
ResponseEntity<Map> get(String query, String operationName) {
106146
buildGetRequest([query: query, operationName: operationName])
107147
}
108-
HttpResponse<Map> get(String query, Map variables) {
148+
ResponseEntity<Map> get(String query, Map variables) {
109149
buildGetRequest([query: query, variables: variables])
110150
}
111-
HttpResponse<Map> get(String query, Map variables, String operationName) {
151+
ResponseEntity<Map> get(String query, Map variables, String operationName) {
112152
buildGetRequest([query: query, operationName: operationName, variables: variables])
113153
}
114154
}

grails-test-examples/graphql/grails-docs-app/build.gradle

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@ dependencies {
5454
implementation 'org.apache.grails:grails-data-mongodb-gson-templates'
5555

5656
implementation "org.hibernate:hibernate-core-jakarta:$hibernate5Version"
57-
implementation "io.micronaut.rxjava2:micronaut-rxjava2-http-client:$micronautRxjava2Version"
58-
// JSON mapper for the micronaut HTTP client used by the GraphQLSpec trait.
59-
implementation "io.micronaut.serde:micronaut-serde-jackson:$micronautSerdeJacksonVersion"
6057

6158
console 'org.apache.grails:grails-console'
6259
profile 'org.apache.grails.profiles:rest-api'

grails-test-examples/graphql/grails-multi-datastore-app/build.gradle

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,6 @@ dependencies {
5555

5656
implementation "org.hibernate:hibernate-core-jakarta:$hibernate5Version"
5757
implementation 'com.graphql-java:graphql-java'
58-
implementation "io.micronaut.rxjava2:micronaut-rxjava2-http-client:$micronautRxjava2Version"
59-
// JSON mapper for the micronaut HTTP client used by the GraphQLSpec trait.
60-
implementation "io.micronaut.serde:micronaut-serde-jackson:$micronautSerdeJacksonVersion"
6158

6259
implementation 'com.h2database:h2'
6360
implementation 'org.apache.tomcat:tomcat-jdbc'

grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/BarIntegrationSpec.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class BarIntegrationSpec extends Specification implements GraphQLSpec {
4242
}
4343
}
4444
""")
45-
Map obj = resp.body().data.barCreate
45+
Map obj = resp.body.data.barCreate
4646

4747
then: 'bar is created in the Mongo datastore with a valid ObjectId'
4848
new ObjectId((String) obj.id)

grails-test-examples/graphql/grails-multi-datastore-app/src/integration-test/groovy/myapp/FooIntegrationSpec.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class FooIntegrationSpec extends Specification implements GraphQLSpec {
4141
}
4242
}
4343
""")
44-
Map obj = resp.body().data.fooCreate
44+
Map obj = resp.body.data.fooCreate
4545

4646
then: 'foo is created in the Hibernate datastore'
4747
obj.id == 1

grails-test-examples/graphql/grails-tenant-app/build.gradle

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@ dependencies {
5454
implementation 'org.apache.grails:grails-data-mongodb-gson-templates'
5555

5656
implementation "org.hibernate:hibernate-core-jakarta:$hibernate5Version"
57-
implementation "io.micronaut.rxjava2:micronaut-rxjava2-http-client:$micronautRxjava2Version"
58-
// JSON mapper for the micronaut HTTP client used by the GraphQLSpec trait.
59-
implementation "io.micronaut.serde:micronaut-serde-jackson:$micronautSerdeJacksonVersion"
6057

6158
console 'org.apache.grails:grails-console'
6259
profile 'org.apache.grails.profiles:rest-api'

grails-test-examples/graphql/grails-tenant-app/src/integration-test/groovy/grails/tenant/app/UserIntegrationSpec.groovy

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec {
4545
}
4646
}
4747
""")
48-
Map obj = resp.body().data.userCreate
48+
Map obj = resp.body.data.userCreate
4949

5050
then: "The company is supplied via multi-tenancy"
5151
obj.id == 1
@@ -77,7 +77,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec {
7777
}
7878
}
7979
""")
80-
Map obj = resp.body().data
80+
Map obj = resp.body.data
8181

8282
then: "The company is supplied via multi-tenancy"
8383
obj.john.name == 'John'
@@ -98,7 +98,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec {
9898
}
9999
}
100100
""")
101-
List obj = resp.body().data.userList
101+
List obj = resp.body.data.userList
102102

103103
then: "The list is filtered by the company"
104104
obj.size() == 1
@@ -117,7 +117,7 @@ class UserIntegrationSpec extends Specification implements GraphQLSpec {
117117
}
118118
}
119119
""")
120-
List obj = resp.body().data.userList
120+
List obj = resp.body.data.userList
121121

122122
then: "The list is filtered by the company"
123123
obj.size() == 2

grails-test-examples/graphql/grails-test-app/build.gradle

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,6 @@ dependencies {
5555

5656
implementation "org.hibernate:hibernate-core-jakarta:$hibernate5Version"
5757
implementation 'com.graphql-java:graphql-java'
58-
implementation "io.micronaut.rxjava2:micronaut-rxjava2-http-client:$micronautRxjava2Version"
59-
// JSON mapper for the micronaut HTTP client used by the GraphQLSpec trait.
60-
implementation "io.micronaut.serde:micronaut-serde-jackson:$micronautSerdeJacksonVersion"
6158

6259
console 'org.apache.grails:grails-console'
6360
profile 'org.apache.grails.profiles:rest-api'

0 commit comments

Comments
 (0)