Skip to content

Commit

Permalink
Implement DI with Guice (Close #22)
Browse files Browse the repository at this point in the history
  • Loading branch information
kalaninja committed Jul 1, 2022
1 parent 7d3b515 commit 7032244
Show file tree
Hide file tree
Showing 191 changed files with 1,498 additions and 985 deletions.
12 changes: 7 additions & 5 deletions api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ dependencies {
implementation project(':scale')
implementation project(':transport')

implementation 'com.google.inject:guice:5.1.0'

annotationProcessor project(':pallet:pallet-codegen')
annotationProcessor project(':rpc:rpc-codegen')

testImplementation project(':tests')

testAnnotationProcessor project(':pallet:pallet-codegen')

testImplementation 'ch.qos.logback:logback-classic:1.2.11'
testImplementation 'org.testcontainers:testcontainers:1.17.1'
testImplementation 'org.testcontainers:junit-jupiter:1.17.1'
testImplementation 'org.testcontainers:testcontainers:1.17.2'
testImplementation 'org.testcontainers:junit-jupiter:1.17.2'
testImplementation 'org.awaitility:awaitility:4.2.0'
}

testAnnotationProcessor project(':pallet:pallet-codegen')
}
46 changes: 22 additions & 24 deletions api/src/main/java/com/strategyobject/substrateclient/api/Api.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
package com.strategyobject.substrateclient.api;

import com.strategyobject.substrateclient.pallet.GeneratedPalletResolver;
import com.strategyobject.substrateclient.pallet.PalletResolver;
import com.strategyobject.substrateclient.rpc.RpcGeneratedSectionFactory;
import com.strategyobject.substrateclient.rpc.api.section.State;
import com.google.inject.Guice;
import com.google.inject.Module;
import com.strategyobject.substrateclient.pallet.PalletFactory;
import com.strategyobject.substrateclient.rpc.RpcSectionFactory;
import com.strategyobject.substrateclient.transport.ProviderInterface;
import lombok.NonNull;
import lombok.val;
import lombok.RequiredArgsConstructor;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/**
* Provides the ability to query a node and interact with the Polkadot or Substrate chains.
* It allows interacting with blockchain in various ways: using RPC's queries directly or
* accessing Pallets and its APIs, such as storages, transactions, etc.
*/
@RequiredArgsConstructor
public class Api implements AutoCloseable {
private final @NonNull ProviderInterface providerInterface;
private final @NonNull PalletResolver palletResolver;
private final @NonNull RpcSectionFactory rpcSectionFactory;
private final @NonNull PalletFactory palletFactory;
private final Map<Class<?>, Object> resolvedCache = new ConcurrentHashMap<>();

private Api(@NonNull ProviderInterface providerInterface) {
this.providerInterface = providerInterface;

val state = RpcGeneratedSectionFactory.create(State.class, providerInterface);
this.palletResolver = GeneratedPalletResolver.with(state);
}

/**
* Resolves the instance of a rpc by its definition.
Expand All @@ -35,9 +31,8 @@ private Api(@NonNull ProviderInterface providerInterface) {
* @param <T> the type of the rpc
* @return appropriate instance of the rpc
*/
public <T> T rpc(Class<T> clazz) {
return clazz.cast(resolvedCache
.computeIfAbsent(clazz, x -> RpcGeneratedSectionFactory.create(x, providerInterface)));
public <T> T rpc(@NonNull Class<T> clazz) {
return clazz.cast(resolvedCache.computeIfAbsent(clazz, rpcSectionFactory::create));
}

/**
Expand All @@ -48,18 +43,21 @@ public <T> T rpc(Class<T> clazz) {
* @return appropriate instance of the pallet
*/
public <T> T pallet(@NonNull Class<T> clazz) {
return clazz.cast(resolvedCache
.computeIfAbsent(clazz, palletResolver::resolve));
}

public static Api with(ProviderInterface providerInterface) {
return new Api(providerInterface);
return clazz.cast(resolvedCache.computeIfAbsent(clazz, palletFactory::create));
}

@Override
public void close() throws Exception {
if (providerInterface instanceof AutoCloseable) {
((AutoCloseable) providerInterface).close();
if (rpcSectionFactory instanceof AutoCloseable) {
((AutoCloseable) rpcSectionFactory).close();
}
}

public static ApiBuilder with(@NonNull Supplier<ProviderInterface> providerInterface) {
return with(new DefaultModule(providerInterface.get()));
}

public static ApiBuilder with(@NonNull Module module) {
return new ApiBuilder(Guice.createInjector(new RequireModule(), module));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.strategyobject.substrateclient.api;

import com.google.inject.Injector;
import com.strategyobject.substrateclient.common.types.AutoRegistry;
import com.strategyobject.substrateclient.rpc.registries.RpcDecoderRegistry;
import com.strategyobject.substrateclient.rpc.registries.RpcEncoderRegistry;
import com.strategyobject.substrateclient.scale.registries.ScaleReaderRegistry;
import com.strategyobject.substrateclient.scale.registries.ScaleWriterRegistry;
import com.strategyobject.substrateclient.transport.ProviderInterface;
import lombok.val;

import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

public class ApiBuilder {
private static final String ROOT_PREFIX = "com.strategyobject.substrateclient";

private final Injector injector;

public ApiBuilder(Injector injector) {
this.injector = injector;

autoRegister(
ScaleReaderRegistry.class,
ScaleWriterRegistry.class,
RpcDecoderRegistry.class,
RpcEncoderRegistry.class);
}

public <T> ApiBuilder configure(Class<T> clazz, Consumer<T> consumer) {
consumer.accept(injector.getInstance(clazz));
return this;
}

public CompletableFuture<Api> build() {
val provider = injector.getInstance(ProviderInterface.class);
val result = provider.isConnected() ?
CompletableFuture.<Void>completedFuture(null) :
provider.connect();

return result.thenApply(ignored -> injector.getInstance(Api.class));
}

@SafeVarargs
private final void autoRegister(Class<? extends AutoRegistry>... registries) {
for (val registry : registries) {
injector.getInstance(registry).registerAnnotatedFrom(ROOT_PREFIX);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.strategyobject.substrateclient.api;

import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.strategyobject.substrateclient.pallet.GeneratedPalletFactory;
import com.strategyobject.substrateclient.pallet.PalletFactory;
import com.strategyobject.substrateclient.rpc.GeneratedRpcSectionFactory;
import com.strategyobject.substrateclient.rpc.RpcSectionFactory;
import com.strategyobject.substrateclient.rpc.api.section.State;
import com.strategyobject.substrateclient.rpc.registries.RpcDecoderRegistry;
import com.strategyobject.substrateclient.rpc.registries.RpcEncoderRegistry;
import com.strategyobject.substrateclient.scale.registries.ScaleReaderRegistry;
import com.strategyobject.substrateclient.scale.registries.ScaleWriterRegistry;
import com.strategyobject.substrateclient.transport.ProviderInterface;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class DefaultModule extends AbstractModule {
private final @NonNull ProviderInterface providerInterface;

@Override
protected void configure() {
try {
bind(ProviderInterface.class).toInstance(providerInterface);
bind(ScaleReaderRegistry.class).asEagerSingleton();
bind(ScaleWriterRegistry.class).asEagerSingleton();
bind(RpcDecoderRegistry.class)
.toConstructor(RpcDecoderRegistry.class.getConstructor(ScaleReaderRegistry.class))
.asEagerSingleton();
bind(RpcEncoderRegistry.class)
.toConstructor(RpcEncoderRegistry.class.getConstructor(ScaleWriterRegistry.class))
.asEagerSingleton();
bind(RpcSectionFactory.class)
.toConstructor(
GeneratedRpcSectionFactory.class.getConstructor(
ProviderInterface.class,
RpcEncoderRegistry.class,
ScaleWriterRegistry.class,
RpcDecoderRegistry.class,
ScaleReaderRegistry.class))
.asEagerSingleton();
bind(PalletFactory.class)
.toConstructor(
GeneratedPalletFactory.class.getConstructor(
ScaleReaderRegistry.class,
ScaleWriterRegistry.class,
State.class
))
.asEagerSingleton();
bind(Api.class)
.toConstructor(
Api.class.getConstructor(
RpcSectionFactory.class,
PalletFactory.class))
.asEagerSingleton();
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

@Singleton
@Provides
public State provideState(RpcSectionFactory rpcSectionFactory) {
return rpcSectionFactory.create(State.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.strategyobject.substrateclient.api;

import com.google.inject.AbstractModule;
import com.strategyobject.substrateclient.pallet.PalletFactory;
import com.strategyobject.substrateclient.rpc.RpcSectionFactory;
import com.strategyobject.substrateclient.rpc.registries.RpcDecoderRegistry;
import com.strategyobject.substrateclient.rpc.registries.RpcEncoderRegistry;
import com.strategyobject.substrateclient.scale.registries.ScaleReaderRegistry;
import com.strategyobject.substrateclient.scale.registries.ScaleWriterRegistry;
import com.strategyobject.substrateclient.transport.ProviderInterface;

public class RequireModule extends AbstractModule {
@Override
protected void configure() {
requireBinding(ProviderInterface.class);
requireBinding(ScaleReaderRegistry.class);
requireBinding(ScaleWriterRegistry.class);
requireBinding(RpcDecoderRegistry.class);
requireBinding(RpcEncoderRegistry.class);
requireBinding(RpcSectionFactory.class);
requireBinding(PalletFactory.class);
requireBinding(Api.class);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package com.strategyobject.substrateclient.api;

import com.google.inject.CreationException;
import com.google.inject.util.Modules;
import com.strategyobject.substrateclient.common.strings.HexConverter;
import com.strategyobject.substrateclient.pallet.PalletFactory;
import com.strategyobject.substrateclient.rpc.api.AccountId;
import com.strategyobject.substrateclient.rpc.api.BlockNumber;
import com.strategyobject.substrateclient.rpc.api.Index;
import com.strategyobject.substrateclient.rpc.api.section.System;
import com.strategyobject.substrateclient.tests.containers.SubstrateVersion;
import com.strategyobject.substrateclient.tests.containers.TestSubstrateContainer;
import com.strategyobject.substrateclient.transport.ws.WsProvider;
Expand All @@ -12,8 +19,7 @@
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;
import static org.junit.jupiter.api.Assertions.*;

@Testcontainers
class ApiTests {
Expand All @@ -25,19 +31,54 @@ class ApiTests {
@Test
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);
.setEndpoint(substrate.getWsAddress());

try (val api = Api.with(wsProvider)) {
try (val api = Api.with(wsProvider).build().join()) {
val systemPallet = api.pallet(SystemPallet.class);
val blockHash = systemPallet
.blockHash()
.get(BlockNumber.GENESIS)
.get(WAIT_TIMEOUT, TimeUnit.SECONDS);

assertNotNull(blockHash);
assertNotEquals(BigInteger.ZERO, new BigInteger(blockHash.getData()));
assertNotEquals(BigInteger.ZERO, new BigInteger(blockHash.getBytes()));
}
}

@Test
void getSystemSectionAndCall() throws Exception {
val wsProvider = WsProvider.builder()
.setEndpoint(substrate.getWsAddress());

try (val api = Api.with(wsProvider).build().join()) {
val system = api.rpc(System.class);
val alicePublicKey = HexConverter.toBytes("0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d");
val actual = system.accountNextIndex(AccountId.fromBytes(alicePublicKey)).join();

assertEquals(Index.ZERO, actual);
}
}

@Test
void reconfigureApi() throws Exception {
val wsProvider = WsProvider.builder()
.setEndpoint(substrate.getWsAddress())
.build();

val expected = "expected";
val module = Modules.override(new DefaultModule(wsProvider)).with(new OverrideModule());
try (val api = Api.with(module)
.configure(PalletFactory.class, x -> ((ThrowingPalletFactory) x).setMessage(expected))
.build()
.join()) {
assertThrows(RuntimeException.class, () -> api.pallet(SystemPallet.class), expected);
}
}

@Test
void validateModule() {
assertThrows(CreationException.class, () -> Api.with(binder -> {
// no bindings provided
}));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.strategyobject.substrateclient.api;

import com.google.inject.AbstractModule;
import com.strategyobject.substrateclient.pallet.PalletFactory;

public class OverrideModule extends AbstractModule {
@Override
protected void configure() {
bind(PalletFactory.class).to(ThrowingPalletFactory.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
@Pallet("System")
public interface SystemPallet {
@Storage(
value = "BlockHash",
name = "BlockHash",
keys = {
@StorageKey(
type = @Scale(BlockNumber.class),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.strategyobject.substrateclient.api;

import com.strategyobject.substrateclient.pallet.PalletFactory;
import lombok.NonNull;
import lombok.Setter;

public class ThrowingPalletFactory implements PalletFactory {
@Setter
private String message;

@Override
public <T> T create(@NonNull Class<T> interfaceClass) {
throw new RuntimeException(message);
}
}
14 changes: 7 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ subprojects {
sourceCompatibility = '1.8'

dependencies {
implementation 'com.google.guava:guava:30.1.1-jre'
implementation 'org.slf4j:slf4j-api:1.7.32'
implementation 'com.google.guava:guava:31.1-jre'
implementation 'org.slf4j:slf4j-api:1.7.36'

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2'
testImplementation 'org.assertj:assertj-core:3.22.0'
testImplementation 'org.mockito:mockito-core:3.12.4'
testImplementation 'org.mockito:mockito-inline:3.12.4'
testImplementation 'org.slf4j:slf4j-simple:1.7.32'
testImplementation 'org.assertj:assertj-core:3.23.1'
testImplementation 'org.mockito:mockito-core:4.6.1'
testImplementation 'org.mockito:mockito-inline:4.6.1'
testImplementation 'org.slf4j:slf4j-simple:1.7.36'

testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
}

test {
Expand Down
Loading

0 comments on commit 7032244

Please sign in to comment.