-
Notifications
You must be signed in to change notification settings - Fork 909
Sync pagination implementation #207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No major concerns so far. It would be nice to see the generated code to get a sense for what that looks like.
info("Emitting paginator classes"); | ||
List<GeneratorTask> tasks = new ArrayList<>(); | ||
|
||
model.getPaginators().entrySet().stream() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just collect into a list here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1. changed
@@ -70,14 +75,16 @@ public IntermediateModel( | |||
CustomizationConfig customizationConfig, | |||
ServiceExamples examples, | |||
Map<String, WaiterDefinitionModel> waiters, | |||
Map<String, AuthorizerModel> customAuthorizers) { | |||
Map<String, AuthorizerModel> customAuthorizers, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Builder might be nice here.
@@ -169,6 +180,10 @@ public boolean getHasWaiters() { | |||
return waiters.size() > 0; | |||
} | |||
|
|||
public boolean getHasPaginators() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This weird naming seems like something we would do for FTL. Do we need it for the poet approach?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
waiters has the same naming. I thought it was used for serialization/deserialization. We might not need it. I can change it to hasPaginators()
* @return the name of the pagination enabled operation | ||
* | ||
*/ | ||
public static String getSyncMethodName(String methodName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this make more sense in NamingStrategy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DefaultNamingStrategy constructor is coupled to ServiceModel and CustomizationConfig. But the method doesn't require any of them. The classes where I use this method don't have enough information to create a NamingStrategy object.
*/ | ||
public interface Paginated<PageT, ItemT> extends SdkIterable<PageT> { | ||
|
||
PageT firstPage(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are going with generating both request/response and paginated methods I question the usefulness of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Customers can use this as return type instead of operation specific class.
For example,
Paginated<ListTablesResponse, String> response = dbClient.listTables();
Instead of
ListTablesPaginator response = dbClient.listTables();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I meant the firstPage specifically. That was added mainly when we were thinking of only exposing the paginator but if that's no longer true I don't think it holds it's weight.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are eagerly loading the responses. So if a customer want a single page and uses next() method, SDK makes two service calls. This method might help in that case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If they just want a single page they can use the method that returns a single page. The only reason this was added was because we weren't planning on exposing the low level API method. Also I'm a little unsure about making service calls the customer didn't ask for, can we make it lazy loaded? You may still have to preemptively fetch the next page if hasNext is called (and you are at the end of a page boundary).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may still have to preemptively fetch the next page if hasNext is called (and you are at the end of a page boundary).
We don't want to make service calls in hasNext(). It can be called multiple times before next() method is called. I didn't get the statement about end of page boundary.
|
||
public class PaginatedItemsIterable<ResponseT, ItemT> implements SdkIterable<ItemT> { | ||
|
||
private final Iterator<ResponseT> responseIterator; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these be iterables too? Otherwise it seems you can only iterate it once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everytime this object is constructed, a new object of iterator is passed. So if customer tries to use the API to iterate through response/paginated member multiple items, it works fine. I have added a sample paginator class in the description.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I'll have to see how this is used in the generated code but I assume it goes something like this.
// I assume this creates the PaginatedItemsIterable
SdkIterable<Reservation> reservations = ec2.describeInstances().allItems();
// Iterate once
for(Reservation r : reserverations) {...}
// What happens when I iterate again?
for(Reservation r : reserverations) {...}
edit - Okay I see the generated code, yeah I think that will be a problem then right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. As discussed offline, will expose a PaginatedResponsesIterator instead of the PaginatedResponsesIterable and pass the paginator ref to this class
return new ItemIterator(responseIterator); | ||
} | ||
|
||
private class ItemIterator implements Iterator<ItemT> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might as well make this static and pass in the getPaginatedItemIterator too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The itemsIterator in the class is separate for each instance. I am not able to use the generics (ItemT) if I make the class static.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right, can you just reference the outer field rather than pass in responseIterator? Just feels weird to pass one in but not the other.
throw new NoSuchElementException(); | ||
} | ||
|
||
if (itemsIterator == null || !itemsIterator.hasNext()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
itemsIterator can never be null here right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't think of a null case. Added it as a safety incase I overlooked any edge case.
|
||
@Override | ||
public boolean hasNext() { | ||
return (itemsIterator != null && itemsIterator.hasNext()) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For items iteration it's possible for the responseIterator to have a next page but that next page have an empty collection of items. We should make sure we are handling that case correctly in accordance with the Iterator interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible for a page to have empty items? Then what is the use of that page? At this point in code, we will throw an exception as there is no next element. Its tricky to handle this case as we don't know if the page with empty items is the last page or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it's possible, at least for Dynamo and I suspect for other services too. I think it's just an implementation limitation, the service may not know what the last item is until it goes to fetch it so if you end a page on the last item it thinks there might be more when in reality there's not. The only way to check for sure is to make another call and get an emtpy response with a empty paging token.
http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html
The primary key of the item where the operation stopped, inclusive of the previous result set. Use this value to start a new operation, excluding this value in the new request.
If LastEvaluatedKey is empty, then the "last page" of results has been processed and there is no more data to be retrieved.
If LastEvaluatedKey is not empty, it does not necessarily mean that there is more data in the result set. The only way to know when you have reached the end of the result set is when LastEvaluatedKey is empty.
private final Function<ResponseT, ResponseT> getNextResponse; | ||
|
||
public PaginatedResponsesIterable(ResponseT firstResponse, | ||
Function<ResponseT, ResponseT> getNextResponse) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of Function
can we create an interface like NextResponseSupplier
and annotate with @FunctionalInterface
instead? That way we can have a more descriptive method like nextResponse
instead of apply
Ditto for PaginatedItemsIterable
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am okay with this change. Will do it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an interface for NextResponseSupplier.
I am not sure if I want to do the same for getIterator in PaginatedItemsIterable class. The result_key can be a list and we need to generate multiple classes for each key in the list. This will bloat the response class for paginated APIs.
860a2bb
to
7aefbeb
Compare
re: naming, instead of |
To join the naming discussion, is it possible to come up with a name that follows our service API standards, since this will be on the service clients?
I'm probably just throwing fuel on the fire, though since I can't come up with an ending noun that doesn't require us to depluralize the resource's noun. |
|
Sounds better as per english semantics. But It doesn't convey that the operation is paginated. |
0bb679d
to
ab338c2
Compare
@@ -43,6 +43,8 @@ | |||
MAPPER.configure(JsonParser.Feature.ALLOW_COMMENTS, true); | |||
// TODO: Un comment this once we know for sure, we capture everything in C2j model. | |||
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |||
// TODO: Risky to enable for all deserialiation? | |||
MAPPER.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In paginators.json, the values for input_token, output_token and result_key can be a single value or a list of values.
Example:
"result_key": ["Folders", "Documents"]
"result_key": "Users"
I am representing these fields as List in PaginatorDefinition class. When deserializing the json file into PaginatorDefinition class, this property has to be set so mapper knows to accept a single value into a list.
@@ -52,6 +53,9 @@ | |||
@JsonIgnore | |||
private final Map<String, WaiterDefinitionModel> waiters; | |||
|
|||
@JsonIgnore | |||
private final Map<String, PaginatorDefinition> paginators; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we keep consistent naming? For waiters, its WaiterDefinitionModel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Waiters are removed. Keep it as PaginatorDefinition.
@@ -147,14 +148,30 @@ private MethodSpec constructorWithAdvancedConfiguration() { | |||
List<MethodSpec> methods = new ArrayList<>(); | |||
ClassName returnType = poetExtensions.getModelClass(opModel.getReturnType().getReturnType()); | |||
|
|||
methods.add(SyncClientInterface.operationMethodSignature(model, opModel) | |||
methods.add(SyncClientInterface.operationMethodSignature(model, opModel, opModel.getMethodName()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the change here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In operationMethodSignature method body, opModel.getMethodName() was being used operation name. For paginators enabled APIs, the name is different. I added the extra parameter in method signature so I can reuse operationMethodSignature() for both normal and pagination enabled APIs.
PaginatorUtils.getSyncMethodName(opModel.getMethodName())) | ||
.addAnnotation(Override.class) | ||
.returns(poetExtensions.getResponseClassForPaginatedSyncOperation(opModel.getOperationName())) | ||
.addCode("\n") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the comment in only for adding new line, I don't see it in my latest revision. I might have added to have clear distinction if the body was complex.
Let me know if I misunderstood the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually the code is still there, I was looking at a different class. The new lines are to make the method body look clear in the generated class. I don't have a strong opinion on them.
.addAnnotation(Override.class) | ||
.addCode(getCustomResponseHandler(opModel, returnType) | ||
.orElseGet(() -> protocolSpec.responseHandler(opModel))) | ||
.addCode(protocolSpec.errorResponseHandler(opModel)) | ||
.addCode(protocolSpec.executionHandler(opModel)) | ||
.build()); | ||
|
||
// Add extra methods for paginated operations | ||
if (model.getPaginators().keySet().contains(opModel.getOperationName())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move this to it's own method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
.addModifiers(Modifier.DEFAULT) | ||
.addStatement("throw new $T()", UnsupportedOperationException.class) | ||
.build()); | ||
|
||
methods.addAll(streamingSimpleMethods(opModel)); | ||
|
||
// Add extra methods for paginated operations | ||
if (model.getPaginators().keySet().contains(opModel.getOperationName())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move code to its own method
|
||
public SdkIterable<String> tableNames() { | ||
Function<ListTablesResponse, Iterator<String>> getPaginatedMemberIterator = response -> response != null | ||
? response.tableNames().iterator() : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If response is null, I am returning null. This is used in PaginatedItemsIterable which checks has a check to handle null values. Are you suggesting to use empty iterator?
ab338c2
to
d70afcb
Compare
Made changes based on spfink@ feedback |
d70afcb
to
b7fe511
Compare
.build()); | ||
|
||
TableUtils.waitUntilActive(dynamo, TABLE_NAME); | ||
// new DynamoDBClientWaiters(dynamo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's remove the commented-out codes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
@JsonProperty("limit_key") | ||
private String limitKey; | ||
|
||
public PaginatorDefinition() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default constructor probably not required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the code style we follow for classes with just default constructor.
*/ | ||
public class Paginators { | ||
|
||
public static final Paginators NONE = new Paginators(Collections.emptyMap()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe rename it to EMPTY_PAGINATOR
or NONE_PAGINATOR
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a fan of NONE since it's clear from the qualifier, Paginators.NONE
|
||
package software.amazon.awssdk.codegen.utils; | ||
|
||
public class PaginatorUtils { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final on class and private constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -33,7 +33,8 @@ public AwsGeneratorTasks(GeneratorTaskParams params) { | |||
} | |||
|
|||
private Iterable<GeneratorTask> createAwsTasks(GeneratorTaskParams params) { | |||
return new CompositeIterable<>(new AsyncClientGeneratorTasks(params)); | |||
return new CompositeIterable<>(new AsyncClientGeneratorTasks(params), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a TODO for the async generator tasks. It's something we'll defintely wanna push up to the generic tasks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -43,6 +43,8 @@ | |||
MAPPER.configure(JsonParser.Feature.ALLOW_COMMENTS, true); | |||
// TODO: Un comment this once we know for sure, we capture everything in C2j model. | |||
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |||
// TODO: Risky to enable for all deserialiation? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, removed the TODO. Thanks
*/ | ||
public class Paginators { | ||
|
||
public static final Paginators NONE = new Paginators(Collections.emptyMap()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a fan of NONE since it's clear from the qualifier, Paginators.NONE
* | ||
* Sample of a generated class with annotations: | ||
|
||
public final class ListTablesPaginator implements Paginated<ListTablesResponse, String> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably would remove this from the javadocs and just have it in the diff test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, what is diff test? Do you mean a new test class to test the generation of sample paginators file?
package software.amazon.awssdk.pagination; | ||
|
||
@FunctionalInterface | ||
public interface NextPageSupplier<ResponseT> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels more internal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
@SdkProtectedApi | ||
public class PaginatedItemsIterable<ResponseT, ItemT> implements SdkIterable<ItemT> { | ||
|
||
private final Paginated paginator; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this have some kind of generics?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. I actually removed the Paginated interface as we are removing the main allItems() method from it. Not the response class directly extends SdkIterable instead of Paginated.
Ex:
public final class ListTablesPaginator implements SdkIterable
instead of
public final class ListTablesPaginator implements Paginated<ListTablesResponse, String>
Let me know if you have any concerns?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
throw new NoSuchElementException(); | ||
} | ||
|
||
// Using while loop here to handle response pages with empty collection of items |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to handle the case you mentioned that there can be pages with empty collection of items. In this code, we retrieve the next page until there is a next element in itemsIterator or no more response pages.
If there is next element in itemsIterator, we return it.
If there is no response page, it means there are no more elements to return and so we return null. I am not satisfied with this. But I don't like throwing error as hasNext() before this next() call would have returned true.
|
||
|
||
if (!itemsIterator.hasNext()) { | ||
// TODO throw error or return null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somethings fishy here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above comment. I am not satisfied with it too. This happens only in cases with empty item collection in last page. I don't see a good way to handle it. Any ideas?
*/ | ||
public class PaginatedResponsesIterator<ResponseT> implements Iterator<ResponseT> { | ||
|
||
private final NextPageSupplier<ResponseT> nextPageSupplier; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Supplier makes me think of a factory. This is really more of a function. NextPageFetcher?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, this is a function. Based on Dongie's comment to have better naming instead of apply(), created this NextPageSupplier functional interface. I am okay with both names.
@aws/aws-java-sdk
Anyone else want to chime in their opinion with naming - NextPageSupplier vs NextPageFetcher?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NextPageFetcher
+1
|
||
ResponseT oldValue = currentResponse; | ||
|
||
currentResponse = nextPageSupplier.nextPage(currentResponse); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The preemptive fetch seems weird to me. I'm hestitant to make service calls the customer didn't really ask for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably want a change log entry for this too since it's a sizable feature.
f3cc14c
to
db87043
Compare
@Override | ||
public ResponseT next() { | ||
if (!hasNext()) { | ||
throw new NoSuchElementException(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps a message here like "No more pages left".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
|
||
@Override | ||
public boolean hasNext() { | ||
return (singlePageItemsIterator != null && singlePageItemsIterator.hasNext()) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think to preserve the semantics of Iterator interface you need to do the loop here. You can't know if there are more items just by the presence of another page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats true. We have to retrieve the next page to verify that right? But we discussed that we should not make service calls in hasNext() method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For Items iteration I can't see another way to do it. I think it's more acceptable in that case since you have given up control of when service calls are made already (i.e. there is no contract of where page boundaries are and when service calls are made). If there's a way to not make that call and still preserve the Iterator contract I'm all for it, but I don't think thats possible.
* @see <a href="http://docs.aws.amazon.com/goto/WebAPI/json-service-2010-05-08/PaginatedOperationWithoutResultKey" | ||
* target="_top">AWS API Documentation</a> | ||
*/ | ||
default PaginatedOperationWithoutResultKeyPaginator paginatedOperationWithoutResultKeyIterable( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are going with the Iterable naming can we change Paginator to just Iterable? I.E. ListTablesIterable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had same idea but thought keeping paginator in return type will be more clear to customer that it is a paginated (or atleast some special) API.
import software.amazon.awssdk.services.jsonprotocoltests.model.SimpleStruct; | ||
|
||
/** | ||
* Represents the output for the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is some really nice Javadoc! Would it be more discoverable on the operation method itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will add docs in client too. Was figuring out some formatting issues
* guarantee that your request is valid. As you iterate through this iterable, SDK will lazily load response pages by | ||
* making service calls until there are no pages left or your iteration stops. | ||
* | ||
* Following are few ways to iterate through the response pages: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following
Here are
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
if (oldPage == null) { | ||
return client.paginatedOperationWithResultKey(firstRequest); | ||
} else { | ||
return client.paginatedOperationWithResultKey(firstRequest.toBuilder().nextToken(oldPage.nextToken()).build()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor but perhaps just a ternary here.
return client.paginatedOperationWithResultKey(firstRequest.toBuilder()
.nextToken(oldPage == null ? null : oldPage.nextToken())
.build();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The output_token is a list i.e., there might be multiple values in response that is needed to get the next page. I feel ternary becomes convoluted to read and i prefer to keep the if-else.
return client.paginatedOperationWithResultKey(firstRequest.toBuilder()
.nextToken1(oldPage == null ? null : oldPage.nextToken1())
.nextToken2(oldPage == null ? null : oldPage.nextToken2())
.build();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor, if using if-else
, It might be cleaner to remove the else.
if (oldPage == null) {
return client.paginatedOperationWithResultKey(firstRequest);
}
return client.paginatedOperationWithResultKey(firstRequest.toBuilder().nextToken(oldPage.nextToken()).build());
* {@link JsonProtocolTestsClient#paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest)} API. | ||
*/ | ||
@Generated("software.amazon.awssdk:codegen") | ||
public final class PaginatedOperationWithResultKeyPaginator implements SdkIterable<PaginatedOperationWithResultKeyResponse> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there are two of these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep was aware of it. Some issue with loading the file in paginators package. Will fix it.
|
||
private final NextPageFetcher nextPageFetcher; | ||
|
||
public PaginatedOperationWithoutResultKeyPaginator(final JsonProtocolTestsClient client, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there are two of these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above.
|
||
public class PaginatedItemsIterableTest { | ||
|
||
private SdkIterable pagesIterable = Mockito.mock(SdkIterable.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you do the RunWith mockito runner you can annotate these with just Mock
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh thats it. I tried with @mock annotations but I was getting NPE. I forgot to annotate the class with MockitoJUnitRunner.
} | ||
|
||
@Test | ||
public void nextMethodDoesnotRetrieveNextPage_WhenItemsIteratorHasAnElement() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DoesNot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do
8aa0122
to
af55b8d
Compare
/** | ||
* Constructs additional documentation on the client operation that is appended to the service documentation. | ||
*/ | ||
public String getDocsForSyncOperation() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a TODO here to link to our developer guide. We'll probably want to go into a little more depth in the guide so a link to it when available would be nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
.addAnnotation(Override.class) | ||
.addCode(getCustomResponseHandler(opModel, returnType) | ||
.orElseGet(() -> protocolSpec.responseHandler(opModel))) | ||
.addCode(protocolSpec.errorResponseHandler(opModel)) | ||
.addCode(protocolSpec.executionHandler(opModel)) | ||
.build()); | ||
|
||
// Add extra methods for paginated operations | ||
if (opModel.isPaginated()) { | ||
methods.add(SyncClientInterface.operationMethodSignature(model, opModel, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If new lining, we typically new line all arguments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
.addAnnotation(Override.class) | ||
.returns(poetExtensions.getResponseClassForPaginatedSyncOperation(opModel.getOperationName())) | ||
.addStatement("return new $T(this, $L)", | ||
poetExtensions.getResponseClassForPaginatedSyncOperation(opModel.getOperationName()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Line these lines up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
.addModifiers(Modifier.DEFAULT) | ||
.addStatement("throw new $T()", UnsupportedOperationException.class) | ||
.build()); | ||
|
||
methods.addAll(streamingSimpleMethods(opModel)); | ||
|
||
// Add extra methods for paginated operations | ||
if (opModel.isPaginated()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move all specific logic to other methods?
Basically end up with:
methods.addAll(paginatedSimpleMethods());
methods.addAll(paginatedMethods());
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if we want to have separate methods that go through all operations agains to find the paginated operations.
I moved the new code to a new method that takes in an operation model and return the method specs if its paginated operation.
List paginatedMethods(OperationModel opModel)
the call will end up with
methods.addAll(paginatedMethods(opModel));
private static final String NEXT_PAGE_FETCHER_MEMBER = "nextPageFetcher"; | ||
private static final String HAS_NEXT_PAGE_METHOD = "hasNextPage"; | ||
private static final String NEXT_PAGE_METHOD = "nextPage"; | ||
private static final String OLD_PAGE_METHOD_ARGUMENT = "oldPage"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previous page?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
public PaginatorResponseClassSpec(IntermediateModel intermediateModel, | ||
String c2jOperationName, | ||
PaginatorDefinition paginatorDefinition) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra new line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
@@ -82,14 +90,14 @@ protected DefaultJsonAsyncClient(AsyncClientConfiguration clientConfiguration) { | |||
public CompletableFuture<APostOperationResponse> aPostOperation(APostOperationRequest aPostOperationRequest) { | |||
|
|||
HttpResponseHandler<APostOperationResponse> responseHandler = protocolFactory.createResponseHandler( | |||
new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the changes in formatting throughout this file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I copied the expected file when the unit test failed. Then checked if new code looks as expected. I guess the generated file in unit test has different formatting. I tried to auto-format the file now which is having lot more changes
* of this type.</li> | ||
* </ul> | ||
* @sample JsonAsyncClient.PaginatedOperationWithResultKey | ||
* @see <a href="http://docs.aws.amazon.com/goto/WebAPI/json-service-2010-05-08/PaginatedOperationWithResultKey" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this link work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is generated for all operations in this class. All of them redirect to AWS documentation official page.
} | ||
|
||
/** | ||
* Some paginated operation with result_key in paginators.json file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you generated the javadoc and made sure this all looks good?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, looks good
@Test | ||
public void test_MultipleIteration_On_Responses_Iterable() { | ||
ScanRequest request = ScanRequest.builder().tableName(TABLE_NAME).limit(2).build(); | ||
ScanPaginator scanResponses = dynamo.scanIterable(request); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seeing it definitely don't know if I like "scanIterable"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we will go with "Iterable". We would want to have consistent naming across sync and Async clients. Based on #185, Iterable doesn't make sense for Async operations. So we need change them for sure.
Added a few comments. Mostly minor stuff. Def need to figure out naming. |
f39cc29
to
3930680
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine with where this is now. We might make changes as we start to using the paginators (probably in naming) but think this is good to ship out
5f024f6
to
3d360ee
Compare
3d360ee
to
dc0a9cc
Compare
Description
Implementation for Sync automatic de-pagination. Supports lists, nested shapes in paginators.json file
Tracking Issue: #26
For all paginated operations, there will be two APIs - one with automatic depagination and one that returns a single page. Example: For listTables operation in DynamoDB, the following APIs are present in the client:
This is the auto paginated API in which SDK will make service calls for the customer.
As per recent api review discussions and inconsistencies in paginator files, going with this option is safe in terms of backwards compatibility. Coming to discoverability, these APIs are highly discoverable compared to high level utilities as they are in same client.
Recent changes
Added more unit tests
Added Javadocs for generated classes
Testing
mvn clean install
Added an integ test class
Types of changes
Checklist
mvn install
succeedsLicense