Ответ Рене дал мне преимущество, и, наконец, я пришел с этим решением.В этом решении я учел все мои требования (те, которые я пропустил в исходном вопросе), так что это стало намного сложнее, чем требовалось в моем первоначальном вопросе.значения или сценарии (фрагменты), работающие в хорошо защищенном контексте.Эти сценарии будут сохранены и выполнены позже.Я хотел включить всю мощь среды IDE при написании сценария, но хотел бы защитить свои сценарии от внедрения кода и помочь пользователю использовать только те значения контекста, которые требуются сценарию.
Уловка, которую я использовал для достижения этой цели, заключается в том, чтобы включить добавление сценария в kotlin, но перед тем, как запустить весь сценарий DSL и создать бизнес-объекты, я преобразую сценарий в строку.(Эта строка будет выполнена позже в защищенном, свернутом контексте с помощью механизма JSR233.) Этот разговор заставил меня токенизировать весь сценарий перед выполнением и найти / заменить некоторые из токенов.(Весь токенизатор и конвертер довольно длинный и скучный, поэтому я не буду здесь вставлять.)
Первый подход
Какова была моя цель - написать что-нибудь из этого:
myobject {
value = static { 42 } // A static solution
value = static { 6 * 7 } // Even this is possible
value = dynamic{ calc(x, y) } // A pure cotlin solution with IDE support
value = dynamic("""calc(x * x)""") // This is the form I convert the above script to
}
где calc
, x
и y
определены в контекстном классе:
class SpecialScriptContext : ScriptContextBase() {
val hello = "Hello"
val x = 29
val y = 13
fun calc(x: Int, y: Int) = x + y
fun greet(name: String) = println("$hello $name!")
}
Итак, давайте посмотрим на решение!Сначала мне нужен класс DynamicValue
для хранения одного из значений:
class DynamicValue<T, C : ScriptContextBase, D: ScriptContextDescriptor<C>>
private constructor(val directValue: T?, val script: String?) {
constructor(value: T?) : this(value, null)
constructor(script: String) : this(null, script)
}
Эта структура гарантирует, что будет установлен только один из параметров (статический, скрипт).(Не беспокойтесь о параметрах типа C и D, они предназначены для поддержки контекстных сценариев.)
Затем я создал функции DSL верхнего уровня для поддержки синтаксиса:
@PlsDsl
fun <T, C : ScriptContextBase, D : ScriptContextDescriptor<C>> static(block: () -> T): DynamicValue<T, C, D>
= DynamicValue<T, C, D>(value = block.invoke())
@PlsDsl
fun <T, C : ScriptContextBase, D : ScriptContextDescriptor<C>> dynamic(s: String): DynamicValue<T, C, D>
= DynamicValue<T, C, D>(script = s)
@PlsDsl
fun <T, C : ScriptContextBase, D : ScriptContextDescriptor<C>> dynamic(block: C.() -> T): DynamicValue<T, C, D> {
throw IllegalStateException("Can't use this format")
}
Anобъяснение к третьей форме.Как я уже писал ранее, я не хочу выполнять блок функции.Когда скрипт выполняется, эта форма преобразуется в строковую форму, поэтому обычно эта функция никогда не появляется в скрипте при выполнении.Исключением является предупреждение о нарушении работоспособности, которое никогда не будет выдано.
Наконец добавлено поле в построитель моих бизнес-объектов:
@PlsDsl
class MyObjectBuilder {
var value: DynamicValue<Int, SpecialScriptContext, SpecialScriptContextDescriptor>? = null
}
Второй подход
Предыдущее решение сработалоно имели некоторые недостатки: выражение не было связано ни с переменной, которую оно установило, ни с сущностью, в которой было установлено значение. С моим вторым подходом я решил эту проблему и удалил необходимость знака равенства и большую часть ненужных фигурных скобок.
Что помогло: функции расширения, инфиксные функции и закрытые классы.
Сначала я разделил два типа значений на отдельные классы, определяющие общего предка:
sealed class Value<T, C : ScriptContextBase> {
abstract val scriptExecutor: ScriptExecutor
abstract val descriptor: ScriptContextDescriptor<C>
abstract val code: String
abstract fun get(context: C): T?
}
class StaticValue<T, C : ScriptContextBase>(override val code: String,
override val scriptExecutor: ScriptExecutor,
override val descriptor: ScriptContextDescriptor<C>,
val value: T? = null
) : Value<T, C>() {
override fun get(context: C) = value
constructor(oldValue: Value<T, C>, value: T?) : this(oldValue.code, oldValue.scriptExecutor, oldValue.descriptor, value)
}
class DynamicValue<T, C : ScriptContextBase>(override val code: String,
script: String,
override val scriptExecutor: ScriptExecutor,
override val descriptor: ScriptContextDescriptor<C>)
: Value<T, C>() {
constructor(oldValue: Value<T, C>, script: String) : this(oldValue.code, script, oldValue.scriptExecutor, oldValue.descriptor)
private val scriptCache = scriptExecutor.register(descriptor)
val source = script?.replace("\\\"\\\"\\\"", "\"\"\"")
private val compiledScript = scriptCache.register(generateUniqueId(code), source)
override fun get(context: C): T? = compiledScript.execute<T?>(context)
}
Примечание, что я сделал основной конструктор внутренним и создал своего рода конструктор копирования и изменения.Затем я определил новые функции как расширение общего предка и пометил их как инфикс:
infix fun <T, C : ScriptContextBase> Value<T, C>.static(value: T?): Value<T, C> = StaticValue(this, value)
infix fun <T, C : ScriptContextBase> Value<T, C>.expr(script: String): Value<T, C> = DynamicValue(this, script)
infix fun <T, C : ScriptContextBase> Value<T, C>.dynamic(block: C.() -> T): Value<T, C> {
throw IllegalStateException("Can't use this format")
}
Использование вторичного конструктора copy-and-alter позволяет наследовать контекстно-зависимые значения.Наконец, я инициализирую значение внутри построителя DSL:
@PlsDsl
class MyDslBuilder {
var value: Value<Int, SpecialScriptContext> = StaticValue("pl.value", scriptExecutor, SpecialScriptContextDescriptor)
var value2: Value<Int, SpecialScriptContext> = StaticValue("pl.value2", scriptExecutor, SpecialScriptContextDescriptor)
}
Все на месте, и теперь я могу использовать его в своем скрипте:
myobject {
value static 42
value2 expr "6 * 7"
value2 dynamic { calc(x, y) }
}