From 5c05ed22adcfd740ab031d865c77d465b24f99a7 Mon Sep 17 00:00:00 2001 From: Giorgio Garofalo Date: Tue, 30 Jul 2024 13:04:48 +0200 Subject: [PATCH] Set up new argument binders --- .../function/call/FunctionArgumentsLinker.kt | 106 +----------------- .../call/binding/AllArgumentsBinder.kt | 33 ++++++ .../function/call/binding/ArgumentsBinder.kt | 25 +++++ .../call/binding/InjectedArgumentsBinder.kt | 19 ++++ .../call/binding/RegularArgumentsBinder.kt | 85 ++++++++++++++ 5 files changed, 164 insertions(+), 104 deletions(-) create mode 100644 core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/AllArgumentsBinder.kt create mode 100644 core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/ArgumentsBinder.kt create mode 100644 core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/InjectedArgumentsBinder.kt create mode 100644 core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/RegularArgumentsBinder.kt diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/FunctionArgumentsLinker.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/FunctionArgumentsLinker.kt index 501398d7..e7c48113 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/FunctionArgumentsLinker.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/FunctionArgumentsLinker.kt @@ -1,17 +1,7 @@ package eu.iamgio.quarkdown.function.call import eu.iamgio.quarkdown.function.FunctionParameter -import eu.iamgio.quarkdown.function.error.InvalidArgumentCountException -import eu.iamgio.quarkdown.function.error.MismatchingArgumentTypeException -import eu.iamgio.quarkdown.function.error.UnnamedArgumentAfterNamedException -import eu.iamgio.quarkdown.function.error.UnresolvedParameterException -import eu.iamgio.quarkdown.function.reflect.DynamicValueConverter -import eu.iamgio.quarkdown.function.reflect.Injected -import eu.iamgio.quarkdown.function.reflect.InjectedValue -import eu.iamgio.quarkdown.function.value.DynamicValue -import eu.iamgio.quarkdown.function.value.StringValue -import eu.iamgio.quarkdown.function.value.ValueFactory -import kotlin.reflect.full.isSubclassOf +import eu.iamgio.quarkdown.function.call.binding.AllArgumentsBinder /** * Parameter-argument pairs for a function call. @@ -25,105 +15,13 @@ private typealias Links = Map, FunctionCallArgument> class FunctionArgumentsLinker(private val call: FunctionCall<*>) { lateinit var links: Links - /** - * @param parameters regular (non-injected) parameters of the function - * @return the parameter-argument pairs for regular user-supplied arguments - */ - private fun generateRegularLinks(parameters: List>): Links = - buildMap { - var encounteredNamedArgument = false - - call.arguments.forEachIndexed { index, argument -> - // Corresponding parameter. - val parameter = - when { - // A body parameter is always the last one in the function signature. - argument.isBody -> parameters.lastOrNull() - // A non-body parameter that refers to a parameter by its name. - argument.isNamed -> { - encounteredNamedArgument = true - parameters.find { it.name == argument.name } - ?: throw UnresolvedParameterException(argument, call) - } - // Non-body, unnamed parameters follow the index and cannot appear after a named argument has been encountered. - !encounteredNamedArgument -> parameters.getOrNull(index) - // Unnamed arguments cannot appear after a named one. - else -> throw UnnamedArgumentAfterNamedException(call) - } ?: throw InvalidArgumentCountException(call) // Error if args count > params count. - - val value = argument.value - // The type of dynamic arguments is determined. - val staticArgument = - when { - // If the expected type is dynamic, the argument is wrapped into a dynamic value. - // For instance, custom functions defined from a Quarkdown function have dynamic-type parameters. - parameter.type == DynamicValue::class -> { - argument.copy(expression = DynamicValue(value.unwrappedValue)) - } - - // The value is dynamic and must be converted to a static type. - value is DynamicValue -> { - // The dynamic value is converted into the expected parameter type. - // Throws error if the conversion could not happen. - val staticValue = - DynamicValueConverter(value).convertTo(parameter.type, call.context) - ?: throw MismatchingArgumentTypeException(call, parameter, argument) - - argument.copy(expression = staticValue) - } - - // If the expected type is a string but the argument isn't, - // it is automatically converted to a string. - value !is StringValue && parameter.type == String::class -> { - argument.copy(expression = ValueFactory.string(value.unwrappedValue.toString())) - } - - else -> argument - } - - // Type match check. - if ( - !staticArgument.value.unwrappedValue!!::class.isSubclassOf(parameter.type) && - !staticArgument.value::class.isSubclassOf(parameter.type) - ) { - throw MismatchingArgumentTypeException(call, parameter, staticArgument) - } - - // Add link. - this[parameter] = staticArgument - } - } - - /** - * @param parameters injected parameters of the function - * @return the parameter-argument pairs for automatically injected values - * @see Injected - * @see FunctionParameter.isInjected - */ - private fun generateInjectedLinks(parameters: List>): Links = - parameters.associateWith { - val value = InjectedValue.fromType(it.type, call) - FunctionCallArgument(value) - } - /** * Stores the associations between [FunctionCallArgument]s and [FunctionParameter]s. * @throws eu.iamgio.quarkdown.function.error.InvalidFunctionCallException or subclass * if there is a mismatch between arguments and parameters */ fun link() { - // Injected and non-injected parameters are handled separately. - val (injected, regular) = call.function.parameters.partition { it.isInjected } - - // Argument-parameter links are generated for both types of parameters and joined together. - this.links = generateRegularLinks(regular) + generateInjectedLinks(injected) - - call.function.parameters.forEach { parameter -> - // If mandatory params count > args count. - if (!parameter.isOptional && parameter !in this.links) { - throw InvalidArgumentCountException(call) - } - } + this.links = AllArgumentsBinder(call).createBindings(call.function.parameters) } /** diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/AllArgumentsBinder.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/AllArgumentsBinder.kt new file mode 100644 index 00000000..85e562a7 --- /dev/null +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/AllArgumentsBinder.kt @@ -0,0 +1,33 @@ +package eu.iamgio.quarkdown.function.call.binding + +import eu.iamgio.quarkdown.function.FunctionParameter +import eu.iamgio.quarkdown.function.call.FunctionCall +import eu.iamgio.quarkdown.function.error.InvalidArgumentCountException + +/** + * Builder of bindings for all arguments of a function call. + * @param call function call to bind arguments for + * @see RegularArgumentsBinder + * @see InjectedArgumentsBinder + */ +class AllArgumentsBinder(private val call: FunctionCall<*>) : ArgumentsBinder { + /** + * Joins the results of the subsets of regular ([RegularArgumentsBinder]) + * and injected ([InjectedArgumentsBinder]) arguments. + */ + override fun createBindings(parameters: List>): ArgumentBindings { + val (injected, regular) = call.function.parameters.partition { it.isInjected } + + // Argument-parameter links are generated for both types of parameters and joined together. + val bindings = + RegularArgumentsBinder(call).createBindings(regular) + + InjectedArgumentsBinder(call).createBindings(injected) + + // If mandatory params count > args count. + if (call.function.parameters.any { !it.isOptional && it !in bindings }) { + throw InvalidArgumentCountException(call) + } + + return bindings + } +} diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/ArgumentsBinder.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/ArgumentsBinder.kt new file mode 100644 index 00000000..72b8b0dc --- /dev/null +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/ArgumentsBinder.kt @@ -0,0 +1,25 @@ +package eu.iamgio.quarkdown.function.call.binding + +import eu.iamgio.quarkdown.function.FunctionParameter +import eu.iamgio.quarkdown.function.call.FunctionCallArgument +import eu.iamgio.quarkdown.function.error.InvalidFunctionCallException + +/** + * Parameter-argument pairs of a function call. + */ +typealias ArgumentBindings = Map, FunctionCallArgument> + +/** + * Builder of parameter-argument pairs of a function call. + * @see InjectedArgumentsBinder + * @see InjectedArgumentsBinder + * @see AllArgumentsBinder + */ +sealed interface ArgumentsBinder { + /** + * @param parameters parameters of the called function (or a subset of them) + * @return the parameter-argument pairs + * @throws InvalidFunctionCallException or subclass if there is arguments and parameters cannot be paired + */ + fun createBindings(parameters: List>): ArgumentBindings +} diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/InjectedArgumentsBinder.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/InjectedArgumentsBinder.kt new file mode 100644 index 00000000..fcedc547 --- /dev/null +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/InjectedArgumentsBinder.kt @@ -0,0 +1,19 @@ +package eu.iamgio.quarkdown.function.call.binding + +import eu.iamgio.quarkdown.function.FunctionParameter +import eu.iamgio.quarkdown.function.call.FunctionCall +import eu.iamgio.quarkdown.function.call.FunctionCallArgument +import eu.iamgio.quarkdown.function.reflect.InjectedValue + +/** + * Builder of bindings for the injected argument subset of a function call. + * @param call function call to bind arguments for + * @see FunctionParameter.isInjected + */ +class InjectedArgumentsBinder(private val call: FunctionCall<*>) : ArgumentsBinder { + override fun createBindings(parameters: List>) = + parameters.associateWith { + val value = InjectedValue.fromType(it.type, call) + FunctionCallArgument(value) + } +} diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/RegularArgumentsBinder.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/RegularArgumentsBinder.kt new file mode 100644 index 00000000..91052307 --- /dev/null +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/call/binding/RegularArgumentsBinder.kt @@ -0,0 +1,85 @@ +package eu.iamgio.quarkdown.function.call.binding + +import eu.iamgio.quarkdown.function.FunctionParameter +import eu.iamgio.quarkdown.function.call.FunctionCall +import eu.iamgio.quarkdown.function.error.InvalidArgumentCountException +import eu.iamgio.quarkdown.function.error.MismatchingArgumentTypeException +import eu.iamgio.quarkdown.function.error.UnnamedArgumentAfterNamedException +import eu.iamgio.quarkdown.function.error.UnresolvedParameterException +import eu.iamgio.quarkdown.function.reflect.DynamicValueConverter +import eu.iamgio.quarkdown.function.value.DynamicValue +import eu.iamgio.quarkdown.function.value.StringValue +import eu.iamgio.quarkdown.function.value.ValueFactory +import kotlin.reflect.full.isSubclassOf + +/** + * Builder of bindings for the regular (not injected) argument subset of a function call. + * @param call function call to bind arguments for + * @see InjectedArgumentsBinder for the injected argument subset + */ +class RegularArgumentsBinder(private val call: FunctionCall<*>) : ArgumentsBinder { + override fun createBindings(parameters: List>) = + buildMap { + var encounteredNamedArgument = false + + call.arguments.forEachIndexed { index, argument -> + // Corresponding parameter. + val parameter = + when { + // A body parameter is always the last one in the function signature. + argument.isBody -> parameters.lastOrNull() + // A non-body parameter that refers to a parameter by its name. + argument.isNamed -> { + encounteredNamedArgument = true + parameters.find { it.name == argument.name } + ?: throw UnresolvedParameterException(argument, call) + } + // Non-body, unnamed parameters follow the index and cannot appear after a named argument has been encountered. + !encounteredNamedArgument -> parameters.getOrNull(index) + // Unnamed arguments cannot appear after a named one. + else -> throw UnnamedArgumentAfterNamedException(call) + } ?: throw InvalidArgumentCountException(call) // Error if args count > params count. + + val value = argument.value + // The type of dynamic arguments is determined. + val staticArgument = + when { + // If the expected type is dynamic, the argument is wrapped into a dynamic value. + // For instance, custom functions defined from a Quarkdown function have dynamic-type parameters. + parameter.type == DynamicValue::class -> { + argument.copy(expression = DynamicValue(value.unwrappedValue)) + } + + // The value is dynamic and must be converted to a static type. + value is DynamicValue -> { + // The dynamic value is converted into the expected parameter type. + // Throws error if the conversion could not happen. + val staticValue = + DynamicValueConverter(value).convertTo(parameter.type, call.context) + ?: throw MismatchingArgumentTypeException(call, parameter, argument) + + argument.copy(expression = staticValue) + } + + // If the expected type is a string but the argument isn't, + // it is automatically converted to a string. + value !is StringValue && parameter.type == String::class -> { + argument.copy(expression = ValueFactory.string(value.unwrappedValue.toString())) + } + + else -> argument + } + + // Type match check. + if ( + !staticArgument.value.unwrappedValue!!::class.isSubclassOf(parameter.type) && + !staticArgument.value::class.isSubclassOf(parameter.type) + ) { + throw MismatchingArgumentTypeException(call, parameter, staticArgument) + } + + // Add link. + this[parameter] = staticArgument + } + } +}