Introduce version DSL

This commit is contained in:
Him188 2020-09-12 22:34:15 +08:00
parent 807df3bbcd
commit fd23a03618
5 changed files with 303 additions and 34 deletions

View File

@ -60,6 +60,7 @@ import kotlin.coroutines.CoroutineContext
/**
* [MiraiConsole] 公开 API 与前端实现的连接桥.
*/
@Suppress("SpellCheckingInspection")
internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleImplementation,
MiraiConsole {
override val pluginCenter: PluginCenter get() = throw UnsupportedOperationException("PluginCenter is not supported yet")
@ -219,11 +220,12 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI
mainLogger.info { "mirai-console started successfully." }
}
@Suppress("SpellCheckingInspection")
@Retention(AnnotationRetention.SOURCE)
@DslMarker
private annotation class MiraiIsCool
private annotation class ILoveOmaeKumikoForever
@MiraiIsCool
@ILoveOmaeKumikoForever
private inline fun phase(block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)

View File

@ -187,7 +187,15 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
private fun <D : PluginDescription> List<D>.sortByDependencies(): List<D> {
val resolved = ArrayList<D>(this.size)
fun D.canBeLoad(): Boolean = this.dependencies.all { it.isOptional || it in resolved }
fun D.canBeLoad(): Boolean = this.dependencies.all { dependency ->
val target = resolved.findDependency(dependency)
if (target == null) {
dependency.isOptional
} else {
target.checkSatisfies(dependency, this@canBeLoad)
true
}
}
fun List<D>.consumeLoadable(): List<D> {
val (canBeLoad, cannotBeLoad) = this.partition { it.canBeLoad() }
@ -196,7 +204,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
}
fun Collection<PluginDependency>.filterIsMissing(): List<PluginDependency> =
this.filterNot { it.isOptional || it in resolved }
this.filterNot { it.isOptional || resolved.findDependency(it) != null }
fun List<D>.doSort() {
if (this.isEmpty()) return
@ -206,8 +214,7 @@ internal object PluginManagerImpl : PluginManager, CoroutineScope by MiraiConsol
check(resultPlugins.size < beforeSize) {
throw PluginMissingDependencyException(resultPlugins.joinToString("\n") { badPlugin ->
"Cannot load plugin ${badPlugin.name}, missing dependencies: ${
badPlugin.dependencies.filterIsMissing()
.joinToString()
badPlugin.dependencies.filterIsMissing().joinToString()
}"
})
}
@ -235,5 +242,13 @@ internal fun PluginDescription.wrapWith(loader: PluginLoader<*, *>, plugin: Plug
loader as PluginLoader<Plugin, PluginDescription>, this, plugin
)
internal operator fun List<PluginDescription>.contains(dependency: PluginDependency): Boolean =
any { it.id == dependency.id }
internal fun List<PluginDescription>.findDependency(dependency: PluginDependency): PluginDescription? {
return find { it.id.equals(dependency.id, ignoreCase = true) }
}
internal fun PluginDescription.checkSatisfies(dependency: PluginDependency, plugin: PluginDescription) {
val requirement = dependency.versionRequirement
if (requirement != null && this.version !in requirement) {
throw PluginLoadException("Plugin '${plugin.id}' ('${plugin.id}') requires '${dependency.id}' with version $requirement while the resolved is ${this.version}")
}
}

View File

@ -11,8 +11,6 @@
package net.mamoe.mirai.console.plugin.description
import com.vdurmont.semver4j.Semver
/**
* 插件的一个依赖的信息.
*
@ -28,13 +26,14 @@ public data class PluginDependency @JvmOverloads constructor(
*
* 版本遵循 [语义化版本 2.0 规范](https://semver.org/lang/zh-CN/),
*
* 允许 [Apache Ivy 风格版本号表示](http://ant.apache.org/ivy/history/latest-milestone/settings/version-matchers.html)
* ### 示例
* `Requirement.buildIvy("[1.0, 2.0)")`
*/
public val version: Semver? = null,
public val versionRequirement: VersionRequirement? = null,
/**
* 若为 `false`, 插件在找不到此依赖时也能正常加载.
*/
public val isOptional: Boolean = false
public val isOptional: Boolean = false,
) {
init {
kotlin.runCatching {
@ -50,13 +49,4 @@ public data class PluginDependency @JvmOverloads constructor(
public constructor(name: String, isOptional: Boolean = false) : this(
name, null, isOptional
)
/**
* @see PluginDependency
*/
public constructor(name: String, version: String, isOptional: Boolean) : this(
name,
Semver(version, Semver.SemverType.IVY),
isOptional
)
}

View File

@ -0,0 +1,210 @@
package net.mamoe.mirai.console.plugin.description
import com.vdurmont.semver4j.Requirement
import com.vdurmont.semver4j.Semver
public sealed class VersionRequirement {
public abstract operator fun contains(version: Semver): Boolean
public fun contains(version: String): Boolean = contains(Semver(version, Semver.SemverType.LOOSE))
public data class Exact(
val version: Semver,
) : VersionRequirement() {
public constructor(version: String) : this(Semver(version, Semver.SemverType.LOOSE))
override fun contains(version: Semver): Boolean = this.version.isEqualTo(version)
}
public data class MatchesNpmPattern(
val pattern: String,
) : VersionRequirement() {
private val requirement = Requirement.buildNPM(pattern)
override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version)
}
public data class MatchesIvyPattern(
val pattern: String,
) : VersionRequirement() {
private val requirement = Requirement.buildIvy(pattern)
override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version)
}
public data class MatchesCocoapodsPattern(
val pattern: String,
) : VersionRequirement() {
private val requirement = Requirement.buildCocoapods(pattern)
override fun contains(version: Semver): Boolean = requirement.isSatisfiedBy(version)
}
public abstract class Custom : VersionRequirement()
public data class InRange(
val begin: Semver,
val beginInclusive: Boolean,
val end: Semver,
val endInclusive: Boolean,
) : VersionRequirement() {
public constructor(
begin: String,
beginInclusive: Boolean,
end: Semver,
endInclusive: Boolean,
) : this(Semver(begin, Semver.SemverType.LOOSE), beginInclusive, end, endInclusive)
public constructor(
begin: String,
beginInclusive: Boolean,
end: String,
endInclusive: Boolean,
) : this(Semver(begin, Semver.SemverType.LOOSE),
beginInclusive,
Semver(end, Semver.SemverType.LOOSE),
endInclusive)
public constructor(
begin: Semver,
beginInclusive: Boolean,
end: String,
endInclusive: Boolean,
) : this(begin, beginInclusive, Semver(end, Semver.SemverType.LOOSE), endInclusive)
override fun contains(version: Semver): Boolean {
return if (beginInclusive) {
version.isGreaterThanOrEqualTo(begin)
} else {
version.isGreaterThan(begin)
} && if (endInclusive) {
version.isLowerThanOrEqualTo(begin)
} else {
version.isLowerThan(begin)
}
}
}
@Suppress("unused")
public class Builder {
@ILoveKafuuChinoForever
public fun exact(version: Semver): VersionRequirement = Exact(version)
@ILoveKafuuChinoForever
public fun exact(version: String): VersionRequirement = Exact(version)
@ILoveKafuuChinoForever
public fun custom(checker: (version: Semver) -> Boolean): VersionRequirement {
return object : Custom() {
override fun contains(version: Semver): Boolean = checker(version)
}
}
/**
* @see Semver.SemverType.NPM
*/
@ILoveKafuuChinoForever
public fun npmPattern(versionPattern: String): VersionRequirement {
return MatchesNpmPattern(versionPattern)
}
/**
* @see Semver.SemverType.IVY
*/
@ILoveKafuuChinoForever
public fun ivyPattern(versionPattern: String): VersionRequirement {
return MatchesIvyPattern(versionPattern)
}
/**
* @see Semver.SemverType.COCOAPODS
*/
@ILoveKafuuChinoForever
public fun cocoapodsPattern(versionPattern: String): VersionRequirement {
return MatchesCocoapodsPattern(versionPattern)
}
@ILoveKafuuChinoForever
public fun range(
begin: Semver,
beginInclusive: Boolean,
end: Semver,
endInclusive: Boolean,
): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive)
@ILoveKafuuChinoForever
public fun range(
begin: String,
beginInclusive: Boolean,
end: Semver,
endInclusive: Boolean,
): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive)
@ILoveKafuuChinoForever
public fun range(
begin: Semver,
beginInclusive: Boolean,
end: String,
endInclusive: Boolean,
): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive)
@ILoveKafuuChinoForever
public fun range(
begin: String,
beginInclusive: Boolean,
end: String,
endInclusive: Boolean,
): VersionRequirement = InRange(begin, beginInclusive, end, endInclusive)
@ILoveKafuuChinoForever
public operator fun Semver.rangeTo(endInclusive: Semver): VersionRequirement {
return InRange(this, true, endInclusive, true)
}
@ILoveKafuuChinoForever
public operator fun Semver.rangeTo(endInclusive: String): VersionRequirement {
return InRange(this, true, Semver(endInclusive, Semver.SemverType.LOOSE), true)
}
@ILoveKafuuChinoForever
public operator fun String.rangeTo(endInclusive: String): VersionRequirement {
return InRange(Semver(this, Semver.SemverType.LOOSE),
true,
Semver(endInclusive, Semver.SemverType.LOOSE),
true)
}
@ILoveKafuuChinoForever
public operator fun String.rangeTo(endInclusive: Semver): VersionRequirement {
return InRange(Semver(this, Semver.SemverType.LOOSE), true, endInclusive, true)
}
@ILoveKafuuChinoForever
public infix fun Semver.until(endExclusive: Semver): VersionRequirement {
return InRange(this, true, endExclusive, false)
}
@ILoveKafuuChinoForever
public infix fun Semver.until(endExclusive: String): VersionRequirement {
return InRange(this, true, Semver(endExclusive, Semver.SemverType.LOOSE), false)
}
@ILoveKafuuChinoForever
public infix fun String.until(endExclusive: String): VersionRequirement {
return InRange(Semver(this, Semver.SemverType.LOOSE),
true,
Semver(endExclusive, Semver.SemverType.LOOSE),
false)
}
@ILoveKafuuChinoForever
public infix fun String.until(endExclusive: Semver): VersionRequirement {
return InRange(Semver(this, Semver.SemverType.LOOSE), true, endExclusive, false)
}
@Suppress("SpellCheckingInspection")
@Retention(AnnotationRetention.SOURCE)
@DslMarker
private annotation class ILoveKafuuChinoForever
}
}

View File

@ -14,6 +14,7 @@ package net.mamoe.mirai.console.plugin.jvm
import com.vdurmont.semver4j.Semver
import net.mamoe.mirai.console.plugin.description.PluginDependency
import net.mamoe.mirai.console.plugin.description.PluginDescription
import net.mamoe.mirai.console.plugin.description.VersionRequirement
/**
* JVM 插件的描述. 通常作为 `plugin.yml`
@ -122,16 +123,6 @@ public class JvmPluginDescriptionBuilder(
@ILoveKuriyamaMiraiForever
public fun info(value: String): JvmPluginDescriptionBuilder = apply { this.info = value.trimIndent() }
@ILoveKuriyamaMiraiForever
public fun dependsOn(
pluginId: String,
version: String? = null,
isOptional: Boolean = false,
): JvmPluginDescriptionBuilder = apply {
if (version == null) this.dependencies.add(PluginDependency(pluginId, version, isOptional))
else this.dependencies.add(PluginDependency(pluginId, version, isOptional))
}
@ILoveKuriyamaMiraiForever
public fun setDependencies(
value: Set<PluginDependency>,
@ -148,18 +139,79 @@ public class JvmPluginDescriptionBuilder(
}
}
/**
* @see PluginDependency
*/
@ILoveKuriyamaMiraiForever
public fun dependsOn(
pluginId: String,
version: Semver? = null,
isOptional: Boolean = false,
): JvmPluginDescriptionBuilder = apply { this.dependencies.add(PluginDependency(pluginId, version, isOptional)) }
versionRequirement: VersionRequirement? = null,
): JvmPluginDescriptionBuilder = apply {
if (versionRequirement == null)
this.dependencies.add(PluginDependency(pluginId, versionRequirement, isOptional))
else this.dependencies.add(PluginDependency(pluginId, versionRequirement, isOptional))
}
/**
* isOptional = false
*
* @see PluginDependency
*/
@ILoveKuriyamaMiraiForever
public fun dependsOn(
pluginId: String,
versionRequirement: VersionRequirement? = null,
): JvmPluginDescriptionBuilder = apply {
if (versionRequirement == null)
this.dependencies.add(PluginDependency(pluginId, versionRequirement, false))
else this.dependencies.add(PluginDependency(pluginId, versionRequirement, false))
}
/**
* 无版本要求
*
* @see PluginDependency
*/
@ILoveKuriyamaMiraiForever
public fun dependsOn(
pluginId: String,
isOptional: Boolean = false,
): JvmPluginDescriptionBuilder = apply {
dependsOn(pluginId, isOptional, null)
}
/**
* 示例:
*
* ```
* dependsOn("org.example.test-plugin") { "1.0.0".."1.2.0" }
* dependsOn("org.example.test-plugin") { npmPattern("1.x || >=2.5.0 || 5.0.0 - 7.2.3") }
* dependsOn("org.example.test-plugin") { ivyPattern("[1.0,2.0[") }
* dependsOn("org.example.test-plugin") { custom { it.toString() == "1.0.0" } }
* ```
*
* @see PluginDependency
* @see VersionRequirement.Builder
*/
@ILoveKuriyamaMiraiForever
public fun dependsOn(
pluginId: String,
isOptional: Boolean = false,
versionRequirement: VersionRequirement.Builder.() -> VersionRequirement,
): JvmPluginDescriptionBuilder =
apply {
this.dependencies.add(PluginDependency(pluginId,
VersionRequirement.Builder().run(versionRequirement),
isOptional))
}
@Suppress("DEPRECATION_ERROR")
public fun build(): JvmPluginDescription =
SimpleJvmPluginDescription(name, version, id, author, info, dependencies)
@Suppress("SpellCheckingInspection")
@Retention(AnnotationRetention.SOURCE)
@DslMarker
private annotation class ILoveKuriyamaMiraiForever // https://zh.moegirl.org.cn/zh-cn/%E6%A0%97%E5%B1%B1%E6%9C%AA%E6%9D%A5