Skip to content

Commit

Permalink
Merge pull request #1682 from riganti/feature/custom-primitive-types-…
Browse files Browse the repository at this point in the history
…interface

Switch custom primitive types to a marker interface
  • Loading branch information
exyi authored Aug 2, 2023
2 parents 3912740 + a9561ea commit 5754127
Show file tree
Hide file tree
Showing 9 changed files with 30 additions and 29 deletions.
8 changes: 0 additions & 8 deletions src/Framework/Core/ViewModel/CustomPrimitiveTypeAttribute.cs

This file was deleted.

8 changes: 8 additions & 0 deletions src/Framework/Core/ViewModel/IDotvvmPrimitiveType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace DotVVM.Framework.ViewModel;

/// <summary>
/// Marker interface instructing DotVVM to treat the type as a primitive type.
/// The type is required to have a static TryParse(string, [IFormatProvider,] out T) method and expected to implement ToString() method which is compatible with the TryParse method.
/// Primitive types are then serialized as string in client-side view models.
/// </summary>
public interface IDotvvmPrimitiveType { }
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal CustomPrimitiveTypeRegistration(Type type)
{
if (ReflectionUtils.IsCollection(type) || ReflectionUtils.IsDictionary(type))
{
throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)}, but it cannot be used as a custom primitive type. Custom primitive types cannot be collections, dictionaries, and cannot be primitive types already supported by DotVVM.");
throw new DotvvmConfigurationException($"The type {type} implements {nameof(IDotvvmPrimitiveType)}, but it cannot be used as a custom primitive type. Custom primitive types cannot be collections, dictionaries, and cannot be primitive types already supported by DotVVM.");
}

Type = type;
Expand All @@ -38,7 +38,7 @@ internal static Func<string, ParseResult> ResolveTryParseMethod(Type type)
new[] { typeof(string), typeof(IFormatProvider), type.MakeByRefType() }, null)
?? type.GetMethod("TryParse", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy, null,
new[] { typeof(string), type.MakeByRefType() }, null)
?? throw new DotvvmConfigurationException($"The type {type} is marked with {nameof(CustomPrimitiveTypeAttribute)} but it does not contain a public static method TryParse(string, IFormatProvider, out {type}) or TryParse(string, out {type})!");
?? throw new DotvvmConfigurationException($"The type {type} implements {nameof(IDotvvmPrimitiveType)} but it does not contain a public static method TryParse(string, IFormatProvider, out {type}) or TryParse(string, out {type})!");

var inputParameter = Expression.Parameter(typeof(string), "arg");
var resultVariable = Expression.Variable(type, "result");
Expand Down
6 changes: 5 additions & 1 deletion src/Framework/Framework/Controls/HtmlGenericControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Text;
using DotVVM.Framework.Compilation.Javascript;
using FastExpressionCompiler;
using DotVVM.Framework.ViewModel;

namespace DotVVM.Framework.Controls
{
Expand Down Expand Up @@ -405,7 +406,10 @@ private static string AttributeValueToString(object? value) =>
Enum enumValue => ReflectionUtils.ToEnumString(enumValue.GetType(), enumValue.ToString()),
Guid guid => guid.ToString(),
_ when ReflectionUtils.IsNumericType(value.GetType()) => Convert.ToString(value, CultureInfo.InvariantCulture) ?? "",
_ when ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(value.GetType()) is {} registration => registration.ToStringMethod(value),
IDotvvmPrimitiveType => value switch {
IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString() ?? ""
},
System.Collections.IEnumerable =>
throw new NotSupportedException($"Attribute value of type '{value.GetType().ToCode(stripNamespace: true)}' is not supported. Consider concatenating the values into a string or use the HtmlGenericControl.AttributeList if you need to pass multiple values."),
_ =>
Expand Down
22 changes: 11 additions & 11 deletions src/Framework/Framework/Utils/ReflectionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using DotVVM.Framework.Compilation;
using DotVVM.Framework.Routing;
using DotVVM.Framework.ViewModel;
using System.Diagnostics;

namespace DotVVM.Framework.Utils
{
Expand Down Expand Up @@ -307,7 +308,7 @@ public record TypeConvertException(object Value, Type Type, Exception InnerExcep
typeof (decimal)
};
// mapping of server-side types to their client-side representation
private static readonly ConcurrentDictionary<Type, CustomPrimitiveTypeRegistration?> CustomPrimitiveTypes = new();
private static readonly ConcurrentDictionary<Type, CustomPrimitiveTypeRegistration> CustomPrimitiveTypes = new();

public static IEnumerable<Type> GetNumericTypes()
{
Expand Down Expand Up @@ -374,31 +375,30 @@ public static bool IsDotvvmNativePrimitiveType(Type type)
/// <summary> Returns true if the type is a custom primitive type.</summary>
public static bool IsCustomPrimitiveType(Type type)
{
return TryGetCustomPrimitiveTypeRegistration(type) is { };
return typeof(IDotvvmPrimitiveType).IsAssignableFrom(type);
}

/// <summary>Returns a custom primitive type registration for the given type, or null if the type is not a custom primitive type.</summary>
public static CustomPrimitiveTypeRegistration? TryGetCustomPrimitiveTypeRegistration(Type type)
{
return CustomPrimitiveTypes.GetOrAdd(type, TryDiscoverCustomPrimitiveType);
if (IsCustomPrimitiveType(type))
return CustomPrimitiveTypes.GetOrAdd(type, DiscoverCustomPrimitiveType);
else
return null;
}

/// <summary> Returns true the type is serialized as a JavaScript primitive (not object nor array) </summary>
public static bool IsPrimitiveType(Type type)
{
return PrimitiveTypes.Contains(type)
|| (IsNullableType(type) && IsDotvvmNativePrimitiveType(type.UnwrapNullableType()))
|| (IsNullableType(type) && IsPrimitiveType(type.UnwrapNullableType()))
|| type.IsEnum
|| TryGetCustomPrimitiveTypeRegistration(type) is {};
|| IsCustomPrimitiveType(type);
}

private static CustomPrimitiveTypeRegistration? TryDiscoverCustomPrimitiveType(Type type)
private static CustomPrimitiveTypeRegistration DiscoverCustomPrimitiveType(Type type)
{
var attribute = type.GetCustomAttribute<CustomPrimitiveTypeAttribute>();
if (attribute == null)
{
return null;
}
Debug.Assert(typeof(IDotvvmPrimitiveType).IsAssignableFrom(type));
return new CustomPrimitiveTypeRegistration(type);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private IEnumerable<ViewModelValidationError> ValidateViewModel(object? viewMode
if (propertyResult != ValidationResult.Success)
{
var propertyPath =
ReflectionUtils.IsCustomPrimitiveType(viewModelType) ? pathPrefix.TrimEnd('/') : pathPrefix + property.Name;
viewModel is IDotvvmPrimitiveType ? pathPrefix.TrimEnd('/') : pathPrefix + property.Name;
yield return new ViewModelValidationError(rule.ErrorMessage, propertyPath, rootObject);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ public DotvvmControl GetContents(

}

[CustomPrimitiveType]
public struct Point : IFormattable
public struct Point : IFormattable, IDotvvmPrimitiveType
{
public int X { get; set; }
public int Y { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes
{
[CustomPrimitiveType]
public record SampleId : TypeId<SampleId>
public record SampleId : TypeId<SampleId>, IDotvvmPrimitiveType
{
public SampleId(Guid idValue) : base(idValue)
{
Expand Down
3 changes: 1 addition & 2 deletions src/Tests/Binding/BindingCompilationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1201,11 +1201,10 @@ public async Task<string> GetStringPropAsync()
}


[CustomPrimitiveType]
record struct VehicleNumber(
[property: Range(100, 999)]
int Value
)
): IDotvvmPrimitiveType
{
public override string ToString() => Value.ToString();
public static bool TryParse(string s, out VehicleNumber result)
Expand Down

0 comments on commit 5754127

Please sign in to comment.