From 996ae0b7296c1dfc3366f679dc4ad2a4e2aad68d Mon Sep 17 00:00:00 2001 From: Vadim Nabiev Date: Thu, 14 Apr 2022 16:23:36 +0300 Subject: [PATCH] [WIP] Annotations for pallets and their storages --- api/build.gradle | 15 + .../substrateclient/api/Api.java | 12 + .../substrateclient/api/DefaultApi.java | 34 ++ .../substrateclient/api/ApiTests.java | 42 ++ .../substrateclient/api/SystemPallet.java | 22 + .../common/codegen/ProcessorContext.java | 6 +- .../common/codegen/TypeTraverser.java | 24 + pallet/build.gradle | 4 + pallet/pallet-codegen/build.gradle | 16 + .../CompoundMethodProcessor.java | 32 ++ .../Constants.java | 15 + .../PalletAnnotatedInterface.java | 83 ++++ .../PalletInterfaceProcessor.java | 69 +++ .../PalletMethodProcessor.java | 23 + .../StorageProcessor.java | 441 ++++++++++++++++++ .../pallet/PalletInterfaceProcessorTests.java | 119 +++++ .../src/test/resources/AmbiguousScale.java | 26 ++ .../src/test/resources/ClassPallet.java | 9 + .../src/test/resources/NotAStorage.java | 19 + .../src/test/resources/TestPallet.java | 55 +++ .../src/test/resources/UnnamedPallet.java | 9 + .../src/test/resources/UnnamedStorage.java | 9 + .../src/test/resources/WithoutScale.java | 13 + .../pallet/GeneratedPalletResolver.java | 33 ++ .../pallet/PalletResolver.java | 5 + .../pallet/annotations/Pallet.java | 15 + .../pallet/annotations/Storage.java | 13 + .../pallet/annotations/StorageHasher.java | 7 + .../pallet/annotations/StorageKey.java | 14 + .../pallet/GeneratedPalletResolverTests.java | 41 ++ .../substrateclient/pallet/TestPallet.java | 7 + .../pallet/TestPalletImpl.java | 9 + .../pallet/TestPalletNotAnnotated.java | 6 + .../pallet/TestPalletNotAnnotatedImpl.java | 9 + .../pallet/TestPalletWithoutConstructor.java | 7 + .../TestPalletWithoutConstructorImpl.java | 4 + .../decoder/RpcDecoderAnnotatedClass.java | 2 +- .../codegen/encoder/EncoderCompositor.java | 8 +- .../encoder/RpcEncoderAnnotatedClass.java | 4 +- .../codegen/sections/RpcMethodProcessor.java | 8 +- .../scale/codegen/ScaleAnnotationParser.java | 19 +- .../codegen/reader/ReaderCompositor.java | 26 +- .../reader/ScaleReaderAnnotatedClass.java | 7 +- .../writer/ScaleWriterAnnotatedClass.java | 7 +- .../codegen/writer/ScaleWriterProcessor.java | 4 +- .../codegen/writer/WriterCompositor.java | 29 +- settings.gradle | 2 + .../substrateclient/types/Size.java | 9 +- 48 files changed, 1358 insertions(+), 34 deletions(-) create mode 100644 api/src/main/java/com/strategyobject/substrateclient/api/DefaultApi.java create mode 100644 api/src/test/java/com/strategyobject/substrateclient/api/ApiTests.java create mode 100644 api/src/test/java/com/strategyobject/substrateclient/api/SystemPallet.java create mode 100644 pallet/build.gradle create mode 100644 pallet/pallet-codegen/build.gradle create mode 100644 pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/CompoundMethodProcessor.java create mode 100644 pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/Constants.java create mode 100644 pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletAnnotatedInterface.java create mode 100644 pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletInterfaceProcessor.java create mode 100644 pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletMethodProcessor.java create mode 100644 pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/StorageProcessor.java create mode 100644 pallet/pallet-codegen/src/test/java/com/strategyobject/substrateclient/pallet/PalletInterfaceProcessorTests.java create mode 100644 pallet/pallet-codegen/src/test/resources/AmbiguousScale.java create mode 100644 pallet/pallet-codegen/src/test/resources/ClassPallet.java create mode 100644 pallet/pallet-codegen/src/test/resources/NotAStorage.java create mode 100644 pallet/pallet-codegen/src/test/resources/TestPallet.java create mode 100644 pallet/pallet-codegen/src/test/resources/UnnamedPallet.java create mode 100644 pallet/pallet-codegen/src/test/resources/UnnamedStorage.java create mode 100644 pallet/pallet-codegen/src/test/resources/WithoutScale.java create mode 100644 pallet/src/main/java/com/strategyobject/substrateclient/pallet/GeneratedPalletResolver.java create mode 100644 pallet/src/main/java/com/strategyobject/substrateclient/pallet/PalletResolver.java create mode 100644 pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/Pallet.java create mode 100644 pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/Storage.java create mode 100644 pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/StorageHasher.java create mode 100644 pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/StorageKey.java create mode 100644 pallet/src/test/java/com/strategyobject/substrateclient/pallet/GeneratedPalletResolverTests.java create mode 100644 pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPallet.java create mode 100644 pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletImpl.java create mode 100644 pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletNotAnnotated.java create mode 100644 pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletNotAnnotatedImpl.java create mode 100644 pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletWithoutConstructor.java create mode 100644 pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletWithoutConstructorImpl.java diff --git a/api/build.gradle b/api/build.gradle index 82ceb33d..5724e2c2 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,3 +1,18 @@ dependencies { implementation project(':rpc') + implementation project(':rpc:rpc-core') + implementation project(':rpc:rpc-sections') + implementation project(':transport') + implementation project(':pallet') + implementation project(':scale') + implementation project(':types') + implementation project(':rpc:rpc-types') + implementation project(':storage') + + testImplementation project(':tests') + + testImplementation 'org.testcontainers:testcontainers:1.16.3' + testImplementation 'org.testcontainers:junit-jupiter:1.16.3' + + testAnnotationProcessor project(':pallet:pallet-codegen') } \ No newline at end of file diff --git a/api/src/main/java/com/strategyobject/substrateclient/api/Api.java b/api/src/main/java/com/strategyobject/substrateclient/api/Api.java index e2c5fd29..f02d1699 100644 --- a/api/src/main/java/com/strategyobject/substrateclient/api/Api.java +++ b/api/src/main/java/com/strategyobject/substrateclient/api/Api.java @@ -1,7 +1,19 @@ package com.strategyobject.substrateclient.api; +import com.strategyobject.substrateclient.pallet.GeneratedPalletResolver; import com.strategyobject.substrateclient.rpc.Rpc; +import com.strategyobject.substrateclient.rpc.RpcImpl; +import com.strategyobject.substrateclient.transport.ProviderInterface; +import lombok.val; public interface Api { + static DefaultApi with(ProviderInterface provider) { + val rpc = new RpcImpl(provider); + + return DefaultApi.with(rpc, GeneratedPalletResolver.with(rpc)); + } + Rpc rpc(); + + T pallet(Class clazz); } diff --git a/api/src/main/java/com/strategyobject/substrateclient/api/DefaultApi.java b/api/src/main/java/com/strategyobject/substrateclient/api/DefaultApi.java new file mode 100644 index 00000000..2ca53390 --- /dev/null +++ b/api/src/main/java/com/strategyobject/substrateclient/api/DefaultApi.java @@ -0,0 +1,34 @@ +package com.strategyobject.substrateclient.api; + +import com.strategyobject.substrateclient.pallet.PalletResolver; +import com.strategyobject.substrateclient.rpc.Rpc; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@RequiredArgsConstructor(staticName = "with") +public class DefaultApi implements Api, AutoCloseable { + private final @NonNull Rpc rpc; + private final @NonNull PalletResolver palletResolver; + private final Map, Object> palletCache = new ConcurrentHashMap<>(); + + @Override + public Rpc rpc() { + return rpc; + } + + @Override + public T pallet(@NonNull Class clazz) { + return clazz.cast(palletCache + .computeIfAbsent(clazz, palletResolver::resolve)); + } + + @Override + public void close() throws Exception { + if (rpc instanceof AutoCloseable) { + ((AutoCloseable) rpc).close(); + } + } +} \ No newline at end of file diff --git a/api/src/test/java/com/strategyobject/substrateclient/api/ApiTests.java b/api/src/test/java/com/strategyobject/substrateclient/api/ApiTests.java new file mode 100644 index 00000000..dab720dc --- /dev/null +++ b/api/src/test/java/com/strategyobject/substrateclient/api/ApiTests.java @@ -0,0 +1,42 @@ +package com.strategyobject.substrateclient.api; + +import com.strategyobject.substrateclient.tests.containers.SubstrateVersion; +import com.strategyobject.substrateclient.tests.containers.TestSubstrateContainer; +import com.strategyobject.substrateclient.transport.ws.WsProvider; +import lombok.val; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.math.BigInteger; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Testcontainers +public class ApiTests { + private static final int WAIT_TIMEOUT = 1000; + + @Container + private final TestSubstrateContainer substrate = new TestSubstrateContainer(SubstrateVersion.V3_0_0); + + @Test + public void getSystemPalletAndCall() throws Exception { // TODO move the test out of the project + val wsProvider = WsProvider.builder() + .setEndpoint(substrate.getWsAddress()) + .build(); + wsProvider.connect().get(WAIT_TIMEOUT, TimeUnit.SECONDS); + + try (val api = Api.with(wsProvider)) { + val systemPallet = api.pallet(SystemPallet.class); + val blockHash = systemPallet + .blockHash() + .get(0) + .get(WAIT_TIMEOUT, TimeUnit.SECONDS); + + assertNotNull(blockHash); + assertNotEquals(BigInteger.ZERO, new BigInteger(blockHash.getData())); + } + } +} diff --git a/api/src/test/java/com/strategyobject/substrateclient/api/SystemPallet.java b/api/src/test/java/com/strategyobject/substrateclient/api/SystemPallet.java new file mode 100644 index 00000000..4b9d0495 --- /dev/null +++ b/api/src/test/java/com/strategyobject/substrateclient/api/SystemPallet.java @@ -0,0 +1,22 @@ +package com.strategyobject.substrateclient.api; + +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import com.strategyobject.substrateclient.pallet.annotations.Storage; +import com.strategyobject.substrateclient.pallet.annotations.StorageHasher; +import com.strategyobject.substrateclient.pallet.annotations.StorageKey; +import com.strategyobject.substrateclient.rpc.types.BlockHash; +import com.strategyobject.substrateclient.scale.annotations.Scale; +import com.strategyobject.substrateclient.storage.StorageNMap; + +@Pallet(name = "System") +public interface SystemPallet { + @Storage( + name = "BlockHash", + keys = { + @StorageKey( + type = @Scale(Integer.class), + hasher = StorageHasher.TwoX64Concat + ) + }) + StorageNMap blockHash(); +} \ No newline at end of file diff --git a/common/src/main/java/com/strategyobject/substrateclient/common/codegen/ProcessorContext.java b/common/src/main/java/com/strategyobject/substrateclient/common/codegen/ProcessorContext.java index e3c35e5d..64710fb1 100644 --- a/common/src/main/java/com/strategyobject/substrateclient/common/codegen/ProcessorContext.java +++ b/common/src/main/java/com/strategyobject/substrateclient/common/codegen/ProcessorContext.java @@ -26,9 +26,13 @@ public String getPackageName(@NonNull TypeElement classElement) { return elementUtils.getPackageOf(classElement).getQualifiedName().toString(); } - public boolean isSubtypeOf(@NonNull TypeMirror candidate, @NonNull TypeMirror supertype) { + public boolean isAssignable(@NonNull TypeMirror candidate, @NonNull TypeMirror supertype) { return typeUtils.isAssignable(candidate, supertype); } + + public boolean isSubtype(@NonNull TypeMirror candidate, @NonNull TypeMirror supertype) { + return typeUtils.isSubtype(candidate, supertype); + } public boolean isGeneric(@NonNull TypeMirror type) { return ((TypeElement) typeUtils.asElement(type)) diff --git a/common/src/main/java/com/strategyobject/substrateclient/common/codegen/TypeTraverser.java b/common/src/main/java/com/strategyobject/substrateclient/common/codegen/TypeTraverser.java index 7a1b2c51..3893622e 100644 --- a/common/src/main/java/com/strategyobject/substrateclient/common/codegen/TypeTraverser.java +++ b/common/src/main/java/com/strategyobject/substrateclient/common/codegen/TypeTraverser.java @@ -101,6 +101,29 @@ public T traverse(@NonNull TypeMirror type, @NonNull TypeTraverser.TypeTreeNode .toArray(x -> (T[]) Array.newInstance(clazz, typeArguments.size()))); } + @SuppressWarnings({"unchecked"}) + public T traverse(@NonNull TypeTraverser.TypeTreeNode typeOverride) { + if (typeOverride.type.getKind().isPrimitive()) { + return whenPrimitiveType((PrimitiveType) typeOverride.type, typeOverride.type); + } + + if (!(typeOverride.type instanceof DeclaredType)) { + throw new IllegalArgumentException("Type is not supported: " + typeOverride.type); + } + + val declaredType = (DeclaredType) typeOverride.type; + if (typeOverride.children.size() == 0) { + return whenNonGenericType(declaredType, typeOverride.type); + } + + return whenGenericType( + declaredType, + typeOverride.type, + typeOverride.children.stream() + .map(this::traverse) + .toArray(x -> (T[]) Array.newInstance(clazz, typeOverride.children.size()))); + } + private List getTypeArgumentsOrDefault(DeclaredType declaredType, TypeMirror override) { return (doTraverseArguments(declaredType, override) ? declaredType.getTypeArguments() : @@ -125,6 +148,7 @@ private boolean typeIsOverriddenByNonGeneric(int typeOverrideSize) { public static class TypeTreeNode { + @Getter private final TypeMirror type; private final List children; diff --git a/pallet/build.gradle b/pallet/build.gradle new file mode 100644 index 00000000..e5b3aca3 --- /dev/null +++ b/pallet/build.gradle @@ -0,0 +1,4 @@ +dependencies { + implementation project(':scale') + implementation project(':rpc') +} \ No newline at end of file diff --git a/pallet/pallet-codegen/build.gradle b/pallet/pallet-codegen/build.gradle new file mode 100644 index 00000000..fc9b225b --- /dev/null +++ b/pallet/pallet-codegen/build.gradle @@ -0,0 +1,16 @@ +dependencies { + implementation project(':common') + implementation project(':rpc') + implementation project(':types') + implementation project(':scale') + implementation project(':scale:scale-codegen') + implementation project(':storage') + implementation project(':pallet') + + implementation 'com.squareup:javapoet:1.13.0' + + compileOnly 'com.google.auto.service:auto-service-annotations:1.0.1' + annotationProcessor 'com.google.auto.service:auto-service:1.0.1' + + testImplementation 'com.google.testing.compile:compile-testing:0.19' +} \ No newline at end of file diff --git a/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/CompoundMethodProcessor.java b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/CompoundMethodProcessor.java new file mode 100644 index 00000000..9e48cb9a --- /dev/null +++ b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/CompoundMethodProcessor.java @@ -0,0 +1,32 @@ +package com.strategyobject.substrateclient.pallet; + +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import com.strategyobject.substrateclient.common.codegen.ProcessingException; +import com.strategyobject.substrateclient.common.codegen.ProcessorContext; +import lombok.NonNull; +import lombok.var; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import java.util.List; + +class CompoundMethodProcessor extends PalletMethodProcessor { + private final List processors; + + public CompoundMethodProcessor(TypeElement typeElement, List processors) { + super(typeElement); + this.processors = processors; + } + + @Override + void process(@NonNull String palletName, @NonNull ExecutableElement method, TypeSpec.@NonNull Builder typeSpecBuilder, MethodSpec.Builder constructorBuilder, @NonNull ProcessorContext context) throws ProcessingException { + for (var processor : processors) { + processor.process(palletName, + method, + typeSpecBuilder, + constructorBuilder, + context); + } + } +} diff --git a/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/Constants.java b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/Constants.java new file mode 100644 index 00000000..0fe76acc --- /dev/null +++ b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/Constants.java @@ -0,0 +1,15 @@ +package com.strategyobject.substrateclient.pallet; + +class Constants { + static final String RPC = "rpc"; + static final String CLASS_NAME_TEMPLATE = "%sImpl"; + static final String SCALE_READER_REGISTRY = "scaleReaderRegistry"; + static final String SCALE_WRITER_REGISTRY = "scaleWriterRegistry"; + static final String STORAGE_FACTORY_METHOD = "with"; + static final String STORAGE_KEY_PROVIDER_FACTORY_METHOD = "of"; + static final String STORAGE_KEY_PROVIDER_ADD_HASHERS = "use"; + static final String KEY_HASHER_FACTORY_METHOD = "with"; + static final String BLAKE_2_128_CONCAT_INSTANCE = "getInstance"; + static final String TWO_X64_CONCAT_INSTANCE = "getInstance"; + static final String IDENTITY_INSTANCE = "getInstance"; +} diff --git a/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletAnnotatedInterface.java b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletAnnotatedInterface.java new file mode 100644 index 00000000..7d2a2e8e --- /dev/null +++ b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletAnnotatedInterface.java @@ -0,0 +1,83 @@ +package com.strategyobject.substrateclient.pallet; + +import com.google.common.base.Strings; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import com.strategyobject.substrateclient.common.codegen.ProcessingException; +import com.strategyobject.substrateclient.common.codegen.ProcessorContext; +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import com.strategyobject.substrateclient.rpc.Rpc; +import lombok.val; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import java.io.IOException; + +import static com.strategyobject.substrateclient.pallet.Constants.CLASS_NAME_TEMPLATE; +import static com.strategyobject.substrateclient.pallet.Constants.RPC; + +public class PalletAnnotatedInterface { + private final TypeElement interfaceElement; + private final String name; + private final PalletMethodProcessor methodProcessor; + + public PalletAnnotatedInterface(TypeElement interfaceElement, PalletMethodProcessor methodProcessor) throws ProcessingException { + this.interfaceElement = interfaceElement; + val annotation = interfaceElement.getAnnotation(Pallet.class); + + if (!interfaceElement.getModifiers().contains(Modifier.PUBLIC)) { + throw new ProcessingException( + interfaceElement, + "`%s` is not public. That is not allowed.", + interfaceElement.getQualifiedName().toString()); + } + + if (Strings.isNullOrEmpty(name = annotation.name())) { + throw new ProcessingException( + interfaceElement, + "`@%s` of `%s` contains null or empty `name`.", + annotation.getClass().getSimpleName(), + interfaceElement.getQualifiedName().toString()); + } + + this.methodProcessor = methodProcessor; + } + + public void generateClass(ProcessorContext context) throws ProcessingException, IOException { + val interfaceName = interfaceElement.getSimpleName().toString(); + val className = String.format(CLASS_NAME_TEMPLATE, interfaceName); + val packageName = context.getPackageName(interfaceElement); + + val typeSpecBuilder = TypeSpec.classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(TypeName.get(interfaceElement.asType())) + .addField(Rpc.class, RPC, Modifier.FINAL, Modifier.PRIVATE); + + val constructorBuilder = createConstructorBuilder(); + + for (val method : interfaceElement.getEnclosedElements()) { + this.methodProcessor.process(name, + (ExecutableElement) method, + typeSpecBuilder, + constructorBuilder, + context); + } + + typeSpecBuilder.addMethod(constructorBuilder.build()); + + JavaFile.builder(packageName, typeSpecBuilder.build()).build().writeTo(context.getFiler()); + } + + private MethodSpec.Builder createConstructorBuilder() { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(Rpc.class, RPC) + .beginControlFlow("if ($L == null)", RPC) + .addStatement("throw new $T(\"$L can't be null.\")", IllegalArgumentException.class, RPC) + .endControlFlow() + .addStatement("this.$1L = $1L", RPC); + } +} diff --git a/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletInterfaceProcessor.java b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletInterfaceProcessor.java new file mode 100644 index 00000000..020cbc7c --- /dev/null +++ b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletInterfaceProcessor.java @@ -0,0 +1,69 @@ +package com.strategyobject.substrateclient.pallet; + +import com.google.auto.service.AutoService; +import com.strategyobject.substrateclient.common.codegen.ProcessingException; +import com.strategyobject.substrateclient.common.codegen.ProcessorContext; +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import lombok.val; + +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +@SupportedAnnotationTypes("com.strategyobject.substrateclient.pallet.annotations.Pallet") +@SupportedSourceVersion(SourceVersion.RELEASE_8) +@AutoService(Processor.class) +public class PalletInterfaceProcessor extends AbstractProcessor { + private ProcessorContext context; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + context = new ProcessorContext(processingEnv.getTypeUtils(), + processingEnv.getElementUtils(), + processingEnv.getFiler(), + processingEnv.getMessager()); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + if (annotations.isEmpty()) { + return false; + } + + for (val annotatedElement : roundEnv.getElementsAnnotatedWith(Pallet.class)) { + if (annotatedElement.getKind() != ElementKind.INTERFACE) { + context.error( + annotatedElement, + "Only interfaces can be annotated with `@%s`.", + Pallet.class.getSimpleName()); + + return true; + } + + val typeElement = (TypeElement) annotatedElement; + try { + val annotatedInterface = new PalletAnnotatedInterface( + typeElement, + new CompoundMethodProcessor(typeElement, + Collections.singletonList( + new StorageProcessor(typeElement) + ))); + + annotatedInterface.generateClass(context); + } catch (ProcessingException e) { + context.error(typeElement, e); + return true; + } catch (IOException e) { + context.error(e); + return true; + } + } + + return true; + } +} diff --git a/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletMethodProcessor.java b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletMethodProcessor.java new file mode 100644 index 00000000..139cb6d3 --- /dev/null +++ b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/PalletMethodProcessor.java @@ -0,0 +1,23 @@ +package com.strategyobject.substrateclient.pallet; + +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import com.strategyobject.substrateclient.common.codegen.ProcessingException; +import com.strategyobject.substrateclient.common.codegen.ProcessorContext; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +@RequiredArgsConstructor +abstract class PalletMethodProcessor { + @NonNull + protected final TypeElement palletElement; + + abstract void process(@NonNull String palletName, + @NonNull ExecutableElement method, + @NonNull TypeSpec.Builder typeSpecBuilder, + MethodSpec.Builder constructorBuilder, + @NonNull ProcessorContext context) throws ProcessingException; +} diff --git a/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/StorageProcessor.java b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/StorageProcessor.java new file mode 100644 index 00000000..ea4bbb05 --- /dev/null +++ b/pallet/pallet-codegen/src/main/java/com.strategyobject.substrateclient.pallet/StorageProcessor.java @@ -0,0 +1,441 @@ +package com.strategyobject.substrateclient.pallet; + +import com.google.common.base.Strings; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import com.strategyobject.substrateclient.common.codegen.AnnotationUtils; +import com.strategyobject.substrateclient.common.codegen.ProcessingException; +import com.strategyobject.substrateclient.common.codegen.ProcessorContext; +import com.strategyobject.substrateclient.common.codegen.TypeTraverser; +import com.strategyobject.substrateclient.pallet.annotations.Storage; +import com.strategyobject.substrateclient.pallet.annotations.StorageHasher; +import com.strategyobject.substrateclient.scale.ScaleReader; +import com.strategyobject.substrateclient.scale.ScaleWriter; +import com.strategyobject.substrateclient.scale.codegen.ScaleAnnotationParser; +import com.strategyobject.substrateclient.scale.codegen.reader.ReaderCompositor; +import com.strategyobject.substrateclient.scale.codegen.writer.WriterCompositor; +import com.strategyobject.substrateclient.scale.registries.ScaleReaderRegistry; +import com.strategyobject.substrateclient.scale.registries.ScaleWriterRegistry; +import com.strategyobject.substrateclient.storage.*; +import com.strategyobject.substrateclient.types.FixedBytes; +import com.strategyobject.substrateclient.types.Size; +import lombok.NonNull; +import lombok.val; +import lombok.var; + +import javax.lang.model.element.*; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import java.util.List; +import java.util.Objects; + +import static com.strategyobject.substrateclient.common.codegen.AnnotationUtils.suppressWarnings; +import static com.strategyobject.substrateclient.pallet.Constants.*; + +class StorageProcessor extends PalletMethodProcessor { + private static final String VALUE_READER = "valueReader"; + private static final String KEY_PROVIDER = "keyProvider"; + private static final String STORAGE_MAP = "storage"; + private static final String HASHERS = "hashers"; + private static final String INITIALIZER_SUFFIX = "Initializer"; + + public StorageProcessor(@NonNull TypeElement palletElement) { + super(palletElement); + } + + @Override + void process(@NonNull String palletName, + @NonNull ExecutableElement method, + TypeSpec.@NonNull Builder typeSpecBuilder, + MethodSpec.Builder constructorBuilder, + @NonNull ProcessorContext context) throws ProcessingException { + val annotation = AnnotationUtils.getAnnotationMirror(method, Storage.class); + if (annotation == null) { + return; + } + + validate(method, annotation, context); + + createBackField(typeSpecBuilder, method); + typeSpecBuilder.addMethod(backFieldInitializer(palletName, method, annotation, context)); + assignBackFieldInConstructor(constructorBuilder, method); + typeSpecBuilder.addMethod(publicMethod(method)); + } + + private void createBackField(TypeSpec.Builder typeSpecBuilder, ExecutableElement method) { + typeSpecBuilder.addField( + TypeName.get(method.getReturnType()), + method.getSimpleName().toString(), + Modifier.FINAL, Modifier.PRIVATE + ); + } + + private void assignBackFieldInConstructor(MethodSpec.Builder constructorBuilder, ExecutableElement method) { + constructorBuilder.addStatement("this.$1L = $1L$2L()", + method.getSimpleName().toString(), + INITIALIZER_SUFFIX); + } + + private MethodSpec publicMethod(ExecutableElement method) { + val returnType = method.getReturnType(); + + return MethodSpec.methodBuilder(method.getSimpleName().toString()) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(TypeName.get(returnType)) + .addStatement("return this.$L", method.getSimpleName().toString()) + .build(); + } + + private MethodSpec backFieldInitializer(String palletName, + ExecutableElement method, + AnnotationMirror storageAnnotation, + ProcessorContext context) throws ProcessingException { + val returnType = method.getReturnType(); + validateMethodSignature(method, returnType, context); + + val valueType = ((DeclaredType) returnType).getTypeArguments().get(0); + + val methodSpecBuilder = + MethodSpec.methodBuilder(method.getSimpleName().toString() + INITIALIZER_SUFFIX) + .addModifiers(Modifier.PRIVATE) + .addAnnotation(suppressWarnings("unchecked", "rawtypes")) + .returns(TypeName.get(returnType)); + + declareReaderAndWriterRegistries(methodSpecBuilder); + + val scaleAnnotationParser = new ScaleAnnotationParser(context); + val readerCompositor = ReaderCompositor.disallowOpenGeneric( + context, + SCALE_READER_REGISTRY); + val writerCompositor = WriterCompositor.disallowOpenGeneric( + context, + SCALE_WRITER_REGISTRY); + + assignStorageMapImpl(methodSpecBuilder, + palletName, + method, + storageAnnotation, + valueType, + scaleAnnotationParser, + readerCompositor, + writerCompositor, + context); + + methodSpecBuilder + .addStatement("return $L", STORAGE_MAP); + + + return methodSpecBuilder.build(); + } + + private void assignStorageMapImpl(MethodSpec.Builder methodSpecBuilder, + String palletName, + ExecutableElement method, + AnnotationMirror storageAnnotation, + TypeMirror valueType, + ScaleAnnotationParser scaleAnnotationParser, + ReaderCompositor readerCompositor, + WriterCompositor writerCompositor, + ProcessorContext context) throws ProcessingException { + assignValueReader(methodSpecBuilder, + method, + valueType, + scaleAnnotationParser, + readerCompositor); + + assignKeyProvider(methodSpecBuilder, + method, + palletName, + storageAnnotation, + scaleAnnotationParser, + readerCompositor, + writerCompositor, + context); + + methodSpecBuilder + .addStatement("$1T $2L = $1T.$3L($4L, $5L, $6L)", + StorageNMapImpl.class, + STORAGE_MAP, + STORAGE_FACTORY_METHOD, + RPC, + VALUE_READER, + KEY_PROVIDER); + } + + private void assignKeyProvider(MethodSpec.Builder methodSpecBuilder, + ExecutableElement method, + String palletName, + AnnotationMirror storageAnnotation, + ScaleAnnotationParser scaleAnnotationParser, + ReaderCompositor readerCompositor, + WriterCompositor writerCompositor, + ProcessorContext context) throws ProcessingException { + val storageName = AnnotationUtils.getValueFromAnnotation(storageAnnotation, + "name"); + + methodSpecBuilder.addStatement("$1T $2L = $1T.$3L($4S, $5S)", + StorageKeyProvider.class, + KEY_PROVIDER, + STORAGE_KEY_PROVIDER_FACTORY_METHOD, + palletName, + storageName); + + val keys = AnnotationUtils + .>getValueFromAnnotation(storageAnnotation, + "keys"); + + if (keys != null && keys.size() > 0) { + methodSpecBuilder.addStatement("$1T[] $2L = new $1T[$3L]", + KeyHasher.class, + HASHERS, + keys.size()); + + for (var i = 0; i < keys.size(); i++) { + putHasher(methodSpecBuilder, + method, + storageName, + context, + keys.get(i), + scaleAnnotationParser, + readerCompositor, + writerCompositor, + CodeBlock + .builder() + .add("$L[$L]", HASHERS, i) + .build()); + } + + methodSpecBuilder.addStatement("$L.$L($L)", + KEY_PROVIDER, + STORAGE_KEY_PROVIDER_ADD_HASHERS, + HASHERS); + } + } + + private void putHasher(MethodSpec.Builder methodSpecBuilder, + ExecutableElement method, + String storageName, + ProcessorContext context, + AnnotationMirror keyAnnotation, + ScaleAnnotationParser scaleAnnotationParser, + ReaderCompositor readerCompositor, + WriterCompositor writerCompositor, + CodeBlock hasher) throws ProcessingException { + val type = AnnotationUtils.getValueFromAnnotation(keyAnnotation, "type"); + val typeOverride = type != null + ? scaleAnnotationParser.parse(type) + : scaleAnnotationParser.parse( + Objects.requireNonNull(AnnotationUtils.getValueFromAnnotation(keyAnnotation, "generic"))); + val keySize = determineKeySize(context, typeOverride); + val readerCode = readerCompositor.traverse(typeOverride); + val writerCode = writerCompositor.traverse(typeOverride); + + methodSpecBuilder + .addStatement( + CodeBlock.builder() + .add(hasher) + .add(" = $T.$L(($T)", + KeyHasher.class, + KEY_HASHER_FACTORY_METHOD, + ScaleWriter.class) + .add(writerCode) + .add(", ($T)", ScaleReader.class) + .add(readerCode) + .add(", ") + .add(resolveHashingAlgorithm(keyAnnotation, + keySize, + storageName, + method)) + .add(")") + .build()); + } + + private int determineKeySize(ProcessorContext context, + TypeTraverser.TypeTreeNode typeOverride) { + if (context.isSubtype(typeOverride.getType(), context.erasure(context.getType(FixedBytes.class)))) { + val fixedBytes = ((TypeElement) ((DeclaredType) typeOverride.getType()) + .asElement()) + .getSuperclass(); + val size = ((DeclaredType) fixedBytes).getTypeArguments().get(0); + + val zero = context.getType(Size.Zero.class); + if (context.isAssignable(size, zero)) { + return Size.zero.getValue(); + } + + val of32 = context.getType(Size.Of32.class); + if (context.isAssignable(size, of32)) { + return Size.of32.getValue(); + } + + val of64 = context.getType(Size.Of64.class); + if (context.isAssignable(size, of64)) { + return Size.of64.getValue(); + } + + val of96 = context.getType(Size.Of96.class); + if (context.isAssignable(size, of96)) { + return Size.of96.getValue(); + } + + val of128 = context.getType(Size.Of128.class); + if (context.isAssignable(size, of128)) { + return Size.of128.getValue(); + } + } + + return -1; + } + + private CodeBlock resolveHashingAlgorithm(AnnotationMirror keyAnnotation, + int keySize, + String storageAnnotation, + ExecutableElement method) throws ProcessingException { + val builder = CodeBlock.builder(); + val hasher = AnnotationUtils.getValueFromAnnotation(keyAnnotation, "hasher"); + val hasherName = Objects.requireNonNull(hasher).getSimpleName().toString(); + + if (hasherName.equals(StorageHasher.Blake2B128Concat.toString())) { + builder.add("$T.$L()", + Blake2B128Concat.class, + BLAKE_2_128_CONCAT_INSTANCE); + } + + if (hasherName.equals(StorageHasher.TwoX64Concat.toString())) { + builder.add("$T.$L()", + TwoX64Concat.class, + TWO_X64_CONCAT_INSTANCE); + } + + if (hasherName.equals(StorageHasher.Identity.toString())) { + if (keySize < 0) { + throw new ProcessingException( + palletElement, + "`@%s` of `%s.%s` contains an incorrect type of key or a hashing algorithm. " + + "`%s` can only be applied to a key with fixed size.", + storageAnnotation.getClass().getSimpleName(), + palletElement.getQualifiedName().toString(), + StorageHasher.Identity.toString(), + method.getSimpleName()); + } + + builder.add("$T.$L()", + Identity.class, + IDENTITY_INSTANCE); + } + + return builder.build(); + } + + private void assignValueReader(MethodSpec.Builder methodSpecBuilder, + ExecutableElement method, + TypeMirror valueType, + ScaleAnnotationParser scaleAnnotationParser, + ReaderCompositor readerCompositor) { + val typeOverride = scaleAnnotationParser.parse(method); + val readerCode = typeOverride != null ? + readerCompositor.traverse(valueType, typeOverride) : + readerCompositor.traverse(valueType); + + methodSpecBuilder + .addStatement(CodeBlock.builder() + .add("$T $L = ", ScaleReader.class, VALUE_READER) + .add(readerCode) + .build()); + } + + private void validateMethodSignature(ExecutableElement method, + TypeMirror returnType, + ProcessorContext context) throws ProcessingException { + val expectedReturnType = context.erasure( + context.getType(StorageNMap.class)); + if (!context.isSameType(expectedReturnType, context.erasure(returnType))) { + throw new ProcessingException( + palletElement, + "Method `%s.%s` has unexpected return type. Must be `%s`.", + palletElement.getQualifiedName().toString(), + method.getSimpleName(), + StorageNMap.class.getSimpleName()); + } + + if (method.getParameters().size() > 0) { + throw new ProcessingException( + palletElement, + "Method `%s.%s` mustn't have parameters.", + palletElement.getQualifiedName().toString(), + method.getSimpleName()); + } + } + + protected void validate(ExecutableElement method, AnnotationMirror storageAnnotation, ProcessorContext context) + throws ProcessingException { + ensureNameIsSet(method, storageAnnotation); + validateKeys(method, storageAnnotation); + + val returnType = method.getReturnType(); + if (!context.isSameType(context.erasure(returnType), context.erasure(context.getType(StorageNMap.class)))) { + throw new ProcessingException( + palletElement, + "`Method `%s.%s` has incorrect return type." + + "Must be `%s`.", + palletElement.getQualifiedName().toString(), + method.getSimpleName(), + StorageNMap.class.getSimpleName()); + } + } + + private void validateKeys(ExecutableElement method, AnnotationMirror storageAnnotation) throws ProcessingException { + val keys = AnnotationUtils.>getValueFromAnnotation(storageAnnotation, + "keys"); + if (keys == null) { + return; + } + + for (val key : keys) { + val type = AnnotationUtils.getValueFromAnnotation(key, "type"); + val generic = AnnotationUtils.getValueFromAnnotation(key, "generic"); + if (type == null && generic == null) { + throw new ProcessingException( + palletElement, + "`@%s` of `%s.%s` isn't adjusted correctly. " + + "Must be set `type` or `generic` of `@StorageKey`.", + storageAnnotation.getClass().getSimpleName(), + palletElement.getQualifiedName().toString(), + method.getSimpleName()); + } + + if (type != null && generic != null) { + throw new ProcessingException( + palletElement, + "`@%s` of `%s.%s` isn't adjusted correctly. " + + "Ambiguous scale type of key. " + + "Must be set only one of both parameters of `@StorageKey`: `type` or `generic`.", + storageAnnotation.getClass().getSimpleName(), + palletElement.getQualifiedName().toString(), + method.getSimpleName()); + } + } + } + + private void ensureNameIsSet(ExecutableElement method, AnnotationMirror storageAnnotation) throws ProcessingException { + val name = AnnotationUtils.getValueFromAnnotation(storageAnnotation, + "name"); + if (Strings.isNullOrEmpty(name)) { + throw new ProcessingException( + palletElement, + "`@%s` of `%s.%s` doesn't have `name`.", + Storage.class.getSimpleName(), + palletElement.getQualifiedName().toString(), + method.getSimpleName()); + } + } + + private void declareReaderAndWriterRegistries(MethodSpec.Builder methodSpecBuilder) { + methodSpecBuilder + .addStatement("$1T $2L = $1T.getInstance()", ScaleReaderRegistry.class, SCALE_READER_REGISTRY) + .addStatement("$1T $2L = $1T.getInstance()", ScaleWriterRegistry.class, SCALE_WRITER_REGISTRY); + } +} diff --git a/pallet/pallet-codegen/src/test/java/com/strategyobject/substrateclient/pallet/PalletInterfaceProcessorTests.java b/pallet/pallet-codegen/src/test/java/com/strategyobject/substrateclient/pallet/PalletInterfaceProcessorTests.java new file mode 100644 index 00000000..585618ac --- /dev/null +++ b/pallet/pallet-codegen/src/test/java/com/strategyobject/substrateclient/pallet/PalletInterfaceProcessorTests.java @@ -0,0 +1,119 @@ +package com.strategyobject.substrateclient.pallet; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import lombok.val; +import org.junit.jupiter.api.Test; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +public class PalletInterfaceProcessorTests { + @Test + void failsWhenAnnotationIsAppliedToClass() { + val clazz = JavaFileObjects.forResource("ClassPallet.java"); + + val compilation = javac() + .withProcessors(new PalletInterfaceProcessor()) + .compile(clazz); + + assertThat(compilation).failed(); + assertThat(compilation) + .hadErrorContaining("Only interfaces"); + } + + @Test + void failsWhenPalletHasEmptyName() { + val clazz = JavaFileObjects.forResource("UnnamedPallet.java"); + + val compilation = javac() + .withProcessors(new PalletInterfaceProcessor()) + .compile(clazz); + + assertThat(compilation).failed(); + assertThat(compilation) + .hadErrorContaining("contains null or empty `name`"); + } + + @Test + void failsWhenStorageHasEmptyName() { + val clazz = JavaFileObjects.forResource("UnnamedStorage.java"); + + val compilation = javac() + .withProcessors(new PalletInterfaceProcessor()) + .compile(clazz); + + assertThat(compilation).failed(); + assertThat(compilation) + .hadErrorContaining("doesn't have `name`"); + } + + @Test + void failsWhenScaleIsNotSet() { + val clazz = JavaFileObjects.forResource("WithoutScale.java"); + + val compilation = javac() + .withProcessors(new PalletInterfaceProcessor()) + .compile(clazz); + + assertThat(compilation).failed(); + assertThat(compilation) + .hadErrorContaining("Must be set `type` or `generic` of `@StorageKey`"); + } + + @Test + void failsWhenScaleTypeOfKeyIsAmbiguous() { + val clazz = JavaFileObjects.forResource("AmbiguousScale.java"); + + val compilation = javac() + .withProcessors(new PalletInterfaceProcessor()) + .compile(clazz); + + assertThat(compilation).failed(); + assertThat(compilation) + .hadErrorContaining("Must be set only one of both parameters of `@StorageKey`: `type` or `generic`."); + } + + @Test + void failsWhenStorageMethodReturnsIncorrectType() { + val clazz = JavaFileObjects.forResource("NotAStorage.java"); + + val compilation = javac() + .withProcessors(new PalletInterfaceProcessor()) + .compile(clazz); + + assertThat(compilation).failed(); + assertThat(compilation) + .hadErrorContaining("has incorrect return type"); + } + + @Test + public void compiles() { + val generatedName = String.format("TestPalletImpl"); + + val clazz = JavaFileObjects.forResource("TestPallet.java"); + val compilation = javac() + .withProcessors(new PalletInterfaceProcessor()) + .compile(clazz); + + assertThat(compilation).succeeded(); + + assertContains(generatedName, compilation, "private final Rpc rpc;"); + assertContains(generatedName, compilation, "private final StorageNMap value;"); + assertContains(generatedName, compilation, "private final StorageNMap map;"); + assertContains(generatedName, compilation, "private final StorageNMap doubleMap;"); + assertContains(generatedName, compilation, "private final StorageNMap tripleMap;"); + assertContains(generatedName, compilation, "public TestPalletImpl(Rpc rpc)"); + assertContains(generatedName, compilation, "public StorageNMap value()"); + assertContains(generatedName, compilation, "public StorageNMap map()"); + assertContains(generatedName, compilation, "public StorageNMap doubleMap()"); + assertContains(generatedName, compilation, "public StorageNMap tripleMap()"); + } + + private void assertContains(String className, Compilation compilation, String target) { + assertThat(compilation) + .generatedSourceFile(className) + .contentsAsUtf8String() + .contains(target); + } +} diff --git a/pallet/pallet-codegen/src/test/resources/AmbiguousScale.java b/pallet/pallet-codegen/src/test/resources/AmbiguousScale.java new file mode 100644 index 00000000..f529206d --- /dev/null +++ b/pallet/pallet-codegen/src/test/resources/AmbiguousScale.java @@ -0,0 +1,26 @@ +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import com.strategyobject.substrateclient.pallet.annotations.Storage; +import com.strategyobject.substrateclient.pallet.annotations.StorageHasher; +import com.strategyobject.substrateclient.pallet.annotations.StorageKey; +import com.strategyobject.substrateclient.scale.ScaleType; +import com.strategyobject.substrateclient.scale.annotations.Scale; +import com.strategyobject.substrateclient.scale.annotations.ScaleGeneric; +import com.strategyobject.substrateclient.storage.StorageNMap; + +@Pallet(name = "Test") +public interface AmbiguousScale { + @Storage(name = "Test", keys = { + @StorageKey( + hasher = StorageHasher.Blake2B128Concat, + type = @Scale(ScaleType.I32.class), + generic = @ScaleGeneric( + template = "Option", + types = { + @Scale(ScaleType.Option.class), + @Scale(ScaleType.I32.class) + } + ) + ) + }) + StorageNMap test(); +} diff --git a/pallet/pallet-codegen/src/test/resources/ClassPallet.java b/pallet/pallet-codegen/src/test/resources/ClassPallet.java new file mode 100644 index 00000000..23194c0f --- /dev/null +++ b/pallet/pallet-codegen/src/test/resources/ClassPallet.java @@ -0,0 +1,9 @@ +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import com.strategyobject.substrateclient.pallet.annotations.Storage; +import com.strategyobject.substrateclient.storage.StorageNMap; + +@Pallet(name = "Class") +public abstract class ClassPallet { + @Storage(name = "Test") + public abstract StorageNMap test(); +} diff --git a/pallet/pallet-codegen/src/test/resources/NotAStorage.java b/pallet/pallet-codegen/src/test/resources/NotAStorage.java new file mode 100644 index 00000000..a7491591 --- /dev/null +++ b/pallet/pallet-codegen/src/test/resources/NotAStorage.java @@ -0,0 +1,19 @@ +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import com.strategyobject.substrateclient.pallet.annotations.Storage; +import com.strategyobject.substrateclient.pallet.annotations.StorageHasher; +import com.strategyobject.substrateclient.pallet.annotations.StorageKey; +import com.strategyobject.substrateclient.scale.ScaleType; +import com.strategyobject.substrateclient.scale.annotations.Scale; +import com.strategyobject.substrateclient.scale.annotations.ScaleGeneric; +import com.strategyobject.substrateclient.storage.StorageNMap; + +@Pallet(name = "Test") +public interface NotAStorage { + @Storage(name = "Test", keys = { + @StorageKey( + hasher = StorageHasher.Blake2B128Concat, + type = @Scale(ScaleType.I32.class) + ) + }) + String test(); +} diff --git a/pallet/pallet-codegen/src/test/resources/TestPallet.java b/pallet/pallet-codegen/src/test/resources/TestPallet.java new file mode 100644 index 00000000..180372c2 --- /dev/null +++ b/pallet/pallet-codegen/src/test/resources/TestPallet.java @@ -0,0 +1,55 @@ +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import com.strategyobject.substrateclient.pallet.annotations.Storage; +import com.strategyobject.substrateclient.pallet.annotations.StorageHasher; +import com.strategyobject.substrateclient.pallet.annotations.StorageKey; +import com.strategyobject.substrateclient.rpc.types.AccountId; +import com.strategyobject.substrateclient.scale.ScaleType; +import com.strategyobject.substrateclient.scale.annotations.Scale; +import com.strategyobject.substrateclient.scale.annotations.ScaleGeneric; +import com.strategyobject.substrateclient.storage.StorageNMap; +import com.strategyobject.substrateclient.types.Result; + +@Pallet(name = "Test") +public interface TestPallet { + @Storage(name = "Value") + StorageNMap value(); + + @Storage( + name = "Map", + keys = { + @StorageKey(type = @Scale(ScaleType.I32.class), + hasher = StorageHasher.TwoX64Concat) + }) + StorageNMap map(); + + @Storage( + name = "DoubleMap", + keys = { + @StorageKey(type = @Scale(ScaleType.I32.class), + hasher = StorageHasher.TwoX64Concat), + @StorageKey( + generic = @ScaleGeneric( + template = "Result", + types = { + @Scale(Result.class), + @Scale(ScaleType.Bool.class), + @Scale(ScaleType.String.class) + } + ), + hasher = StorageHasher.Blake2B128Concat + ) + }) + StorageNMap doubleMap(); + + @Storage( + name = "TripleMap", + keys = { + @StorageKey(type = @Scale(String.class), + hasher = StorageHasher.Blake2B128Concat), + @StorageKey(type = @Scale(ScaleType.I32.class), + hasher = StorageHasher.TwoX64Concat), + @StorageKey(type = @Scale(AccountId.class), + hasher = StorageHasher.Identity) + }) + StorageNMap tripleMap(); +} diff --git a/pallet/pallet-codegen/src/test/resources/UnnamedPallet.java b/pallet/pallet-codegen/src/test/resources/UnnamedPallet.java new file mode 100644 index 00000000..90e7bcb5 --- /dev/null +++ b/pallet/pallet-codegen/src/test/resources/UnnamedPallet.java @@ -0,0 +1,9 @@ +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import com.strategyobject.substrateclient.pallet.annotations.Storage; +import com.strategyobject.substrateclient.storage.StorageNMap; + +@Pallet(name = "") +public interface UnnamedPallet { + @Storage(name = "Test") + StorageNMap test(); +} diff --git a/pallet/pallet-codegen/src/test/resources/UnnamedStorage.java b/pallet/pallet-codegen/src/test/resources/UnnamedStorage.java new file mode 100644 index 00000000..e54f67b8 --- /dev/null +++ b/pallet/pallet-codegen/src/test/resources/UnnamedStorage.java @@ -0,0 +1,9 @@ +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import com.strategyobject.substrateclient.pallet.annotations.Storage; +import com.strategyobject.substrateclient.storage.StorageNMap; + +@Pallet(name = "Test") +public interface UnnamedStorage { + @Storage(name = "") + StorageNMap unnamed(); +} diff --git a/pallet/pallet-codegen/src/test/resources/WithoutScale.java b/pallet/pallet-codegen/src/test/resources/WithoutScale.java new file mode 100644 index 00000000..b45c7c76 --- /dev/null +++ b/pallet/pallet-codegen/src/test/resources/WithoutScale.java @@ -0,0 +1,13 @@ +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import com.strategyobject.substrateclient.pallet.annotations.Storage; +import com.strategyobject.substrateclient.pallet.annotations.StorageHasher; +import com.strategyobject.substrateclient.pallet.annotations.StorageKey; +import com.strategyobject.substrateclient.storage.StorageNMap; + +@Pallet(name = "Test") +public interface WithoutScale { + @Storage(name = "Test", keys = { + @StorageKey(hasher = StorageHasher.Blake2B128Concat) + }) + StorageNMap test(); +} diff --git a/pallet/src/main/java/com/strategyobject/substrateclient/pallet/GeneratedPalletResolver.java b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/GeneratedPalletResolver.java new file mode 100644 index 00000000..8b2a9829 --- /dev/null +++ b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/GeneratedPalletResolver.java @@ -0,0 +1,33 @@ +package com.strategyobject.substrateclient.pallet; + +import com.strategyobject.substrateclient.pallet.annotations.Pallet; +import com.strategyobject.substrateclient.rpc.Rpc; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.val; + +@RequiredArgsConstructor(staticName = "with") +public class GeneratedPalletResolver implements PalletResolver { + private static final String CLASS_NAME_TEMPLATE = "%sImpl"; + private final @NonNull Rpc rpc; + + @Override + public T resolve(Class interfaceClass) { + if (interfaceClass.getDeclaredAnnotationsByType(Pallet.class).length == 0) { + throw new IllegalArgumentException( + String.format("`%s` can't be constructed because isn't annotated with `@%s`.", + interfaceClass.getSimpleName(), + Pallet.class.getSimpleName())); + } + + Class implClazz; + try { + implClazz = Class.forName(String.format(CLASS_NAME_TEMPLATE, interfaceClass.getCanonicalName())); + val ctor = implClazz.getConstructor(Rpc.class); + + return interfaceClass.cast(ctor.newInstance(rpc)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/pallet/src/main/java/com/strategyobject/substrateclient/pallet/PalletResolver.java b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/PalletResolver.java new file mode 100644 index 00000000..22bfeec7 --- /dev/null +++ b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/PalletResolver.java @@ -0,0 +1,5 @@ +package com.strategyobject.substrateclient.pallet; + +public interface PalletResolver { + T resolve(Class clazz); +} diff --git a/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/Pallet.java b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/Pallet.java new file mode 100644 index 00000000..74b24540 --- /dev/null +++ b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/Pallet.java @@ -0,0 +1,15 @@ +package com.strategyobject.substrateclient.pallet.annotations; + +import lombok.NonNull; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Pallet { + @NonNull + String name(); +} diff --git a/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/Storage.java b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/Storage.java new file mode 100644 index 00000000..8aa00f56 --- /dev/null +++ b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/Storage.java @@ -0,0 +1,13 @@ +package com.strategyobject.substrateclient.pallet.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.SOURCE) +public @interface Storage { + String name(); + StorageKey[] keys() default {}; +} diff --git a/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/StorageHasher.java b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/StorageHasher.java new file mode 100644 index 00000000..2be4a764 --- /dev/null +++ b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/StorageHasher.java @@ -0,0 +1,7 @@ +package com.strategyobject.substrateclient.pallet.annotations; + +public enum StorageHasher { + Blake2B128Concat, + TwoX64Concat, + Identity +} diff --git a/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/StorageKey.java b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/StorageKey.java new file mode 100644 index 00000000..00aca7d5 --- /dev/null +++ b/pallet/src/main/java/com/strategyobject/substrateclient/pallet/annotations/StorageKey.java @@ -0,0 +1,14 @@ +package com.strategyobject.substrateclient.pallet.annotations; + +import com.strategyobject.substrateclient.scale.annotations.Scale; +import com.strategyobject.substrateclient.scale.annotations.ScaleGeneric; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +public @interface StorageKey { + Scale type() default @Scale(); + ScaleGeneric generic() default @ScaleGeneric(template = "", types = {}); + StorageHasher hasher(); +} diff --git a/pallet/src/test/java/com/strategyobject/substrateclient/pallet/GeneratedPalletResolverTests.java b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/GeneratedPalletResolverTests.java new file mode 100644 index 00000000..1cf0d39e --- /dev/null +++ b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/GeneratedPalletResolverTests.java @@ -0,0 +1,41 @@ +package com.strategyobject.substrateclient.pallet; + +import com.strategyobject.substrateclient.rpc.Rpc; +import lombok.val; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +public class GeneratedPalletResolverTests { + @Test + public void throwsWhenPalletIsNotAnnotated() { + val rpc = mock(Rpc.class); + + val resolver = GeneratedPalletResolver.with(rpc); + + assertThrows(IllegalArgumentException.class, + () -> resolver.resolve(TestPalletNotAnnotated.class)); + } + + @Test + public void throwsWhenPalletImplementationDoesNotHaveAppropriateConstructor() { + val rpc = mock(Rpc.class); + + val resolver = GeneratedPalletResolver.with(rpc); + + assertThrows(RuntimeException.class, + () -> resolver.resolve(TestPalletWithoutConstructor.class)); + } + + @Test + public void resolve() { + val rpc = mock(Rpc.class); + + val resolver = GeneratedPalletResolver.with(rpc); + val pallet = resolver.resolve(TestPallet.class); + + assertNotNull(pallet); + assertEquals(TestPalletImpl.class, pallet.getClass()); + } +} diff --git a/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPallet.java b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPallet.java new file mode 100644 index 00000000..a65bd1e2 --- /dev/null +++ b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPallet.java @@ -0,0 +1,7 @@ +package com.strategyobject.substrateclient.pallet; + +import com.strategyobject.substrateclient.pallet.annotations.Pallet; + +@Pallet(name = "Test") +public interface TestPallet { +} diff --git a/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletImpl.java b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletImpl.java new file mode 100644 index 00000000..3eb675a9 --- /dev/null +++ b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletImpl.java @@ -0,0 +1,9 @@ +package com.strategyobject.substrateclient.pallet; + +import com.strategyobject.substrateclient.rpc.Rpc; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class TestPalletImpl implements TestPallet { + private final Rpc rpc; +} diff --git a/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletNotAnnotated.java b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletNotAnnotated.java new file mode 100644 index 00000000..79e99755 --- /dev/null +++ b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletNotAnnotated.java @@ -0,0 +1,6 @@ +package com.strategyobject.substrateclient.pallet; + +import com.strategyobject.substrateclient.pallet.annotations.Pallet; + +public interface TestPalletNotAnnotated { +} diff --git a/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletNotAnnotatedImpl.java b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletNotAnnotatedImpl.java new file mode 100644 index 00000000..5d5ca68e --- /dev/null +++ b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletNotAnnotatedImpl.java @@ -0,0 +1,9 @@ +package com.strategyobject.substrateclient.pallet; + +import com.strategyobject.substrateclient.rpc.Rpc; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class TestPalletNotAnnotatedImpl implements TestPalletNotAnnotated { + private final Rpc rpc; +} diff --git a/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletWithoutConstructor.java b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletWithoutConstructor.java new file mode 100644 index 00000000..70fafa4b --- /dev/null +++ b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletWithoutConstructor.java @@ -0,0 +1,7 @@ +package com.strategyobject.substrateclient.pallet; + +import com.strategyobject.substrateclient.pallet.annotations.Pallet; + +@Pallet(name = "Test") +public interface TestPalletWithoutConstructor { +} diff --git a/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletWithoutConstructorImpl.java b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletWithoutConstructorImpl.java new file mode 100644 index 00000000..9d754f3e --- /dev/null +++ b/pallet/src/test/java/com/strategyobject/substrateclient/pallet/TestPalletWithoutConstructorImpl.java @@ -0,0 +1,4 @@ +package com.strategyobject.substrateclient.pallet; + +public class TestPalletWithoutConstructorImpl implements TestPalletWithoutConstructor { +} diff --git a/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/decoder/RpcDecoderAnnotatedClass.java b/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/decoder/RpcDecoderAnnotatedClass.java index 3602af64..4ddf40fe 100644 --- a/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/decoder/RpcDecoderAnnotatedClass.java +++ b/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/decoder/RpcDecoderAnnotatedClass.java @@ -126,7 +126,7 @@ private void setFields(MethodSpec.Builder methodSpec, ProcessorContext context) DECODER_REGISTRY, SCALE_READER_REGISTRY); val scaleAnnotationParser = new ScaleAnnotationParser(context); - val scaleReaderCompositor = new ReaderCompositor(context, + val scaleReaderCompositor = ReaderCompositor.forAnyType(context, typeVarMap, String.format("%s[$L].%s", DECODERS_ARG, READER_ACCESSOR), SCALE_READER_REGISTRY); diff --git a/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/encoder/EncoderCompositor.java b/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/encoder/EncoderCompositor.java index 735715d0..16fae5be 100644 --- a/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/encoder/EncoderCompositor.java +++ b/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/encoder/EncoderCompositor.java @@ -53,7 +53,7 @@ protected CodeBlock whenTypeVar(@NonNull TypeVariable type, TypeMirror _override val builder = CodeBlock.builder() .add("$T.$L(($T) ", EncoderPair.class, PAIR_FACTORY_METHOD, RpcEncoder.class); - if (context.isSubtypeOf(type, selfEncodable)) { + if (context.isAssignable(type, selfEncodable)) { builder.add("$L.resolve($T.class)", encoderRegistryVarName, selfEncodable); } else { builder.add(encoderAccessor, typeVarMap.get(type.toString())); @@ -80,7 +80,7 @@ private CodeBlock getNonGenericCodeBlock(TypeMirror type) { .add("$T.$L(($T) ", EncoderPair.class, PAIR_FACTORY_METHOD, RpcEncoder.class) .add("$L.resolve($T.class)", encoderRegistryVarName, - context.isSubtypeOf(type, selfEncodable) ? + context.isAssignable(type, selfEncodable) ? selfEncodable : type) .add(", ($T) ", ScaleWriter.class) @@ -95,7 +95,7 @@ protected CodeBlock whenGenericType(@NonNull DeclaredType type, TypeMirror _over val builder = CodeBlock.builder() .add("$T.$L(", EncoderPair.class, PAIR_FACTORY_METHOD); - if (context.isSubtypeOf(resolveType, selfEncodable)) { + if (context.isAssignable(resolveType, selfEncodable)) { builder.add("($T) registry.resolve($T.class)", selfEncodable, RpcEncoder.class); } else { builder.add("$T.$L($T.class, ", RpcRegistryHelper.class, RESOLVE_AND_INJECT_METHOD, resolveType); @@ -118,6 +118,6 @@ protected CodeBlock whenGenericType(@NonNull DeclaredType type, TypeMirror _over @Override protected boolean doTraverseArguments(@NonNull DeclaredType type, TypeMirror override) { - return override != null || !context.isSubtypeOf(context.erasure(type), selfEncodable); + return override != null || !context.isAssignable(context.erasure(type), selfEncodable); } } diff --git a/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/encoder/RpcEncoderAnnotatedClass.java b/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/encoder/RpcEncoderAnnotatedClass.java index 5b1c9276..5cc2c689 100644 --- a/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/encoder/RpcEncoderAnnotatedClass.java +++ b/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/encoder/RpcEncoderAnnotatedClass.java @@ -124,7 +124,7 @@ private void setFields(MethodSpec.Builder methodSpec, ProcessorContext context) ENCODER_REGISTRY, SCALE_WRITER_REGISTRY); val scaleAnnotationParser = new ScaleAnnotationParser(context); - val scaleWriterCompositor = new WriterCompositor(context, + val scaleWriterCompositor = WriterCompositor.forAnyType(context, typeVarMap, String.format("%s[$L].%s", ENCODERS_ARG, WRITER_ACCESSOR), SCALE_WRITER_REGISTRY); @@ -212,7 +212,7 @@ private void setScaleField(MethodSpec.Builder methodSpec, private void addValidationRules(MethodSpec.Builder methodSpec, ProcessorContext context) { val classTypeParametersSize = classElement.getTypeParameters().size(); - if (classTypeParametersSize == 0 || context.isSubtypeOf(classElement.asType(), context.erasure(context.getType(RPC_SELF_ENCODABLE)))) { + if (classTypeParametersSize == 0 || context.isAssignable(classElement.asType(), context.erasure(context.getType(RPC_SELF_ENCODABLE)))) { methodSpec.addStatement("if ($1L != null && $1L.length > 0) throw new $2T()", ENCODERS_ARG, IllegalArgumentException.class); } else { methodSpec diff --git a/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/sections/RpcMethodProcessor.java b/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/sections/RpcMethodProcessor.java index 75c2ab1d..1d8b5faa 100644 --- a/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/sections/RpcMethodProcessor.java +++ b/rpc/rpc-codegen/src/main/java/com/strategyobject/substrateclient/rpc/codegen/sections/RpcMethodProcessor.java @@ -146,9 +146,7 @@ private CodeBlock getScaleReadCodeBlock(AnnotatedConstruct annotated, String arg, ScaleAnnotationParser scaleAnnotationParser, ProcessorContext context) { - val readerCompositor = new ReaderCompositor(context, - EMPTY_TYPE_VAR_MAP, - String.format("%s[$L].%s", DECODERS_ARG, READER_ACCESSOR), + val readerCompositor = ReaderCompositor.disallowOpenGeneric(context, SCALE_READER_REGISTRY); val typeOverride = scaleAnnotationParser.parse(annotated); val readerCode = typeOverride != null ? @@ -176,9 +174,7 @@ private void processParameters(MethodSpec.Builder methodSpecBuilder, return; } - val writerCompositor = new WriterCompositor(context, - EMPTY_TYPE_VAR_MAP, - String.format("%s[$L].%s", ENCODERS_ARG, WRITER_ACCESSOR), + val writerCompositor = WriterCompositor.disallowOpenGeneric(context, SCALE_WRITER_REGISTRY); val encoderCompositor = new EncoderCompositor( context, diff --git a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/ScaleAnnotationParser.java b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/ScaleAnnotationParser.java index 6ee8fe64..2dfd0dee 100644 --- a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/ScaleAnnotationParser.java +++ b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/ScaleAnnotationParser.java @@ -45,6 +45,23 @@ public TypeTraverser.TypeTreeNode parse(@NonNull AnnotatedConstruct annotated) { return null; } + public TypeTraverser.TypeTreeNode parse(AnnotationMirror annotation) { + if (context.isSameType(annotation.getAnnotationType(), context.getType(Scale.class))) { + val scaleType = AnnotationUtils.getValueFromAnnotation(annotation, "value"); + + return new TypeTraverser.TypeTreeNode(scaleType); + } + + if (context.isSameType(annotation.getAnnotationType(), context.getType(ScaleGeneric.class))) { + val template = AnnotationUtils.getValueFromAnnotation(annotation, "template"); + val typesMap = getTypesMap(annotation); + + return parseTemplate(template, typesMap); + } + + return null; + } + private TypeTraverser.TypeTreeNode parseTemplate(String template, Map typesMap) { val indexes = StringUtils.allIndexesOfAny(template, "<,>"); if (indexes.size() == 0 || indexes.get(0) == 0) { @@ -92,7 +109,7 @@ private TypeTraverser.TypeTreeNode parseTemplate(String template, Map typesMap, String name) { val type = typesMap.get(name); - return type == null || context.isSubtypeOf(type, context.getType(SCALE_ANNOTATIONS_DEFAULT)) ? null : type; + return type == null || context.isAssignable(type, context.getType(SCALE_ANNOTATIONS_DEFAULT)) ? null : type; } private Map getTypesMap(AnnotationMirror scaleGeneric) { diff --git a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/reader/ReaderCompositor.java b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/reader/ReaderCompositor.java index 8a795d5a..ef55bb37 100644 --- a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/reader/ReaderCompositor.java +++ b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/reader/ReaderCompositor.java @@ -1,5 +1,6 @@ package com.strategyobject.substrateclient.scale.codegen.reader; +import com.google.common.base.Strings; import com.squareup.javapoet.CodeBlock; import com.strategyobject.substrateclient.common.codegen.ProcessorContext; import com.strategyobject.substrateclient.common.codegen.TypeTraverser; @@ -18,19 +19,36 @@ public class ReaderCompositor extends TypeTraverser { private final String readerAccessor; private final String registryVarName; - public ReaderCompositor(@NonNull ProcessorContext context, - @NonNull Map typeVarMap, - @NonNull String readerAccessor, - @NonNull String registryVarName) { + private ReaderCompositor(ProcessorContext context, + Map typeVarMap, + String readerAccessor, + String registryVarName) { super(CodeBlock.class); + this.context = context; this.typeVarMap = typeVarMap; this.readerAccessor = readerAccessor; this.registryVarName = registryVarName; } + public static ReaderCompositor forAnyType(@NonNull ProcessorContext context, + @NonNull Map typeVarMap, + @NonNull String readerAccessor, + @NonNull String registryVarName) { + return new ReaderCompositor(context, typeVarMap, readerAccessor, registryVarName); + } + + public static ReaderCompositor disallowOpenGeneric(@NonNull ProcessorContext context, + @NonNull String registryVarName) { + return new ReaderCompositor(context, null, null, registryVarName); + } + @Override protected CodeBlock whenTypeVar(@NonNull TypeVariable type, TypeMirror _override) { + if (Strings.isNullOrEmpty(readerAccessor)) { + throw new IllegalStateException("The compositor doesn't support open generics."); + } + return CodeBlock.builder() .add(readerAccessor, typeVarMap.get(type.toString())) .build(); diff --git a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/reader/ScaleReaderAnnotatedClass.java b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/reader/ScaleReaderAnnotatedClass.java index de1d5664..40ff4b4a 100644 --- a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/reader/ScaleReaderAnnotatedClass.java +++ b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/reader/ScaleReaderAnnotatedClass.java @@ -7,8 +7,8 @@ import com.strategyobject.substrateclient.scale.ScaleReader; import com.strategyobject.substrateclient.scale.annotations.AutoRegister; import com.strategyobject.substrateclient.scale.annotations.Ignore; -import com.strategyobject.substrateclient.scale.codegen.ScaleProcessorHelper; import com.strategyobject.substrateclient.scale.codegen.ScaleAnnotationParser; +import com.strategyobject.substrateclient.scale.codegen.ScaleProcessorHelper; import com.strategyobject.substrateclient.scale.registries.ScaleReaderRegistry; import lombok.NonNull; import lombok.val; @@ -109,7 +109,10 @@ private void addMethodBody(MethodSpec.Builder methodSpec, .beginControlFlow("try"); val scaleAnnotationParser = new ScaleAnnotationParser(context); - val compositor = new ReaderCompositor(context, typeVarMap, String.format("%s[$L]", READERS_ARG), REGISTRY); + val compositor = ReaderCompositor.forAnyType(context, + typeVarMap, + String.format("%s[$L]", READERS_ARG), + REGISTRY); for (Element element : classElement.getEnclosedElements()) { if (element instanceof VariableElement) { val field = (VariableElement) element; diff --git a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/ScaleWriterAnnotatedClass.java b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/ScaleWriterAnnotatedClass.java index 57d251a0..bf27c1c5 100644 --- a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/ScaleWriterAnnotatedClass.java +++ b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/ScaleWriterAnnotatedClass.java @@ -90,7 +90,7 @@ private void addValidationRules(MethodSpec.Builder methodSpec, methodSpec.addStatement("if (value == null) throw new IllegalArgumentException(\"value is null\")"); val classTypeParametersSize = classElement.getTypeParameters().size(); - if (classTypeParametersSize == 0 || context.isSubtypeOf(classElement.asType(), context.erasure(context.getType(SCALE_SELF_WRITABLE)))) { + if (classTypeParametersSize == 0 || context.isAssignable(classElement.asType(), context.erasure(context.getType(SCALE_SELF_WRITABLE)))) { methodSpec.addStatement("if (writers != null && writers.length > 0) throw new IllegalArgumentException()"); } else { methodSpec @@ -109,7 +109,10 @@ private void addMethodBody(MethodSpec.Builder methodSpec, .beginControlFlow("try"); val scaleAnnotationParser = new ScaleAnnotationParser(context); - val compositor = new WriterCompositor(context, typeVarMap, String.format("%s[$L]", WRITERS_ARG), REGISTRY); + val compositor = WriterCompositor.forAnyType(context, + typeVarMap, + String.format("%s[$L]", WRITERS_ARG), + REGISTRY); for (Element element : classElement.getEnclosedElements()) { if (element instanceof VariableElement) { val field = (VariableElement) element; diff --git a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/ScaleWriterProcessor.java b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/ScaleWriterProcessor.java index 31f0f851..cdd2e061 100644 --- a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/ScaleWriterProcessor.java +++ b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/ScaleWriterProcessor.java @@ -71,12 +71,12 @@ public boolean process(Set annotations, RoundEnvironment private boolean validateScaleSelfWritable(TypeElement typeElement) { val selfWritable = context.erasure(context.getType(SCALE_SELF_WRITABLE)); - if (!context.isSubtypeOf(typeElement.asType(), selfWritable)) { + if (!context.isAssignable(typeElement.asType(), selfWritable)) { return true; } val typeParameters = typeElement.getTypeParameters(); return typeParameters.size() == 0 || - typeParameters.stream().allMatch(x -> context.isSubtypeOf(x.asType(), selfWritable)); + typeParameters.stream().allMatch(x -> context.isAssignable(x.asType(), selfWritable)); } } diff --git a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/WriterCompositor.java b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/WriterCompositor.java index de56c0b2..316486ce 100644 --- a/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/WriterCompositor.java +++ b/scale/scale-codegen/src/main/java/com/strategyobject/substrateclient/scale/codegen/writer/WriterCompositor.java @@ -21,11 +21,12 @@ public class WriterCompositor extends TypeTraverser { private final String writerAccessor; private final String registryVarName; - public WriterCompositor(ProcessorContext context, - Map typeVarMap, - String writerAccessor, - String registryVarName) { + private WriterCompositor(ProcessorContext context, + Map typeVarMap, + String writerAccessor, + String registryVarName) { super(CodeBlock.class); + this.context = context; this.typeVarMap = typeVarMap; this.selfWritable = context.erasure(context.getType(SCALE_SELF_WRITABLE)); @@ -33,9 +34,21 @@ public WriterCompositor(ProcessorContext context, this.registryVarName = registryVarName; } + public static WriterCompositor forAnyType(@NonNull ProcessorContext context, + @NonNull Map typeVarMap, + @NonNull String writerAccessor, + @NonNull String registryVarName) { + return new WriterCompositor(context, typeVarMap, writerAccessor, registryVarName); + } + + public static WriterCompositor disallowOpenGeneric(@NonNull ProcessorContext context, + @NonNull String registryVarName) { + return new WriterCompositor(context, null, null, registryVarName); + } + @Override protected CodeBlock whenTypeVar(@NonNull TypeVariable type, TypeMirror _override) { - return context.isSubtypeOf(type, selfWritable) ? + return context.isAssignable(type, selfWritable) ? CodeBlock.builder() .add("$L.resolve($T.class)", registryVarName, selfWritable) .build() : @@ -60,7 +73,7 @@ private CodeBlock getNonGenericCodeBlock(TypeMirror type, TypeMirror override) { registryVarName, override != null ? override : - context.isSubtypeOf(type, selfWritable) ? + context.isAssignable(type, selfWritable) ? selfWritable : type) .build(); @@ -80,7 +93,7 @@ protected CodeBlock whenGenericType(@NonNull DeclaredType type, TypeMirror overr } else { resolveType = context.erasure(type); - if (context.isSubtypeOf(resolveType, selfWritable)) { + if (context.isAssignable(resolveType, selfWritable)) { return CodeBlock.builder().add("$L.resolve($T.class)", registryVarName, selfWritable).build(); } } @@ -96,6 +109,6 @@ protected CodeBlock whenGenericType(@NonNull DeclaredType type, TypeMirror overr @Override protected boolean doTraverseArguments(@NonNull DeclaredType type, TypeMirror override) { - return override != null || !context.isSubtypeOf(context.erasure(type), selfWritable); + return override != null || !context.isAssignable(context.erasure(type), selfWritable); } } diff --git a/settings.gradle b/settings.gradle index ee9ac553..64e2f05a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,4 +15,6 @@ include 'tests' include 'transport' include 'types' include 'storage' +include 'pallet' +include 'pallet:pallet-codegen' diff --git a/types/src/main/java/com/strategyobject/substrateclient/types/Size.java b/types/src/main/java/com/strategyobject/substrateclient/types/Size.java index 0f64b3c7..11a1c49b 100644 --- a/types/src/main/java/com/strategyobject/substrateclient/types/Size.java +++ b/types/src/main/java/com/strategyobject/substrateclient/types/Size.java @@ -3,13 +3,20 @@ public interface Size { int getValue(); + Zero zero = new Zero(); Of32 of32 = new Of32(); Of64 of64 = new Of64(); Of96 of96 = new Of96(); Of128 of128 = new Of128(); - class Of32 implements Size { + class Zero implements Size { + @Override + public int getValue() { + return 0; + } + } + class Of32 implements Size { @Override public int getValue() { return 32;