Skip to content

Commit 7c07a64

Browse files
authored
Add support for pagination in compile-time codegen (Azure#44394)
1 parent 08a7652 commit 7c07a64

8 files changed

Lines changed: 697 additions & 70 deletions

File tree

sdk/clientcore/annotation-processor-test/src/main/java/io/clientcore/annotation/processor/test/implementation/TestInterfaceClientService.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package io.clientcore.annotation.processor.test.implementation;
55

66
import io.clientcore.annotation.processor.test.implementation.models.Foo;
7+
import io.clientcore.annotation.processor.test.implementation.models.FooListResult;
78
import io.clientcore.annotation.processor.test.implementation.models.HttpBinJSON;
89
import io.clientcore.core.annotations.ServiceInterface;
910
import io.clientcore.core.http.annotations.BodyParam;
@@ -13,17 +14,17 @@
1314
import io.clientcore.core.http.annotations.PathParam;
1415
import io.clientcore.core.http.annotations.QueryParam;
1516
import io.clientcore.core.http.annotations.UnexpectedResponseExceptionDetail;
16-
import io.clientcore.core.implementation.http.ContentType;
1717
import io.clientcore.core.http.models.HttpMethod;
1818
import io.clientcore.core.http.models.RequestOptions;
1919
import io.clientcore.core.http.models.Response;
2020
import io.clientcore.core.http.pipeline.HttpPipeline;
21+
import io.clientcore.core.implementation.http.ContentType;
2122
import io.clientcore.core.models.binarydata.BinaryData;
2223
import io.clientcore.core.serialization.ObjectSerializer;
23-
2424
import java.io.InputStream;
2525
import java.lang.reflect.InvocationTargetException;
2626
import java.nio.ByteBuffer;
27+
import java.util.List;
2728

2829
@ServiceInterface(name = "myService")
2930
public interface TestInterfaceClientService {
@@ -78,6 +79,20 @@ Response<Foo> getFoo(@PathParam("key") String key, @QueryParam("label") String l
7879
Response<Void> deleteFoo(@PathParam("key") String key, @QueryParam("label") String label,
7980
@HeaderParam("Sync-Token") String syncToken);
8081

82+
83+
@HttpRequestInformation(method = HttpMethod.GET, path = "foos", expectedStatusCodes = { 200 })
84+
Response<FooListResult> listFooListResult(@HostParam("uri") String uri, RequestOptions requestOptions);
85+
86+
@HttpRequestInformation(method = HttpMethod.GET, path = "{nextLink}", expectedStatusCodes = { 200 })
87+
Response<FooListResult> listNextFooListResult(@PathParam(value = "nextLink", encoded = true) String nextLink,
88+
RequestOptions requestOptions);
89+
90+
@HttpRequestInformation(method = HttpMethod.GET, path = "foos", expectedStatusCodes = { 200 })
91+
Response<List<Foo>> listFoo(@HostParam("uri") String uri, RequestOptions requestOptions);
92+
93+
@HttpRequestInformation(method = HttpMethod.GET, path = "{nextLink}", expectedStatusCodes = { 200 })
94+
Response<List<Foo>> listNextFoo(@PathParam(value = "nextLink", encoded = true) String nextLink,
95+
RequestOptions requestOptions);
8196
// HttpClientTests
8297
// Need to add RequestOptions to specify ResponseBodyMode, which is otherwise provided by convenience methods
8398
@SuppressWarnings({ "unchecked", "cast" })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package io.clientcore.annotation.processor.test.implementation.models;
5+
6+
import io.clientcore.core.serialization.json.JsonReader;
7+
import io.clientcore.core.serialization.json.JsonSerializable;
8+
import io.clientcore.core.serialization.json.JsonToken;
9+
import io.clientcore.core.serialization.json.JsonWriter;
10+
11+
import java.io.IOException;
12+
import java.util.List;
13+
14+
/**
15+
* The result of a list request.
16+
*/
17+
public final class FooListResult implements JsonSerializable<FooListResult> {
18+
/*
19+
* The collection value.
20+
*/
21+
private List<Foo> items;
22+
23+
/*
24+
* The URI that can be used to request the next set of paged results.
25+
*/
26+
private String nextLink;
27+
28+
/**
29+
* Creates an instance of FooListResult class.
30+
*/
31+
public FooListResult() {
32+
}
33+
34+
/**
35+
* Get the items property: The collection value.
36+
*
37+
* @return the items value.
38+
*/
39+
public List<Foo> getItems() {
40+
return this.items;
41+
}
42+
43+
/**
44+
* Set the items property: The collection value.
45+
*
46+
* @param items the items value to set.
47+
* @return the FooListResult object itself.
48+
*/
49+
public FooListResult setItems(List<Foo> items) {
50+
this.items = items;
51+
return this;
52+
}
53+
54+
/**
55+
* Get the nextLink property: The URI that can be used to request the next set of paged results.
56+
*
57+
* @return the nextLink value.
58+
*/
59+
public String getNextLink() {
60+
return this.nextLink;
61+
}
62+
63+
/**
64+
* Set the nextLink property: The URI that can be used to request the next set of paged results.
65+
*
66+
* @param nextLink the nextLink value to set.
67+
* @return the FooListResult object itself.
68+
*/
69+
public FooListResult setNextLink(String nextLink) {
70+
this.nextLink = nextLink;
71+
return this;
72+
}
73+
74+
/**
75+
* {@inheritDoc}
76+
*/
77+
@Override
78+
public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
79+
jsonWriter.writeStartObject();
80+
jsonWriter.writeArrayField("items", this.items, (writer, element) -> writer.writeJson(element));
81+
jsonWriter.writeStringField("@nextLink", this.nextLink);
82+
return jsonWriter.writeEndObject();
83+
}
84+
85+
/**
86+
* Reads an instance of FooListResult from the JsonReader.
87+
*
88+
* @param jsonReader The JsonReader being read.
89+
* @return An instance of FooListResult if the JsonReader was pointing to an instance of it, or null if it was
90+
* pointing to JSON null.
91+
* @throws IOException If an error occurs while reading the FooListResult.
92+
*/
93+
public static FooListResult fromJson(JsonReader jsonReader) throws IOException {
94+
return jsonReader.readObject(reader -> {
95+
FooListResult deserializedFooListResult = new FooListResult();
96+
while (reader.nextToken() != JsonToken.END_OBJECT) {
97+
String fieldName = reader.getFieldName();
98+
reader.nextToken();
99+
100+
if ("items".equals(fieldName)) {
101+
List<Foo> items = reader.readArray(reader1 -> Foo.fromJson(reader1));
102+
deserializedFooListResult.items = items;
103+
} else if ("@nextLink".equals(fieldName)) {
104+
deserializedFooListResult.nextLink = reader.getString();
105+
} else {
106+
reader.skipChildren();
107+
}
108+
}
109+
110+
return deserializedFooListResult;
111+
});
112+
}
113+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package io.clientcore.annotation.processor.test;
5+
6+
import io.clientcore.annotation.processor.test.implementation.TestInterfaceClientService;
7+
import io.clientcore.annotation.processor.test.implementation.models.Foo;
8+
import io.clientcore.annotation.processor.test.implementation.models.FooListResult;
9+
import io.clientcore.core.http.models.HttpHeaderName;
10+
import io.clientcore.core.http.models.HttpHeaders;
11+
import io.clientcore.core.http.models.HttpMethod;
12+
import io.clientcore.core.http.models.HttpRequest;
13+
import io.clientcore.core.http.models.PagedIterable;
14+
import io.clientcore.core.http.models.PagedResponse;
15+
import io.clientcore.core.http.models.RequestOptions;
16+
import io.clientcore.core.http.models.Response;
17+
import io.clientcore.core.http.models.ResponseBodyMode;
18+
import io.clientcore.core.http.pipeline.HttpPipeline;
19+
import io.clientcore.core.http.pipeline.HttpPipelineBuilder;
20+
import io.clientcore.core.models.binarydata.BinaryData;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.Set;
24+
import java.util.stream.Collectors;
25+
import org.junit.jupiter.api.Disabled;
26+
import org.junit.jupiter.api.Test;
27+
28+
import static org.junit.jupiter.api.Assertions.assertEquals;
29+
import static org.junit.jupiter.api.Assertions.assertNotNull;
30+
31+
/**
32+
* Test class for verifying the deserialization of paged responses and their handling in code generation.
33+
*/
34+
public class PagingOperationTests {
35+
private static final BinaryData FIRST_PAGE_RESPONSE = BinaryData.fromString("[{\"bar\":\"hello.world\",\"baz\":[\"hello\",\"hello.world\"],\"qux\":{\"a.b\":\"c.d\",\"bar.a\":\"ttyy\",\"bar.b\":\"uuzz\",\"hello\":\"world\"}}]");
36+
private static final BinaryData NEXTLINK_RESPONSE = BinaryData.fromString("[{\"bar\":\"hello.world2\",\"additionalProperties\":{\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}}]");
37+
38+
/**
39+
* Verifies that a response containing a List is correctly handled when returning a List<Foo>.
40+
*/
41+
@Test
42+
public void testListFoo() {
43+
String uri = "https://example.com";
44+
String firstPageUri = uri + "/foos";
45+
String nextLinkUri = uri + "/foos?page=2";
46+
RequestOptions requestOptions = new RequestOptions().setResponseBodyMode(ResponseBodyMode.DESERIALIZE);
47+
HttpPipeline pipeline = new HttpPipelineBuilder()
48+
.httpClient(request -> {
49+
String requestUri = request.getUri().toString();
50+
request.setRequestOptions(requestOptions);
51+
if (firstPageUri.equals(requestUri)) {
52+
return createMockResponse(request, 200, FIRST_PAGE_RESPONSE, nextLinkUri);
53+
} else if (nextLinkUri.equals(requestUri)) {
54+
return createMockResponse(request, 200, NEXTLINK_RESPONSE, null);
55+
}
56+
57+
return new MockHttpResponse(request, 404);
58+
})
59+
.build();
60+
TestInterfaceClientService testInterface = TestInterfaceClientService.getNewInstance(pipeline, null);
61+
62+
// Retrieve initial response
63+
Response<List<Foo>> initialResponse = testInterface.listFoo(uri, RequestOptions.none());
64+
65+
List<Foo> fooFirstPageResponse = initialResponse.getValue();
66+
assertNotNull(fooFirstPageResponse);
67+
assertNotNull(fooFirstPageResponse.get(0).bar());
68+
69+
// Convert List<Foo> response to PagedResponse<Foo>
70+
PagedResponse<Foo> firstPage = toPagedResponse(initialResponse, null);
71+
72+
PagedIterable<Foo> pagedIterable = new PagedIterable<>(
73+
pagingOptions -> firstPage, // First page
74+
(pagingOptions, nextLink) -> {
75+
Response<List<Foo>> nextResponse = testInterface.listNextFoo(nextLink, RequestOptions.none());
76+
return toPagedResponse(nextResponse, nextLink);
77+
}
78+
);
79+
80+
assertNotNull(pagedIterable);
81+
Set<Foo> allItems = pagedIterable.stream().collect(Collectors.toSet());
82+
assertEquals(1, allItems.size());
83+
}
84+
85+
@Test
86+
public void testListFooListResult() {
87+
String uri = "https://example.com";
88+
String firstPageUri = uri + "/foos";
89+
String nextLinkUri = uri + "/foos?page=2";
90+
RequestOptions requestOptions = new RequestOptions().setResponseBodyMode(ResponseBodyMode.DESERIALIZE);
91+
HttpPipeline pipeline = new HttpPipelineBuilder()
92+
.httpClient(request -> {
93+
String requestUri = request.getUri().toString();
94+
request.setRequestOptions(requestOptions);
95+
if (firstPageUri.equals(requestUri)) {
96+
return createMockResponse(request, 200, BinaryData.fromString(
97+
"{\"items\":[{\"bar\":\"hello.world\",\"baz\":[\"hello\",\"hello.world\"],\"qux\":{\"a" +
98+
".b\":\"c.d\"," +
99+
"\"bar.a\":\"ttyy\",\"bar.b\":\"uuzz\",\"hello\":\"world\"}}], \"nextLink\":\"" + nextLinkUri + "\"}"),
100+
nextLinkUri);
101+
} else if (nextLinkUri.equals(requestUri)) {
102+
return createMockResponse(request, 200, BinaryData.fromString(
103+
"{\"items\":[{\"bar\":\"hello.world2\",\"additionalProperties\":{\"bar\":\"baz\",\"a" +
104+
".b\":\"c.d\",\"properties.bar\":\"barbar\"}}]"),
105+
null);
106+
}
107+
return new MockHttpResponse(request, 404);
108+
})
109+
.build();
110+
111+
TestInterfaceClientService testInterface = TestInterfaceClientService.getNewInstance(pipeline, null);
112+
113+
// Fetch the first page
114+
PagedIterable<Foo> pagedIterable = new PagedIterable<>(
115+
pagingOptions -> toPagedResponse(testInterface.listFooListResult(uri, RequestOptions.none()), nextLinkUri),
116+
(pagingOptions, nextLink) -> toPagedResponse(testInterface.listNextFooListResult(nextLink, RequestOptions.none()), null)
117+
);
118+
119+
assertNotNull(pagedIterable);
120+
Set<Foo> allItems = pagedIterable.stream().collect(Collectors.toSet());
121+
assertEquals(1, allItems.size());
122+
}
123+
124+
/**
125+
* Creates a mock HTTP response with JSON body and optional nextLink header.
126+
*/
127+
private MockHttpResponse createMockResponse(HttpRequest request, int statusCode, BinaryData jsonBody, String nextLink) {
128+
HttpHeaders headers = new HttpHeaders();
129+
if (nextLink != null) {
130+
headers.set(HttpHeaderName.fromString("nextLink"), nextLink);
131+
}
132+
133+
return new MockHttpResponse(request, statusCode, headers, jsonBody);
134+
}
135+
136+
/**
137+
* Converts a Response<T> to a PagedResponse<Foo>.
138+
* Supports both Response<FooListResult> and Response<List<Foo>>.
139+
*/
140+
@SuppressWarnings({ "unchecked", "cast" })
141+
private <T> PagedResponse<Foo> toPagedResponse(Response<T> response, String nextLink) {
142+
if (response == null || response.getValue() == null) {
143+
return new PagedResponse<>(
144+
response != null ? response.getRequest() : new HttpRequest().setMethod(HttpMethod.GET).setUri("https://example.com"),
145+
200,
146+
response != null ? response.getHeaders() : new HttpHeaders(),
147+
response != null ? response.getBody() : null,
148+
Collections.emptyList() // Return an empty list when null
149+
);
150+
}
151+
152+
List<Foo> items;
153+
if (response.getValue() instanceof FooListResult) {
154+
items = ((FooListResult) response.getValue()).getItems(); // Extract list from FooListResult
155+
nextLink = ((FooListResult) response.getValue()).getNextLink();
156+
} else if (response.getValue() instanceof List) {
157+
items = (List<Foo>) response.getValue(); // Directly use the List<Foo>
158+
} else {
159+
throw new IllegalArgumentException("Unsupported response type: " + response.getValue().getClass().getName());
160+
}
161+
162+
return new PagedResponse<>(
163+
response.getRequest(),
164+
response.getStatusCode(),
165+
response.getHeaders(),
166+
response.getBody(),
167+
items,
168+
nextLink,
169+
null,
170+
null,
171+
null,
172+
null
173+
);
174+
}
175+
}

sdk/clientcore/annotation-processor-test/src/test/java/io/clientcore/annotation/processor/test/TestInterfaceGenerationTests.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import static org.junit.jupiter.api.Assertions.assertNotNull;
3535
import static org.junit.jupiter.api.Assertions.assertNull;
3636
import static org.junit.jupiter.api.Assertions.assertTrue;
37-
import static org.junit.jupiter.api.Assertions.fail;
3837

3938
public class TestInterfaceGenerationTests {
4039
private static LocalTestServer server;

0 commit comments

Comments
 (0)