-
Notifications
You must be signed in to change notification settings - Fork 117
Dynamodb insertions core actions and executor #1728
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
Open
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
162 changes: 162 additions & 0 deletions
162
.../src/main/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| package org.evomaster.client.java.controller.dynamodb; | ||
|
|
||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto; | ||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto; | ||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionResultsDto; | ||
|
|
||
| import java.lang.reflect.InvocationTargetException; | ||
| import java.lang.reflect.Method; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.concurrent.CompletionStage; | ||
|
|
||
| /** | ||
| * Executes DynamoDB insertions without binding the controller API to an AWS SDK version. | ||
| */ | ||
| public final class DynamoDbCommandExecutor { | ||
|
|
||
| private static final String ATTRIBUTE_VALUE_CLASS_NAME = | ||
| "software.amazon.awssdk.services.dynamodb.model.AttributeValue"; | ||
| private static final String ATTRIBUTE_VALUE_BUILDER_CLASS_NAME = ATTRIBUTE_VALUE_CLASS_NAME + "$Builder"; | ||
| private static final String PUT_ITEM_REQUEST_CLASS_NAME = | ||
| "software.amazon.awssdk.services.dynamodb.model.PutItemRequest"; | ||
| private static final String PUT_ITEM_REQUEST_BUILDER_CLASS_NAME = PUT_ITEM_REQUEST_CLASS_NAME + "$Builder"; | ||
| private static final String DYNAMODB_CLIENT_CLASS_NAME = | ||
| "software.amazon.awssdk.services.dynamodb.DynamoDbClient"; | ||
| private static final String DYNAMODB_ASYNC_CLIENT_CLASS_NAME = | ||
| "software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient"; | ||
|
|
||
| private static final String BUILDER_METHOD_NAME = "builder"; | ||
| private static final String BUILD_METHOD_NAME = "build"; | ||
| private static final String TABLE_NAME_METHOD_NAME = "tableName"; | ||
| private static final String ITEM_METHOD_NAME = "item"; | ||
| private static final String PUT_ITEM_METHOD_NAME = "putItem"; | ||
| private static final String STRING_VALUE_METHOD_NAME = "s"; | ||
| private static final String NUMBER_VALUE_METHOD_NAME = "n"; | ||
| private static final String BOOLEAN_VALUE_METHOD_NAME = "bool"; | ||
|
|
||
| /** | ||
| * Prevents instantiation of this utility class. | ||
| */ | ||
| private DynamoDbCommandExecutor() { | ||
| } | ||
|
|
||
| /** | ||
| * Executes insertions using a synchronous or asynchronous AWS SDK v2 client. | ||
| * | ||
| * @param client DynamoDB client | ||
| * @param insertions items to insert | ||
| * @return per-insertion results, stopping at the first failed insertion | ||
| * @throws NullPointerException when the client or insertion list is {@code null} | ||
| * @throws IllegalArgumentException when the insertion list is empty | ||
| */ | ||
| public static DynamoDbInsertionResultsDto executeInsert(Object client, List<DynamoDbInsertionDto> insertions) { | ||
| Objects.requireNonNull(client, "DynamoDB client cannot be null"); | ||
| Objects.requireNonNull(insertions, "DynamoDB insertions cannot be null"); | ||
| if (insertions.isEmpty()) { | ||
| throw new IllegalArgumentException("No data to insert"); | ||
| } | ||
|
|
||
| DynamoDbInsertionResultsDto results = new DynamoDbInsertionResultsDto(); | ||
| results.executionResults = new ArrayList<>(Collections.nCopies(insertions.size(), false)); | ||
| for (int i = 0; i < insertions.size(); i++) { | ||
| try { | ||
| executeOne(client, insertions.get(i)); | ||
| results.executionResults.set(i, true); | ||
| } catch (RuntimeException ignored) { | ||
| results.failedInsertionIndex = i; | ||
| return results; | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
|
|
||
| /** | ||
| * Executes one DynamoDB insertion through the AWS SDK v2 reflection API. | ||
| * | ||
| * @param client synchronous or asynchronous DynamoDB client | ||
| * @param insertion item to insert | ||
| */ | ||
| private static void executeOne(Object client, DynamoDbInsertionDto insertion) { | ||
| try { | ||
| ClassLoader loader = client.getClass().getClassLoader(); | ||
| Class<?> attributeValueClass = Class.forName( | ||
| ATTRIBUTE_VALUE_CLASS_NAME, true, loader); | ||
| Class<?> attributeValueBuilderClass = Class.forName( | ||
| ATTRIBUTE_VALUE_BUILDER_CLASS_NAME, true, loader); | ||
| Class<?> putItemRequestClass = Class.forName( | ||
| PUT_ITEM_REQUEST_CLASS_NAME, true, loader); | ||
| Class<?> putItemRequestBuilderClass = Class.forName( | ||
| PUT_ITEM_REQUEST_BUILDER_CLASS_NAME, true, loader); | ||
|
|
||
| Map<String, Object> item = new LinkedHashMap<>(); | ||
| for (DynamoDbAttributeValueDto attribute : insertion.attributes) { | ||
| Object builder = attributeValueClass.getMethod(BUILDER_METHOD_NAME).invoke(null); | ||
| String setter; | ||
| Object value = attribute.value; | ||
| switch (attribute.type) { | ||
| case STRING: | ||
| setter = STRING_VALUE_METHOD_NAME; | ||
| break; | ||
| case NUMBER: | ||
| setter = NUMBER_VALUE_METHOD_NAME; | ||
| break; | ||
| case BOOLEAN: | ||
| setter = BOOLEAN_VALUE_METHOD_NAME; | ||
| value = Boolean.valueOf(attribute.value); | ||
| break; | ||
| default: | ||
| throw new IllegalArgumentException("Unsupported DynamoDB attribute type: " + attribute.type); | ||
| } | ||
| attributeValueBuilderClass.getMethod(setter, value.getClass()).invoke(builder, value); | ||
| item.put(attribute.attributeName, | ||
| attributeValueBuilderClass.getMethod(BUILD_METHOD_NAME).invoke(builder)); | ||
| } | ||
|
|
||
| Object requestBuilder = putItemRequestClass.getMethod(BUILDER_METHOD_NAME).invoke(null); | ||
| putItemRequestBuilderClass.getMethod(TABLE_NAME_METHOD_NAME, String.class) | ||
| .invoke(requestBuilder, insertion.tableName); | ||
| putItemRequestBuilderClass.getMethod(ITEM_METHOD_NAME, Map.class).invoke(requestBuilder, item); | ||
| Object request = putItemRequestBuilderClass.getMethod(BUILD_METHOD_NAME).invoke(requestBuilder); | ||
| Method putItem = findPutItemMethod(client, loader, putItemRequestClass); | ||
| Object response = putItem.invoke(client, request); | ||
| if (response instanceof CompletionStage) { | ||
| ((CompletionStage<?>) response).toCompletableFuture().join(); | ||
| } | ||
| } catch (InvocationTargetException e) { | ||
| Throwable cause = e.getCause() == null ? e : e.getCause(); | ||
| throw new RuntimeException("Failed DynamoDB insertion into table '" + insertion.tableName + "'", cause); | ||
| } catch (ReflectiveOperationException e) { | ||
| throw new RuntimeException("Failed DynamoDB insertion into table '" + insertion.tableName + "'", e); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Finds the insertion method exposed by a synchronous or asynchronous AWS SDK v2 client. | ||
| * | ||
| * @param client DynamoDB client | ||
| * @param loader client class loader | ||
| * @param putItemRequestClass reflected request class | ||
| * @return reflected {@code putItem} method | ||
| * @throws ClassNotFoundException when the AWS client types are unavailable | ||
| * @throws NoSuchMethodException when the client does not expose the expected method | ||
| */ | ||
| private static Method findPutItemMethod(Object client, ClassLoader loader, Class<?> putItemRequestClass) | ||
| throws ClassNotFoundException, NoSuchMethodException { | ||
| Class<?> syncClientClass = Class.forName(DYNAMODB_CLIENT_CLASS_NAME, true, loader); | ||
| if (syncClientClass.isInstance(client)) { | ||
| return syncClientClass.getMethod(PUT_ITEM_METHOD_NAME, putItemRequestClass); | ||
| } | ||
|
|
||
| Class<?> asyncClientClass = Class.forName(DYNAMODB_ASYNC_CLIENT_CLASS_NAME, true, loader); | ||
| if (asyncClientClass.isInstance(client)) { | ||
| return asyncClientClass.getMethod(PUT_ITEM_METHOD_NAME, putItemRequestClass); | ||
| } | ||
|
|
||
| throw new IllegalArgumentException("Unsupported DynamoDB client: " + client.getClass().getName()); | ||
| } | ||
| } | ||
96 changes: 96 additions & 0 deletions
96
.../test/java/org/evomaster/client/java/controller/dynamodb/DynamoDbCommandExecutorTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package org.evomaster.client.java.controller.dynamodb; | ||
|
|
||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto; | ||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto; | ||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionResultsDto; | ||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.mockito.ArgumentCaptor; | ||
| import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; | ||
| import software.amazon.awssdk.services.dynamodb.DynamoDbClient; | ||
| import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; | ||
| import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.concurrent.CompletableFuture; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
| import static org.mockito.ArgumentMatchers.any; | ||
| import static org.mockito.Mockito.mock; | ||
| import static org.mockito.Mockito.times; | ||
| import static org.mockito.Mockito.verify; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| /** Tests DynamoDB item insertion through synchronous and asynchronous AWS clients. */ | ||
| public class DynamoDbCommandExecutorTest { | ||
|
|
||
| @Test | ||
| public void testExecuteInsertWithSynchronousClient() { | ||
| DynamoDbClient client = mock(DynamoDbClient.class); | ||
| when(client.putItem(any(PutItemRequest.class))).thenReturn(PutItemResponse.builder().build()); | ||
|
|
||
| DynamoDbInsertionResultsDto results = DynamoDbCommandExecutor.executeInsert( | ||
| client, Collections.singletonList(worldCupPlayer())); | ||
|
|
||
| ArgumentCaptor<PutItemRequest> requestCaptor = ArgumentCaptor.forClass(PutItemRequest.class); | ||
| verify(client).putItem(requestCaptor.capture()); | ||
| PutItemRequest request = requestCaptor.getValue(); | ||
| assertEquals("WorldCupPlayers", request.tableName()); | ||
| assertEquals("Argentina", request.item().get("country").s()); | ||
| assertEquals("10", request.item().get("fifaId").n()); | ||
| assertTrue(request.item().get("captain").bool()); | ||
| assertEquals(Collections.singletonList(true), results.executionResults); | ||
| assertNull(results.failedInsertionIndex); | ||
| } | ||
|
|
||
| @Test | ||
| public void testExecuteInsertWithAsynchronousClient() { | ||
| DynamoDbAsyncClient client = mock(DynamoDbAsyncClient.class); | ||
| when(client.putItem(any(PutItemRequest.class))).thenReturn( | ||
| CompletableFuture.completedFuture(PutItemResponse.builder().build())); | ||
|
|
||
| DynamoDbInsertionResultsDto results = DynamoDbCommandExecutor.executeInsert( | ||
| client, Collections.singletonList(worldCupPlayer())); | ||
|
|
||
| verify(client).putItem(any(PutItemRequest.class)); | ||
| assertEquals(Collections.singletonList(true), results.executionResults); | ||
| } | ||
|
|
||
| @Test | ||
| public void testFailureReturnsPartialResults() { | ||
| DynamoDbClient client = mock(DynamoDbClient.class); | ||
| when(client.putItem(any(PutItemRequest.class))) | ||
| .thenReturn(PutItemResponse.builder().build()) | ||
| .thenThrow(new IllegalStateException("DynamoDB unavailable")); | ||
|
|
||
| DynamoDbInsertionResultsDto results = DynamoDbCommandExecutor.executeInsert( | ||
| client, Arrays.asList(worldCupPlayer(), worldCupPlayer(), worldCupPlayer())); | ||
|
|
||
| assertEquals(Arrays.asList(true, false, false), results.executionResults); | ||
| assertEquals(Integer.valueOf(1), results.failedInsertionIndex); | ||
| verify(client, times(2)).putItem(any(PutItemRequest.class)); | ||
| } | ||
|
|
||
| @Test | ||
| public void testRejectsMissingClientOrInsertions() { | ||
| DynamoDbClient client = mock(DynamoDbClient.class); | ||
|
|
||
| assertThrows(NullPointerException.class, | ||
| () -> DynamoDbCommandExecutor.executeInsert(null, Collections.singletonList(worldCupPlayer()))); | ||
| assertThrows(NullPointerException.class, | ||
| () -> DynamoDbCommandExecutor.executeInsert(client, null)); | ||
| assertThrows(IllegalArgumentException.class, | ||
| () -> DynamoDbCommandExecutor.executeInsert(client, Collections.emptyList())); | ||
| verify(client, times(0)).putItem(any(PutItemRequest.class)); | ||
| } | ||
|
|
||
| private DynamoDbInsertionDto worldCupPlayer() { | ||
| DynamoDbInsertionDto insertion = new DynamoDbInsertionDto(); | ||
| insertion.tableName = "WorldCupPlayers"; | ||
| insertion.attributes.add(new DynamoDbAttributeValueDto("country", DynamoDbScalarTypeDto.STRING, "Argentina")); | ||
| insertion.attributes.add(new DynamoDbAttributeValueDto("fifaId", DynamoDbScalarTypeDto.NUMBER, "10")); | ||
| insertion.attributes.add(new DynamoDbAttributeValueDto("captain", DynamoDbScalarTypeDto.BOOLEAN, "true")); | ||
| return insertion; | ||
| } | ||
| } |
65 changes: 65 additions & 0 deletions
65
core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbAction.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package org.evomaster.core.database.dynamodb | ||
|
|
||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto | ||
| import org.evomaster.core.search.action.Action | ||
| import org.evomaster.core.search.action.EnvironmentAction | ||
| import org.evomaster.core.search.gene.Gene | ||
|
|
||
| /** | ||
| * A typed attribute gene belonging to a DynamoDB item. | ||
| * | ||
| * @property attributeName name of the DynamoDB item attribute | ||
| * @property type supported DynamoDB scalar type | ||
| * @property gene evolvable value for the attribute | ||
| */ | ||
| data class DynamoDbAttributeGene( | ||
| val attributeName: String, | ||
| val type: DynamoDbScalarTypeDto, | ||
| val gene: Gene | ||
| ) | ||
|
|
||
| /** | ||
| * An initialization action that inserts one DynamoDB item. | ||
| * | ||
| * @property tableName target DynamoDB table | ||
| * @property attributes item attributes to insert | ||
| */ | ||
| class DynamoDbAction( | ||
| val tableName: String, | ||
| val attributes: List<DynamoDbAttributeGene> | ||
| ) : EnvironmentAction(listOf()) { | ||
|
|
||
| companion object { | ||
| private const val ATTRIBUTE_SEPARATOR = '|' | ||
| private const val TYPE_SEPARATOR = ':' | ||
| private const val VALUE_SEPARATOR = '=' | ||
| } | ||
|
|
||
| init { | ||
| addChildren(attributes.map { it.gene }) | ||
| } | ||
|
|
||
| /** Returns the genes that determine the inserted item values. */ | ||
| override fun seeTopGenes(): List<Gene> = attributes.map { it.gene } | ||
|
|
||
| /** Creates an independent action with copies of all attribute genes. */ | ||
| override fun copyContent(): Action = DynamoDbAction( | ||
| tableName, | ||
| attributes.map { DynamoDbAttributeGene(it.attributeName, it.type, it.gene.copy()) } | ||
| ) | ||
|
|
||
| /** Returns the descriptive name of this insertion action. */ | ||
| override fun getName(): String = "DynamoDB_INSERT_$tableName" | ||
|
|
||
| /** Returns the grouping key for DynamoDB initialization actions. */ | ||
| override fun getActionGroupKey(): String = DynamoDbAction::class.java.name | ||
|
|
||
| /** Stable key used to avoid adding the same inferred insertion twice. */ | ||
| fun insertionKey(): String = buildString { | ||
| append(tableName) | ||
| attributes.forEach { | ||
| append(ATTRIBUTE_SEPARATOR).append(it.attributeName).append(TYPE_SEPARATOR).append(it.type) | ||
| .append(VALUE_SEPARATOR).append(it.gene.getValueAsRawString()) | ||
| } | ||
| } | ||
| } |
32 changes: 32 additions & 0 deletions
32
core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionResult.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package org.evomaster.core.database.dynamodb | ||
|
|
||
| import org.evomaster.core.search.action.Action | ||
| import org.evomaster.core.search.action.ActionResult | ||
|
|
||
| /** Result of executing a [DynamoDbAction]. */ | ||
| class DynamoDbActionResult : ActionResult { | ||
|
|
||
| /** Creates a result for the action identified by [sourceLocalId]. */ | ||
| constructor(sourceLocalId: String, stopping: Boolean = false) : super(sourceLocalId, stopping) | ||
|
|
||
| /** Creates a copy of another DynamoDB action result. */ | ||
| constructor(other: DynamoDbActionResult) : super(other) | ||
|
|
||
| companion object { | ||
| const val INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY = "INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY" | ||
| } | ||
|
|
||
| /** Creates an independent copy of this result. */ | ||
| override fun copy(): DynamoDbActionResult = DynamoDbActionResult(this) | ||
|
|
||
| /** Records whether the insertion completed successfully. */ | ||
| fun setInsertExecutionResult(success: Boolean) = | ||
| addResultValue(INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY, success.toString()) | ||
|
|
||
| /** Returns whether the insertion completed successfully. */ | ||
| fun getInsertExecutionResult(): Boolean = | ||
| getResultValue(INSERT_DYNAMODB_EXECUTE_SUCCESSFULLY)?.toBoolean() ?: false | ||
|
|
||
| /** Returns whether [action] is a DynamoDB insertion action. */ | ||
| override fun matchedType(action: Action): Boolean = action is DynamoDbAction | ||
| } |
34 changes: 34 additions & 0 deletions
34
core/src/main/kotlin/org/evomaster/core/database/dynamodb/DynamoDbActionTransformer.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package org.evomaster.core.database.dynamodb | ||
|
|
||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbAttributeValueDto | ||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbDatabaseCommandsDto | ||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbInsertionDto | ||
| import org.evomaster.client.java.controller.api.dto.database.operations.DynamoDbScalarTypeDto | ||
| import org.evomaster.core.search.gene.BooleanGene | ||
| import org.evomaster.core.search.gene.numeric.BigDecimalGene | ||
| import org.evomaster.core.search.gene.string.StringGene | ||
|
|
||
| /** Transforms DynamoDB actions into controller insertion commands. */ | ||
| object DynamoDbActionTransformer { | ||
|
|
||
| /** Converts initialization actions to the controller's DynamoDB insertion DTO. */ | ||
| fun transform(actions: List<DynamoDbAction>): DynamoDbDatabaseCommandsDto = | ||
| DynamoDbDatabaseCommandsDto().also { commands -> | ||
| commands.insertions = actions.map { action -> | ||
| DynamoDbInsertionDto().also { insertion -> | ||
| insertion.tableName = action.tableName | ||
| insertion.attributes = action.attributes.map { attribute -> | ||
| DynamoDbAttributeValueDto( | ||
| attribute.attributeName, | ||
| attribute.type, | ||
| when (attribute.type) { | ||
| DynamoDbScalarTypeDto.STRING -> (attribute.gene as StringGene).value | ||
| DynamoDbScalarTypeDto.NUMBER -> (attribute.gene as BigDecimalGene).value.toPlainString() | ||
| DynamoDbScalarTypeDto.BOOLEAN -> (attribute.gene as BooleanGene).value.toString() | ||
| } | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 not having the same d action for all printable values?
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 there's a confusion. Here we determine which method to call by reflection in line 115. We need to use the exact method name (s,n,bool).
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