From 68c49e7ff367079e8ac3dd3d3568ecdf4af45d4e Mon Sep 17 00:00:00 2001 From: Giorgio Garofalo Date: Fri, 13 Sep 2024 15:14:32 +0200 Subject: [PATCH] Link source function to type conversion errors (improves error messages for invalid enum entry name) --- .../call/binding/RegularArgumentsBinder.kt | 11 ++++++++++- .../error/InvalidFunctionCallException.kt | 18 ++++++++++++------ ...nException.kt => NoSuchElementException.kt} | 2 +- .../function/reflect/DynamicValueConverter.kt | 6 ++++-- .../iamgio/quarkdown/StandaloneFunctionTest.kt | 5 +++-- 5 files changed, 30 insertions(+), 12 deletions(-) rename core/src/main/kotlin/eu/iamgio/quarkdown/function/error/{NoSuchElementFunctionException.kt => NoSuchElementException.kt} (88%) 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 index f472e6f3..5ebf41a3 100644 --- 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 @@ -4,6 +4,7 @@ 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.error.InvalidArgumentCountException +import eu.iamgio.quarkdown.function.error.InvalidFunctionCallException import eu.iamgio.quarkdown.function.error.MismatchingArgumentTypeException import eu.iamgio.quarkdown.function.error.UnnamedArgumentAfterNamedException import eu.iamgio.quarkdown.function.error.UnresolvedParameterException @@ -11,6 +12,7 @@ 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 eu.iamgio.quarkdown.pipeline.error.PipelineException import kotlin.reflect.full.isSubclassOf /** @@ -80,7 +82,14 @@ class RegularArgumentsBinder(private val call: FunctionCall<*>) : ArgumentsBinde // 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) + try { + DynamicValueConverter(value).convertTo(parameter.type, call.context) + } catch (e: PipelineException) { + // In case the conversion fails, the error is wrapped so that it can refer to this function call as a source. + throw InvalidFunctionCallException(call, e.message, includeArguments = false) + } + // convertTo returns null if the called ValueFactory method returns null. + // This means the supplied value cannot be converted to the expected type. ?: throw MismatchingArgumentTypeException(call, parameter, argument) argument.copy(expression = staticValue) diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/InvalidFunctionCallException.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/InvalidFunctionCallException.kt index 852dc006..f78b89b8 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/InvalidFunctionCallException.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/InvalidFunctionCallException.kt @@ -9,11 +9,17 @@ import eu.iamgio.quarkdown.function.call.asString * An exception thrown if a [FunctionCall] could not be executed. * @param call the invalid call * @param reason optional additional reason the call failed for + * @param includeArguments whether to include supplied function call arguments in the error message */ -open class InvalidFunctionCallException(val call: FunctionCall<*>, reason: String? = null) : +open class InvalidFunctionCallException( + val call: FunctionCall<*>, + reason: String? = null, + includeArguments: Boolean = true, +) : FunctionException( - "Cannot call function ${call.function.asString()} with arguments ${call.arguments.asString()}" + - (reason?.let { ": $it" } ?: ""), - code = BAD_FUNCTION_CALL_EXIT_CODE, - function = call.function, - ) + "Cannot call function ${call.function.asString()}" + + (" with arguments ${call.arguments.asString()}".takeIf { includeArguments } ?: "") + + (reason?.let { ": $it" } ?: ""), + code = BAD_FUNCTION_CALL_EXIT_CODE, + function = call.function, + ) diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/NoSuchElementFunctionException.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/NoSuchElementException.kt similarity index 88% rename from core/src/main/kotlin/eu/iamgio/quarkdown/function/error/NoSuchElementFunctionException.kt rename to core/src/main/kotlin/eu/iamgio/quarkdown/function/error/NoSuchElementException.kt index 2eeacd66..c6bbc609 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/NoSuchElementFunctionException.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/error/NoSuchElementException.kt @@ -8,7 +8,7 @@ import eu.iamgio.quarkdown.pipeline.error.PipelineException * Exception thrown when an element (e.g. an enum value from a Quarkdown function argument) * does not exist among elements of a look-up table. */ -class NoSuchElementFunctionException(element: Any, values: Iterable<*>) : +class NoSuchElementException(element: Any, values: Iterable<*>) : PipelineException("No such element '$element' among values $values", NO_SUCH_ELEMENT_EXIT_CODE) { constructor(element: Any, values: Array>) : this(element, values.map { it.quarkdownName }) } diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/DynamicValueConverter.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/DynamicValueConverter.kt index c58ae2a1..32e06886 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/DynamicValueConverter.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/reflect/DynamicValueConverter.kt @@ -2,7 +2,7 @@ package eu.iamgio.quarkdown.function.reflect import eu.iamgio.quarkdown.context.Context import eu.iamgio.quarkdown.function.call.FunctionCall -import eu.iamgio.quarkdown.function.error.NoSuchElementFunctionException +import eu.iamgio.quarkdown.function.error.NoSuchElementException import eu.iamgio.quarkdown.function.value.DynamicValue import eu.iamgio.quarkdown.function.value.InputValue import eu.iamgio.quarkdown.function.value.Value @@ -25,6 +25,8 @@ class DynamicValueConverter(private val value: DynamicValue) { * This type is unwrapped (e.g. if [type] is `String`, the output is of type `StringValue`) * @param context context to evaluate the value for * @return a new typed [InputValue], automatically determined from [type], or `null` if it could not be converted + * @throws IllegalArgumentException if the value could not be converted to the target type or if [context] is required and it's `null` + * @throws NoSuchElementException if the value could not be converted to an enum entry */ @Suppress("UNCHECKED_CAST") fun convertTo( @@ -46,7 +48,7 @@ class DynamicValueConverter(private val value: DynamicValue) { val values = valuesFunction.call() return ValueFactory.enum(raw.toString(), values) - ?: throw NoSuchElementFunctionException(element = raw, values) + ?: throw NoSuchElementException(element = raw, values) } // Gets ValueFactory methods annotated with @FromDynamicType(X::class), diff --git a/core/src/test/kotlin/eu/iamgio/quarkdown/StandaloneFunctionTest.kt b/core/src/test/kotlin/eu/iamgio/quarkdown/StandaloneFunctionTest.kt index ad1c48ea..d3cad436 100644 --- a/core/src/test/kotlin/eu/iamgio/quarkdown/StandaloneFunctionTest.kt +++ b/core/src/test/kotlin/eu/iamgio/quarkdown/StandaloneFunctionTest.kt @@ -11,8 +11,9 @@ import eu.iamgio.quarkdown.function.call.FunctionCall import eu.iamgio.quarkdown.function.call.FunctionCallArgument import eu.iamgio.quarkdown.function.call.binding.ArgumentBindings import eu.iamgio.quarkdown.function.error.InvalidArgumentCountException +import eu.iamgio.quarkdown.function.error.InvalidFunctionCallException import eu.iamgio.quarkdown.function.error.MismatchingArgumentTypeException -import eu.iamgio.quarkdown.function.error.NoSuchElementFunctionException +import eu.iamgio.quarkdown.function.error.NoSuchElementException import eu.iamgio.quarkdown.function.error.UnnamedArgumentAfterNamedException import eu.iamgio.quarkdown.function.error.UnresolvedParameterException import eu.iamgio.quarkdown.function.expression.ComposedExpression @@ -580,7 +581,7 @@ class StandaloneFunctionTest { ), ) - assertFailsWith { + assertFailsWith { call.execute() } }