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
@@ -1,8 +1,9 @@
using System.Text.Json;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Expressions;
using Altinn.App.Core.Models;
using Altinn.App.Core.Models.Calculation;
using Altinn.App.Core.Models.Expressions;
using Altinn.App.Core.Models.Layout;
using Altinn.Platform.Storage.Interface.Models;
using Microsoft.Extensions.Logging;
Expand All @@ -12,12 +13,6 @@

internal sealed class DataModelFieldCalculator
{
private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

private readonly ILogger<DataModelFieldCalculator> _logger;
private readonly IAppResources _appResourceService;
private readonly IDataElementAccessChecker _dataElementAccessChecker;
Expand All @@ -41,32 +36,31 @@
using var activity = _telemetry?.StartCalculateActivity(dataAccessor.Instance.Id, taskId);
foreach (var (dataType, dataElement) in dataAccessor.GetDataElementsWithFormDataForTask(taskId))
{
if (await _dataElementAccessChecker.CanRead(dataAccessor.Instance, dataType) is false)

Check warning on line 39 in src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unnecessary Boolean literal(s).

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHpqmUk2Yti1dLGb&open=AZ7cVHpqmUk2Yti1dLGb&pullRequest=1821
{
continue;
}

var calculationConfig = _appResourceService.GetCalculationConfiguration(dataType.Id);
if (!string.IsNullOrEmpty(calculationConfig))
var calculationSchema = _appResourceService.GetCalculationConfiguration(dataType.Id);
if (calculationSchema is not null)
{
await CalculateFormData(dataAccessor, dataElement, calculationConfig);
await CalculateFormData(dataAccessor, dataElement, calculationSchema);
}
}
}

internal async Task CalculateFormData(
private async Task CalculateFormData(
IInstanceDataAccessor dataAccessor,
DataElement dataElement,
string rawCalculationConfig
CalculationSchema calculationSchema
)
{
DataElementIdentifier dataElementIdentifier = dataElement;
var dataModelFieldCalculations = ParseDataModelFieldCalculationConfig(rawCalculationConfig);
var formDataWrapper = await dataAccessor.GetFormDataWrapper(dataElement);

foreach (var (baseField, calculation) in dataModelFieldCalculations)
foreach (var calculation in calculationSchema.Calculations)
{
var resolvedFields = formDataWrapper.GetResolvedKeys(baseField);
var resolvedFields = formDataWrapper.GetResolvedKeys(calculation.Field);
foreach (var resolvedField in resolvedFields)
{
var resolvedFieldReference = new DataReference()
Expand All @@ -88,7 +82,7 @@
formDataWrapper,
resolvedFieldReference,
positionalArguments,
calculation
calculation.Expression
);
}
}
Expand All @@ -100,14 +94,14 @@
IFormDataWrapper formDataWrapper,
DataReference resolvedField,
ExpressionValue[] positionalArguments,
DataModelFieldCalculation calculation
Expression calculation
)
{
try
{
var calculationResult = await ExpressionEvaluator.EvaluateExpressionToExpressionValue(
dataAccessor,
calculation.Expression,
calculation,
context,
positionalArguments
);
Expand All @@ -121,78 +115,10 @@
);
}
}
catch (Exception e)

Check warning on line 118 in src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either log this exception and handle it, or rethrow it with some contextual information.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHpqmUk2Yti1dLGc&open=AZ7cVHpqmUk2Yti1dLGc&pullRequest=1821
{
_logger.LogError(e, "Error while evaluating calculation for field {Field}", resolvedField.Field);
throw;
}
}

private Dictionary<string, DataModelFieldCalculation> ParseDataModelFieldCalculationConfig(
string rawCalculationConfig
)
{
JsonDocument calculationConfigDocument;
try
{
calculationConfigDocument = JsonDocument.Parse(
rawCalculationConfig,
new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip }
);
}
catch (JsonException e)
{
_logger.LogError(e, "Failed to parse calculation configuration JSON");
return new Dictionary<string, DataModelFieldCalculation>();
}
using (calculationConfigDocument)
{
var dataModelFieldCalculations = new Dictionary<string, DataModelFieldCalculation>();
var hasCalculations = calculationConfigDocument.RootElement.TryGetProperty(
"calculations",
out JsonElement calculationsObject
);
if (hasCalculations)
{
foreach (var calculationArray in calculationsObject.EnumerateObject())
{
var field = calculationArray.Name;
var calculation = calculationArray.Value;
var resolvedDataModelFieldCalculation = ResolveDataModelFieldCalculation(field, calculation);
if (resolvedDataModelFieldCalculation == null)
{
_logger.LogError("Calculation for field {Field} could not be resolved", field);
continue;
}
dataModelFieldCalculations[field] = resolvedDataModelFieldCalculation;
}
}
return dataModelFieldCalculations;
}
}

private DataModelFieldCalculation? ResolveDataModelFieldCalculation(string field, JsonElement definition)
{
var dataModelFieldCalculationDefinition = definition.Deserialize<RawDataModelFieldCalculation>(
_jsonSerializerOptions
);
if (dataModelFieldCalculationDefinition == null)
{
_logger.LogError("Calculation for field {Field} could not be parsed", field);
return null;
}

if (dataModelFieldCalculationDefinition.Expression == null)
{
_logger.LogError("Calculation for field {Field} is missing expression", field);
return null;
}

var dataModelFieldCalculation = new DataModelFieldCalculation
{
Expression = dataModelFieldCalculationDefinition.Expression.Value,
};

return dataModelFieldCalculation;
}
}
32 changes: 27 additions & 5 deletions src/Altinn.App.Core/Helpers/Extensions/Utf8JsonReaderExtentions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text;
using System.Text.Json;

namespace Altinn.App.Core.Helpers.Extensions;
Expand All @@ -13,16 +14,22 @@
Copy(ref reader, writer);
writer.Flush();

return System.Text.Encoding.UTF8.GetString(stream.ToArray());
return Encoding.UTF8.GetString(stream.ToArray());
}

internal static void WriteRawFormattedValue(this Utf8JsonWriter writer, string json)
{
var jsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json), isFinalBlock: true, state: default);
jsonReader.Read(); // Need to read first token to initialize the reader
Copy(ref jsonReader, writer);
}

private static void Copy(ref Utf8JsonReader reader, Utf8JsonWriter writer)

Check failure on line 27 in src/Altinn.App.Core/Helpers/Extensions/Utf8JsonReaderExtentions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHp_mUk2Yti1dLGe&open=AZ7cVHp_mUk2Yti1dLGe&pullRequest=1821
{
switch (reader.TokenType)
{
case JsonTokenType.None:
writer.WriteNullValue();
break;
throw new JsonException("Reader is not initialized");
case JsonTokenType.StartObject:
writer.WriteStartObject();
while (reader.Read())
Expand All @@ -41,9 +48,13 @@
writer.WriteEndObject();
return;
default:
throw new JsonException($"Something is wrong, did not expect {reader.TokenType} here2");
throw new JsonException($"Something is wrong, did not expect {reader.TokenType} here");
}
}
if (reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException("Something is wrong, did not find end of object");
}
break;
case JsonTokenType.StartArray:
writer.WriteStartArray();
Expand All @@ -52,6 +63,10 @@
Copy(ref reader, writer);
}
writer.WriteEndArray();
if (reader.TokenType != JsonTokenType.EndArray)
{
throw new JsonException("Something is wrong, did not find end of array");
}
break;
case JsonTokenType.Comment:
writer.WriteCommentValue(reader.ValueSpan);
Expand All @@ -60,7 +75,14 @@
writer.WriteStringValue(reader.ValueSpan);
break;
case JsonTokenType.Number:
writer.WriteNumberValue(reader.GetDouble());
if (reader.HasValueSequence)
{
writer.WriteRawValue(reader.ValueSequence);
}
else
{
writer.WriteRawValue(reader.ValueSpan);
}
break;
case JsonTokenType.True:
writer.WriteBooleanValue(true);
Expand Down
12 changes: 7 additions & 5 deletions src/Altinn.App.Core/Implementation/AppResourcesSI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Models;
using Altinn.App.Core.Models.Calculation;
using Altinn.App.Core.Models.Layout;
using Altinn.App.Core.Models.Layout.Components;
using Altinn.Platform.Storage.Interface.Models;
Expand Down Expand Up @@ -78,7 +79,7 @@
return null;
}

await using FileStream fileStream = new(fullFileName, FileMode.Open, FileAccess.Read);

Check failure on line 82 in src/Altinn.App.Core/Implementation/AppResourcesSI.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMcgs8nzK2s3KjzO&open=AZ8YMMcgs8nzK2s3KjzO&pullRequest=1821
TextResource textResource =
await System.Text.Json.JsonSerializer.DeserializeAsync<TextResource>(fileStream, _jsonSerializerOptions)
?? throw new System.Text.Json.JsonException("Failed to deserialize text resource");
Expand Down Expand Up @@ -218,7 +219,7 @@
}

/// <inheritdoc />
[Obsolete("Use GetLayoutsForSet or GetLayoutModelForTask instead")]

Check warning on line 222 in src/Altinn.App.Core/Implementation/AppResourcesSI.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMcgs8nzK2s3KjzX&open=AZ8YMMcgs8nzK2s3KjzX&pullRequest=1821
public string GetLayouts()
{
using var activity = _telemetry?.StartGetLayoutsActivity();
Expand Down Expand Up @@ -295,7 +296,7 @@

PathHelper.EnsureLegalPath(Path.Join(_settings.AppBasePath, _settings.UiFolder), layoutsPath);

if (Directory.Exists(layoutsPath))

Check warning on line 299 in src/Altinn.App.Core/Implementation/AppResourcesSI.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMcgs8nzK2s3KjzW&open=AZ8YMMcgs8nzK2s3KjzW&pullRequest=1821
{
foreach (string file in Directory.GetFiles(layoutsPath))
{
Expand Down Expand Up @@ -407,9 +408,9 @@
PathHelper.EnsureLegalPath(Path.Join(_settings.AppBasePath, _settings.UiFolder), filename);

string? filedata = null;
if (File.Exists(filename))

Check warning on line 411 in src/Altinn.App.Core/Implementation/AppResourcesSI.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMcgs8nzK2s3KjzU&open=AZ8YMMcgs8nzK2s3KjzU&pullRequest=1821
{
filedata = File.ReadAllText(filename, Encoding.UTF8);

Check failure on line 413 in src/Altinn.App.Core/Implementation/AppResourcesSI.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMcgs8nzK2s3KjzP&open=AZ8YMMcgs8nzK2s3KjzP&pullRequest=1821
}

return filedata;
Expand All @@ -428,9 +429,9 @@

PathHelper.EnsureLegalPath(Path.Join(_settings.AppBasePath, _settings.UiFolder), filename);

if (File.Exists(filename))

Check warning on line 432 in src/Altinn.App.Core/Implementation/AppResourcesSI.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMcgs8nzK2s3KjzT&open=AZ8YMMcgs8nzK2s3KjzT&pullRequest=1821
{
var fileData = File.ReadAllText(filename, Encoding.UTF8);

Check failure on line 434 in src/Altinn.App.Core/Implementation/AppResourcesSI.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMcgs8nzK2s3KjzS&open=AZ8YMMcgs8nzK2s3KjzS&pullRequest=1821
LayoutSettings? layoutSettings = JsonConvert.DeserializeObject<LayoutSettings>(fileData);
return layoutSettings;
}
Expand Down Expand Up @@ -465,9 +466,9 @@
private static byte[] ReadFileByte(string fileName)
{
byte[]? filedata = null;
if (File.Exists(fileName))

Check warning on line 469 in src/Altinn.App.Core/Implementation/AppResourcesSI.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMcgs8nzK2s3KjzV&open=AZ8YMMcgs8nzK2s3KjzV&pullRequest=1821
{
filedata = File.ReadAllBytes(fileName);

Check failure on line 471 in src/Altinn.App.Core/Implementation/AppResourcesSI.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMcgs8nzK2s3KjzQ&open=AZ8YMMcgs8nzK2s3KjzQ&pullRequest=1821
}

#nullable disable
Expand Down Expand Up @@ -534,26 +535,27 @@
string? filedata = null;
if (File.Exists(filename))
{
filedata = File.ReadAllText(filename, Encoding.UTF8);

Check failure on line 538 in src/Altinn.App.Core/Implementation/AppResourcesSI.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not construct the path from user-controlled data.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMcgs8nzK2s3KjzR&open=AZ8YMMcgs8nzK2s3KjzR&pullRequest=1821
}

return filedata;
}

/// <inheritdoc />
public string? GetCalculationConfiguration(string dataTypeId)
public CalculationSchema? GetCalculationConfiguration(string dataTypeId)
{
using var activity = _telemetry?.StartGetCalculationConfigurationActivity();
string legalPath = Path.Join(_settings.AppBasePath, _settings.ModelsFolder);
string filename = Path.Join(legalPath, $"{dataTypeId}.{_settings.CalculationConfigurationFileName}");
PathHelper.EnsureLegalPath(legalPath, filename);

string? fileData = null;
if (File.Exists(filename))
if (!File.Exists(filename))
{
fileData = File.ReadAllText(filename, Encoding.UTF8);
return null;
}

return fileData;
return System.Text.Json.JsonSerializer.Deserialize<CalculationSchema>(
File.ReadAllText(filename, Encoding.UTF8)
);
}
}
3 changes: 2 additions & 1 deletion src/Altinn.App.Core/Internal/App/IAppResources.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Altinn.App.Core.Models;
using Altinn.App.Core.Models.Calculation;
using Altinn.App.Core.Models.Layout;
using Altinn.Platform.Storage.Interface.Models;

Expand Down Expand Up @@ -82,7 +83,7 @@
/// Gets the layouts for the app.
/// </summary>
/// <returns>A dictionary of FormLayout objects serialized to JSON</returns>
[Obsolete("Use GetLayoutsForSet or GetLayoutModelForTask instead")]

Check warning on line 86 in src/Altinn.App.Core/Internal/App/IAppResources.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMYMs8nzK2s3KjzL&open=AZ8YMMYMs8nzK2s3KjzL&pullRequest=1821
string GetLayouts();

/// <summary>
Expand Down Expand Up @@ -134,7 +135,7 @@
/// <summary>
/// Gets the full layout model for the optional set
/// </summary>
[Obsolete("Use GetLayoutModelForTask instead", false)]

Check warning on line 138 in src/Altinn.App.Core/Internal/App/IAppResources.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ8YMMYMs8nzK2s3KjzM&open=AZ8YMMYMs8nzK2s3KjzM&pullRequest=1821
LayoutModel GetLayoutModel(string? layoutSetId = null);

/// <summary>
Expand Down Expand Up @@ -177,5 +178,5 @@
/// Gets the calculation configuration for a given data type
/// </summary>
/// <returns>The calculation configuration in JSON format represented as string</returns>
string? GetCalculationConfiguration(string dataTypeId);
CalculationSchema? GetCalculationConfiguration(string dataTypeId);
}
36 changes: 30 additions & 6 deletions src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
Expand All @@ -12,7 +13,7 @@
/// Discriminated union for the JSON types that can be arguments and result of expressions
/// </summary>
[JsonConverter(typeof(ExpressionTypeUnionConverter))]
[DebuggerDisplay("{ToString(),nq}")]
[DebuggerDisplay("{ToStringForText(),nq}")]
public readonly struct ExpressionValue : IEquatable<ExpressionValue>
{
private readonly string? _stringValue = null;
Expand All @@ -22,12 +23,12 @@
private readonly double _numberValue = 0;

/// <summary>
/// Constructor for NULL value (structs require a public parameterless constructor)
/// Constructor for Undefined value (structs require a public parameterless constructor)
/// </summary>
public ExpressionValue()
: this(JsonValueKind.Null) { }
: this(JsonValueKind.Undefined) { }

private ExpressionValue(JsonValueKind valueKind)

Check warning on line 31 in src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

All 'ExpressionValue' method overloads should be adjacent.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHl3mUk2Yti1dLGV&open=AZ7cVHl3mUk2Yti1dLGV&pullRequest=1821
{
ValueKind = valueKind;
}
Expand Down Expand Up @@ -221,10 +222,10 @@
/// <summary>
/// Convert the value to the relevant CLR type
/// </summary>
[Obsolete(
"ToObject is not type safe and should be avoided. Use the type-specific properties or TryDeserialize<T> instead.",
error: false
)]

Check warning on line 228 in src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHl3mUk2Yti1dLGX&open=AZ7cVHl3mUk2Yti1dLGX&pullRequest=1821
public object? ToObject() =>
ValueKind switch
{
Expand Down Expand Up @@ -257,7 +258,7 @@
/// <summary>
/// Get the value as a string (or throw if it isn't a string ValueKind)
/// </summary>
public string String

Check warning on line 261 in src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Identifier 'String' contains type name

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHl3mUk2Yti1dLGa&open=AZ7cVHl3mUk2Yti1dLGa&pullRequest=1821
{
get
{
Expand Down Expand Up @@ -354,11 +355,11 @@
JsonValueKind.Null => "null",
JsonValueKind.Undefined => "undefined",
JsonValueKind.True => "true",
JsonValueKind.False => "false",

Check warning on line 358 in src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of using this literal 'false' 6 times.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHl3mUk2Yti1dLGW&open=AZ7cVHl3mUk2Yti1dLGW&pullRequest=1821
JsonValueKind.String => JsonSerializer.Serialize(String, _unsafeSerializerOptionsForSerializingDates),
JsonValueKind.Number => Number.ToString(CultureInfo.InvariantCulture),
JsonValueKind.Object or JsonValueKind.Array => _stringValueNotNull,
_ => throw new InvalidOperationException($"Invalid value kind {ValueKind}"),

Check failure on line 362 in src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this 'throw' expression.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHl3mUk2Yti1dLGY&open=AZ7cVHl3mUk2Yti1dLGY&pullRequest=1821
};

/// <summary>
Expand Down Expand Up @@ -410,7 +411,7 @@
{
throw new NotImplementedException("Equals is not used for ExpressionValue");
// First compare value kinds
// if (_valueKind != other._valueKind)

Check warning on line 414 in src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHl3mUk2Yti1dLGS&open=AZ7cVHl3mUk2Yti1dLGS&pullRequest=1821
// return false;

// // Then compare actual values based on the kind
Expand Down Expand Up @@ -439,7 +440,7 @@
{
throw new NotImplementedException("GetHashCode is not implemented for ExpressionValue");
// return ValueKind switch
// {

Check warning on line 443 in src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHl3mUk2Yti1dLGT&open=AZ7cVHl3mUk2Yti1dLGT&pullRequest=1821
// JsonValueKind.Null => 0,
// JsonValueKind.True => 1,
// JsonValueKind.False => 0,
Expand Down Expand Up @@ -556,7 +557,7 @@
/// <param name="result">The result (null or default if unsuccessful), but note that null might also be a valid result</param>
/// <param name="type">The type to convert to</param>
/// <returns>Whether the conversion was successful</returns>
public bool TryDeserialize(Type type, out object? result)

Check failure on line 560 in src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 32 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHl3mUk2Yti1dLGZ&open=AZ7cVHl3mUk2Yti1dLGZ&pullRequest=1821
{
// Value types can be Nullable<T>, so assign underlyingType accordingly
Type underlyingType;
Expand Down Expand Up @@ -622,6 +623,28 @@
return false;
}
}
case JsonValueKind.Array when underlyingType.IsAssignableTo(typeof(IEnumerable)):
case JsonValueKind.Object:
try
{
// For complex types we serialize the expressionValue to json
// and then deserialize to the target type.
// This allows us to leverage the normal JSON deserialization rules and also handle cases where the
// ValueKind doesn't exactly match the target type (e.g., deserialize a JsonObject to a Dictionary<string, object> or similar)
var json = JsonSerializer.SerializeToUtf8Bytes(this);
result = JsonSerializer.Deserialize(json, type);
return true;
}
catch (JsonException)
{
result = null;
return false;
}
catch (NotSupportedException)
{
result = null;
return false;
}
}

// Add special handling for bool to support loose conversion rules
Expand Down Expand Up @@ -673,7 +696,7 @@

private static bool IsSupportedNumericType(Type type)
{
// TODO: consider supporting enums as numeric types as well, but currently we

Check warning on line 699 in src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this 'TODO' comment.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7cVHl3mUk2Yti1dLGU&open=AZ7cVHl3mUk2Yti1dLGU&pullRequest=1821
// don't use C# enums in datamodels, so it isn't very urgent.
return type == typeof(double)
|| type == typeof(int)
Expand Down Expand Up @@ -707,7 +730,7 @@
return new(doc.RootElement);
}

internal void WriteJson(Utf8JsonWriter writer, JsonSerializerOptions options)
internal void WriteJson(Utf8JsonWriter writer)
{
switch (ValueKind)
{
Expand All @@ -729,7 +752,8 @@
break;
case JsonValueKind.Object:
case JsonValueKind.Array:
writer.WriteRawValue(_stringValueNotNull);
// writer.WriteRawFormattedValue(_stringValueNotNull);

Check warning on line 755 in src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZ7uRXfSbb0SHGvLbshi&open=AZ7uRXfSbb0SHGvLbshi&pullRequest=1821
JsonSerializer.Serialize(writer, JsonElement);
break;
default:
throw new JsonException();
Expand Down Expand Up @@ -768,5 +792,5 @@

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, ExpressionValue value, JsonSerializerOptions options) =>
value.WriteJson(writer, options);
value.WriteJson(writer);
}
23 changes: 23 additions & 0 deletions src/Altinn.App.Core/Models/Calculation/CalculationItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Text.Json.Serialization;
using Altinn.App.Core.Models.Expressions;

namespace Altinn.App.Core.Models.Calculation;

/// <summary>
/// Calculation item in the calculation configuration
/// </summary>
public class CalculationItem
{
/// <summary>
/// The base field to be calculated.
/// Note that missing indexes will be added to the field name when calculating array items. For example, if the field is "myArray[].myField", the calculation will be applied to all items in the array.
/// </summary>
[JsonPropertyName("field")]
public required string Field { get; init; }

/// <summary>
/// The expression to be used for the calculation. Note that this will be run in the context of the field, so you can use relative paths in the expression.
/// </summary>
[JsonPropertyName("expression")]
public required Expression Expression { get; init; }
}
Loading
Loading