Add options param to external dependencies resolver API

This commit is contained in:
Mathias Quintero
2020-06-03 13:59:07 +02:00
committed by Ilya Chernikov
parent 83087291df
commit e45e491718
10 changed files with 266 additions and 11 deletions
@@ -55,6 +55,7 @@ class MavenDependenciesResolver : ExternalDependenciesResolver {
override suspend fun resolve(
artifactCoordinates: String,
options: ExternalDependenciesResolver.Options,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<List<File>> {
@@ -76,6 +77,7 @@ class MavenDependenciesResolver : ExternalDependenciesResolver {
override fun addRepository(
repositoryCoordinates: RepositoryCoordinates,
options: ExternalDependenciesResolver.Options,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<Boolean> {
val url = repositoryCoordinates.toRepositoryUrlOrNull()
@@ -26,6 +26,7 @@ class CompoundDependenciesResolver(private val resolvers: List<ExternalDependenc
override fun addRepository(
repositoryCoordinates: RepositoryCoordinates,
options: ExternalDependenciesResolver.Options,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<Boolean> {
var success = false
@@ -34,7 +35,7 @@ class CompoundDependenciesResolver(private val resolvers: List<ExternalDependenc
for (resolver in resolvers) {
if (resolver.acceptsRepository(repositoryCoordinates)) {
when (val resolveResult = resolver.addRepository(repositoryCoordinates, sourceCodeLocation)) {
when (val resolveResult = resolver.addRepository(repositoryCoordinates, options, sourceCodeLocation)) {
is ResultWithDiagnostics.Success -> {
success = true
repositoryAdded = repositoryAdded || resolveResult.value
@@ -57,13 +58,14 @@ class CompoundDependenciesResolver(private val resolvers: List<ExternalDependenc
override suspend fun resolve(
artifactCoordinates: String,
options: ExternalDependenciesResolver.Options,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<List<File>> {
val reports = mutableListOf<ScriptDiagnostic>()
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
}
@@ -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<List<File>>
fun addRepository(
repositoryCoordinates: RepositoryCoordinates,
options: Options = Options.Empty,
sourceCodeLocation: SourceCode.LocationWithId? = null
): ResultWithDiagnostics<Boolean>
}
@@ -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)
) = addRepository(RepositoryCoordinates(repositoryCoordinates), options, sourceCodeLocation)
@@ -22,6 +22,7 @@ class FileSystemDependenciesResolver(vararg paths: File) : ExternalDependenciesR
override fun addRepository(
repositoryCoordinates: RepositoryCoordinates,
options: ExternalDependenciesResolver.Options,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<Boolean> {
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<List<File>> {
if (!acceptsArtifact(artifactCoordinates)) throw IllegalArgumentException("Path is invalid")
@@ -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<String> = [])
/**
* 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<String> = [])
/**
* 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<DependsOn>()
.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)
}
}
}
}
@@ -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<ExternalDependenciesResolver.Options> {
val map = mutableMapOf<String, String>()
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 <K, V> MutableMap<K, V>.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)
}
@@ -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<String, String>): ExternalDependenciesResolver.Options =
MapExternalDependenciesResolverOptions(map)
suspend fun ExternalDependenciesResolver.resolve(
artifactCoordinates: String,
options: Map<String, String>,
sourceCodeLocation: SourceCode.LocationWithId? = null
): ResultWithDiagnostics<List<File>> = resolve(artifactCoordinates, makeExternalDependenciesResolverOptions(options), sourceCodeLocation)
private class MapExternalDependenciesResolverOptions(
private val map: Map<String, String>
) : 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
}
}
}
@@ -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")
}
}
@@ -87,6 +87,7 @@ class ResolversTest : ResolversTestBase() {
override suspend fun resolve(
artifactCoordinates: String,
options: ExternalDependenciesResolver.Options,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<List<File>> {
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<Boolean> {
if (!acceptsRepository(repositoryCoordinates)) return false.asSuccess()
@@ -38,6 +38,7 @@ class IvyResolver : ExternalDependenciesResolver {
override suspend fun resolve(
artifactCoordinates: String,
options: ExternalDependenciesResolver.Options,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<List<File>> {
@@ -135,6 +136,7 @@ class IvyResolver : ExternalDependenciesResolver {
override fun addRepository(
repositoryCoordinates: RepositoryCoordinates,
options: ExternalDependenciesResolver.Options,
sourceCodeLocation: SourceCode.LocationWithId?
): ResultWithDiagnostics<Boolean> {
val url = repositoryCoordinates.toRepositoryUrlOrNull()