Skip to content

Commit

Permalink
Set up new argument binders
Browse files Browse the repository at this point in the history
  • Loading branch information
iamgio committed Jul 30, 2024
1 parent 6902153 commit 5c05ed2
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 104 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -25,105 +15,13 @@ private typealias Links = Map<FunctionParameter<*>, 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<FunctionParameter<*>>): 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<FunctionParameter<*>>): 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)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FunctionParameter<*>>): 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
}
}
Original file line number Diff line number Diff line change
@@ -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<FunctionParameter<*>, 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<FunctionParameter<*>>): ArgumentBindings
}
Original file line number Diff line number Diff line change
@@ -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<FunctionParameter<*>>) =
parameters.associateWith {
val value = InjectedValue.fromType(it.type, call)
FunctionCallArgument(value)
}
}
Original file line number Diff line number Diff line change
@@ -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<FunctionParameter<*>>) =
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
}
}
}

0 comments on commit 5c05ed2

Please sign in to comment.