Move binding coroutine implementation to separate subproject
This commit is contained in:
parent
ce0180f5cd
commit
b16fb559a1
30
README.md
30
README.md
@ -136,6 +136,36 @@ resources on the topic of monad comprehensions.
|
|||||||
- [Monad comprehensions - Bow (Swift)][bow-monad-comprehension]
|
- [Monad comprehensions - Bow (Swift)][bow-monad-comprehension]
|
||||||
- [For comprehensions - Scala][scala-for-comprehension]
|
- [For comprehensions - Scala][scala-for-comprehension]
|
||||||
|
|
||||||
|
#### Coroutine Support
|
||||||
|
|
||||||
|
Use of coroutines within a `binding` block requires an additional dependency:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
dependencies {
|
||||||
|
implementation("com.michael-bull.kotlin-result:kotlin-result:1.1.8")
|
||||||
|
implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:1.1.8")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows for asynchronous binds to operate so that if a bind were to fail,
|
||||||
|
the binding block will return with the first failing async result:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
|
||||||
|
suspend fun failsIn5ms(): Result<Int, DomainErrorA> { ... }
|
||||||
|
suspend fun failsIn1ms(): Result<Int, DomainErrorB> { ... }
|
||||||
|
|
||||||
|
runBlocking{
|
||||||
|
val result = binding<Int, BindingError> {
|
||||||
|
val x = async { failsIn5ms().bind() }
|
||||||
|
val y = async { failsIn1ms().bind() }
|
||||||
|
x.await() + y.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
// result will be Err(DomainErrorB)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Inspiration
|
## Inspiration
|
||||||
|
|
||||||
Inspiration for this library has been drawn from other languages in which the
|
Inspiration for this library has been drawn from other languages in which the
|
||||||
|
123
build.gradle.kts
123
build.gradle.kts
@ -1,5 +1,7 @@
|
|||||||
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
|
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
|
||||||
import org.jetbrains.dokka.gradle.DokkaTask
|
import org.jetbrains.dokka.gradle.DokkaTask
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
|
||||||
|
|
||||||
val ossrhUsername: String? by ext
|
val ossrhUsername: String? by ext
|
||||||
val ossrhPassword: String? by ext
|
val ossrhPassword: String? by ext
|
||||||
@ -7,14 +9,14 @@ val ossrhPassword: String? by ext
|
|||||||
description = "A Result monad for modelling success or failure operations."
|
description = "A Result monad for modelling success or failure operations."
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
`maven-publish`
|
base
|
||||||
signing
|
|
||||||
kotlin("multiplatform") version "1.3.72"
|
|
||||||
id("org.jetbrains.dokka") version "0.10.1"
|
|
||||||
id("com.github.ben-manes.versions") version "0.28.0"
|
id("com.github.ben-manes.versions") version "0.28.0"
|
||||||
id("net.researchgate.release") version "2.8.1"
|
|
||||||
id("kotlinx.benchmark") version "0.2.0-dev-8"
|
kotlin("multiplatform") version "1.3.72" apply false
|
||||||
id("org.jetbrains.kotlin.plugin.allopen") version "1.3.72"
|
id("kotlinx.benchmark") version "0.2.0-dev-8" apply false
|
||||||
|
id("net.researchgate.release") version "2.8.1" apply false
|
||||||
|
id("org.jetbrains.dokka") version "0.10.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.plugin.allopen") version "1.3.72" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<DependencyUpdatesTask> {
|
tasks.withType<DependencyUpdatesTask> {
|
||||||
@ -25,6 +27,29 @@ tasks.withType<DependencyUpdatesTask> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
jcenter()
|
||||||
|
maven("https://dl.bintray.com/kotlin/kotlinx")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
plugins.withType<MavenPublishPlugin> {
|
||||||
|
apply(plugin = "net.researchgate.release")
|
||||||
|
apply(plugin = "org.gradle.signing")
|
||||||
|
|
||||||
|
val afterReleaseBuild by tasks.existing(DefaultTask::class)
|
||||||
|
val publish by tasks.existing(Task::class)
|
||||||
|
|
||||||
|
afterReleaseBuild {
|
||||||
|
dependsOn(publish)
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.withType<KotlinMultiplatformPluginWrapper> {
|
||||||
|
apply(plugin = "org.jetbrains.dokka")
|
||||||
|
|
||||||
val dokka by tasks.existing(DokkaTask::class) {
|
val dokka by tasks.existing(DokkaTask::class) {
|
||||||
outputFormat = "javadoc"
|
outputFormat = "javadoc"
|
||||||
outputDirectory = "$buildDir/docs/javadoc"
|
outputDirectory = "$buildDir/docs/javadoc"
|
||||||
@ -38,86 +63,16 @@ val javadocJar by tasks.registering(Jar::class) {
|
|||||||
from(dokka.get().outputDirectory)
|
from(dokka.get().outputDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
configure<KotlinMultiplatformExtension> {
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
jcenter()
|
|
||||||
maven("https://dl.bintray.com/kotlin/kotlinx")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allOpen {
|
|
||||||
annotation("org.openjdk.jmh.annotations.State")
|
|
||||||
annotation("org.openjdk.jmh.annotations.BenchmarkMode")
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets.create("benchmark")
|
|
||||||
|
|
||||||
benchmark {
|
|
||||||
targets {
|
|
||||||
register("jvmBenchmark")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvm {
|
jvm {
|
||||||
withJava()
|
|
||||||
|
|
||||||
mavenPublication {
|
mavenPublication {
|
||||||
artifact(javadocJar.get())
|
artifact(javadocJar.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
compilations.all {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
configure<PublishingExtension> {
|
||||||
all {
|
|
||||||
languageSettings.apply {
|
|
||||||
useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val commonMain by getting {
|
|
||||||
dependencies {
|
|
||||||
implementation(kotlin("stdlib-common"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val commonTest by getting {
|
|
||||||
dependencies {
|
|
||||||
implementation(kotlin("test-common"))
|
|
||||||
implementation(kotlin("test-annotations-common"))
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.7")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val jvmMain by getting {
|
|
||||||
dependencies {
|
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val jvmTest by getting {
|
|
||||||
dependencies {
|
|
||||||
implementation(kotlin("test-junit"))
|
|
||||||
implementation(kotlin("test"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val jvmBenchmark by getting {
|
|
||||||
dependsOn(jvmMain)
|
|
||||||
dependencies {
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx.benchmark.runtime-jvm:0.2.0-dev-8")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
publishing {
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
if (project.version.toString().endsWith("SNAPSHOT")) {
|
if (project.version.toString().endsWith("SNAPSHOT")) {
|
||||||
@ -188,13 +143,11 @@ publishing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
signing {
|
configure<SigningExtension> {
|
||||||
useGpgCmd()
|
useGpgCmd()
|
||||||
sign(publishing.publications)
|
sign(publications)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.afterReleaseBuild {
|
|
||||||
dependsOn(tasks.publish)
|
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
val ktorVersion = "1.3.2"
|
val ktorVersion = "1.3.2"
|
||||||
|
|
||||||
implementation(rootProject)
|
implementation(project(":kotlin-result"))
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
implementation("ch.qos.logback:logback-classic:1.2.3")
|
||||||
implementation("io.ktor:ktor-server-core:$ktorVersion")
|
implementation("io.ktor:ktor-server-core:$ktorVersion")
|
||||||
|
41
kotlin-result-coroutines/build.gradle.kts
Normal file
41
kotlin-result-coroutines/build.gradle.kts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
description = "Extensions for using kotlin-result with kotlinx-coroutines."
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
`maven-publish`
|
||||||
|
kotlin("multiplatform")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
all {
|
||||||
|
languageSettings.apply {
|
||||||
|
useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val commonMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("stdlib-common"))
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.8")
|
||||||
|
implementation(project(":kotlin-result"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val commonTest by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test-common"))
|
||||||
|
implementation(kotlin("test-annotations-common"))
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.3.8")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val jvmTest by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test-junit"))
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
|||||||
|
package com.github.michaelbull.result.coroutines.binding
|
||||||
|
|
||||||
|
import com.github.michaelbull.result.Err
|
||||||
|
import com.github.michaelbull.result.Ok
|
||||||
|
import com.github.michaelbull.result.Result
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suspending variant of [binding][com.github.michaelbull.result.binding].
|
||||||
|
*/
|
||||||
|
suspend inline fun <V, E> binding(crossinline block: suspend SuspendableResultBinding<E>.() -> V): Result<V, E> {
|
||||||
|
contract {
|
||||||
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val receiver = SuspendableResultBindingImpl<E>()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
with(receiver) { Ok(block()) }
|
||||||
|
} catch (ex: BindCancellationException) {
|
||||||
|
receiver.internalError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object BindCancellationException : CancellationException(null)
|
||||||
|
|
||||||
|
interface SuspendableResultBinding<E> {
|
||||||
|
suspend fun <V> Result<V, E>.bind(): V
|
||||||
|
}
|
||||||
|
|
||||||
|
@PublishedApi
|
||||||
|
internal class SuspendableResultBindingImpl<E> : SuspendableResultBinding<E> {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
lateinit var internalError: Err<E>
|
||||||
|
|
||||||
|
override suspend fun <V> Result<V, E>.bind(): V {
|
||||||
|
return when (this) {
|
||||||
|
is Ok -> value
|
||||||
|
is Err -> {
|
||||||
|
mutex.withLock {
|
||||||
|
if (::internalError.isInitialized.not()){
|
||||||
|
internalError = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw BindCancellationException
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package com.github.michaelbull.result
|
package com.github.michaelbull.result.coroutines
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -1,9 +1,9 @@
|
|||||||
package com.github.michaelbull.result.coroutines
|
package com.github.michaelbull.result.coroutines.binding
|
||||||
|
|
||||||
import com.github.michaelbull.result.Err
|
import com.github.michaelbull.result.Err
|
||||||
import com.github.michaelbull.result.Ok
|
import com.github.michaelbull.result.Ok
|
||||||
import com.github.michaelbull.result.Result
|
import com.github.michaelbull.result.Result
|
||||||
import com.github.michaelbull.result.runBlockingTest
|
import com.github.michaelbull.result.coroutines.runBlockingTest
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
@ -1,4 +1,4 @@
|
|||||||
package com.github.michaelbull.result
|
package com.github.michaelbull.result.coroutines
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
@ -0,0 +1,79 @@
|
|||||||
|
package com.github.michaelbull.result.coroutines.binding
|
||||||
|
|
||||||
|
import com.github.michaelbull.result.Err
|
||||||
|
import com.github.michaelbull.result.Ok
|
||||||
|
import com.github.michaelbull.result.Result
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class AsyncSuspendableBindingTest {
|
||||||
|
|
||||||
|
private sealed class BindingError {
|
||||||
|
object BindingErrorA : BindingError()
|
||||||
|
object BindingErrorB : BindingError()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun returnsOkIfAllBindsSuccessful() {
|
||||||
|
suspend fun provideX(): Result<Int, BindingError> {
|
||||||
|
delay(100)
|
||||||
|
return Ok(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun provideY(): Result<Int, BindingError> {
|
||||||
|
delay(100)
|
||||||
|
return Ok(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
val result = binding<Int, BindingError> {
|
||||||
|
val x = async { provideX().bind() }
|
||||||
|
val y = async { provideY().bind() }
|
||||||
|
x.await() + y.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(result is Ok)
|
||||||
|
assertEquals(
|
||||||
|
expected = 3,
|
||||||
|
actual = result.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun returnsFirstErrIfBindingFailed() {
|
||||||
|
suspend fun provideX(): Result<Int, BindingError> {
|
||||||
|
delay(1)
|
||||||
|
return Ok(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun provideY(): Result<Int, BindingError.BindingErrorA> {
|
||||||
|
delay(2)
|
||||||
|
return Err(BindingError.BindingErrorA)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun provideZ(): Result<Int, BindingError.BindingErrorB> {
|
||||||
|
delay(1)
|
||||||
|
return Err(BindingError.BindingErrorB)
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
val result = binding<Int, BindingError> {
|
||||||
|
val x = async { provideX().bind() }
|
||||||
|
val y = async { provideY().bind() }
|
||||||
|
val z = async { provideZ().bind() }
|
||||||
|
x.await() + y.await() + z.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(result is Err)
|
||||||
|
assertEquals(
|
||||||
|
expected = BindingError.BindingErrorB,
|
||||||
|
actual = result.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
75
kotlin-result/build.gradle.kts
Normal file
75
kotlin-result/build.gradle.kts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
description = "A Result monad for modelling success or failure operations."
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
`maven-publish`
|
||||||
|
kotlin("multiplatform")
|
||||||
|
id("org.jetbrains.kotlin.plugin.allopen")
|
||||||
|
id("kotlinx.benchmark")
|
||||||
|
}
|
||||||
|
|
||||||
|
allOpen {
|
||||||
|
annotation("org.openjdk.jmh.annotations.State")
|
||||||
|
annotation("org.openjdk.jmh.annotations.BenchmarkMode")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets.create("benchmark")
|
||||||
|
|
||||||
|
benchmark {
|
||||||
|
targets {
|
||||||
|
register("jvmBenchmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm {
|
||||||
|
withJava()
|
||||||
|
|
||||||
|
compilations.all {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
all {
|
||||||
|
languageSettings.apply {
|
||||||
|
useExperimentalAnnotation("kotlin.contracts.ExperimentalContracts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val commonMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("stdlib-common"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val commonTest by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test-common"))
|
||||||
|
implementation(kotlin("test-annotations-common"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val jvmMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val jvmTest by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(kotlin("test-junit"))
|
||||||
|
implementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val jvmBenchmark by getting {
|
||||||
|
dependsOn(jvmMain)
|
||||||
|
dependencies {
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx.benchmark.runtime-jvm:0.2.0-dev-8")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,12 @@ import kotlin.contracts.contract
|
|||||||
/**
|
/**
|
||||||
* Suspending variant of [binding][com.github.michaelbull.result.binding].
|
* Suspending variant of [binding][com.github.michaelbull.result.binding].
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
message = "Will throw a runtime exception if used with async requests that fail to bind. " +
|
||||||
|
"See https://github.com/michaelbull/kotlin-result/pull/28 " +
|
||||||
|
"Please import the kotlin-result-coroutines library to continue using this feature.",
|
||||||
|
level = DeprecationLevel.WARNING
|
||||||
|
)
|
||||||
suspend inline fun <V, E> binding(crossinline block: suspend ResultBinding<E>.() -> V): Result<V, E> {
|
suspend inline fun <V, E> binding(crossinline block: suspend ResultBinding<E>.() -> V): Result<V, E> {
|
||||||
contract {
|
contract {
|
||||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
@ -1,6 +1,10 @@
|
|||||||
rootProject.name = "kotlin-result"
|
rootProject.name = "kotlin-result"
|
||||||
|
|
||||||
include("example")
|
include(
|
||||||
|
"example",
|
||||||
|
"kotlin-result",
|
||||||
|
"kotlin-result-coroutines"
|
||||||
|
)
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
|
Loading…
Reference in New Issue
Block a user