Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ internal static async Task<ExpressionValue> EvaluateExpression_internal(
ExpressionFunction.multiply => Multiply(args),
ExpressionFunction.divide => Divide(args),
ExpressionFunction.list => List(args),
ExpressionFunction.@object => Object(args),
ExpressionFunction.INVALID => throw new ExpressionEvaluatorTypeErrorException(
$"Function {expr.Args.FirstOrDefault()} not implemented in backend {expr}"
),
Expand Down Expand Up @@ -1004,6 +1005,11 @@ private static JsonArray List(ExpressionValue[] args)
return new JsonArray(args.Select(a => JsonSerializer.SerializeToNode(a)).ToArray());
}

private static JsonObject Object(ExpressionValue[] args)
{
return new ObjectFunctionEvaluator(args).Evaluate();
}

/// <summary>
/// Performs arithmetic operation using decimal precision to avoid floating point precision issues.
/// Converts doubles to decimal, performs the operation, and converts back to double.
Expand Down
81 changes: 47 additions & 34 deletions src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
Expand All @@ -19,7 +20,7 @@ namespace Altinn.App.Core.Internal.Expressions;
// double is a value type where nullable takes extra space, and we only read it when it should be set
private readonly double _numberValue = 0;

// private readonly Dictionary<string, ExpressionValue>? _objectValue = null;
private readonly JsonObject? _objectValue = null;
private readonly JsonArray? _arrayValue = null;

/// <summary>
Expand Down Expand Up @@ -84,11 +85,12 @@ private ExpressionValue(string? value)
_stringValue = value;
}

// private ExpressionValue(Dictionary<string, ExpressionValue>? value)
// {
// _valueKind = value is null ? JsonValueKind.Null : JsonValueKind.Object;
// _objectValue = value;
// }
/// <summary>Constructor for object value</summary>
public ExpressionValue(JsonObject value)
{
ValueKind = JsonValueKind.Object;
_objectValue = value;
}

/// <summary>Constructor for array value</summary>
public ExpressionValue(JsonArray value)
Expand All @@ -112,10 +114,10 @@ public ExpressionValue(JsonArray value)
/// </summary>
public static implicit operator ExpressionValue(string? value) => new(value);

// /// <summary>
// /// Convert a Dictionary to ExpressionValue
// /// </summary>
// public static implicit operator ExpressionValue(Dictionary<string, ExpressionValue>? value) => new(value);
/// <summary>
/// Convert a Dictionary to ExpressionValue
/// </summary>
public static implicit operator ExpressionValue(JsonObject value) => new(value);

/// <summary>
/// Convert an array to ExpressionValue
Expand Down Expand Up @@ -174,17 +176,20 @@ public static ExpressionValue FromObject(object? value)
'"'
) // Trim quotes to match the string representation
,
JsonArray jsonArrayValue => jsonArrayValue,
_ => ToJsonArrayOrNull(value),
BigInteger => Null,
JsonObject jsonObject => jsonObject,
JsonArray jsonArray => jsonArray,
_ => ToJsonNodeOrNull(value),
};
}

private static ExpressionValue ToJsonArrayOrNull(object? value)
private static ExpressionValue ToJsonNodeOrNull(object? value)
{
var node = JsonSerializer.SerializeToNode(value);
return node switch
{
JsonArray jsonArray => jsonArray,
JsonObject jsonObject => jsonObject,
_ => Null,
};
}
Expand All @@ -202,7 +207,7 @@ private static ExpressionValue ToJsonArrayOrNull(object? value)
JsonValueKind.False => false,
JsonValueKind.String => String,
JsonValueKind.Number => Number,
// JsonValueKind.Object => Object,
JsonValueKind.Object => Dictionary,
JsonValueKind.Array => Array,
_ => throw new InvalidOperationException("Invalid value kind"),
};
Expand Down Expand Up @@ -249,14 +254,15 @@ private static ExpressionValue ToJsonArrayOrNull(object? value)
),
};

// public Dictionary<string, ExpressionValue> Object =>
// _valueKind switch
// {
// JsonValueKind.Object => _objectValue ?? throw new UnreachableException($"{this} is not an object"),
// _ => throw new InvalidCastException(
// $"The .Object property can't be used on an expression value that represent a {_valueKind}"
// ),
// };
/// <summary>Get the value as an object (or throw if it isn't an object ValueKind)</summary>
public JsonObject Dictionary =>
ValueKind switch
{
JsonValueKind.Object => _objectValue ?? throw new UnreachableException($"{this} is not an object"),
_ => throw new InvalidCastException(
$"The .Object property can't be used on an expression value that represent a {ValueKind}"
),
};

/// <summary>Get the value as an array (or throw if it isn't an array ValueKind)</summary>
public JsonArray Array =>
Expand All @@ -280,8 +286,8 @@ public override string ToString() =>
JsonValueKind.False => "false",
JsonValueKind.String => JsonSerializer.Serialize(String, _unsafeSerializerOptionsForSerializingDates),
JsonValueKind.Number => Number.ToString(CultureInfo.InvariantCulture),
// JsonValueKind.Object => JsonSerializer.Serialize(Object),
// JsonValueKind.Array => JsonSerializer.Serialize(Array),
JsonValueKind.Object => JsonSerializer.Serialize(Dictionary),
JsonValueKind.Array => JsonSerializer.Serialize(Array),
_ => throw new InvalidOperationException($"Invalid value kind {ValueKind}"),
};

Expand Down Expand Up @@ -330,8 +336,8 @@ public override string ToString() =>
{ } sValue => sValue,
},
JsonValueKind.Number => Number.ToString(CultureInfo.InvariantCulture),
// JsonValueKind.Object => JsonSerializer.Serialize(Object),
// JsonValueKind.Array => JsonSerializer.Serialize(Array),
JsonValueKind.Object => JsonSerializer.Serialize(Dictionary),
JsonValueKind.Array => JsonSerializer.Serialize(Array),
_ => throw new NotImplementedException($"ToStringForEquals not implemented for {ValueKind}"),
};

Expand Down Expand Up @@ -634,7 +640,7 @@ public override ExpressionValue Read(ref Utf8JsonReader reader, Type typeToConve
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDouble(),
JsonTokenType.Null => ExpressionValue.Null,
// JsonTokenType.StartObject => ReadObject(ref reader),
JsonTokenType.StartObject => ReadObject(ref reader, options),
JsonTokenType.StartArray => ReadArray(ref reader, options),
_ => throw new JsonException(),
};
Expand All @@ -652,10 +658,17 @@ private static ExpressionValue ReadArray(ref Utf8JsonReader reader, JsonSerializ
return new ExpressionValue(values);
}

// private ExpressionValue ReadObject(ref Utf8JsonReader reader)
// {
// throw new NotImplementedException();
// }
private static ExpressionValue ReadObject(ref Utf8JsonReader reader, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("Expected StartObject token.");
}
var values =
JsonSerializer.Deserialize<JsonObject>(ref reader, options)
?? throw new JsonException("Expected EndObject token.");
return new ExpressionValue(values);
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, ExpressionValue value, JsonSerializerOptions options)
Expand All @@ -678,9 +691,9 @@ public override void Write(Utf8JsonWriter writer, ExpressionValue value, JsonSer
case JsonValueKind.Number:
writer.WriteNumberValue(value.Number);
break;
// case JsonValueKind.Object:
// JsonSerializer.Serialize(writer, value.Object, options);
// break;
case JsonValueKind.Object:
JsonSerializer.Serialize(writer, value.Dictionary, options);
break;
case JsonValueKind.Array:
JsonSerializer.Serialize(writer, value.Array, options);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Text.Json;
using System.Text.Json.Nodes;

namespace Altinn.App.Core.Internal.Expressions;

internal class ObjectFunctionEvaluator
{
private readonly ExpressionValue[] _args;

public ObjectFunctionEvaluator(ExpressionValue[] args) => _args = args;

public JsonObject Evaluate()
{
AssertEvenNumberOfArguments();
string[] keys = ExtractKeys();
AssertKeysAreUnique(keys);
JsonNode?[] values = ExtractValues();
Dictionary<string, JsonNode?> keyValuePairs = DictionaryFromKeysAndValues(keys, values);
return new JsonObject(keyValuePairs);
}

private void AssertEvenNumberOfArguments()
{
if (_args.Length % 2 == 1)
{
throw new ExpressionEvaluatorTypeErrorException(
"The object function must have an even number of arguments."
);
}
}

private string[] ExtractKeys()
{
try
{
return _args.Where((_, index) => index % 2 == 0).Select(v => v.String).ToArray();
}
catch (InvalidCastException)
{
throw new ExpressionEvaluatorTypeErrorException("Object keys must be strings.");
}
}

private static void AssertKeysAreUnique(string[] keys)
{
if (keys.Length != keys.Distinct().Count())
{
throw new ExpressionEvaluatorTypeErrorException("Object keys must be unique.");
}
}

private JsonNode?[] ExtractValues() =>
_args.Where((_, index) => index % 2 == 1).Select(v => JsonSerializer.SerializeToNode(v)).ToArray();

private static Dictionary<string, JsonNode?> DictionaryFromKeysAndValues(string[] keys, JsonNode?[] values) =>
keys.Zip(values, (k, v) => new { k, v }).ToDictionary(x => x.k, x => x.v);
}
5 changes: 5 additions & 0 deletions src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,9 @@ public enum ExpressionFunction

/// <summary>Create a list from the arguments.</summary>
list,

/// <summary>Create a dictionary from the arguments, which must be alternating keys and values.</summary>
#pragma warning disable CA1720
@object,
#pragma warning restore CA1720
Comment on lines +233 to +235
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sonar complained about this variable having a name that contains "object", but we need this for the object function to work.

}
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ public async Task Divide_Theory(string testName, ExpressionTestCaseRoot.TestCase
public async Task List_Theory(string testName, ExpressionTestCaseRoot.TestCaseItem testCaseItem) =>
await RunTestCase(testName, new ExpressionTestCaseRoot(testCaseItem));

[Theory]
[SharedTestCases("object")]
public async Task Object_Theory(string testName, ExpressionTestCaseRoot.TestCaseItem testCaseItem) =>
await RunTestCase(testName, new ExpressionTestCaseRoot(testCaseItem));

private static async Task<ExpressionTestCaseRoot> LoadTestCase(string file, string folder)
{
ExpressionTestCaseRoot testCase = new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
"booleanList": [true, false],
"nullList": [null, null],
"multidimensionalList": [[[1, 2], [3, 4]], [[5, 6], [7, 8]]],
"objectList": [{ "a": 1 }, { "a": 2 }],
"emptyList": [],
"differentTypesList": [1, "string", true, null, [null]],
"differentTypesList": [1, "string", true, null, [null], { "a": null }],
"listThatLooksLikeAnExpression": ["equals", 1, 1]
}
}
Expand Down Expand Up @@ -44,6 +45,11 @@
"expression": ["dataModel", "multidimensionalList"],
"expects": [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
},
{
"name": "Object list lookup",
"expression": ["dataModel", "objectList"],
"expects": [{ "a": 1 }, { "a": 2 }]
},
{
"name": "Empty list lookup",
"expression": ["dataModel", "emptyList"],
Expand All @@ -52,7 +58,7 @@
{
"name": "Lookup of list with different types",
"expression": ["dataModel", "differentTypesList"],
"expects": [1, "string", true, null, [null]]
"expects": [1, "string", true, null, [null], { "a": null }]
},
{
"name": "Lookup of list that looks like an expression",
Expand Down
Loading
Loading