Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 40 additions & 22 deletions src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
Comment thread
ivarne marked this conversation as resolved.
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -80,19 +81,21 @@ internal static async Task<ExpressionValue> EvaluateExpression_internal(
ExpressionValue[]? positionalArguments = null
)
{
if (!expr.IsFunctionExpression)
if (expr.IsLiteralValue)
{
return expr.ValueUnion;
}

ValidateExpressionArgs(expr);
var args = new ExpressionValue[expr.Args.Count];
var args = new ExpressionValue[expr.Args.Length];
for (var i = 0; i < args.Length; i++)
{
args[i] = await EvaluateExpression_internal(state, expr.Args[i], context, positionalArguments);
}

ExpressionValue ret = expr.Function switch
{
//ExpressionFunction.LITERAL_VALUE => expr.ValueUnion, // Handled above
ExpressionFunction.dataModel => await DataModel(args, context, state),
ExpressionFunction.component => await Component(args, context, state),
ExpressionFunction.countDataElements => CountDataElements(args, state),
Expand Down Expand Up @@ -129,27 +132,31 @@ internal static async Task<ExpressionValue> EvaluateExpression_internal(
ExpressionFunction.argv => Argv(args, positionalArguments),
ExpressionFunction.gatewayAction => state.GetGatewayAction(),
ExpressionFunction.language => state.GetLanguage(),
_ => throw new ExpressionEvaluatorTypeErrorException("Function not implemented", expr.Function, args),
ExpressionFunction.INVALID => throw new ExpressionEvaluatorTypeErrorException(
$"Function {expr.Args.FirstOrDefault()} not implemented in backend {expr}"
),
_ => throw new UnreachableException($"Function {(int)expr.Function} not a valid enum value {expr}"),
};
return ret;
}

private static void ValidateExpressionArgs(Expression expr)
{
// Some functions have restrictions that arguments must be literal values and not subexpressions.
switch (expr)
{
case { Function: ExpressionFunction.dataModel, Args: [_, { IsFunctionExpression: true }] }:
case { Function: ExpressionFunction.dataModel, Args: [_, { IsLiteralValue: false }] }:
throw new ExpressionEvaluatorTypeErrorException(
"The data type must be a string (expressions cannot be used here)"
);
case { Function: ExpressionFunction.@if, Args: [_, _, { IsFunctionExpression: true }, _] }:
case { Function: ExpressionFunction.@if, Args: [_, _, { IsLiteralValue: false }, _] }:
throw new ExpressionEvaluatorTypeErrorException("Expected third argument to be \"else\"");
case { Function: ExpressionFunction.compare, Args: [_, { IsFunctionExpression: true }, _] }:
case { Function: ExpressionFunction.compare, Args: [_, _, { IsFunctionExpression: true }, _] }:
case { Function: ExpressionFunction.compare, Args: [_, { IsLiteralValue: false }, _] }:
case { Function: ExpressionFunction.compare, Args: [_, _, { IsLiteralValue: false }, _] }:
throw new ExpressionEvaluatorTypeErrorException(
"Invalid operator (it cannot be an expression or null)"
);
case { Function: ExpressionFunction.compare, Args: [_, { IsFunctionExpression: true }, _, _] }:
case { Function: ExpressionFunction.compare, Args: [_, { IsLiteralValue: false }, _, _] }:
throw new ExpressionEvaluatorTypeErrorException(
"Second argument must be \"not\" when providing 4 arguments in total"
);
Expand Down Expand Up @@ -333,7 +340,7 @@ private static bool Contains(ExpressionValue[] args)
date = TimeZoneInfo.ConvertTime(date.Value, timezone);
}

string? language = state.GetLanguage();
string language = state.GetLanguage();
return UnicodeDateTimeTokenConverter.Format(
date,
args.Length == 2 ? args[1].ToStringForEquals() : null,
Expand Down Expand Up @@ -653,12 +660,7 @@ private static string Round(ExpressionValue[] args)
);
}

var number = PrepareNumericArg(args[0]);

if (number is null)
{
number = 0;
}
var number = PrepareNumericArg(args[0]) ?? 0;

int precision = 0;

Expand All @@ -667,7 +669,7 @@ private static string Round(ExpressionValue[] args)
precision = (int)(PrepareNumericArg(args[1]) ?? 0);
}

return number.Value.ToString($"N{precision}", CultureInfo.InvariantCulture);
return number.ToString($"N{precision}", CultureInfo.InvariantCulture);
}

private static string? UpperCase(ExpressionValue[] args)
Expand Down Expand Up @@ -770,9 +772,17 @@ private static bool PrepareBooleanArg(ExpressionValue arg)
throw new ExpressionEvaluatorTypeErrorException("Expected 1+ argument(s), got 0");
}

var preparedArgs = args.Select(arg => PrepareBooleanArg(arg)).ToArray();
// Ensure all args gets converted, because they might throw an Exception
return preparedArgs.All(a => a);
var all = true;
foreach (var arg in args)
{
// the LINQ All() method would short-circuit and not evaluate all args, so we do it manually to ensure exceptions are thrown correctly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, but no need to keep this comment.

if (!PrepareBooleanArg(arg))
{
all = false;
}
}
Comment thread Fixed
Comment thread Dismissed

return all;
}

private static async Task<string?> Text(
Expand Down Expand Up @@ -804,9 +814,17 @@ ExpressionValue[] args
throw new ExpressionEvaluatorTypeErrorException("Expected 1+ argument(s), got 0");
}

var preparedArgs = args.Select(arg => PrepareBooleanArg(arg)).ToArray();
// Ensure all args gets converted, because they might throw an Exception
return preparedArgs.Any(a => a);
bool any = false;
foreach (var arg in args)
{
// the LINQ Any() method would short-circuit and not evaluate all args, so we do it manually to ensure exceptions are thrown correctly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, but no need to keep this comment.

if (PrepareBooleanArg(arg))
{
any = true;
}
}
Comment thread Fixed
Comment thread Dismissed

return any;
}

private static bool? Not(ExpressionValue[] args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ public async Task<DataReference> AddInidicies(ModelBinding binding, ComponentCon
$"Failed to add indexes to path {binding.Field} with indexes "
+ $"{(context.RowIndices is null ? "null" : string.Join(", ", context.RowIndices))} on {dataElementId}"
);
;

return new DataReference() { Field = field, DataElementIdentifier = dataElementId };
}

Expand Down Expand Up @@ -401,7 +401,7 @@ public async Task<string> TranslateText(string textKey, ComponentContext context

// private void GetModelErrorsForExpression(Expression expr, BaseComponent component, List<string> errors)
// {
// if (!expr.IsFunctionExpression)
// if (expr.IsLiteralValue)
// {
// return;
// }
Expand Down
6 changes: 3 additions & 3 deletions src/Altinn.App.Core/Models/Expressions/ComponentContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,9 @@ public DebuggerEvaluatedExpression(Expression expression, ComponentContext conte
public IEnumerable<DebuggerEvaluatedExpression>? Args =>
_expression.Args?.Select(e => new DebuggerEvaluatedExpression(e, _context));
public Task<ExpressionValue> EvaluationResult =>
_expression.IsFunctionExpression
? ExpressionEvaluator.EvaluateExpression_internal(_context.State, _expression, _context, null)
: Task.FromResult(_expression.ValueUnion);
_expression.IsLiteralValue
? Task.FromResult(_expression.ValueUnion)
: ExpressionEvaluator.EvaluateExpression_internal(_context.State, _expression, _context, null);

public override string ToString()
{
Expand Down
74 changes: 45 additions & 29 deletions src/Altinn.App.Core/Models/Expressions/Expression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,60 +15,75 @@ namespace Altinn.App.Core.Models.Expressions;
public readonly struct Expression : IEquatable<Expression>
{
/// <summary>
/// Construct a value expression with the given value
/// Construct a value expression with the given literal value
/// </summary>
/// <param name="value"></param>
public Expression(ExpressionValue value)
{
Function = ExpressionFunction.LITERAL_VALUE;
Args = null;
ValueUnion = value;
}

/// <summary>
/// Construct a value expression with the given value
/// Construct a function expression with the given function and arguments
/// </summary>
/// <param name="value"></param>
[Obsolete("Use the constructor with ExpressionValue instead")]
public Expression(object? value)
public Expression(ExpressionFunction function, Expression[] args)
{
ValueUnion = ExpressionValue.FromObject(value);
if (function == ExpressionFunction.LITERAL_VALUE)
{
throw new ArgumentException("Function LITERAL_VALUE cannot have arguments", nameof(function));
}
if (args == null)
{
throw new ArgumentNullException(
nameof(args),
"Args cannot be null when constructing a function expression"
);
}
Function = function;
Args = args;
}

/// <summary>
/// Construct a function expression with the given function and arguments
/// </summary>
public Expression(ExpressionFunction function, List<Expression>? args)
{
Function = function;
Args = args;
}
[Obsolete("Use the constructor with Expression[] instead")]
public Expression(ExpressionFunction function, List<Expression> args)
: this(function, args.ToArray()) { }

/// <summary>
/// Construct a value expression with the given value
/// </summary>
/// <param name="value"></param>
[Obsolete("Use the constructor with ExpressionValue instead")]
public Expression(object? value)
: this(ExpressionValue.FromObject(value)) { }

/// <summary>
/// Construct a function expression with the given function and arguments
/// </summary>
public Expression(ExpressionFunction function, Expression arg1)
{
Function = function;
Args = [arg1];
}
: this(function, new[] { arg1 }) { }

/// <summary>
/// Construct a function expression with the given function and arguments
/// </summary>
public Expression(ExpressionFunction function, Expression arg1, Expression arg2)
{
Function = function;
Args = [arg1, arg2];
}
: this(function, new[] { arg1, arg2 }) { }

/// <summary>
/// Test function to see if this is representing a function with args.
/// </summary>
[MemberNotNullWhen(true, nameof(Function), nameof(Args))]
#pragma warning disable CS0618 // Type or member is obsolete
[MemberNotNullWhen(false, nameof(Value))]
#pragma warning restore CS0618 // Type or member is obsolete
[MemberNotNullWhen(false, nameof(ValueUnion))]
public bool IsFunctionExpression => Function != ExpressionFunction.INVALID && Args != null;
[MemberNotNullWhen(true, nameof(Args))]
[Obsolete("Use !IsLiteralValue instead")]
public bool IsFunctionExpression => !IsLiteralValue;

/// <summary>
/// Test to see if this expression must be evaluated or is just a literal value.
/// </summary>
[MemberNotNullWhen(false, nameof(Args))]
public bool IsLiteralValue => Args == null;

/// <summary>
/// Name of the function. Must be one those actually implemented in <see cref="ExpressionEvaluator" />
Expand All @@ -78,7 +93,7 @@ public Expression(ExpressionFunction function, Expression arg1, Expression arg2)
/// <summary>
/// List of arguments to the function. These expressions will be evaluated before passed to the function.
/// </summary>
public List<Expression>? Args { get; }
public Expression[]? Args { get; }

/// <summary>
/// Get the object value for backwards compatibility
Expand Down Expand Up @@ -115,7 +130,8 @@ public Expression(ExpressionFunction function, Expression arg1, Expression arg2)
public bool IsLiteralString => ValueUnion.ValueKind == JsonValueKind.String;

/// <summary>
/// Overridden for better debugging experience
/// The custom <see cref="ExpressionConverter"/> is a <see cref="JsonConverter{T}"/>
/// that serializes the expression to JSON (array for function or literal value).
/// </summary>
public override string ToString()
{
Expand All @@ -131,7 +147,7 @@ public bool Equals(Expression other)
// return false;
//
// // For function expressions, compare arguments
// if (IsFunctionExpression)
// if (!IsLiteralValue)
// {
// if (other.Args == null || Args.Count != other.Args.Count)
// return false;
Expand All @@ -153,7 +169,7 @@ public override bool Equals(object? obj)
public override int GetHashCode()
{
throw new NotImplementedException();
// if (IsFunctionExpression)
// if (!IsLiteralValue)
// {
// var hash = Function.GetHashCode();
// if (Args != null)
Expand Down
Loading
Loading