diff --git a/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt b/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt index 8754cf1dc0d..4ab7e57728b 100644 --- a/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt +++ b/libraries/scripting/dependencies-maven/src/kotlin/script/experimental/dependencies/maven/MavenDependenciesResolver.kt @@ -55,6 +55,7 @@ class MavenDependenciesResolver : ExternalDependenciesResolver { override suspend fun resolve( artifactCoordinates: String, + options: ExternalDependenciesResolver.Options, sourceCodeLocation: SourceCode.LocationWithId? ): ResultWithDiagnostics> { @@ -76,6 +77,7 @@ class MavenDependenciesResolver : ExternalDependenciesResolver { override fun addRepository( repositoryCoordinates: RepositoryCoordinates, + options: ExternalDependenciesResolver.Options, sourceCodeLocation: SourceCode.LocationWithId? ): ResultWithDiagnostics { val url = repositoryCoordinates.toRepositoryUrlOrNull() diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/CompoundDependenciesResolver.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/CompoundDependenciesResolver.kt index a587e69e7c4..75443c11083 100644 --- a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/CompoundDependenciesResolver.kt +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/CompoundDependenciesResolver.kt @@ -26,6 +26,7 @@ class CompoundDependenciesResolver(private val resolvers: List { var success = false @@ -34,7 +35,7 @@ class CompoundDependenciesResolver(private val resolvers: List { success = true repositoryAdded = repositoryAdded || resolveResult.value @@ -57,13 +58,14 @@ class CompoundDependenciesResolver(private val resolvers: List> { val reports = mutableListOf() for (resolver in resolvers) { if (resolver.acceptsArtifact(artifactCoordinates)) { - when (val resolveResult = resolver.resolve(artifactCoordinates, sourceCodeLocation)) { + when (val resolveResult = resolver.resolve(artifactCoordinates, options, sourceCodeLocation)) { is ResultWithDiagnostics.Failure -> reports.addAll(resolveResult.reports) else -> return resolveResult } diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/ExternalDependenciesResolver.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/ExternalDependenciesResolver.kt index a5612abaa5a..a9c365a4c95 100644 --- a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/ExternalDependenciesResolver.kt +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/ExternalDependenciesResolver.kt @@ -6,24 +6,34 @@ package kotlin.script.experimental.dependencies import java.io.File -import kotlin.script.experimental.api.ResultWithDiagnostics -import kotlin.script.experimental.api.SourceCode -import kotlin.script.experimental.api.valueOrNull +import kotlin.script.experimental.api.* +import kotlin.script.experimental.dependencies.ExternalDependenciesResolver.Options open class RepositoryCoordinates(val string: String) interface ExternalDependenciesResolver { + interface Options { + object Empty : Options { + override fun value(name: String): String? = null + override fun flag(name: String): Boolean? = null + } + + fun value(name: String): String? + fun flag(name: String): Boolean? + } fun acceptsRepository(repositoryCoordinates: RepositoryCoordinates): Boolean fun acceptsArtifact(artifactCoordinates: String): Boolean suspend fun resolve( artifactCoordinates: String, + options: Options = Options.Empty, sourceCodeLocation: SourceCode.LocationWithId? = null ): ResultWithDiagnostics> fun addRepository( repositoryCoordinates: RepositoryCoordinates, + options: Options = Options.Empty, sourceCodeLocation: SourceCode.LocationWithId? = null ): ResultWithDiagnostics } @@ -33,5 +43,7 @@ fun ExternalDependenciesResolver.acceptsRepository(repositoryCoordinates: String fun ExternalDependenciesResolver.addRepository( repositoryCoordinates: String, + options: Options = Options.Empty, sourceCodeLocation: SourceCode.LocationWithId? = null -) = addRepository(RepositoryCoordinates(repositoryCoordinates), sourceCodeLocation) \ No newline at end of file +) = addRepository(RepositoryCoordinates(repositoryCoordinates), options, sourceCodeLocation) + diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/FileSystemDependenciesResolver.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/FileSystemDependenciesResolver.kt index 039333eb2a5..bfc2fdad729 100644 --- a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/FileSystemDependenciesResolver.kt +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/FileSystemDependenciesResolver.kt @@ -22,6 +22,7 @@ class FileSystemDependenciesResolver(vararg paths: File) : ExternalDependenciesR override fun addRepository( repositoryCoordinates: RepositoryCoordinates, + options: ExternalDependenciesResolver.Options, sourceCodeLocation: SourceCode.LocationWithId? ): ResultWithDiagnostics { if (!acceptsRepository(repositoryCoordinates)) return false.asSuccess() @@ -36,6 +37,7 @@ class FileSystemDependenciesResolver(vararg paths: File) : ExternalDependenciesR override suspend fun resolve( artifactCoordinates: String, + options: ExternalDependenciesResolver.Options, sourceCodeLocation: SourceCode.LocationWithId? ): ResultWithDiagnostics> { if (!acceptsArtifact(artifactCoordinates)) throw IllegalArgumentException("Path is invalid") diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/annotations.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/annotations.kt index 80aefd0387e..8fc6dd0bc22 100644 --- a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/annotations.kt +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/annotations.kt @@ -8,6 +8,7 @@ package kotlin.script.experimental.dependencies import java.io.File import kotlin.script.experimental.api.* import kotlin.script.experimental.util.filterByAnnotationType +import kotlin.script.experimental.dependencies.impl.SimpleExternalDependenciesResolverOptionsParser /** * A common annotation that could be used in a script to denote a dependency @@ -17,7 +18,7 @@ import kotlin.script.experimental.util.filterByAnnotationType @Target(AnnotationTarget.FILE) @Repeatable @Retention(AnnotationRetention.SOURCE) -annotation class DependsOn(vararg val artifactsCoordinates: String) +annotation class DependsOn(vararg val artifactsCoordinates: String, val options: Array = []) /** * A common annotation that could be used in a script to denote a repository for an ExternalDependenciesResolver @@ -27,7 +28,7 @@ annotation class DependsOn(vararg val artifactsCoordinates: String) @Target(AnnotationTarget.FILE) @Repeatable @Retention(AnnotationRetention.SOURCE) -annotation class Repository(vararg val repositoriesCoordinates: String) +annotation class Repository(vararg val repositoriesCoordinates: String, val options: Array = []) /** * An extension function that configures repositories and resolves artifacts denoted by the [Repository] and [DependsOn] annotations @@ -39,8 +40,11 @@ suspend fun ExternalDependenciesResolver.resolveFromScriptSourceAnnotations( annotations.forEach { (annotation, locationWithId) -> when (annotation) { is Repository -> { + val options = SimpleExternalDependenciesResolverOptionsParser(*annotation.options, locationWithId = locationWithId) + .valueOr { return it } + for (coordinates in annotation.repositoriesCoordinates) { - val added = addRepository(coordinates, locationWithId) + val added = addRepository(coordinates, options, locationWithId) .also { reports.addAll(it.reports) } .valueOr { return it } @@ -58,8 +62,13 @@ suspend fun ExternalDependenciesResolver.resolveFromScriptSourceAnnotations( return reports + annotations.filterByAnnotationType() .flatMapSuccess { (annotation, locationWithId) -> - annotation.artifactsCoordinates.asIterable().flatMapSuccess { artifactCoordinates -> - resolve(artifactCoordinates, locationWithId) + SimpleExternalDependenciesResolverOptionsParser( + *annotation.options, + locationWithId = locationWithId + ).onSuccess { options -> + annotation.artifactsCoordinates.asIterable().flatMapSuccess { artifactCoordinates -> + resolve(artifactCoordinates, options, locationWithId) + } } } } diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/SimpleExternalDependenciesResolverOptionsParser.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/SimpleExternalDependenciesResolverOptionsParser.kt new file mode 100644 index 00000000000..3e71eaf573c --- /dev/null +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/SimpleExternalDependenciesResolverOptionsParser.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.script.experimental.dependencies.impl + +import kotlin.script.experimental.api.* +import kotlin.script.experimental.dependencies.ExternalDependenciesResolver + +private val nameRegex = Regex("^[^\\S\\r\\n]*([a-zA-Z][a-zA-Z0-9-_]*)\\b") +private val valueRegex = Regex("^[^\\S\\r\\n]*([a-zA-Z0-9-_]+)\\b") +private val equalsRegex = Regex("^[^\\S\\r\\n]*=") + +/** + * Simple lightweight options parser for external dependency resolvers. + * + * This parser expects the input to be a series of equality statements: + * `foo=Foo bar=Bar` + * + * And additionally supports flags without any equality statement: + * `foo bar` + */ +internal object SimpleExternalDependenciesResolverOptionsParser { + private sealed class Token { + data class Name(val name: String) : Token() + data class Value(val value: String) : Token() + object Equals + } + + private class Scanner(text: String) { + private var consumed = "" + + var remaining = text + private set + + private fun take(regex: Regex) = regex + .find(remaining) + ?.also { match -> + consumed += match.value + remaining = remaining.removePrefix(match.value) + } + + fun takeName(): Token.Name? = take(nameRegex)?.let { Token.Name(it.groups[1]!!.value) } + fun takeValue(): Token.Value? = take(valueRegex)?.let { Token.Value(it.groups[1]!!.value) } + fun takeEquals(): Token.Equals? = take(equalsRegex)?.let { Token.Equals } + + fun hasFinished(): Boolean = remaining.isBlank() + } + + operator fun invoke( + vararg options: String, + locationWithId: SourceCode.LocationWithId? = null + ): ResultWithDiagnostics { + + val map = mutableMapOf() + + for (option in options) { + val scanner = Scanner(option) + + while (!scanner.hasFinished()) { + val name = scanner.takeName()?.name ?: return makeFailureResult( + "Failed to parse options from annotation. Expected a valid option name but received:\n${scanner.remaining}", + locationWithId + ) + + if (scanner.takeEquals() != null) { + // TODO: Consider supporting string literals + val value = scanner.takeValue()?.value ?: return makeFailureResult( + "Failed to parse options from annotation. Expected a valid option value but received:\n${scanner.remaining}", + locationWithId + ) + + map.tryToAddOption(name, value)?.let { return it } + } else { + map.tryToAddOption(name, "true")?.let { return it } + } + } + } + + return makeExternalDependenciesResolverOptions(map).asSuccess() + } +} + +private fun MutableMap.tryToAddOption( + key: K, + value: V, + locationWithId: SourceCode.LocationWithId? = null +): ResultWithDiagnostics.Failure? = when (val previousValue = this[key]) { + null, value -> { + this[key] = value + null + } + else -> makeFailureResult("Conflicting values for option $key: $previousValue and $value", locationWithId) +} \ No newline at end of file diff --git a/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/makeExternalDependenciesResolverOptions.kt b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/makeExternalDependenciesResolverOptions.kt new file mode 100644 index 00000000000..c930ebb8abc --- /dev/null +++ b/libraries/scripting/dependencies/src/kotlin/script/experimental/dependencies/impl/makeExternalDependenciesResolverOptions.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.script.experimental.dependencies.impl + +import java.io.File +import kotlin.script.experimental.api.ResultWithDiagnostics +import kotlin.script.experimental.api.SourceCode +import kotlin.script.experimental.dependencies.ExternalDependenciesResolver + +fun makeExternalDependenciesResolverOptions(map: Map): ExternalDependenciesResolver.Options = + MapExternalDependenciesResolverOptions(map) + +suspend fun ExternalDependenciesResolver.resolve( + artifactCoordinates: String, + options: Map, + sourceCodeLocation: SourceCode.LocationWithId? = null +): ResultWithDiagnostics> = resolve(artifactCoordinates, makeExternalDependenciesResolverOptions(options), sourceCodeLocation) + +private class MapExternalDependenciesResolverOptions( + private val map: Map +) : ExternalDependenciesResolver.Options { + + override fun value(name: String): String? = map[name] + + override fun flag(name: String): Boolean? = map[name]?.let { value -> + when (value.toLowerCase()) { + "1", "true", "t", "yes", "y" -> true + "0", "false", "f", "no", "n" -> false + else -> null + } + } +} \ No newline at end of file diff --git a/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolverOptionsTest.kt b/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolverOptionsTest.kt new file mode 100644 index 00000000000..9dd3ec3a953 --- /dev/null +++ b/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolverOptionsTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.script.experimental.test + +import junit.framework.TestCase +import kotlin.script.experimental.api.ResultWithDiagnostics +import kotlin.script.experimental.api.valueOrThrow +import kotlin.script.experimental.dependencies.impl.SimpleExternalDependenciesResolverOptionsParser +import kotlin.script.experimental.dependencies.impl.makeExternalDependenciesResolverOptions + +class ResolverOptionsTest : TestCase() { + fun testValueInMapAppearsIfPresent() { + val map = mapOf("option" to "value") + val options = makeExternalDependenciesResolverOptions(map) + + assertEquals(options.value("option"), "value") + } + + fun testFlagInMapAppearsIfPresent() { + val map = mapOf("option" to "true") + val options = makeExternalDependenciesResolverOptions(map) + + assertEquals(options.value("option"), "true") + assertEquals(options.flag("option"), true) + } + + fun testValueInMapDoesNotAppearsIfPresent() { + val options = makeExternalDependenciesResolverOptions(emptyMap()) + assertNull(options.value("option")) + } + + fun testFlagInMapDoesNotAppearsIfPresent() { + val options = makeExternalDependenciesResolverOptions(emptyMap()) + assertNull(options.flag("option")) + } + + fun testParserReturnsSingleValue() { + val parser = SimpleExternalDependenciesResolverOptionsParser + val options = parser("option1 = hello").valueOrThrow() + assertEquals(options.value("option1"), "hello") + } + + fun testParserReturnsMultipleValue() { + val parser = SimpleExternalDependenciesResolverOptionsParser + val options = parser("option1 = hello option2 = 42").valueOrThrow() + assertEquals(options.value("option1"), "hello") + assertEquals(options.value("option2"), "42") + } + + + fun testParserReturnsSingleFlag() { + val parser = SimpleExternalDependenciesResolverOptionsParser + val options = parser("option1 = hello").valueOrThrow() + assertEquals(options.value("option1"), "hello") + } + + fun testParserReturnsMultipleFlags() { + val parser = SimpleExternalDependenciesResolverOptionsParser + val options = parser("option1 option2=false option3").valueOrThrow() + assertEquals(options.flag("option1"), true) + assertEquals(options.flag("option2"), false) + assertEquals(options.flag("option3"), true) + } + + fun testParserReturnsMixOfValuesAndFlags() { + val parser = SimpleExternalDependenciesResolverOptionsParser + val options = parser("option1 = hello option2 option3=world option4 option5 = false").valueOrThrow() + assertEquals(options.value("option1"), "hello") + assertEquals(options.flag("option2"), true) + assertEquals(options.value("option3"), "world") + assertEquals(options.flag("option4"), true) + assertEquals(options.flag("option5"), false) + } + + fun testParserReportsClashWithConflictingOptions() { + val parser = SimpleExternalDependenciesResolverOptionsParser + when (val result = parser("option1 = hello option1 = world")) { + is ResultWithDiagnostics.Success -> fail("Managed to parse options despite conflicting options: ${result.value}") + is ResultWithDiagnostics.Failure -> { + assertEquals(result.reports.count(), 1) + assertEquals(result.reports.first().message, "Conflicting values for option option1: hello and world") + } + } + } + + fun testParserDoesNotClashWithTheSameOptionTwice() { + val parser = SimpleExternalDependenciesResolverOptionsParser + val options = parser("option1 = hello option1 = hello").valueOrThrow() + assertEquals(options.value("option1"), "hello") + } +} \ No newline at end of file diff --git a/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolversTest.kt b/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolversTest.kt index f19f3d74b5c..697625dd4cc 100644 --- a/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolversTest.kt +++ b/libraries/scripting/dependencies/test/kotlin/script/experimental/test/ResolversTest.kt @@ -87,6 +87,7 @@ class ResolversTest : ResolversTestBase() { override suspend fun resolve( artifactCoordinates: String, + options: ExternalDependenciesResolver.Options, sourceCodeLocation: SourceCode.LocationWithId? ): ResultWithDiagnostics> { if (!acceptsArtifact(artifactCoordinates)) throw Exception("Path is invalid") @@ -97,6 +98,7 @@ class ResolversTest : ResolversTestBase() { override fun addRepository( repositoryCoordinates: RepositoryCoordinates, + options: ExternalDependenciesResolver.Options, sourceCodeLocation: SourceCode.LocationWithId? ): ResultWithDiagnostics { if (!acceptsRepository(repositoryCoordinates)) return false.asSuccess() diff --git a/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/impl/ivy.kt b/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/impl/ivy.kt index f90477eadfc..ecf5d0e3d4d 100644 --- a/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/impl/ivy.kt +++ b/libraries/tools/kotlin-main-kts/src/org/jetbrains/kotlin/mainKts/impl/ivy.kt @@ -38,6 +38,7 @@ class IvyResolver : ExternalDependenciesResolver { override suspend fun resolve( artifactCoordinates: String, + options: ExternalDependenciesResolver.Options, sourceCodeLocation: SourceCode.LocationWithId? ): ResultWithDiagnostics> { @@ -135,6 +136,7 @@ class IvyResolver : ExternalDependenciesResolver { override fun addRepository( repositoryCoordinates: RepositoryCoordinates, + options: ExternalDependenciesResolver.Options, sourceCodeLocation: SourceCode.LocationWithId? ): ResultWithDiagnostics { val url = repositoryCoordinates.toRepositoryUrlOrNull()