diff --git a/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/Altinn.App.Core/Configuration/AppSettings.cs index 2b98c9be97..0a69702862 100644 --- a/src/Altinn.App.Core/Configuration/AppSettings.cs +++ b/src/Altinn.App.Core/Configuration/AppSettings.cs @@ -8,14 +8,19 @@ public class AppSettings { #pragma warning disable CA1707 // Identifiers should not contain underscores /// - /// Constant for the location of json schema file + /// Constant for the suffix on json schema file names /// public const string JSON_SCHEMA_FILENAME = "schema.json"; /// - /// Constant for the location of validation configuration file + /// Constant for the suffix on validation file names /// public const string VALIDATION_CONFIG_FILENAME = "validation.json"; + + /// + /// Constant for the suffix on calculation file names + /// + public const string CALCULATION_CONFIG_FILENAME = "calculation.json"; #pragma warning restore CA1707 // Identifiers should not contain underscores /// @@ -95,15 +100,20 @@ public class AppSettings public string RuleConfigurationJSONFileName { get; set; } = "RuleConfiguration.json"; /// - /// Gets or sets The JSON schema file name + /// Gets or sets the file names suffix for the json schema files /// public string JsonSchemaFileName { get; set; } = JSON_SCHEMA_FILENAME; /// - /// Gets or sets The JSON schema file name + /// Gets or sets the file names suffix for the validation files /// public string ValidationConfigurationFileName { get; set; } = VALIDATION_CONFIG_FILENAME; + /// + /// Gets or sets the file names suffix for the calculation files + /// + public string CalculationConfigurationFileName { get; set; } = CALCULATION_CONFIG_FILENAME; + /// /// Gets or sets the filename for application meta data /// diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index ea5b298ea1..0238718fd7 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -185,6 +185,7 @@ IWebHostEnvironment env #pragma warning restore CS0618, CS0612 // Type or member is obsolete services.TryAddTransient(); services.TryAddTransient(); + services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); @@ -192,6 +193,7 @@ IWebHostEnvironment env services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddTransient(); services.AddSingleton(); diff --git a/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs b/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs index f04d517c45..a78f276089 100644 --- a/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs +++ b/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs @@ -11,7 +11,7 @@ public class InstanceDataListsFactory private readonly AppImplementationFactory _appImplementationFactory; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public InstanceDataListsFactory(IServiceProvider serviceProvider) { diff --git a/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs b/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs new file mode 100644 index 0000000000..13bba02d07 --- /dev/null +++ b/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs @@ -0,0 +1,227 @@ +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.Layout; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging; +using ComponentContext = Altinn.App.Core.Models.Expressions.ComponentContext; + +namespace Altinn.App.Core.Features.DataProcessing; + +internal sealed class DataModelFieldCalculator +{ + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + private readonly ILogger _logger; + private readonly IAppResources _appResourceService; + private readonly ILayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer; + private readonly IDataElementAccessChecker _dataElementAccessChecker; + private readonly Telemetry? _telemetry; + + public DataModelFieldCalculator( + ILogger logger, + ILayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, + IAppResources appResourceService, + IDataElementAccessChecker dataElementAccessChecker, + Telemetry? telemetry = null + ) + { + _logger = logger; + _appResourceService = appResourceService; + _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; + _dataElementAccessChecker = dataElementAccessChecker; + _telemetry = telemetry; + } + + public async Task Calculate(IInstanceDataAccessor dataAccessor, string taskId) + { + 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) + { + continue; + } + + var calculationConfig = _appResourceService.GetCalculationConfiguration(dataType.Id); + if (!string.IsNullOrEmpty(calculationConfig)) + { + await CalculateFormData(dataAccessor, dataElement, taskId, calculationConfig); + } + } + } + + internal async Task CalculateFormData( + IInstanceDataAccessor dataAccessor, + DataElement dataElement, + string taskId, + string rawCalculationConfig + ) + { + var evaluatorState = await _layoutEvaluatorStateInitializer.Init(dataAccessor, taskId); + var hiddenFields = await LayoutEvaluator.GetHiddenFieldsForRemoval( + evaluatorState, + evaluateRemoveWhenHidden: false + ); + DataElementIdentifier dataElementIdentifier = dataElement; + var dataModelFieldCalculations = ParseDataModelFieldCalculationConfig(rawCalculationConfig); + var formDataWrapper = await dataAccessor.GetFormDataWrapper(dataElement); + + foreach (var (baseField, calculation) in dataModelFieldCalculations) + { + var resolvedFields = await evaluatorState.GetResolvedKeys( + new DataReference() { Field = baseField, DataElementIdentifier = dataElementIdentifier }, + true + ); + foreach (var resolvedField in resolvedFields) + { + if ( + hiddenFields.Exists(d => + d.DataElementIdentifier == resolvedField.DataElementIdentifier + && IsSameOrDescendantField(resolvedField.Field, d.Field) + ) + ) + { + continue; + } + + var context = new ComponentContext( + evaluatorState, + component: null, + rowIndices: ExpressionHelper.GetRowIndices(resolvedField.Field), + dataElementIdentifier: resolvedField.DataElementIdentifier + ); + var positionalArguments = new object[] { resolvedField.Field }; + + await RunCalculation( + formDataWrapper, + evaluatorState, + resolvedField, + context, + positionalArguments, + calculation + ); + } + } + } + + private async Task RunCalculation( + IFormDataWrapper formDataWrapper, + LayoutEvaluatorState evaluatorState, + DataReference resolvedField, + ComponentContext context, + object[] positionalArguments, + DataModelFieldCalculation calculation + ) + { + try + { + var calculationResult = await ExpressionEvaluator.EvaluateExpressionToExpressionValue( + evaluatorState, + calculation.Expression, + context, + positionalArguments + ); + if (!formDataWrapper.Set(resolvedField.Field, calculationResult)) + { + _logger.LogWarning( + "Could not set calculated value for field {Field} in data element {DataElementId}. " + + "This is because the type conversion failed.", + resolvedField.Field, + resolvedField.DataElementIdentifier.Id + ); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error while evaluating calculation for field {Field}", resolvedField.Field); + throw; + } + } + + private Dictionary 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(); + } + using (calculationConfigDocument) + { + var dataModelFieldCalculations = new Dictionary(); + 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( + _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; + } + + private static bool IsSameOrDescendantField(string candidate, string hiddenField) + { + if (candidate.Equals(hiddenField, StringComparison.Ordinal)) + { + return true; + } + + return candidate.StartsWith(hiddenField, StringComparison.Ordinal) + && candidate.Length > hiddenField.Length + && (candidate[hiddenField.Length] == '.' || candidate[hiddenField.Length] == '['); + } +} diff --git a/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculatorProcessor.cs b/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculatorProcessor.cs new file mode 100644 index 0000000000..2491a5c484 --- /dev/null +++ b/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculatorProcessor.cs @@ -0,0 +1,37 @@ +using Altinn.App.Core.Models; + +namespace Altinn.App.Core.Features.DataProcessing; + +/// +/// Processing data model fields that is calculated by expressions provided in [modelName].calculation.json. +/// +internal sealed class DataModelFieldCalculatorProcessor : IDataWriteProcessor +{ + private readonly DataModelFieldCalculator _dataModelFieldCalculator; + + /// + /// Initializes a new instance of the class. + /// + /// + public DataModelFieldCalculatorProcessor(DataModelFieldCalculator dataModelFieldCalculator) + { + _dataModelFieldCalculator = dataModelFieldCalculator; + } + + /// + /// Processes data write operations on properties in the data model. + /// + /// Object to fetch data elements not included in changes + /// The current task ID + /// Not used in this context + /// Not used in this context + public async Task ProcessDataWrite( + IInstanceDataMutator instanceDataMutator, + string taskId, + DataElementChanges changes, + string? language + ) + { + await _dataModelFieldCalculator.Calculate(instanceDataMutator, taskId); + } +} diff --git a/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs b/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs index 317191959c..acc961d743 100644 --- a/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs +++ b/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs @@ -16,7 +16,7 @@ public abstract class GenericDataProcessor : IDataProcessor /// /// Do changes to the model before it is written to storage, and report back to frontend. - /// Tyipically used to add calculated values to the model. + /// Typically used to add calculated values to the model. /// public abstract Task ProcessDataWrite( Instance instance, diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.ApplicationMetadata.Service.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.ApplicationMetadata.Service.cs index 399cb752f8..ed7a7ae451 100644 --- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.ApplicationMetadata.Service.cs +++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.ApplicationMetadata.Service.cs @@ -49,6 +49,9 @@ partial class Telemetry internal Activity? StartGetValidationConfigurationActivity() => ActivitySource.StartActivity($"{Prefix}.GetValidationConfiguration"); + internal Activity? StartGetCalculationConfigurationActivity() => + ActivitySource.StartActivity($"{Prefix}.GetCalculationConfiguration"); + internal Activity? StartGetLayoutModelActivity() => ActivitySource.StartActivity($"{Prefix}.GetLayoutModel"); internal Activity? StartGetClassRefActivity() => ActivitySource.StartActivity($"{Prefix}.GetClassRef"); diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.DataFieldValueCalculator.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.DataFieldValueCalculator.cs new file mode 100644 index 0000000000..f320c85535 --- /dev/null +++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.DataFieldValueCalculator.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; +using static Altinn.App.Core.Features.Telemetry.DataModelFieldCalculator; + +namespace Altinn.App.Core.Features; + +partial class Telemetry +{ + internal Activity? StartCalculateActivity(string instanceId, string taskId) + { + var activity = ActivitySource.StartActivity($"{Prefix}.Calculate"); + activity?.SetInstanceId(instanceId); + activity?.SetTaskId(taskId); + return activity; + } + + internal static class DataModelFieldCalculator + { + internal const string Prefix = "DataModelFieldCalculator"; + } +} diff --git a/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs index 3defcd9177..899b05ecfb 100644 --- a/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs +++ b/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs @@ -147,7 +147,7 @@ internal async Task> ValidateFormData( var context = new ComponentContext( evaluatorState, component: null, - rowIndices: GetRowIndices(resolvedField.Field), + rowIndices: ExpressionHelper.GetRowIndices(resolvedField.Field), dataElementIdentifier: resolvedField.DataElementIdentifier ); var positionalArguments = new object[] { resolvedField.Field }; @@ -168,44 +168,6 @@ await RunValidation( return validationIssues; } - private static int[]? GetRowIndices(string field) - { - Span rowIndicesSpan = stackalloc int[200]; // Assuming max 200 indices for simplicity recursion will never go deeper than 3-4 - int count = 0; - for (int index = 0; index < field.Length; index++) - { - if (field[index] == '[') - { - int startIndex = index + 1; - int endIndex = field.IndexOf(']', startIndex); - if (endIndex == -1) - { - throw new InvalidOperationException($"Unpaired [ character in field: {field}"); - } - string indexString = field[startIndex..endIndex]; - if (int.TryParse(indexString, out int rowIndex)) - { - rowIndicesSpan[count] = rowIndex; - count++; - index = endIndex; // Move index to the end of the current bracket - } - else - { - throw new InvalidOperationException( - $"Invalid row index in field: {field} at position {startIndex}" - ); - } - } - } - if (count == 0) - { - return null; // No indices found - } - int[] rowIndices = new int[count]; - rowIndicesSpan[..count].CopyTo(rowIndices); - return rowIndices; - } - private async Task RunValidation( LayoutEvaluatorState evaluatorState, List validationIssues, diff --git a/src/Altinn.App.Core/Helpers/DataModel/DataModelWrapper.cs b/src/Altinn.App.Core/Helpers/DataModel/DataModelWrapper.cs index 6c2fa1b89a..ab535df7d2 100644 --- a/src/Altinn.App.Core/Helpers/DataModel/DataModelWrapper.cs +++ b/src/Altinn.App.Core/Helpers/DataModel/DataModelWrapper.cs @@ -122,6 +122,22 @@ ReadOnlySpan rowIndexes /// ] /// public string[] GetResolvedKeys(string field) + { + return GetResolvedKeys(field, isCalculating: false); + } + + /// + /// Get all valid indexed keys for the field, depending on the number of rows in repeating groups + /// + /// + /// GetResolvedKeys("data.bedrifter.styre.medlemmer") => + /// [ + /// "data.bedrifter[0].styre.medlemmer", + /// "data.bedrifter[1].styre.medlemmer" + /// ... + /// ] + /// + public string[] GetResolvedKeys(string field, bool isCalculating) { if (_dataModel is null) { @@ -129,7 +145,7 @@ public string[] GetResolvedKeys(string field) } var fieldParts = field.Split('.'); - return GetResolvedKeysRecursive(fieldParts, _dataModel); + return GetResolvedKeysRecursive(fieldParts, _dataModel, _dataModel.GetType(), isCalculating: isCalculating); } private static string JoinFieldKeyParts(string? currentKey, string? key) @@ -148,61 +164,111 @@ private static string JoinFieldKeyParts(string? currentKey, string? key) private static string[] GetResolvedKeysRecursive( string[] keyParts, - object currentModel, + object? currentModel, + Type currentType, int currentIndex = 0, - string currentKey = "" + string currentKey = "", + bool isCalculating = false ) { - if (currentModel is null) - { - return []; - } - if (currentIndex == keyParts.Length) { return [currentKey]; } var (key, groupIndex) = ParseKeyPart(keyParts[currentIndex]); - var prop = Array.Find(currentModel.GetType().GetProperties(), p => IsPropertyWithJsonName(p, key)); - var childModel = prop?.GetValue(currentModel); - if (childModel is null) + var lookupType = currentModel?.GetType() ?? currentType; + var prop = Array.Find(lookupType.GetProperties(), p => IsPropertyWithJsonName(p, key)); + if (prop is null) { return []; } - if (childModel is not string && childModel is System.Collections.IEnumerable childModelList) + var childType = prop.PropertyType; + // Check if the property is a collection type (but not string) + if (childType != typeof(string) && childType.IsAssignableTo(typeof(System.Collections.IEnumerable))) { - // childModel is a list - if (groupIndex is null) + // For collections, we need the actual object to enumerate + var childModel = currentModel is not null ? prop.GetValue(currentModel) : null; + if (childModel is System.Collections.IEnumerable childModelList) { + // Get the element type of the collection + var elementType = childType.IsArray + ? childType.GetElementType() ?? typeof(object) + : childType.GetGenericArguments().FirstOrDefault() ?? typeof(object); // Index not specified, recurse on all elements - int i = 0; - var resolvedKeys = new List(); - foreach (var child in childModelList) + if (groupIndex is null) + { + int i = 0; + var resolvedKeys = new List(); + foreach (var child in childModelList) + { + if (child is null) + { + i++; + continue; + } + var newResolvedKeys = GetResolvedKeysRecursive( + keyParts, + child, + elementType, + currentIndex + 1, + JoinFieldKeyParts(currentKey, $"{key}[{i}]"), + isCalculating + ); + resolvedKeys.AddRange(newResolvedKeys); + i++; + } + return resolvedKeys.ToArray(); + } + // Index specified, recurse on that element + var elementAt = GetElementAt(childModelList, groupIndex.Value); + if (elementAt is null) { - var newResolvedKeys = GetResolvedKeysRecursive( - keyParts, - child, - currentIndex + 1, - JoinFieldKeyParts(currentKey, key + "[" + i + "]") - ); - resolvedKeys.AddRange(newResolvedKeys); - i++; + return []; } - return resolvedKeys.ToArray(); + return GetResolvedKeysRecursive( + keyParts, + elementAt, + elementType, + currentIndex + 1, + JoinFieldKeyParts(currentKey, $"{key}[{groupIndex}]"), + isCalculating + ); + } + + if (isCalculating && currentIndex == keyParts.Length - 1) + { + return [JoinFieldKeyParts(currentKey, key)]; } - // Index specified, recurse on that element - return GetResolvedKeysRecursive( - keyParts, - childModel, - currentIndex + 1, - JoinFieldKeyParts(currentKey, key + "[" + groupIndex + "]") - ); + return []; } - // Otherwise, just recurse - return GetResolvedKeysRecursive(keyParts, childModel, currentIndex + 1, JoinFieldKeyParts(currentKey, key)); + // For non-collection properties, we can work with just the type + // Get the child object for further traversal + var childValue = currentModel is not null ? prop.GetValue(currentModel) : null; + // If this is the last key part + if (currentIndex == keyParts.Length - 1) + { + // Return the key if value exists, or if calculating (to allow null fields during calculation) + return childValue is not null || isCalculating ? [JoinFieldKeyParts(currentKey, key)] : []; + } + + // If child is null and we're not calculating, we can't continue + if (childValue is null && !isCalculating) + { + return []; + } + + // Continue recursion using type information (childValue may be null if isCalculating=true) + return GetResolvedKeysRecursive( + keyParts, + childValue, + childType, + currentIndex + 1, + JoinFieldKeyParts(currentKey, key), + isCalculating + ); } private static object? GetElementAt(System.Collections.IEnumerable enumerable, int index) diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs index 95be38aa60..2d36cb2524 100644 --- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs +++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs @@ -523,4 +523,21 @@ private static byte[] ReadFileContentsFromLegalPath(string legalPath, string fil return filedata; } + + /// + public string? 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)) + { + fileData = File.ReadAllText(filename, Encoding.UTF8); + } + + return fileData; + } } diff --git a/src/Altinn.App.Core/Internal/App/IAppResources.cs b/src/Altinn.App.Core/Internal/App/IAppResources.cs index 6a08382f28..d6fd507522 100644 --- a/src/Altinn.App.Core/Internal/App/IAppResources.cs +++ b/src/Altinn.App.Core/Internal/App/IAppResources.cs @@ -166,4 +166,10 @@ public interface IAppResources /// Gets the validation configuration for a given data type /// string? GetValidationConfiguration(string dataTypeId); + + /// + /// Gets the calculation configuration for a given data type + /// + /// The calculation configuration in JSON format represented as string + string? GetCalculationConfiguration(string dataTypeId); } diff --git a/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs b/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs index be0cda83e4..9696087d89 100644 --- a/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs +++ b/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs @@ -285,7 +285,11 @@ private static int InvokeReturnIntOrError(MethodInfo info, object instance) /// group[1].name /// group[1].age /// - public static DataReference[] GetResolvedKeys(this IFormDataWrapper formDataWrapper, DataReference reference) + public static DataReference[] GetResolvedKeys( + this IFormDataWrapper formDataWrapper, + DataReference reference, + bool isCalculating = false + ) { //TODO: write more efficient code that uses the formDataWrapper to resolve keys instead of reflection in DataModelWrapper var data = formDataWrapper.BackingData(); @@ -293,7 +297,7 @@ public static DataReference[] GetResolvedKeys(this IFormDataWrapper formDataWrap var dataModelWrapper = new DataModelWrapper(data); #pragma warning restore CS0618 // Type or member is obsolete return dataModelWrapper - .GetResolvedKeys(reference.Field) + .GetResolvedKeys(reference.Field, isCalculating) .Select(resolvedField => reference with { Field = resolvedField }) .ToArray(); } diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs index 8af17e5afd..327fe06169 100644 --- a/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs @@ -51,6 +51,20 @@ bool defaultReturn } } + /// + /// Evaluate a from a given in a + /// + public static async Task EvaluateExpressionToExpressionValue( + LayoutEvaluatorState state, + Expression expr, + ComponentContext context, + object?[]? positionalArguments = null + ) + { + var positionalArgumentUnions = positionalArguments?.Select(ExpressionValue.FromObject).ToArray(); + return await EvaluateExpression_internal(state, expr, context, positionalArgumentUnions); + } + /// /// Evaluate a from a given in a /// diff --git a/src/Altinn.App.Core/Internal/Expressions/ExpressionHelper.cs b/src/Altinn.App.Core/Internal/Expressions/ExpressionHelper.cs new file mode 100644 index 0000000000..c2eb708765 --- /dev/null +++ b/src/Altinn.App.Core/Internal/Expressions/ExpressionHelper.cs @@ -0,0 +1,50 @@ +namespace Altinn.App.Core.Internal.Expressions; + +internal static class ExpressionHelper +{ + internal static int[]? GetRowIndices(string? field) + { + if (field == null) + return null; + Span rowIndicesSpan = stackalloc int[200]; // Assuming max 200 indices for simplicity recursion will never go deeper than 3-4 + int count = 0; + for (int index = 0; index < field.Length; index++) + { + if (field[index] == '[') + { + int startIndex = index + 1; + int endIndex = field.IndexOf(']', startIndex); + if (endIndex == -1) + { + throw new InvalidOperationException($"Unpaired [ character in field: {field}"); + } + string indexString = field[startIndex..endIndex]; + if (int.TryParse(indexString, out int rowIndex)) + { + if (count >= rowIndicesSpan.Length) + { + throw new InvalidOperationException( + $"Too many row indices in field: {field}. Max supported: {rowIndicesSpan.Length}" + ); + } + rowIndicesSpan[count] = rowIndex; + count++; + index = endIndex; // Move index to the end of the current bracket + } + else + { + throw new InvalidOperationException( + $"Invalid row index in field: {field} at position {startIndex}" + ); + } + } + } + if (count == 0) + { + return null; // No indices found + } + int[] rowIndices = new int[count]; + rowIndicesSpan[..count].CopyTo(rowIndices); + return rowIndices; + } +} diff --git a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs index ba81e5ffe1..7e393ec0d8 100644 --- a/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs +++ b/src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs @@ -203,6 +203,15 @@ public async Task GetResolvedKeys(DataReference reference) return data.GetResolvedKeys(reference); } + /// + /// Get all the resolved keys (including all possible indexes) from a data model key + /// + public async Task GetResolvedKeys(DataReference reference, bool isCalculating) + { + var data = await _dataAccessor.GetFormDataWrapper(reference.DataElementIdentifier); + return data.GetResolvedKeys(reference, isCalculating); + } + /// /// Set the value of a field to null. /// diff --git a/src/Altinn.App.Core/Models/RawDataModelFieldCalculation.cs b/src/Altinn.App.Core/Models/RawDataModelFieldCalculation.cs new file mode 100644 index 0000000000..31a5609ca3 --- /dev/null +++ b/src/Altinn.App.Core/Models/RawDataModelFieldCalculation.cs @@ -0,0 +1,25 @@ +using Altinn.App.Core.Models.Expressions; + +namespace Altinn.App.Core.Models; + +/// +/// Resolved data field calculation +/// +internal sealed class DataModelFieldCalculation +{ + /// + /// Expression to evaluate + /// + public required Expression Expression { get; set; } +} + +/// +/// Raw value calculation expression from the calculation configuration file +/// +internal sealed class RawDataModelFieldCalculation +{ + /// + /// Expression to evaluate + /// + public Expression? Expression { get; set; } +} diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/DataModelFieldCalculatorTests.cs b/test/Altinn.App.Core.Tests/Features/DataProcessing/DataModelFieldCalculatorTests.cs new file mode 100644 index 0000000000..c5e2cae651 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/DataModelFieldCalculatorTests.cs @@ -0,0 +1,237 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; +using Altinn.App.Core.Features.DataProcessing; +using Altinn.App.Core.Internal.Data; +using Altinn.App.Core.Internal.Expressions; +using Altinn.App.Core.Internal.Texts; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Layout; +using Altinn.App.Core.Tests.LayoutExpressions.TestUtilities; +using Altinn.App.Core.Tests.TestUtils; +using Altinn.Platform.Storage.Interface.Models; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Options; +using Moq; +using Xunit.Abstractions; +using IAppResources = Altinn.App.Core.Internal.App.IAppResources; + +namespace Altinn.App.Core.Tests.Features.DataProcessing; + +public sealed class DataModelFieldCalculatorTests +{ + private readonly ITestOutputHelper _output; + private readonly DataModelFieldCalculator _dataModelFieldCalculator; + private readonly FakeLogger _logger = new(); + private readonly Mock _appResources = new(MockBehavior.Strict); + private readonly Mock _layoutInitializer = new(MockBehavior.Strict); + private readonly IOptions _frontendSettings = Microsoft.Extensions.Options.Options.Create( + new FrontEndSettings() + ); + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + private DataElement _dataElement = null!; + private IInstanceDataAccessor _instanceDataAccessor = null!; + + public DataModelFieldCalculatorTests(ITestOutputHelper output) + { + var dataElementAccessChecker = new Mock(); + dataElementAccessChecker.Setup(x => x.CanRead(It.IsAny(), It.IsAny())).ReturnsAsync(true); + + var telemetry = new TelemetrySink(); + + _output = output; + _dataModelFieldCalculator = new DataModelFieldCalculator( + _logger, + _layoutInitializer.Object, + _appResources.Object, + dataElementAccessChecker.Object, + telemetry.Object + ); + } + + private async Task LoadData(string fileName, string folder) + { + var data = await File.ReadAllTextAsync(Path.Join(folder, fileName)); + _output.WriteLine(data); + return JsonSerializer.Deserialize(data, _jsonSerializerOptions)!; + } + + [Fact] + public async Task ShouldLogErrorAndThrowWhenExpressionEvaluatorThrowsException() + { + var testCaseJson = """ + { + "name": "Should log error and throw when ExpressionEvaluator throws exception", + "expects": [ + { + "logMessage": "Error while evaluating calculation for field form.formDataWrapperThrows" + } + ], + "calculationConfig": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/calculation/calculation.schema.v1.json", + "calculations": { + "form.formDataWrapperThrows": { + "expression": ["noneExistingExpression"] + } + } + }, + "formData": { + "form": { + "formDataWrapperThrows": true + } + }, + "layouts": {} + } + """; + _output.WriteLine(testCaseJson); + var testCase = JsonSerializer.Deserialize( + testCaseJson, + _jsonSerializerOptions + )!; + + Setup(testCase); + + var exception = await Assert.ThrowsAsync(() => + _dataModelFieldCalculator.CalculateFormData( + _instanceDataAccessor, + _dataElement, + "Task_1", + JsonSerializer.Serialize(testCase.CalculationConfig) + ) + ); + + Assert.Contains(testCase.Expects.First().LogMessage, _logger.Collector.GetSnapshot().Select(x => x.Message)); + Assert.Contains( + $"Function \"noneExistingExpression\" not implemented in backend [\"noneExistingExpression\"]", + exception.Message + ); + } + + [Theory] + [FileNamesInFolderData(["Features", "DataProcessing", "data-field-value-calculator-tests", "assert-logger"])] + public async Task RunDataModelFieldCalculationTestsThatAssertLogger(string fileName, string folder) + { + var (_, testCase) = await RunDataModelFieldCalculatorTest(fileName, folder); + + foreach (var expected in testCase.Expects) + { + Assert.Contains(expected.LogMessage, _logger.Collector.GetSnapshot().Select(x => x.Message)); + } + } + + [Theory] + [FileNamesInFolderData(["Features", "DataProcessing", "data-field-value-calculator-tests"])] + public async Task RunDataModelFieldCalculationTests(string fileName, string folder) + { + var (result, testCase) = await RunDataModelFieldCalculatorTest(fileName, folder); + + foreach (var expected in testCase.Expects) + { + if (expected.Result.HasValue) + { + Assert.Equal(expected.Result.Value.ToObject(), result.Get(expected.Field)); + } + else + { + Assert.Fail($"Expected result for field {expected.Field} not found"); + } + } + } + + private async Task<(IFormDataWrapper, DataModelFieldCalculatorTestModel)> RunDataModelFieldCalculatorTest( + string fileName, + string folder + ) + { + var testCase = await LoadData(fileName, folder); + + Setup(testCase); + + await _dataModelFieldCalculator.CalculateFormData( + _instanceDataAccessor, + _dataElement, + "Task_1", + JsonSerializer.Serialize(testCase.CalculationConfig) + ); + + var formDataWrapper = await _instanceDataAccessor.GetFormDataWrapper(_dataElement); + + return (formDataWrapper, testCase); + } + + private void Setup(DataModelFieldCalculatorTestModel testCase) + { + var instance = new Instance() { Id = "1337/fa0678ad-960d-4307-aba2-ba29c9804c9d", AppId = "org/app" }; + var dataType = new DataType() { Id = "default" }; + + _dataElement = new DataElement { Id = "30844cc0-81af-4429-9f9e-035d78f1f9da", DataType = "default" }; + _instanceDataAccessor = DynamicClassBuilder.DataAccessorFromJsonDocument( + instance, + testCase.FormData, + _dataElement + ); + + var layout = new LayoutSetComponent(testCase.Layouts, "layout", dataType); + var componentModel = new LayoutModel([layout], null); + var translationService = new TranslationService( + new AppIdentifier("org", "app"), + _appResources.Object, + FakeLoggerXunit.Get(_output) + ); + var evaluatorState = new LayoutEvaluatorState( + _instanceDataAccessor, + componentModel, + translationService, + _frontendSettings.Value + ); + _layoutInitializer + .Setup(init => + init.Init(It.IsAny(), "Task_1", It.IsAny(), It.IsAny()) + ) + .ReturnsAsync(evaluatorState); + + _appResources + .Setup(ar => ar.GetTexts("org", "app", "nb")) + .ReturnsAsync( + testCase.TextResources is null + ? null + : new TextResource { Language = "nb", Resources = testCase.TextResources } + ); + } + + private record DataModelFieldCalculatorTestModel + { + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("expects")] + public required Expected[] Expects { get; set; } + + [JsonPropertyName("calculationConfig")] + public required JsonElement CalculationConfig { get; set; } + + [JsonPropertyName("formData")] + public required JsonElement FormData { get; set; } + + [JsonPropertyName("layouts")] + public required IReadOnlyDictionary Layouts { get; set; } + + [JsonPropertyName("textResources")] + public List? TextResources { get; set; } + } + + private record Expected + { + public string? Field { get; set; } + + public ExpressionValue? Result { get; set; } + + public string? LogMessage { get; set; } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/assert-logger/parse-none-existing-definition.json b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/assert-logger/parse-none-existing-definition.json new file mode 100644 index 0000000000..83b9d63ce1 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/assert-logger/parse-none-existing-definition.json @@ -0,0 +1,23 @@ +{ + "name": "Should log error when trying to parse none existing definition", + "expects": [ + { + "logMessage": "Calculation for field form.noneExistingExpression could not be parsed" + }, + { + "logMessage": "Calculation for field form.noneExistingExpression could not be resolved" + } + ], + "calculationConfig": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/calculation/calculation.schema.v1.json", + "calculations": { + "form.noneExistingExpression": null + } + }, + "formData": { + "form": { + "noneExistingExpression": true + } + }, + "layouts": {} +} diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/assert-logger/parse-none-existing-expression.json b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/assert-logger/parse-none-existing-expression.json new file mode 100644 index 0000000000..f4fc211966 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/assert-logger/parse-none-existing-expression.json @@ -0,0 +1,23 @@ +{ + "name": "Should log error when trying to parse none existing expression", + "expects": [ + { + "logMessage": "Calculation for field form.noneExistingExpression is missing expression" + }, + { + "logMessage": "Calculation for field form.noneExistingExpression could not be resolved" + } + ], + "calculationConfig": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/calculation/calculation.schema.v1.json", + "calculations": { + "form.noneExistingExpression": {} + } + }, + "formData": { + "form": { + "noneExistingExpression": true + } + }, + "layouts": {} +} diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/assert-logger/unsupported-data-type.json b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/assert-logger/unsupported-data-type.json new file mode 100644 index 0000000000..12ed160ff2 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/assert-logger/unsupported-data-type.json @@ -0,0 +1,30 @@ +{ + "name": "Should log warning when trying to set field with unsupported data type", + "expects": [ + { + "logMessage": "Could not set calculated value for field form.unsupportedDataType in data element 30844cc0-81af-4429-9f9e-035d78f1f9da. This is because the type conversion failed." + } + ], + "calculationConfig": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/calculation/calculation.schema.v1.json", + "calculations": { + "form.unsupportedDataType": { + "expression": ["language"] + } + } + }, + "formData": { + "form": { + "unsupportedDataType": true + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/component-lookup-hidden.json b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/component-lookup-hidden.json new file mode 100644 index 0000000000..2461db3571 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/component-lookup-hidden.json @@ -0,0 +1,39 @@ +{ + "name": "Should not set field when looking up hidden component", + "expects": [ + { + "field": "form.name", + "result": "feil" + } + ], + "calculationConfig": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/calculation/calculation.schema.v1.json", + "calculations": { + "form.name": { + "expression": ["equals", ["dataModel", ["argv", 0]], "feil"] + } + } + }, + "formData": { + "form": { + "name": "feil" + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.name" + }, + "hidden": true + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/hidden-field.json b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/hidden-field.json new file mode 100644 index 0000000000..baa55931ee --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/hidden-field.json @@ -0,0 +1,39 @@ +{ + "name": "Should not set field when component is hidden", + "expects": [ + { + "field": "form.name", + "result": "feil" + } + ], + "calculationConfig": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/calculation/calculation.schema.v1.json", + "calculations": { + "form.name": { + "expression": ["equals", ["dataModel", ["argv", 0]], "feil"] + } + } + }, + "formData": { + "form": { + "name": "feil" + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.name" + }, + "hidden": true + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/hidden-page.json b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/hidden-page.json new file mode 100644 index 0000000000..b28a322e30 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/hidden-page.json @@ -0,0 +1,39 @@ +{ + "name": "Should not set field if field is on hidden page", + "expects": [ + { + "field": "form.name", + "result": "none" + } + ], + "calculationConfig": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/calculation/calculation.schema.v1.json", + "calculations": { + "form.name": { + "expression": ["equals", ["dataModel", ["argv", 0]], "none"] + } + } + }, + "formData": { + "form": { + "name": "none" + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout.schema.v1.json", + "data": { + "hidden": true, + "layout": [ + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.name" + } + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/single-expression-boolean.json b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/single-expression-boolean.json new file mode 100644 index 0000000000..46577b9b21 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/single-expression-boolean.json @@ -0,0 +1,67 @@ +{ + "name": "Should set boolean value to field with supported data type", + "expects": [ + { + "field": "form.nameResultBoolean", "result": true + }, + { + "field": "form.emailResultBoolean", "result": false + }, + { + "field": "form.nameResultNumber", "result": 1 + }, + { + "field": "form.emailResultNumber", "result": 0 + } + ], + "calculationConfig": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/calculation/calculation.schema.v1.json", + "calculations": { + "form.nameResultBoolean": { + "expression": ["equals", ["dataModel", "form.name"], "none"] + }, + "form.emailResultBoolean": { + "expression": ["equals", ["dataModel", "form.email"], "none"] + }, + "form.nameResultNumber": { + "expression": ["equals", ["dataModel", "form.name"], "none"] + }, + "form.emailResultNumber": { + "expression": ["equals", ["dataModel", "form.email"], "none"] + } + } + }, + "formData": { + "form": { + "name": "none", + "email": "email@address.com", + "nameResultBoolean": false, + "emailResultBoolean": true, + "nameResultNumber": 0, + "emailResultNumber": 1 + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "name-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.name" + } + }, + { + "id": "email-input", + "type": "Input", + "dataModelBindings": { + "simpleBinding": "form.email" + } + } + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/single-expression-number.json b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/single-expression-number.json new file mode 100644 index 0000000000..4dba27e298 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/DataProcessing/data-field-value-calculator-tests/single-expression-number.json @@ -0,0 +1,37 @@ +{ + "name": "Should set number value to field with supported data type. TODO: Move when #1651 is merged", + "expects": [ + { + "field": "form.fourTimesTwoString", "result": "8" + }, + { + "field": "form.fourTimesTwoNumber", "result": 8 + } + ], + "calculationConfig": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/calculation/calculation.schema.v1.json", + "calculations": { + "form.fourTimesTwoString": { + "expression": ["multiply", 4, 2] + }, + "form.fourTimesTwoNumber": { + "expression": ["multiply", 4, 2] + } + } + }, + "formData": { + "form": { + "fourTimesTwoString": "0", + "fourTimesTwoNumber": 0 + } + }, + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + ] + } + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Validators/expression-validation-tests/shared/component-lookup-hidden.json b/test/Altinn.App.Core.Tests/Features/Validators/expression-validation-tests/shared/component-lookup-hidden.json index 051283f600..9ccf423881 100644 --- a/test/Altinn.App.Core.Tests/Features/Validators/expression-validation-tests/shared/component-lookup-hidden.json +++ b/test/Altinn.App.Core.Tests/Features/Validators/expression-validation-tests/shared/component-lookup-hidden.json @@ -41,7 +41,7 @@ }, "layouts": { "Page": { - "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout.schema.v1.json", "data": { "layout": [ { diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 654be3d2b5..5a8d7984d7 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -4,6 +4,7 @@ namespace Altinn.App.Core.Configuration { public class AppSettings { + public const string CALCULATION_CONFIG_FILENAME = "calculation.json"; public const string JSON_SCHEMA_FILENAME = "schema.json"; public const string VALIDATION_CONFIG_FILENAME = "validation.json"; public AppSettings() { } @@ -16,6 +17,7 @@ namespace Altinn.App.Core.Configuration [System.Obsolete("This is not used, and will be removed in the next major version")] public string BaseResourceFolderContainer { get; set; } public int CacheResourceLifeTimeInSeconds { get; set; } + public string CalculationConfigurationFileName { get; set; } public string ConfigurationFolder { get; set; } public string DefaultBootstrapUrl { get; set; } public bool DisableCsrfCheck { get; set; } @@ -2326,6 +2328,7 @@ namespace Altinn.App.Core.Helpers.DataModel public object? GetModelData(string field, System.ReadOnlySpan rowIndexes = default) { } public int? GetModelDataCount(string field, System.ReadOnlySpan rowIndexes = default) { } public string[] GetResolvedKeys(string field) { } + public string[] GetResolvedKeys(string field, bool isCalculating) { } public void RemoveField(string field, Altinn.App.Core.Helpers.RowRemovalOption rowRemovalOption) { } } } @@ -2375,6 +2378,7 @@ namespace Altinn.App.Core.Implementation public Altinn.Platform.Storage.Interface.Models.Application GetApplication() { } public string? GetApplicationBPMNProcess() { } public string? GetApplicationXACMLPolicy() { } + public string? GetCalculationConfiguration(string dataTypeId) { } public string GetClassRefForLogicDataType(string dataType) { } public System.Threading.Tasks.Task GetFooter() { } [System.Obsolete("Use GetLayoutModelForTask instead")] @@ -2938,6 +2942,7 @@ namespace Altinn.App.Core.Internal.App [System.Obsolete("GetApplication is scheduled for removal. Use Altinn.App.Core.Internal.App.IAppMet" + "adata.GetApplicationXACMLPolicy instead", false)] string? GetApplicationXACMLPolicy(); + string? GetCalculationConfiguration(string dataTypeId); string GetClassRefForLogicDataType(string dataType); System.Threading.Tasks.Task GetFooter(); [System.Obsolete("Use GetLayoutModelForTask instead", false)] @@ -3145,6 +3150,7 @@ namespace Altinn.App.Core.Internal.Expressions [System.Obsolete("Use ComponentContext.IsHidden or ComponentContext.EvaluateExpression instead")] public static System.Threading.Tasks.Task EvaluateBooleanExpression(Altinn.App.Core.Internal.Expressions.LayoutEvaluatorState state, Altinn.App.Core.Models.Expressions.ComponentContext context, string property, bool defaultReturn) { } public static System.Threading.Tasks.Task EvaluateExpression(Altinn.App.Core.Internal.Expressions.LayoutEvaluatorState state, Altinn.App.Core.Models.Expressions.Expression expr, Altinn.App.Core.Models.Expressions.ComponentContext context, object?[]? positionalArguments = null) { } + public static System.Threading.Tasks.Task EvaluateExpressionToExpressionValue(Altinn.App.Core.Internal.Expressions.LayoutEvaluatorState state, Altinn.App.Core.Models.Expressions.Expression expr, Altinn.App.Core.Models.Expressions.ComponentContext context, object?[]? positionalArguments = null) { } } public class ExpressionEvaluatorTypeErrorException : Altinn.App.Core.Exceptions.AltinnException { @@ -3217,6 +3223,7 @@ namespace Altinn.App.Core.Internal.Expressions public string GetLanguage() { } public System.Threading.Tasks.Task GetModelData(Altinn.App.Core.Models.Layout.ModelBinding key, Altinn.App.Core.Models.DataElementIdentifier? defaultDataElementIdentifier, int[]? indexes) { } public System.Threading.Tasks.Task GetResolvedKeys(Altinn.App.Core.Models.Layout.DataReference reference) { } + public System.Threading.Tasks.Task GetResolvedKeys(Altinn.App.Core.Models.Layout.DataReference reference, bool isCalculating) { } public System.TimeZoneInfo? GetTimeZone() { } public System.Threading.Tasks.Task RemoveDataField(Altinn.App.Core.Models.Layout.DataReference key, Altinn.App.Core.Helpers.RowRemovalOption rowRemovalOption) { } public System.Threading.Tasks.Task TranslateText(string textKey, Altinn.App.Core.Models.Expressions.ComponentContext context) { }