diff --git a/backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs b/backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs new file mode 100644 index 00000000000..0dc69bc495d --- /dev/null +++ b/backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs @@ -0,0 +1,25 @@ +using System; + +namespace Altinn.Studio.Designer.Exceptions.Options; + +/// +/// Indicates that an error occurred during json serialization of options. +/// +[Serializable] +public class InvalidOptionsFormatException : Exception +{ + /// + public InvalidOptionsFormatException() + { + } + + /// + public InvalidOptionsFormatException(string message) : base(message) + { + } + + /// + public InvalidOptionsFormatException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/backend/src/Designer/Filters/Options/OptionsErrorCodes.cs b/backend/src/Designer/Filters/Options/OptionsErrorCodes.cs new file mode 100644 index 00000000000..9053087b1b5 --- /dev/null +++ b/backend/src/Designer/Filters/Options/OptionsErrorCodes.cs @@ -0,0 +1,6 @@ +namespace Altinn.Studio.Designer.Filters.Options; + +public class OptionsErrorCodes +{ + public const string InvalidOptionsFormat = nameof(InvalidOptionsFormat); +} diff --git a/backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs b/backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs new file mode 100644 index 00000000000..ead5e0a589a --- /dev/null +++ b/backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs @@ -0,0 +1,26 @@ +using System.Net; +using Altinn.Studio.Designer.Exceptions.Options; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Altinn.Studio.Designer.Filters.Options; + +public class OptionsExceptionFilterAttribute : ExceptionFilterAttribute +{ + public override void OnException(ExceptionContext context) + { + base.OnException(context); + + if (context.ActionDescriptor is not ControllerActionDescriptor) + { + return; + } + + if (context.Exception is InvalidOptionsFormatException) + { + context.Result = new ObjectResult(ProblemDetailsUtils.GenerateProblemDetails(context.Exception, OptionsErrorCodes.InvalidOptionsFormat, HttpStatusCode.BadRequest)) { StatusCode = (int)HttpStatusCode.BadRequest }; + } + } + +} diff --git a/backend/src/Designer/Filters/ProblemDetailsExtensionsCodes.cs b/backend/src/Designer/Filters/ProblemDetailsExtensionsCodes.cs index 4064393c200..a70c0f6f152 100644 --- a/backend/src/Designer/Filters/ProblemDetailsExtensionsCodes.cs +++ b/backend/src/Designer/Filters/ProblemDetailsExtensionsCodes.cs @@ -3,5 +3,5 @@ namespace Altinn.Studio.Designer.Filters; public static class ProblemDetailsExtensionsCodes { public const string ErrorCode = "errorCode"; - + public const string Detail = "detail"; } diff --git a/backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs b/backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs new file mode 100644 index 00000000000..c4a556f977c --- /dev/null +++ b/backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Altinn.Studio.Designer.Helpers.JsonConverterHelpers; + +public class NotNullableAttribute : ValidationAttribute +{ + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + if (value == null) + { + return new ValidationResult("The field is required."); + } + return ValidationResult.Success; + } +} diff --git a/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs b/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs index a15ad94deeb..9ba74b172ef 100644 --- a/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs +++ b/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs @@ -1,10 +1,11 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; +using Altinn.Studio.Designer.Exceptions.Options; namespace Altinn.Studio.Designer.Helpers.JsonConverterHelpers; -public class OptionConverter : JsonConverter +public class OptionValueConverter : JsonConverter { public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -14,7 +15,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS JsonTokenType.Number when reader.TryGetDouble(out double d) => d, JsonTokenType.True => true, JsonTokenType.False => false, - _ => throw new JsonException($"Unsupported JSON token for Option.Value: {reader.TokenType}") + _ => throw new InvalidOptionsFormatException($"Unsupported JSON token for Value field, {typeToConvert}: {reader.TokenType}.") }; } @@ -32,7 +33,7 @@ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOp writer.WriteBooleanValue(b); break; default: - throw new JsonException("Unsupported type for Option.Value."); + throw new InvalidOptionsFormatException($"{value} is an unsupported type for Value field. Accepted types are string, double and bool."); } } } diff --git a/backend/src/Designer/Infrastructure/MvcConfiguration.cs b/backend/src/Designer/Infrastructure/MvcConfiguration.cs index b135b5a35cc..990b05a5498 100644 --- a/backend/src/Designer/Infrastructure/MvcConfiguration.cs +++ b/backend/src/Designer/Infrastructure/MvcConfiguration.cs @@ -2,6 +2,7 @@ using Altinn.Studio.Designer.Filters.DataModeling; using Altinn.Studio.Designer.Filters.Git; using Altinn.Studio.Designer.Filters.IO; +using Altinn.Studio.Designer.Filters.Options; using Altinn.Studio.Designer.ModelBinding; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -26,6 +27,7 @@ public static IServiceCollection ConfigureMvc(this IServiceCollection services) options.Filters.Add(typeof(DataModelingExceptionFilterAttribute)); options.Filters.Add(typeof(GitExceptionFilterAttribute)); options.Filters.Add(typeof(IoExceptionFilterAttribute)); + options.Filters.Add(typeof(OptionsExceptionFilterAttribute)); }) .AddNewtonsoftJson(options => options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter())); diff --git a/backend/src/Designer/Models/Option.cs b/backend/src/Designer/Models/Option.cs index ddfa6b9559e..b49a499ad28 100644 --- a/backend/src/Designer/Models/Option.cs +++ b/backend/src/Designer/Models/Option.cs @@ -1,4 +1,3 @@ -using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Altinn.Studio.Designer.Helpers.JsonConverterHelpers; @@ -12,17 +11,17 @@ public class Option /// /// Value that connects the option to the data model. /// - [Required] + [NotNullable] [JsonPropertyName("value")] - [JsonConverter(typeof(OptionConverter))] - public object Value { get; set; } + [JsonConverter(typeof(OptionValueConverter))] + public required object Value { get; set; } /// /// Label to present to the user. /// - [Required] + [NotNullable] [JsonPropertyName("label")] - public string Label { get; set; } + public required string Label { get; set; } /// /// Description, typically displayed below the label. diff --git a/backend/src/Designer/Services/Implementation/OptionsService.cs b/backend/src/Designer/Services/Implementation/OptionsService.cs index f1834c3156a..137b0cf5c34 100644 --- a/backend/src/Designer/Services/Implementation/OptionsService.cs +++ b/backend/src/Designer/Services/Implementation/OptionsService.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; -using System.Linq; +using System.ComponentModel.DataAnnotations; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Altinn.Studio.Designer.Exceptions.Options; using Altinn.Studio.Designer.Models; using Altinn.Studio.Designer.Services.Interfaces; using LibGit2Sharp; @@ -50,9 +51,26 @@ public async Task> GetOptionsList(string org, string repo, string d string optionsListString = await altinnAppGitRepository.GetOptionsList(optionsListId, cancellationToken); var optionsList = JsonSerializer.Deserialize>(optionsListString); + + try + { + optionsList.ForEach(ValidateOption); + } + catch (ValidationException) + { + throw new InvalidOptionsFormatException($"One or more of the options have an invalid format in option list: {optionsListId}."); + } + + return optionsList; } + private void ValidateOption(Option option) + { + var validationContext = new ValidationContext(option); + Validator.ValidateObject(option, validationContext, validateAllProperties: true); + } + /// public async Task> CreateOrOverwriteOptionsList(string org, string repo, string developer, string optionsListId, List