Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch custom primitive types to a marker interface #1682

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace DotVVM.Framework.ViewModel;
Copy link
Member

Choose a reason for hiding this comment

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

Remove the class completely


[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
[Obsolete("Implement IDotvvmPrimitiveType.", error: true)]
public class CustomPrimitiveTypeAttribute : Attribute
{
}
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 CustomPrimitiveTypeRegistration(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 void AddHtmlAttribute(IHtmlWriter writer, string name, 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, TryDiscoverCustomPrimitiveType);
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 TryDiscoverCustomPrimitiveType(Type type)
Copy link
Member

Choose a reason for hiding this comment

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

Remove Try from the name since the method can crash

{
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 class ControlWithCustomPrimitiveTypeProperties : CompositeControl

}

[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
Loading